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
|
#
#
# 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"])
## let smtpConn = newSmtp(useSsl = true, debug=true)
## smtpConn.connect("smtp.gmail.com", Port 465)
## smtpConn.auth("username", "password")
## smtpConn.sendmail("username@gmail.com", @["foo@gmail.com"], $msg)
##
##
## Example for startTls use:
##
##
## .. code-block:: Nim
## var msg = createMessage("Hello from Nim's SMTP",
## "Hello!.\n Is this awesome or what?",
## @["foo@gmail.com"])
## let smtpConn = newSmtp(debug=true)
## smtpConn.connect("smtp.mailtrap.io", Port 2525)
## smtpConn.startTls()
## 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
Message* = object
msgTo: seq[string]
msgCc: seq[string]
msgSubject: string
msgOtherHeaders: StringTableRef
msgBody: string
ReplyError* = object of IOError
SmtpBase[SocketType] = ref object
sock: SocketType
debug: bool
Smtp* = SmtpBase[Socket]
AsyncSmtp* = SmtpBase[AsyncSocket]
proc debugSend(smtp: Smtp | AsyncSmtp, cmd: string) {.multisync.} =
if smtp.debug:
echo("C:" & cmd)
await smtp.sock.send(cmd)
proc debugRecv(smtp: Smtp | AsyncSmtp): Future[TaintedString] {.multisync.} =
result = await smtp.sock.recvLine()
if smtp.debug:
echo("S:" & result.string)
proc quitExcpt(smtp: Smtp, msg: string) =
smtp.debugSend("QUIT")
raise newException(ReplyError, msg)
const compiledWithSsl = defined(ssl)
when not defined(ssl):
type PSSLContext = ref object
let defaultSSLContext: PSSLContext = nil
else:
var defaultSSLContext {.threadvar.}: SSLContext
proc getSSLContext(): SSLContext =
if defaultSSLContext == nil:
defaultSSLContext = newContext(verifyMode = CVerifyNone)
result = defaultSSLContext
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 newSmtp*(useSsl = false, debug=false,
sslContext: SSLContext = nil): Smtp =
## Creates a new ``Smtp`` instance.
new result
result.debug = debug
result.sock = newSocket()
if useSsl:
when compiledWithSsl:
if sslContext == nil:
getSSLContext().wrapSocket(result.sock)
else:
sslContext.wrapSocket(result.sock)
else:
{.error: "SMTP module compiled without SSL support".}
proc newAsyncSmtp*(useSsl = false, debug=false,
sslContext: SSLContext = nil): AsyncSmtp =
## Creates a new ``AsyncSmtp`` instance.
new result
result.debug = debug
result.sock = newAsyncSocket()
if useSsl:
when compiledWithSsl:
if sslContext == nil:
getSSLContext().wrapSocket(result.sock)
else:
sslContext.wrapSocket(result.sock)
else:
{.error: "SMTP module compiled without SSL support".}
proc quitExcpt(smtp: AsyncSmtp, msg: string): Future[void] =
var retFuture = newFuture[void]()
var sendFut = smtp.debugSend("QUIT")
sendFut.callback =
proc () =
retFuture.fail(newException(ReplyError, msg))
return retFuture
proc checkReply(smtp: Smtp | AsyncSmtp, reply: string) {.multisync.} =
var line = await smtp.debugRecv()
if not line.startswith(reply):
await quitExcpt(smtp, "Expected " & reply & " reply, got: " & line)
proc connect*(smtp: Smtp | AsyncSmtp,
address: string, port: Port) {.multisync.} =
## Establishes a connection with a SMTP server.
## May fail with ReplyError or with a socket error.
await smtp.sock.connect(address, port)
await smtp.checkReply("220")
await smtp.debugSend("HELO " & address & "\c\L")
await smtp.checkReply("250")
proc startTls*(smtp: Smtp | AsyncSmtp, sslContext: SSLContext = nil) {.multisync.} =
## Put the SMTP connection in TLS (Transport Layer Security) mode.
## May fail with ReplyError
await smtp.debugSend("STARTTLS\c\L")
await smtp.checkReply("220")
when compiledWithSsl:
if sslContext == nil:
getSSLContext().wrapConnectedSocket(smtp.sock, handshakeAsClient)
else:
sslContext.wrapConnectedSocket(smtp.sock, handshakeAsClient)
else:
{.error: "SMTP module compiled without SSL support".}
proc auth*(smtp: Smtp | AsyncSmtp, username, password: string) {.multisync.} =
## Sends an AUTH command to the server to login as the `username`
## using `password`.
## May fail with ReplyError.
await smtp.debugSend("AUTH LOGIN\c\L")
await smtp.checkReply("334") # TODO: Check whether it's asking for the "Username:"
# i.e "334 VXNlcm5hbWU6"
await smtp.debugSend(encode(username) & "\c\L")
await smtp.checkReply("334") # TODO: Same as above, only "Password:" (I think?)
await smtp.debugSend(encode(password) & "\c\L")
await smtp.checkReply("235") # Check whether the authentification was successful.
proc sendMail*(smtp: Smtp | AsyncSmtp, fromAddr: string,
toAddrs: seq[string], msg: string) {.multisync.} =
## 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.debugSend("MAIL FROM:<" & fromAddr & ">\c\L")
await smtp.checkReply("250")
for address in items(toAddrs):
await smtp.debugSend("RCPT TO:<" & address & ">\c\L")
await smtp.checkReply("250")
# Send the message
await smtp.debugSend("DATA " & "\c\L")
await smtp.checkReply("354")
await smtp.sock.send(msg & "\c\L")
await smtp.debugSend(".\c\L")
await smtp.checkReply("250")
proc close*(smtp: Smtp | AsyncSmtp) {.multisync.} =
## Disconnects from the SMTP server and closes the socket.
await smtp.debugSend("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["use_tls"].parseBool,
debug=true
)
await client.connect(conf["smtphost"], conf["port"].parseInt.Port)
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 = newSmtp(
conf["use_tls"].parseBool,
debug=true
)
smtpConn.connect(conf["smtphost"], conf["port"].parseInt.Port)
smtpConn.auth(conf["username"], conf["password"])
smtpConn.sendMail(conf["sender"], @[conf["recipient"]], $msg)
smtpConn.close()
echo "sync email sent"
waitFor async_test()
sync_test()
|