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