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