1 ///
2 module curlwrap.sendmail;
3 
4 import std.range : put;
5 import std.exception : enforce;
6 import std.format : format, formattedWrite;
7 import std.array : appender, replace;
8 import std.datetime : SysTime, DateTime;
9 import std..string : fromStringz, toStringz;
10 import std.conv : to;
11 
12 import etc.c.curl;
13 
14 ///
15 struct SMTPSettings
16 {
17     ///
18     string server;
19     ///
20     ushort port;
21 
22     /// need for auth on smtp server
23     string username;
24     /// ditto
25     string password;
26 
27     ///
28     enum Security
29     {
30         none, ///
31         starttls, ///
32         ssl_tls ///
33     }
34     /// if used `ssl_tls` algorithm try connect by `smtps` protocol instead `smtp`
35     Security security = Security.ssl_tls;
36 
37     ///
38     bool verifyPeer = false;
39     ///
40     bool verifyHost = false;
41 
42     ///
43     enum Auth
44     {
45         none, ///
46         usernameAndPassword ///
47     }
48     ///
49     Auth auth = Auth.usernameAndPassword;
50 }
51 
52 ///
53 struct Mail
54 {
55     ///
56     struct User
57     {
58         /// e-mail address
59         string addr;
60         ///
61         string name;
62     }
63     /// for some services 'from' e-mail in header must be equal user login
64     User from;
65     ///
66     User[] recipients;
67     ///
68     string subject;
69     ///
70     string body;
71     ///
72     SysTime date;
73     ///
74     bool html;
75 }
76 
77 ///
78 interface MailBuilder
79 {
80     ///
81     string build(Mail mail);
82 }
83 
84 ///
85 class BasicMailBuilder : MailBuilder
86 {
87     ///
88     string build(Mail mail)
89     {
90         auto buf = appender!string;
91 
92         enum nl = "\r\n";
93 
94         if (mail.from.addr.length)
95             formattedWrite(buf, "From: %s <%s>%s", mail.from.name,
96                             mail.from.addr, nl);
97 
98         if (mail.recipients.length)
99         {
100             formattedWrite(buf, "To: ");
101             foreach (i, r; mail.recipients)
102             {
103                 formattedWrite(buf, "%s <%s>", r.name, r.addr);
104                 if (i != mail.recipients.length-1) put(buf, ", ");
105             }
106             put(buf, nl);
107         }
108 
109         if (mail.subject.length)
110             formattedWrite(buf, "Subject: %s%s", mail.subject, nl);
111 
112         auto c = mail.date;
113         formattedWrite(buf, "Date: %s, %d %s %d %02d:%02d:%02d%s",
114                     c.dayOfWeek(), c.day, (cast(DateTime)c).month,
115                     c.year, c.hour, c.minute, c.second, nl);
116 
117         if (mail.html)
118             formattedWrite(buf, "Content-Type: text/html; charset=\"UTF-8\"%s", nl);
119         
120         put(buf, nl);
121 
122         put(buf, mail..body.replace("\n", "\r\n"));
123 
124         return buf.data;
125     }
126 }
127 
128 unittest
129 {
130     auto bmb = new BasicMailBuilder;
131     auto ct = SysTime(DateTime(2019, 7, 19, 13, 45, 15));
132 
133     {
134         auto m = Mail(Mail.User("user@example.com", "Sender"),
135                         [Mail.User("r1@e.com", "R1")],
136                         "Some subj", "text\nof\nemail", ct);
137         assert (bmb.build(m) == "From: Sender <user@example.com>\r\n" ~
138             "To: R1 <r1@e.com>\r\nSubject: Some subj\r\n" ~
139             "Date: fri, 19 jul 2019 13:45:15\r\n" ~
140             "\r\ntext\r\nof\r\nemail"
141         );
142     }
143 
144     {
145         auto m = Mail(Mail.User("user@example.com", "Sender"),
146                         [Mail.User("r1@e.com", "R1"),
147                          Mail.User("r2@x.com", "R2")],
148                         "Some subj", "text\nof\nemail", ct);
149         assert (bmb.build(m) == "From: Sender <user@example.com>\r\n" ~
150             "To: R1 <r1@e.com>, R2 <r2@x.com>\r\n" ~
151             "Subject: Some subj\r\nDate: fri, 19 jul 2019 13:45:15\r\n" ~
152             "\r\ntext\r\nof\r\nemail"
153         );
154     }
155 
156     {
157         auto m = Mail(Mail.User("user@example.com", "Sender"), [],
158                         "Some subj", "text\nof\nemail", ct);
159         assert (bmb.build(m) == "From: Sender <user@example.com>\r\n" ~
160             "Subject: Some subj\r\nDate: fri, 19 jul 2019 13:45:15\r\n" ~
161             "\r\ntext\r\nof\r\nemail"
162         );
163     }
164 
165     {
166         auto m = Mail(Mail.User("", "Sender"), [],
167                         "Some subj", "text\nof\nemail", ct);
168         assert (bmb.build(m) == "Subject: Some subj\r\n" ~
169             "Date: fri, 19 jul 2019 13:45:15\r\n" ~
170             "\r\ntext\r\nof\r\nemail"
171         );
172     }
173 
174     {
175         auto m = Mail(Mail.User("", "Sender"), [],
176                         "", "text\nof\nemail", ct);
177         assert (bmb.build(m) == "Date: fri, 19 jul 2019 13:45:15\r\n" ~
178             "\r\ntext\r\nof\r\nemail"
179         );
180     }
181 }
182 
183 ///
184 class MailSender
185 {
186 protected:
187     CURL* curl;
188     bool selfCreatedCURL;
189 
190     MailBuilder mailBuilder;
191     BasicMailBuilder basicMailBuilder;
192 
193 public:
194 
195     ///
196     SMTPSettings settings;
197 
198     /// 
199     bool verbose;
200 
201     ///
202     this(CURL* curl, SMTPSettings sets)
203     {
204         settings = sets;
205         this.curl = curl;
206         basicMailBuilder = new BasicMailBuilder();
207         mailBuilder = basicMailBuilder;
208     }
209 
210     ///
211     this(SMTPSettings sets)
212     {
213         curl = enforce(curl_easy_init(), "fail to init curl");
214         selfCreatedCURL = true;
215         this(curl, sets);
216     }
217 
218     void setMailBuilder(MailBuilder builder)
219     {
220         if (builder) mailBuilder = builder;
221         else mailBuilder = basicMailBuilder;
222     }
223 
224     void cleanup()
225     {
226         if (!selfCreatedCURL) return;
227         curl_easy_cleanup(curl);
228         curl = null;
229     }
230 
231     ~this() { if (curl) cleanup(); }
232 
233     ///
234     void send(Mail mail)
235     {
236         void ces(Arg)(int opt, Arg arg)
237         {
238             static if (is(Arg == string))
239                 checkCurlCall!curl_easy_setopt(curl, opt, arg.toStringz);
240             else
241                 checkCurlCall!curl_easy_setopt(curl, opt, arg);
242         }
243 
244         curl_easy_reset(curl);
245 
246         if (verbose) ces(CurlOption.verbose, 1L);
247 
248         auto secureProto = settings.security == settings.Security.ssl_tls;
249         auto url = format("smtp%s://%s:%s", (secureProto ? "s" : ""),
250                             settings.server, settings.port);
251 
252         ces(CurlOption.url, url);
253 
254         if (settings.security == settings.Security.ssl_tls)
255             ces(CurlOption.use_ssl, CurlUseSSL.all);
256         else if (settings.security == settings.Security.starttls)
257             ces(CurlOption.use_ssl, CurlUseSSL.tryssl);
258 
259         ces(CurlOption.mail_from, mail.from.addr);
260         ces(10_217, settings.username); // mail_auth
261 
262         curl_slist* rcpt_list;
263         foreach (r; mail.recipients)
264             rcpt_list = curl_slist_append(rcpt_list, r.addr.toStringz);
265         scope (exit) curl_slist_free_all(rcpt_list);
266         ces(CurlOption.mail_rcpt, rcpt_list);
267 
268         if (!settings.verifyHost) ces(CurlOption.ssl_verifyhost, 0L);
269         if (!settings.verifyPeer) ces(CurlOption.ssl_verifypeer, 0L);
270 
271         if (settings.auth == settings.Auth.usernameAndPassword)
272         {
273             ces(CurlOption.username, settings.username);
274             ces(CurlOption.password, settings.password);
275         }
276 
277         auto mailBody = mailBuilder.build(mail);
278         ces(CurlOption.readfunction, &readCallback);
279         ces(CurlOption.readdata, &mailBody);
280         ces(CurlOption.upload, 1L);
281 
282         checkCurlCall!curl_easy_perform(curl);
283     }
284 }
285 
286 private
287 {
288     void checkCurlCall(alias fn, Args...)(Args args)
289     {
290         auto r = fn(args);
291         if (r)
292         {
293             auto s = curl_easy_strerror(r).fromStringz.idup;
294             throw new Exception(s);
295         }
296     }
297 
298     extern (C)
299     size_t readCallback(char *buffer, size_t size, size_t nitems, void *instream)
300     {
301         auto data = (cast(string*)instream);
302 
303         const dl = data.length;
304         const ln = size * nitems;
305 
306         auto l = ln < dl ? ln : dl;
307 
308         if (l < 1) return 0;
309 
310         buffer[0..l] = (*data)[0..l];
311         (*data) = (*data)[l..$];
312 
313         return l;
314     }
315 }