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 }