1 module smtp.client; 2 3 import std.algorithm; 4 import std.base64; 5 import std.conv; 6 import std.socket; 7 import std.stdio; 8 import std.string; 9 10 import smtp.reply; 11 import smtp.ssl; 12 13 /++ 14 Authentication types supported by SMTP protocol. 15 +/ 16 enum SmtpAuthType { 17 PLAIN = 0, 18 LOGIN, 19 }; 20 21 /++ 22 Low-level synchronous API for implementing SMTP clients. 23 24 SmtpClient handles: 25 + TCP data transmission and buffers management 26 + TLS/SSL channel encryption 27 + SMTP protocol request/reply handling 28 + transport layer exceptions 29 30 SmtpClient does NOT handle: 31 * semantic class usage mistakes like bad commands sequences 32 33 Supported SMTP commands: 34 NOOP, HELO, EHLO, MAIL, RSET, RCPT, DATA (data, dataBody), AUTH (auth, authPlain), 35 STARTTLS, VRFY, EXPN QUIT 36 37 Supported authentication methods: 38 PLAIN 39 +/ 40 class SmtpClient { 41 protected: 42 char[1024] _recvbuf; 43 44 bool _secure; 45 bool _authenticated; 46 47 InternetAddress server; 48 Socket transport; 49 50 // SSL Enabled 51 version (ssl) { 52 SocketSSL secureTransport; 53 } 54 55 /++ 56 Convenience method to send whole buffer of data into socket. 57 +/ 58 bool sendData(in char[] data) { 59 // SSL Enabled 60 version(ssl) { 61 if (!this._secure) { 62 ptrdiff_t sent = 0; 63 while (sent < data.length) { 64 sent += this.transport.send(data); 65 } 66 return true; 67 } else { 68 return secureTransport.write(data) ? true : false; 69 } 70 // SSL Disabled 71 } else { 72 ptrdiff_t sent = 0; 73 while (sent < data.length) { 74 sent += this.transport.send(data); 75 } 76 return true; 77 } 78 } 79 80 /++ 81 Convenience method to receive data from socket as a string. 82 +/ 83 string receiveData() { 84 // SSL Enabled 85 version(ssl) { 86 if (!this._secure) { 87 ptrdiff_t bytesReceived = this.transport.receive(_recvbuf); 88 return to!string(_recvbuf[0 .. bytesReceived]); 89 } else { 90 return secureTransport.read(); 91 } 92 // SSL Disabled 93 } else { 94 ptrdiff_t bytesReceived = this.transport.receive(_recvbuf); 95 return to!string(_recvbuf[0 .. bytesReceived]); 96 } 97 } 98 99 /++ 100 Parses server reply and converts it to 'SmtpReply' structure. 101 +/ 102 SmtpReply parseReply(string rawReply) { 103 auto reply = SmtpReply( 104 true, 105 to!uint(rawReply[0 .. 3]), 106 (rawReply[3 .. $]).idup 107 ); 108 if (reply.code >= 400) { 109 reply.success = false; 110 } 111 return reply; 112 } 113 114 /++ 115 Implementation of request/response pattern for easifying 116 communication with SMTP server. 117 +/ 118 string getResponse(string command, string suffix="\r\n") { 119 sendData(command ~ suffix); 120 auto response = ""; 121 while (!response.endsWith("\n")) { 122 auto received = receiveData(); 123 response ~= received; 124 if (received == "") break; 125 } 126 return response; 127 } 128 129 public: 130 this(string host, ushort port = 25) { 131 auto addr = new InternetHost; 132 if (addr.getHostByName(host)) { 133 server = new InternetAddress(addr.addrList[0], port); 134 } else { 135 } 136 transport = new TcpSocket(AddressFamily.INET); 137 } 138 139 @property bool secure() const { return _secure; } 140 @property Address address() { return this.server; } 141 @property authenticated() const { return _authenticated; } 142 143 /++ 144 Performs socket connection establishment. 145 connect is the first method to be called after SmtpClient instantiation. 146 +/ 147 SmtpReply connect() { 148 149 if (this.server is null) 150 return SmtpReply(false, 0, ""); 151 152 try { 153 this.transport.connect(this.server); 154 } catch (SocketOSException) { 155 return SmtpReply(false, 0, ""); 156 } 157 return parseReply(receiveData()); 158 } 159 160 /++ 161 Send command indicating that TLS encrypting of socket data stream has started. 162 +/ 163 final SmtpReply starttls(EncryptionMethod enctype = EncryptionMethod.SSLv3, bool verifyCertificate = false) { 164 version(ssl) { 165 auto response = parseReply(getResponse("STARTTLS")); 166 if (response.success) { 167 secureTransport = new SocketSSL(transport, enctype); 168 _secure = verifyCertificate ? secureTransport.ready && secureTransport.certificateIsVerified : secureTransport.ready; 169 } 170 return response; 171 } else { 172 return SmtpReply(false, 0, ""); 173 } 174 } 175 176 /++ 177 A `no operation` message. essage that does not invoke any specific routine 178 on server, but still the reply is received. 179 +/ 180 final SmtpReply noop() { 181 return parseReply(getResponse("NOOP")); 182 } 183 184 /++ 185 Initial message to send after connection. 186 Nevertheless it is recommended to use `ehlo()` instead of this method 187 in order to get more information about SMTP server configuration. 188 +/ 189 final SmtpReply helo() { 190 return parseReply(getResponse("HELO " ~ transport.hostName)); 191 } 192 193 /++ 194 Initial message to send after connection. 195 Retrieves information about SMTP server configuration 196 +/ 197 final SmtpReply ehlo() { 198 return parseReply(getResponse("EHLO " ~ transport.hostName)); 199 } 200 201 /++ 202 Perform authentication (according to RFC 4954) 203 +/ 204 final SmtpReply auth(in SmtpAuthType authType) { 205 return parseReply(getResponse("AUTH " ~ to!string(authType))); 206 } 207 208 /++ 209 Send base64-encoded authentication data according to RFC 2245. 210 Need to be performed after `data` method call; 211 +/ 212 final SmtpReply authPlain(string login, string password) { 213 string data = login ~ "\0" ~ login ~ "\0" ~ password; 214 const(char)[] encoded = Base64.encode(cast(ubyte[])data); 215 return parseReply(getResponse(to!string(encoded))); 216 } 217 218 /++ 219 Sends base64-encoded login 220 +/ 221 final SmtpReply authLoginUsername(string login) { 222 const(char)[] encoded = Base64.encode(cast(ubyte[]) login); 223 return parseReply(getResponse(to!string(encoded))); 224 } 225 226 /++ 227 Sends base64-encode password 228 +/ 229 final SmtpReply authLoginPassword(string password) { 230 const(char)[] encoded = Base64.encode(cast(ubyte[]) password); 231 return parseReply(getResponse(to!string(encoded))); 232 } 233 234 /++ 235 Low-level method to initiate process of sending mail. 236 This can be called either after connect or after helo/ehlo methods call. 237 +/ 238 final SmtpReply mail(string address) { 239 return parseReply(getResponse("MAIL FROM:<" ~ address ~ ">")); 240 } 241 242 /++ 243 Low-level method to specify recipients of the mail. Must be called 244 after 245 +/ 246 final SmtpReply rcpt(string to) { 247 return parseReply(getResponse("RCPT TO:<" ~ to ~ ">")); 248 } 249 250 /++ 251 Low-level method to initiate sending of the message body. 252 Must be called after rcpt method call. 253 +/ 254 final SmtpReply data() { 255 return parseReply(getResponse("DATA")); 256 } 257 258 /++ 259 Sends the body of message to server. Must be called after `data` method. 260 Also dataBody sends needed suffix to signal about the end of the message body. 261 +/ 262 final SmtpReply dataBody(string message) { 263 return parseReply(getResponse(message, "\r\n.\r\n")); 264 } 265 266 /++ 267 This method cancels mail transmission if mail session is in progress 268 (after `mail` method was called). 269 +/ 270 final SmtpReply rset() { 271 return parseReply(getResponse("RSET")); 272 } 273 274 /++ 275 This method asks server to verify if the user's mailbox exists on server. 276 You can pass [username] or <[e-mail]> as an argument. The result is a reply 277 that contains e-mail of the user, or an error code. 278 279 IMPORTANT NOTE: most of servers forbid using of VRFY considering it to 280 be a security hole. 281 +/ 282 final SmtpReply vrfy(string username) { 283 return parseReply(getResponse("VRFY " ~ username)); 284 } 285 286 /++ 287 EXPN expands mailing list on the server. If mailing list is not available 288 for you, you receive appropriate error reply, else you receive a list of 289 mailiing list subscribers. 290 +/ 291 final SmtpReply expn(string mailinglist) { 292 return parseReply(getResponse("EXPN " ~ mailinglist)); 293 } 294 295 /++ 296 Performs disconnection from server. In one session several mails can be sent, 297 and it is recommended to do so. quit forces server to close connection with 298 client. 299 +/ 300 final SmtpReply quit() { 301 return parseReply(getResponse("QUIT")); 302 } 303 304 /++ 305 Performs clean disconnection from server. 306 It is recommended to use disconnect after quit method which signals 307 SMTP server about end of the session. 308 +/ 309 void disconnect() { 310 this.transport.shutdown(SocketShutdown.BOTH); 311 this.transport.close(); 312 } 313 314 }