1 module mail.stmp; 2 3 import core.thread; 4 5 import std.algorithm; 6 import std.base64; 7 import std.conv; 8 import std.string; 9 import std.encoding; 10 import std.uri; 11 12 public import mail.msg : Msg; 13 import mail.socket; 14 15 enum SmtpAuthType 16 { 17 PLAIN = 0, 18 LOGIN, 19 }; 20 21 enum SmtpReplyCode : ushort 22 { 23 HELP_STATUS = 211, // Information reply 24 HELP = 214, // Information reply 25 26 ready = 220, // After connection is established 27 QUIT = 221, // After connected aborted 28 AUTH_SUCCESS = 235, // Authentication succeeded 29 OK = 250, // Transaction success 30 FORWARD = 251, // Non-local user, message is forwarded 31 VRFY_FAIL = 252, // Verification failed (still attempt to deliver) 32 33 AUTH_CONTINUE = 334, // Answer to AUTH <method> prompting to send auth data 34 DATA_START = 354, // Server starts to accept mail data 35 36 NA = 421, // Not Available. Shutdown must follow after this reply 37 NEED_PASSWORD = 435, // Password transition is needed 38 BUSY = 450, // Mail action failed 39 ABORTED = 451, // Action aborted (internal server error) 40 STORAGE = 452, // Not enough system storage on server 41 TLS = 454, // TLS unavailable | Temporary Auth fail 42 43 SYNTAX = 500, // Command syntax error | Too long auth command line 44 SYNTAX_PARAM = 501, // Command parameter syntax error 45 NI = 502, // Command not implemented 46 BAD_SEQUENCE = 503, // This command breaks specified allowed sequences 47 NI_PARAM = 504, // Command parameter not implemented 48 49 AUTH_REQUIRED = 530, // Authentication required 50 AUTH_TOO_WEAK = 534, // Need stronger authentication type 51 AUTH_CRED = 535, // Wrong authentication credentials 52 AUTH_ENCRYPTION = 538, // Encryption reqiured for current authentication type 53 54 MAILBOX = 550, // Mailbox is not found (for different reasons) 55 TRY_FORWARD = 551, // Non-local user, forwarding is needed 56 MAILBOX_STORAGE = 552, // Storage for mailbox exceeded 57 MAILBOX_NAME = 553, // Unallowed name for the mailbox 58 FAIL = 554 // Transaction fail 59 }; 60 61 struct SmtpReply 62 { 63 bool success; 64 ushort code; 65 string message; 66 67 void toString(scope void delegate(const(char)[]) sink) const 68 { 69 sink(code.to!string); 70 sink(message); 71 } 72 }; 73 74 unittest 75 { 76 auto reply = SmtpReply(true, 220, "-Test\r\n"); 77 assert(reply.to!string == "220-Test\r\n"); 78 } 79 80 class Smtp 81 { 82 private 83 { 84 Socket _sock; 85 } 86 87 this(in string host, in ushort port = 25) 88 { 89 _sock = new Socket(host, port); 90 } 91 92 SmtpReply connect() 93 { 94 SmtpReply r; 95 if (_sock.connect()) 96 { 97 r = parseReply(receiveAll); 98 } 99 return r; 100 } 101 102 void disconnect() 103 { 104 _sock.disconnect(); 105 } 106 107 SmtpReply startTLS(EncryptionMethod encMethod = EncryptionMethod.TLSv1_2) 108 { 109 auto r = query("STARTTLS"); 110 if (r.success) 111 { 112 r.success = _sock.SSLbegin(encMethod); 113 } 114 return r; 115 } 116 117 SmtpReply noop() 118 { 119 return query("NOOP"); 120 } 121 122 SmtpReply data() 123 { 124 return query("data"); 125 } 126 127 SmtpReply helo() 128 { 129 return query("HELO " ~ _sock.hostName); 130 } 131 132 SmtpReply ehlo() 133 { 134 return query("EHLO " ~ _sock.hostName); 135 } 136 137 SmtpReply auth(in SmtpAuthType authType) 138 { 139 return query("AUTH " ~ authType.to!string); 140 } 141 142 SmtpReply authPlain(in string login, in string password) 143 { 144 return query(Base64.encode(cast(ubyte[])(login ~ "\0" ~ login ~ "\0" ~ password))); 145 } 146 147 SmtpReply authLoginUsername(string username) 148 { 149 return query(Base64.encode(cast(ubyte[]) username)); 150 } 151 152 SmtpReply authLoginPassword(string password) 153 { 154 return query(Base64.encode(cast(ubyte[]) password)); 155 } 156 157 SmtpReply mailFrom(in string addr) 158 { 159 return query("MAIL FROM: <" ~ addr ~ ">"); 160 } 161 162 SmtpReply rcptTo(in string addr) 163 { 164 return query("RCPT TO: <" ~ addr ~ ">"); 165 } 166 167 SmtpReply dataBody(in string data) 168 { 169 return query(data ~ "\r\n."); 170 } 171 172 SmtpReply send(in string fromAddr, in string[] toAddr, Msg msg) 173 { 174 if (!toAddr.length) 175 return SmtpReply(false); 176 177 auto r = mailFrom(fromAddr); 178 179 msg.headers["from"] = fromAddr; 180 181 if (r.success) 182 { 183 foreach (i; toAddr) 184 { 185 r = rcptTo(i); 186 if (!r.success) 187 break; 188 189 msg.headers.add("to", i); 190 } 191 if (r.success) 192 { 193 r = data(); 194 } 195 if (r.success) 196 { 197 r = dataBody(msg.to!string); 198 } 199 } 200 return r; 201 } 202 203 SmtpReply quit() 204 { 205 return query("quit"); 206 } 207 208 private: 209 SmtpReply parseReply(string data) 210 { 211 auto reply = SmtpReply(true, data[0 .. 3].to!ushort, data[3 .. $].idup); 212 if (reply.code >= 400) 213 { 214 reply.success = false; 215 } 216 return reply; 217 } 218 219 SmtpReply query(string command) 220 { 221 if (!_sock.isOpen) 222 return SmtpReply(false); 223 _sock.send(command ~ "\r\n"); 224 return parseReply(receiveAll); 225 } 226 227 string receiveAll() 228 { 229 string tmp; 230 string data; 231 do 232 { 233 tmp = _sock.receive(); 234 data ~= tmp; 235 } 236 while (tmp.length && !data.endsWith("\n")); 237 return data; 238 } 239 }