1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
|
#
#
# Nim's Runtime Library
# (c) Copyright 2012 Dominik Picheta
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## This module implements the SMTP client protocol as specified by RFC 5321,
## this can be used to send mail to any SMTP Server.
##
## This module also implements the protocol used to format messages,
## as specified by RFC 2822.
##
## Example gmail use:
##
##
## .. code-block:: Nim
## var msg = createMessage("Hello from Nim's SMTP",
## "Hello!.\n Is this awesome or what?",
## @["foo@gmail.com"])
## var smtpConn = connect("smtp.gmail.com", Port 465, true, true)
## smtpConn.auth("username", "password")
## smtpConn.sendmail("username@gmail.com", @["foo@gmail.com"], $msg)
##
##
## For SSL support this module relies on OpenSSL. If you want to
## enable SSL, compile with ``-d:ssl``.
import net, strutils, strtabs, base64, os
import asyncnet, asyncdispatch
export Port
type
Smtp* = object
sock: Socket
debug: bool
Message* = object
msgTo: seq[string]
msgCc: seq[string]
msgSubject: string
msgOtherHeaders: StringTableRef
msgBody: string
ReplyError* = object of IOError
AsyncSmtp* = ref object
sock: AsyncSocket
address: string
port: Port
useSsl: bool
debug: bool
{.deprecated: [EInvalidReply: ReplyError, TMessage: Message, TSMTP: Smtp].}
proc debugSend(smtp: Smtp, cmd: string) =
if smtp.debug:
echo("C:" & cmd)
smtp.sock.send(cmd)
proc debugRecv(smtp: var Smtp): TaintedString =
var line = TaintedString""
smtp.sock.readLine(line)
if smtp.debug:
echo("S:" & line.string)
return line
proc quitExcpt(smtp: Smtp, msg: string) =
smtp.debugSend("QUIT")
raise newException(ReplyError, msg)
proc checkReply(smtp: var Smtp, reply: string) =
var line = smtp.debugRecv()
if not line.string.startswith(reply):
quitExcpt(smtp, "Expected " & reply & " reply, got: " & line.string)
const compiledWithSsl = defined(ssl)
when not defined(ssl):
type PSSLContext = ref object
let defaultSSLContext: PSSLContext = nil
else:
let defaultSSLContext = newContext(verifyMode = CVerifyNone)
proc connect*(address: string, port = Port(25),
ssl = false, debug = false,
sslContext = defaultSSLContext): Smtp =
## Establishes a connection with a SMTP server.
## May fail with ReplyError or with a socket error.
result.sock = newSocket()
if ssl:
when compiledWithSsl:
sslContext.wrapSocket(result.sock)
else:
raise newException(ESystem,
"SMTP module compiled without SSL support")
result.sock.connect(address, port)
result.debug = debug
result.checkReply("220")
result.debugSend("HELO " & address & "\c\L")
result.checkReply("250")
proc auth*(smtp: var Smtp, username, password: string) =
## Sends an AUTH command to the server to login as the `username`
## using `password`.
## May fail with ReplyError.
smtp.debugSend("AUTH LOGIN\c\L")
smtp.checkReply("334") # TODO: Check whether it's asking for the "Username:"
# i.e "334 VXNlcm5hbWU6"
smtp.debugSend(encode(username) & "\c\L")
smtp.checkReply("334") # TODO: Same as above, only "Password:" (I think?)
smtp.debugSend(encode(password) & "\c\L")
smtp.checkReply("235") # Check whether the authentification was successful.
proc sendmail*(smtp: var Smtp, fromaddr: string,
toaddrs: seq[string], msg: string) =
## Sends `msg` from `fromaddr` to `toaddr`.
## Messages may be formed using ``createMessage`` by converting the
## Message into a string.
smtp.debugSend("MAIL FROM:<" & fromaddr & ">\c\L")
smtp.checkReply("250")
for address in items(toaddrs):
smtp.debugSend("RCPT TO:<" & address & ">\c\L")
smtp.checkReply("250")
# Send the message
smtp.debugSend("DATA " & "\c\L")
smtp.checkReply("354")
smtp.debugSend(msg & "\c\L")
smtp.debugSend(".\c\L")
smtp.checkReply("250")
proc close*(smtp: Smtp) =
## Disconnects from the SMTP server and closes the socket.
smtp.debugSend("QUIT\c\L")
smtp.sock.close()
proc createMessage*(mSubject, mBody: string, mTo, mCc: seq[string],
otherHeaders: openarray[tuple[name, value: string]]): Message =
## Creates a new MIME compliant message.
result.msgTo = mTo
result.msgCc = mCc
result.msgSubject = mSubject
result.msgBody = mBody
result.msgOtherHeaders = newStringTable()
for n, v in items(otherHeaders):
result.msgOtherHeaders[n] = v
proc createMessage*(mSubject, mBody: string, mTo,
mCc: seq[string] = @[]): Message =
## Alternate version of the above.
result.msgTo = mTo
result.msgCc = mCc
result.msgSubject = mSubject
result.msgBody = mBody
result.msgOtherHeaders = newStringTable()
proc `$`*(msg: Message): string =
## stringify for ``Message``.
result = ""
if msg.msgTo.len() > 0:
result = "TO: " & msg.msgTo.join(", ") & "\c\L"
if msg.msgCc.len() > 0:
result.add("CC: " & msg.msgCc.join(", ") & "\c\L")
# TODO: Folding? i.e when a line is too long, shorten it...
result.add("Subject: " & msg.msgSubject & "\c\L")
for key, value in pairs(msg.msgOtherHeaders):
result.add(key & ": " & value & "\c\L")
result.add("\c\L")
result.add(msg.msgBody)
proc newAsyncSmtp*(address: string, port: Port, useSsl = false,
sslContext = defaultSslContext): AsyncSmtp =
## Creates a new ``AsyncSmtp`` instance.
new result
result.address = address
result.port = port
result.useSsl = useSsl
result.sock = newAsyncSocket()
if useSsl:
when compiledWithSsl:
sslContext.wrapSocket(result.sock)
else:
raise newException(ESystem,
"SMTP module compiled without SSL support")
proc quitExcpt(smtp: AsyncSmtp, msg: string): Future[void] =
var retFuture = newFuture[void]()
var sendFut = smtp.sock.send("QUIT")
sendFut.callback =
proc () =
# TODO: Fix this in async procs.
raise newException(ReplyError, msg)
return retFuture
proc checkReply(smtp: AsyncSmtp, reply: string) {.async.} =
var line = await smtp.sock.recvLine()
if not line.string.startswith(reply):
await quitExcpt(smtp, "Expected " & reply & " reply, got: " & line.string)
proc connect*(smtp: AsyncSmtp) {.async.} =
## Establishes a connection with a SMTP server.
## May fail with ReplyError or with a socket error.
await smtp.sock.connect(smtp.address, smtp.port)
await smtp.checkReply("220")
await smtp.sock.send("HELO " & smtp.address & "\c\L")
await smtp.checkReply("250")
proc auth*(smtp: AsyncSmtp, username, password: string) {.async.} =
## Sends an AUTH command to the server to login as the `username`
## using `password`.
## May fail with ReplyError.
await smtp.sock.send("AUTH LOGIN\c\L")
await smtp.checkReply("334") # TODO: Check whether it's asking for the "Username:"
# i.e "334 VXNlcm5hbWU6"
await smtp.sock.send(encode(username) & "\c\L")
await smtp.checkReply("334") # TODO: Same as above, only "Password:" (I think?)
await smtp.sock.send(encode(password) & "\c\L")
await smtp.checkReply("235") # Check whether the authentification was successful.
proc sendMail*(smtp: AsyncSmtp, fromAddr: string,
toAddrs: seq[string], msg: string) {.async.} =
## Sends ``msg`` from ``fromAddr`` to the addresses specified in ``toAddrs``.
## Messages may be formed using ``createMessage`` by converting the
## Message into a string.
await smtp.sock.send("MAIL FROM:<" & fromAddr & ">\c\L")
await smtp.checkReply("250")
for address in items(toAddrs):
await smtp.sock.send("RCPT TO:<" & address & ">\c\L")
await smtp.checkReply("250")
# Send the message
await smtp.sock.send("DATA " & "\c\L")
await smtp.checkReply("354")
await smtp.sock.send(msg & "\c\L")
await smtp.sock.send(".\c\L")
await smtp.checkReply("250")
proc close*(smtp: AsyncSmtp) {.async.} =
## Disconnects from the SMTP server and closes the socket.
await smtp.sock.send("QUIT\c\L")
smtp.sock.close()
when not defined(testing) and isMainModule:
# To test with a real SMTP service, create a smtp.ini file, e.g.:
# username = ""
# password = ""
# smtphost = "smtp.gmail.com"
# port = 465
# use_tls = true
# sender = ""
# recipient = ""
import parsecfg
proc `[]`(c: Config, key: string): string = c.getSectionValue("", key)
let
conf = loadConfig("smtp.ini")
msg = createMessage("Hello from Nim's SMTP!",
"Hello!\n Is this awesome or what?", @[conf["recipient"]])
assert conf["smtphost"] != ""
proc async_test() {.async.} =
let client = newAsyncSmtp(
conf["smtphost"],
conf["port"].parseInt.Port,
conf["use_tls"].parseBool
)
await client.connect()
await client.auth(conf["username"], conf["password"])
await client.sendMail(conf["sender"], @[conf["recipient"]], $msg)
await client.close()
echo "async email sent"
proc sync_test() =
var smtpConn = connect(
conf["smtphost"],
conf["port"].parseInt.Port,
conf["use_tls"].parseBool,
true, # debug
)
smtpConn.auth(conf["username"], conf["password"])
smtpConn.sendmail(conf["sender"], @[conf["recipient"]], $msg)
echo "sync email sent"
waitFor async_test()
sync_test()
|