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 }