remove IP allow list, depend on domain reputation from FCrDNS
[ghsmtp.git] / snd.cpp
blobf166a3f09a676babf8f595fd2d8ba4a6c0c76669
1 // Toy program to send email. This is used to test my SMTP server,
2 // mostly. It's overgrown a bit.
4 #include <gflags/gflags.h>
5 namespace gflags {
6 // in case we didn't have one
9 // This needs to be at least the length of each string it's trying to match.
10 DEFINE_uint64(bfr_size, 4 * 1024, "parser buffer size");
12 DEFINE_bool(selftest, false, "run a self test");
14 DEFINE_bool(pipeline_quit, false, "pipeline the QUIT command");
15 DEFINE_bool(badpipline, false, "send two NOOPs back-to-back");
16 DEFINE_bool(bare_lf, false, "send a bare LF");
17 DEFINE_bool(huge_size, false, "attempt with huge size");
18 DEFINE_bool(long_line, false, "super long text line");
19 DEFINE_bool(noconn, false, "don't connect to any host");
20 DEFINE_bool(noop, false, "send a NOOP right after EHLO");
21 DEFINE_bool(nosend, false, "don't actually send any mail");
22 DEFINE_bool(pipe, false, "send to stdin/stdout");
23 DEFINE_bool(rawdog,
24 false,
25 "send the body exactly as is, don't fix CRLF issues "
26 "or escape leading dots");
27 DEFINE_bool(require_tls, true, "use STARTTLS or die");
28 DEFINE_bool(save, false, "save mail in .Sent");
29 DEFINE_bool(slow_strangle, false, "super slow mo");
30 DEFINE_bool(to_the_neck, false, "shove data forever");
32 DEFINE_bool(use_8bitmime, true, "use 8BITMIME extension");
33 DEFINE_bool(use_binarymime, true, "use BINARYMIME extension");
34 DEFINE_bool(use_chunking, true, "use CHUNKING extension");
35 DEFINE_bool(use_deliverby, false, "use DELIVERBY extension");
36 DEFINE_bool(use_esmtp, true, "use ESMTP (EHLO)");
37 DEFINE_bool(use_pipelining, true, "use PIPELINING extension");
38 DEFINE_bool(use_size, true, "use SIZE extension");
39 DEFINE_bool(use_smtputf8, true, "use SMTPUTF8 extension");
40 DEFINE_bool(use_tls, true, "use STARTTLS extension");
42 // To force it, set if you have UTF8 in the local part of any RFC5321
43 // address.
44 DEFINE_bool(force_smtputf8, false, "force SMTPUTF8 extension");
46 DEFINE_string(sender, "", "FQDN of sending node");
48 DEFINE_string(local_address, "", "local address to bind");
49 DEFINE_string(mx_host, "", "FQDN of receiving node");
50 DEFINE_string(service, "smtp-test", "service name");
51 DEFINE_string(client_id, "", "client name (ID) for EHLO/HELO");
53 DEFINE_string(from, "", "RFC5322 From: address");
54 DEFINE_string(from_name, "", "RFC5322 From: name");
56 DEFINE_string(to, "", "RFC5322 To: address");
57 DEFINE_string(to_name, "", "RFC5322 To: name");
59 DEFINE_string(smtp_from, "", "RFC5321 MAIL FROM address");
60 DEFINE_string(smtp_to, "", "RFC5321 RCPT TO address");
62 DEFINE_string(content_type, "", "RFC5322 Content-Type");
63 DEFINE_string(content_transfer_encoding,
64 "",
65 "RFC5322 Content-Transfer-Encoding");
67 DEFINE_string(subject, "testing one, two, three...", "RFC5322 Subject");
68 DEFINE_string(keywords, "", "RFC5322 Keywords: header");
69 DEFINE_string(references, "", "RFC5322 References: header");
70 DEFINE_string(in_reply_to, "", "RFC5322 In-Reply-To: header");
71 DEFINE_string(reply_to, "", "RFC5322 Reply-To: header");
72 DEFINE_string(reply_2, "", "Second RFC5322 Reply-To: header");
74 DEFINE_bool(4, false, "use only IP version 4");
75 DEFINE_bool(6, false, "use only IP version 6");
77 DEFINE_string(username, "", "AUTH username");
78 DEFINE_string(password, "", "AUTH password");
80 DEFINE_bool(use_dkim, true, "sign with DKIM");
81 DEFINE_string(selector, "ghsmtp", "DKIM selector");
82 DEFINE_string(dkim_key_file, "", "DKIM key file");
84 #include "Base64.hpp"
85 #include "DNS-fcrdns.hpp"
86 #include "DNS.hpp"
87 #include "Domain.hpp"
88 #include "IP4.hpp"
89 #include "IP6.hpp"
90 #include "Magic.hpp"
91 #include "Mailbox.hpp"
92 #include "MessageStore.hpp"
93 #include "Now.hpp"
94 #include "OpenDKIM.hpp"
95 #include "Pill.hpp"
96 #include "Sock.hpp"
97 #include "fs.hpp"
98 #include "imemstream.hpp"
99 #include "osutil.hpp"
100 #include "sa.hpp"
102 #include <algorithm>
103 #include <fstream>
104 #include <functional>
105 #include <iomanip>
106 #include <iostream>
107 #include <iterator>
108 #include <random>
109 #include <string>
110 #include <string_view>
111 #include <unordered_map>
113 #include <netdb.h>
114 #include <sys/socket.h>
115 #include <sys/types.h>
117 #include <fmt/format.h>
118 #include <fmt/ostream.h>
120 #include <boost/algorithm/string/case_conv.hpp>
122 #include <boost/iostreams/device/mapped_file.hpp>
124 #include <tao/pegtl.hpp>
125 #include <tao/pegtl/contrib/abnf.hpp>
127 using namespace tao::pegtl;
128 using namespace tao::pegtl::abnf;
130 using namespace std::string_literals;
132 namespace Config {
133 constexpr auto read_timeout = std::chrono::minutes(24 * 60);
134 constexpr auto write_timeout = std::chrono::minutes(24 * 60);
135 } // namespace Config
137 // clang-format off
139 namespace chars {
140 struct tail : range<'\x80', '\xBF'> {};
142 struct ch_1 : range<'\x00', '\x7F'> {};
144 struct ch_2 : seq<range<'\xC2', '\xDF'>, tail> {};
146 struct ch_3 : sor<seq<one<'\xE0'>, range<'\xA0', '\xBF'>, tail>,
147 seq<range<'\xE1', '\xEC'>, rep<2, tail>>,
148 seq<one<'\xED'>, range<'\x80', '\x9F'>, tail>,
149 seq<range<'\xEE', '\xEF'>, rep<2, tail>>> {};
151 struct ch_4 : sor<seq<one<'\xF0'>, range<'\x90', '\xBF'>, rep<2, tail>>,
152 seq<range<'\xF1', '\xF3'>, rep<3, tail>>,
153 seq<one<'\xF4'>, range<'\x80', '\x8F'>, rep<2, tail>>> {};
155 struct u8char : sor<ch_1, ch_2, ch_3, ch_4> {};
157 struct non_ascii : sor<ch_2, ch_3, ch_4> {};
159 struct ascii_only : seq<star<ch_1>, eof> {};
161 struct utf8_only : seq<star<u8char>, eof> {};
164 namespace RFC5322 {
166 struct VUCHAR : sor<VCHAR, chars::non_ascii> {};
168 using dot = one<'.'>;
169 using colon = one<':'>;
171 // All 7-bit ASCII except NUL (0), LF (10) and CR (13).
172 struct text_ascii : ranges<1, 9, 11, 12, 14, 127> {};
174 // Short lines of ASCII text. LF or CRLF line separators.
175 struct body_ascii : seq<star<seq<rep_max<998, text_ascii>, eol>>,
176 opt<rep_max<998, text_ascii>>, eof> {};
178 struct text_utf8 : sor<text_ascii, chars::non_ascii> {};
180 // Short lines of UTF-8 text. LF or CRLF line separators.
181 struct body_utf8 : seq<star<seq<rep_max<998, text_utf8>, eol>>,
182 opt<rep_max<998, text_utf8>>, eof> {};
184 struct FWS : seq<opt<seq<star<WSP>, eol>>, plus<WSP>> {};
186 struct qtext : sor<one<33>, ranges<35, 91, 93, 126>, chars::non_ascii> {};
188 struct quoted_pair : seq<one<'\\'>, sor<VUCHAR, WSP>> {};
190 struct atext : sor<ALPHA, DIGIT,
191 one<'!', '#',
192 '$', '%',
193 '&', '\'',
194 '*', '+',
195 '-', '/',
196 '=', '?',
197 '^', '_',
198 '`', '{',
199 '|', '}',
200 '~'>,
201 chars::non_ascii> {};
203 // ctext is ASCII not '(' or ')' or '\\'
204 struct ctext : sor<ranges<33, 39, 42, 91, 93, 126>, chars::non_ascii> {};
206 struct comment;
208 struct ccontent : sor<ctext, quoted_pair, comment> {};
210 struct comment
211 : seq<one<'('>, star<seq<opt<FWS>, ccontent>>, opt<FWS>, one<')'>> {};
213 struct CFWS : sor<seq<plus<seq<opt<FWS>, comment>, opt<FWS>>>, FWS> {};
215 struct qcontent : sor<qtext, quoted_pair> {};
217 // Corrected in errata ID: 3135
218 struct quoted_string
219 : seq<opt<CFWS>,
220 DQUOTE,
221 sor<seq<star<seq<opt<FWS>, qcontent>>, opt<FWS>>, FWS>,
222 DQUOTE,
223 opt<CFWS>> {};
225 // *([FWS] VCHAR) *WSP
226 struct unstructured : seq<star<seq<opt<FWS>, VUCHAR>>, star<WSP>> {};
228 struct atom : seq<opt<CFWS>, plus<atext>, opt<CFWS>> {};
230 struct dot_atom_text : list<plus<atext>, dot> {};
232 struct dot_atom : seq<opt<CFWS>, dot_atom_text, opt<CFWS>> {};
234 struct word : sor<atom, quoted_string> {};
236 struct phrase : plus<word> {};
238 struct local_part : sor<dot_atom, quoted_string> {};
240 // from '!' to '~' excluding 91 92 93 '[' '\\' ']'
242 struct dtext : ranges<33, 90, 94, 126> {};
244 struct domain_literal : seq<opt<CFWS>,
245 one<'['>,
246 star<seq<opt<FWS>, dtext>>,
247 opt<FWS>,
248 one<']'>,
249 opt<CFWS>> {};
251 struct domain : sor<dot_atom, domain_literal> {};
253 struct addr_spec : seq<local_part, one<'@'>, domain> {};
255 struct postmaster : TAO_PEGTL_ISTRING("Postmaster") {};
257 struct addr_spec_or_postmaster : sor<addr_spec, postmaster> {};
259 struct addr_spec_only : seq<addr_spec_or_postmaster, eof> {};
261 struct display_name : phrase {};
263 struct display_name_only : seq<display_name, eof> {};
265 // clang-format on
267 // struct name_addr : seq<opt<display_name>, angle_addr> {};
269 // struct mailbox : sor<name_addr, addr_spec> {};
271 template <typename Rule>
272 struct inaction : nothing<Rule> {
275 template <typename Rule>
276 struct action : nothing<Rule> {
279 template <>
280 struct action<local_part> {
281 template <typename Input>
282 static void apply(Input const& in, Mailbox& mbx)
284 mbx.set_local(in.string());
288 template <>
289 struct action<domain> {
290 template <typename Input>
291 static void apply(Input const& in, Mailbox& mbx)
293 mbx.set_domain(in.string());
296 } // namespace RFC5322
298 namespace RFC5321 {
300 struct Connection {
301 Sock sock;
303 std::string server_id;
305 std::string ehlo_keyword;
306 std::vector<std::string> ehlo_param;
307 std::unordered_map<std::string, std::vector<std::string>> ehlo_params;
309 std::string reply_code;
311 bool greeting_ok{false};
312 bool ehlo_ok{false};
314 bool has_extension(char const* name) const
316 return ehlo_params.find(name) != end(ehlo_params);
319 Connection(int fd_in, int fd_out, std::function<void(void)> read_hook)
320 : sock(
321 fd_in, fd_out, read_hook, Config::read_timeout, Config::write_timeout)
326 // clang-format off
328 using dot = one<'.'>;
329 using colon = one<':'>;
330 using dash = one<'-'>;
331 using underscore = one<'_'>;
333 struct u_let_dig : sor<ALPHA, DIGIT, chars::non_ascii> {};
335 struct u_ldh_tail : star<sor<seq<plus<one<'-'>>, u_let_dig>, u_let_dig>> {};
337 struct u_label : seq<u_let_dig, u_ldh_tail> {};
339 struct let_dig : sor<ALPHA, DIGIT> {};
341 struct ldh_tail : star<sor<seq<plus<one<'-'>>, let_dig>, let_dig>> {};
343 struct ldh_str : seq<let_dig, ldh_tail> {};
345 struct label : ldh_str {};
347 struct sub_domain : sor<label, u_label> {};
349 struct domain : list<sub_domain, dot> {};
351 struct dec_octet : sor<seq<string<'2','5'>, range<'0','5'>>,
352 seq<one<'2'>, range<'0','4'>, DIGIT>,
353 seq<range<'0', '1'>, rep<2, DIGIT>>,
354 rep_min_max<1, 2, DIGIT>> {};
356 struct IPv4_address_literal
357 : seq<dec_octet, dot, dec_octet, dot, dec_octet, dot, dec_octet> {};
359 struct h16 : rep_min_max<1, 4, HEXDIG> {};
361 struct ls32 : sor<seq<h16, colon, h16>, IPv4_address_literal> {};
363 struct dcolon : two<':'> {};
365 struct IPv6address : sor<seq< rep<6, h16, colon>, ls32>,
366 seq< dcolon, rep<5, h16, colon>, ls32>,
367 seq<opt<h16 >, dcolon, rep<4, h16, colon>, ls32>,
368 seq<opt<h16, opt< colon, h16>>, dcolon, rep<3, h16, colon>, ls32>,
369 seq<opt<h16, rep_opt<2, colon, h16>>, dcolon, rep<2, h16, colon>, ls32>,
370 seq<opt<h16, rep_opt<3, colon, h16>>, dcolon, h16, colon, ls32>,
371 seq<opt<h16, rep_opt<4, colon, h16>>, dcolon, ls32>,
372 seq<opt<h16, rep_opt<5, colon, h16>>, dcolon, h16>,
373 seq<opt<h16, rep_opt<6, colon, h16>>, dcolon >> {};
375 struct IPv6_address_literal : seq<TAO_PEGTL_ISTRING("IPv6:"), IPv6address> {};
377 struct dcontent : ranges<33, 90, 94, 126> {};
379 struct standardized_tag : ldh_str {};
381 struct general_address_literal : seq<standardized_tag, colon, plus<dcontent>> {};
383 // See rfc 5321 Section 4.1.3
384 struct address_literal : seq<one<'['>,
385 sor<IPv4_address_literal,
386 IPv6_address_literal,
387 general_address_literal>,
388 one<']'>> {};
391 struct qtextSMTP : sor<ranges<32, 33, 35, 91, 93, 126>, chars::non_ascii> {};
392 struct graphic : range<32, 126> {};
393 struct quoted_pairSMTP : seq<one<'\\'>, graphic> {};
394 struct qcontentSMTP : sor<qtextSMTP, quoted_pairSMTP> {};
396 // excluded from atext: "(),.@[]"
397 struct atext : sor<ALPHA, DIGIT,
398 one<'!', '#',
399 '$', '%',
400 '&', '\'',
401 '*', '+',
402 '-', '/',
403 '=', '?',
404 '^', '_',
405 '`', '{',
406 '|', '}',
407 '~'>,
408 chars::non_ascii> {};
409 struct atom : plus<atext> {};
410 struct dot_string : list<atom, dot> {};
411 struct quoted_string : seq<one<'"'>, star<qcontentSMTP>, one<'"'>> {};
412 struct local_part : sor<dot_string, quoted_string> {};
413 struct non_local_part : sor<domain, address_literal> {};
414 struct mailbox : seq<local_part, one<'@'>, non_local_part> {};
416 struct at_domain : seq<one<'@'>, domain> {};
418 struct a_d_l : list<at_domain, one<','>> {};
420 struct path : seq<opt<seq<a_d_l, colon>>, mailbox> {};
422 struct path_only : seq<path, eof> {};
424 // textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
426 // Although not explicit in the grammar of RFC-6531, in practice UTF-8
427 // is used in the replys.
429 // struct textstring : plus<sor<one<9>, range<32, 126>>> {};
431 struct textstring : plus<sor<one<9>, range<32, 126>, chars::non_ascii>> {};
433 struct server_id : sor<domain, address_literal> {};
435 // Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF )
436 // /
437 // ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
438 // *( "220-" [ textstring ] CRLF )
439 // "220 " [ textstring ] CRLF )
441 struct greeting_ok
442 : sor<seq<TAO_PEGTL_ISTRING("220 "), server_id, opt<textstring>, CRLF>,
443 seq<TAO_PEGTL_ISTRING("220-"), server_id, opt<textstring>, CRLF,
444 star<seq<TAO_PEGTL_ISTRING("220-"), opt<textstring>, CRLF>>,
445 seq<TAO_PEGTL_ISTRING("220 "), opt<textstring>, CRLF>>> {};
447 // Reply-code = %x32-35 %x30-35 %x30-39
449 struct reply_code
450 : seq<range<0x32, 0x35>, range<0x30, 0x35>, range<0x30, 0x39>> {};
452 // Reply-line = *( Reply-code "-" [ textstring ] CRLF )
453 // Reply-code [ SP textstring ] CRLF
455 struct reply_lines
456 : seq<star<seq<reply_code, one<'-'>, opt<textstring>, CRLF>>,
457 seq<reply_code, opt<seq<SP, textstring>>, CRLF>> {};
459 struct greeting
460 : sor<greeting_ok, reply_lines> {};
462 // ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
463 // ; string of any characters other than CR or LF
465 struct ehlo_greet : plus<ranges<0, 9, 11, 12, 14, 127>> {};
467 // ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
468 // ; additional syntax of ehlo-params depends on
469 // ; ehlo-keyword
471 // The '.' we also allow in ehlo-keyword since it has been seen in the
472 // wild at least at 263.net.
474 struct ehlo_keyword : seq<sor<ALPHA, DIGIT>, star<sor<ALPHA, DIGIT, dash, dot, underscore>>> {};
476 // ehlo-param = 1*(%d33-126)
477 // ; any CHAR excluding <SP> and all
478 // ; control characters (US-ASCII 0-31 and 127
479 // ; inclusive)
481 struct ehlo_param : plus<range<33, 126>> {};
483 // ehlo-line = ehlo-keyword *( SP ehlo-param )
485 // The AUTH= thing is so common with some servers (postfix) that I
486 // guess we have to accept it.
488 struct ehlo_line
489 : seq<ehlo_keyword, star<seq<sor<SP,one<'='>>, ehlo_param>>> {};
491 // ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF )
492 // /
493 // ( "250-" Domain [ SP ehlo-greet ] CRLF
494 // *( "250-" ehlo-line CRLF )
495 // "250 " ehlo-line CRLF )
497 // The last line having the optional ehlo_line is not strictly correct.
498 // Was added to work with postfix/src/smtpstone/smtp-sink.c.
500 struct ehlo_ok_rsp
501 : sor<seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF>,
503 seq<TAO_PEGTL_ISTRING("250-"), server_id, opt<ehlo_greet>, CRLF,
504 star<seq<TAO_PEGTL_ISTRING("250-"), ehlo_line, CRLF>>,
505 seq<TAO_PEGTL_ISTRING("250 "), opt<ehlo_line>, CRLF>>
506 > {};
508 struct ehlo_rsp
509 : sor<ehlo_ok_rsp, reply_lines> {};
511 struct helo_ok_rsp
512 : seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF> {};
514 struct auth_login_username
515 : seq<TAO_PEGTL_STRING("334 VXNlcm5hbWU6"), CRLF> {};
517 struct auth_login_password
518 : seq<TAO_PEGTL_STRING("334 UGFzc3dvcmQ6"), CRLF> {};
520 // clang-format on
522 template <typename Rule>
523 struct inaction : nothing<Rule> {
526 template <typename Rule>
527 struct action : nothing<Rule> {
530 template <>
531 struct action<server_id> {
532 template <typename Input>
533 static void apply(Input const& in, Connection& cnn)
535 cnn.server_id = in.string();
539 template <>
540 struct action<local_part> {
541 template <typename Input>
542 static void apply(Input const& in, Mailbox& mbx)
544 mbx.set_local(in.string());
548 template <>
549 struct action<non_local_part> {
550 template <typename Input>
551 static void apply(Input const& in, Mailbox& mbx)
553 mbx.set_domain(in.string());
557 template <>
558 struct action<greeting_ok> {
559 template <typename Input>
560 static void apply(Input const& in, Connection& cnn)
562 cnn.greeting_ok = true;
563 imemstream stream{begin(in), size(in)};
564 std::string line;
565 while (std::getline(stream, line)) {
566 LOG(INFO) << " S: " << line;
571 template <>
572 struct action<ehlo_ok_rsp> {
573 template <typename Input>
574 static void apply(Input const& in, Connection& cnn)
576 cnn.ehlo_ok = true;
577 imemstream stream{begin(in), size(in)};
578 std::string line;
579 while (std::getline(stream, line)) {
580 LOG(INFO) << " S: " << line;
585 template <>
586 struct action<ehlo_keyword> {
587 template <typename Input>
588 static void apply(Input const& in, Connection& cnn)
590 cnn.ehlo_keyword = in.string();
591 boost::to_upper(cnn.ehlo_keyword);
595 template <>
596 struct action<ehlo_param> {
597 template <typename Input>
598 static void apply(Input const& in, Connection& cnn)
600 cnn.ehlo_param.push_back(in.string());
604 template <>
605 struct action<ehlo_line> {
606 template <typename Input>
607 static void apply(Input const& in, Connection& cnn)
609 cnn.ehlo_params.emplace(std::move(cnn.ehlo_keyword),
610 std::move(cnn.ehlo_param));
614 template <>
615 struct action<reply_lines> {
616 template <typename Input>
617 static void apply(Input const& in, Connection& cnn)
619 imemstream stream{begin(in), size(in)};
620 std::string line;
621 while (std::getline(stream, line)) {
622 LOG(INFO) << " S: " << line;
627 template <>
628 struct action<reply_code> {
629 template <typename Input>
630 static void apply(Input const& in, Connection& cnn)
632 cnn.reply_code = in.string();
635 } // namespace RFC5321
637 namespace {
639 int conn(DNS::Resolver& res, Domain const& node, uint16_t port)
641 auto const use_4{!FLAGS_6};
642 auto const use_6{!FLAGS_4};
644 if (use_6) {
645 auto const fd{socket(AF_INET6, SOCK_STREAM, 0)};
646 PCHECK(fd >= 0) << "socket() failed";
648 if (!FLAGS_local_address.empty()) {
649 auto loc{sockaddr_in6{}};
650 loc.sin6_family = AF_INET6;
651 if (1 != inet_pton(AF_INET6, FLAGS_local_address.c_str(),
652 reinterpret_cast<void*>(&loc.sin6_addr))) {
653 LOG(FATAL) << "can't interpret " << FLAGS_local_address
654 << " as IPv6 address";
656 PCHECK(0 == bind(fd, reinterpret_cast<sockaddr*>(&loc), sizeof(loc)));
659 auto addrs{std::vector<std::string>{}};
661 if (node.is_address_literal()) {
662 if (IP6::is_address(node.ascii())) {
663 addrs.push_back(node.ascii());
665 if (IP6::is_address_literal(node.ascii())) {
666 auto const addr = IP6::as_address(node.ascii());
667 addrs.push_back(std::string(addr.data(), addr.length()));
670 else {
671 addrs = res.get_strings(DNS::RR_type::AAAA, node.ascii());
673 for (auto const& addr : addrs) {
674 auto in6{sockaddr_in6{}};
675 in6.sin6_family = AF_INET6;
676 in6.sin6_port = htons(port);
677 CHECK_EQ(inet_pton(AF_INET6, addr.c_str(),
678 reinterpret_cast<void*>(&in6.sin6_addr)),
680 if (connect(fd, reinterpret_cast<const sockaddr*>(&in6), sizeof(in6))) {
681 PLOG(WARNING) << "connect failed [" << addr << "]:" << port;
682 continue;
685 LOG(INFO) << fd << " connected to [" << addr << "]:" << port;
686 return fd;
689 close(fd);
691 if (use_4) {
692 auto fd{socket(AF_INET, SOCK_STREAM, 0)};
693 PCHECK(fd >= 0) << "socket() failed";
695 if (!FLAGS_local_address.empty()) {
696 auto loc{sockaddr_in{}};
697 loc.sin_family = AF_INET;
698 if (1 != inet_pton(AF_INET, FLAGS_local_address.c_str(),
699 reinterpret_cast<void*>(&loc.sin_addr))) {
700 LOG(FATAL) << "can't interpret " << FLAGS_local_address
701 << " as IPv4 address";
703 PCHECK(0 == bind(fd, reinterpret_cast<sockaddr*>(&loc), sizeof(loc)));
706 auto addrs{std::vector<std::string>{}};
707 if (node.is_address_literal()) {
708 if (IP4::is_address(node.ascii())) {
709 addrs.push_back(node.ascii());
711 if (IP4::is_address_literal(node.ascii())) {
712 auto const addr = IP4::as_address(node.ascii());
713 addrs.push_back(std::string(addr.data(), addr.length()));
716 else {
717 addrs = res.get_strings(DNS::RR_type::A, node.ascii());
719 for (auto addr : addrs) {
720 auto in4{sockaddr_in{}};
721 in4.sin_family = AF_INET;
722 in4.sin_port = htons(port);
723 CHECK_EQ(inet_pton(AF_INET, addr.c_str(),
724 reinterpret_cast<void*>(&in4.sin_addr)),
726 if (connect(fd, reinterpret_cast<const sockaddr*>(&in4), sizeof(in4))) {
727 PLOG(WARNING) << "connect failed " << addr << ":" << port;
728 continue;
731 LOG(INFO) << " connected to " << addr << ":" << port;
732 return fd;
735 close(fd);
738 return -1;
741 class Eml {
742 public:
743 void add_hdr(std::string name, std::string value)
745 hdrs_.push_back(std::make_pair(name, value));
748 void foreach_hdr(std::function<void(std::string const& name,
749 std::string const& value)> func)
751 for (auto const& [name, value] : hdrs_) {
752 func(name, value);
756 private:
757 std::vector<std::pair<std::string, std::string>> hdrs_;
759 friend std::ostream& operator<<(std::ostream& os, Eml const& eml)
761 for (auto const& [name, value] : eml.hdrs_) {
762 os << name << ": " << value << "\r\n";
764 return os << "\r\n"; // end of headers
768 // // clang-format off
769 // char const* const signhdrs[] = {
770 // "From",
772 // "Message-ID",
774 // "Cc",
775 // "Date",
776 // "In-Reply-To",
777 // "References",
778 // "Reply-To",
779 // "Sender",
780 // "Subject",
781 // "To",
783 // "MIME-Version",
784 // "Content-Type",
785 // "Content-Transfer-Encoding",
787 // nullptr
788 // };
789 // clang-format on
791 enum class transfer_encoding {
792 seven_bit,
793 quoted_printable,
794 base64,
795 eight_bit,
796 binary,
799 enum class data_type {
800 ascii, // 7bit, quoted-printable and base64
801 utf8, // 8bit
802 binary, // binary
805 data_type type(std::string_view d)
808 auto in{memory_input<>{d.data(), d.size(), "data"}};
809 if (parse<RFC5322::body_ascii>(in)) {
810 return data_type::ascii;
814 auto in{memory_input<>{d.data(), d.size(), "data"}};
815 if (parse<RFC5322::body_utf8>(in)) {
816 return data_type::utf8;
819 // anything else is
820 return data_type::binary;
823 class content {
824 public:
825 content(char const* path)
826 : path_(path)
828 auto const body_sz{fs::file_size(path_)};
829 CHECK(body_sz) << "no body";
830 file_.open(path_);
831 type_ = ::type(*this);
834 char const* data() const { return file_.data(); }
835 size_t size() const { return file_.size(); }
836 data_type type() const { return type_; }
838 bool empty() const { return size() == 0; }
839 operator std::string_view() const { return std::string_view(data(), size()); }
841 private:
842 data_type type_;
843 fs::path path_;
844 boost::iostreams::mapped_file_source file_;
847 template <typename Input>
848 void fail(Input& in, RFC5321::Connection& cnn)
850 LOG(INFO) << " C: QUIT";
851 cnn.sock.out() << "QUIT\r\n" << std::flush;
852 // we might have a few error replies stacked up if we're pipelining
853 // CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
854 exit(EXIT_FAILURE);
857 template <typename Input>
858 void check_for_fail(Input& in, RFC5321::Connection& cnn, std::string_view cmd)
860 cnn.sock.out() << std::flush;
861 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
862 if (cnn.reply_code.at(0) != '2') {
863 LOG(ERROR) << cmd << " returned " << cnn.reply_code;
864 fail(in, cnn);
866 in.discard();
869 bool validate_name(const char* flagname, std::string const& value)
871 if (value.empty()) // empty name needs to validate, but
872 return true; // will not be used
873 memory_input<> name_in(value.c_str(), "name");
874 if (!parse<RFC5322::display_name_only, RFC5322::inaction>(name_in)) {
875 LOG(ERROR) << "bad name syntax " << value;
876 return false;
878 return true;
881 DEFINE_validator(from_name, &validate_name);
882 DEFINE_validator(to_name, &validate_name);
884 bool validate_address_RFC5322(const char* flagname, std::string const& value)
886 if (value.empty()) // empty name needs to validate, but
887 return true; // will not be used
888 memory_input<> name_in(value.c_str(), "address");
889 if (!parse<RFC5322::addr_spec_only, RFC5322::inaction>(name_in)) {
890 LOG(ERROR) << "bad address syntax " << value;
891 return false;
893 return true;
896 DEFINE_validator(from, &validate_address_RFC5322);
897 DEFINE_validator(to, &validate_address_RFC5322);
899 bool validate_address_RFC5321(const char* flagname, std::string const& value)
901 if (value.empty()) // empty name needs to validate, but
902 return true; // will not be used
903 memory_input<> name_in(value.c_str(), "path");
904 if (!parse<RFC5321::path_only, RFC5321::inaction>(name_in)) {
905 LOG(ERROR) << "bad address syntax " << value;
906 return false;
908 return true;
911 DEFINE_validator(smtp_from, &validate_address_RFC5321);
912 DEFINE_validator(smtp_to, &validate_address_RFC5321);
914 void selftest()
916 CHECK(validate_name("selftest", ""s));
917 CHECK(validate_name("selftest", "Elmer J Fudd"s));
918 CHECK(validate_name("selftest", "\"Elmer J. Fudd\""s));
919 CHECK(validate_name("selftest", "Elmer! J! Fudd!"s));
921 CHECK(validate_address_RFC5321("selftest", "foo@digilicious.com"s));
922 CHECK(validate_address_RFC5321("selftest", "\"foo\"@digilicious.com"s));
923 CHECK(validate_address_RFC5321(
924 "selftest",
925 "\"very.(),:;<>[]\\\".VERY.\\\"very@\\\\ \\\"very\\\".unusual\"@digilicious.com"s));
927 CHECK(validate_address_RFC5322("selftest", "foo@digilicious.com"s));
928 CHECK(validate_address_RFC5322("selftest", "\"foo\"@digilicious.com"s));
929 CHECK(validate_address_RFC5322(
930 "selftest",
931 "\"very.(),:;<>[]\\\".VERY.\\\"very@\\\\ \\\"very\\\".unusual\"@digilicious.com"s));
933 auto const read_hook{[]() {}};
935 const char* greet_list[]{
936 "220-mtaig-aak03.mx.aol.com ESMTP Internet Inbound\r\n"
937 "220-AOL and its affiliated companies do not\r\n"
938 "220-authorize the use of its proprietary computers and computer\r\n"
939 "220-networks to accept, transmit, or distribute unsolicited bulk\r\n"
940 "220-e-mail sent from the internet.\r\n"
941 "220-Effective immediately:\r\n"
942 "220-AOL may no longer accept connections from IP addresses\r\n"
943 "220 which no do not have reverse-DNS (PTR records) assigned.\r\n",
945 "421 mtaig-maa02.mx.aol.com Service unavailable - try again later\r\n",
948 for (auto i : greet_list) {
949 auto cnn{RFC5321::Connection(0, 1, read_hook)};
950 auto in{memory_input<>{i, i}};
951 if (!parse<RFC5321::greeting, RFC5321::action /*, tao::pegtl::tracer*/>(
952 in, cnn)) {
953 LOG(FATAL) << "Error parsing greeting \"" << i << "\"";
955 if (cnn.greeting_ok) {
956 LOG(WARNING) << "greeting ok";
958 else {
959 LOG(WARNING) << "greeting was not in the affirmative";
963 const char* ehlo_rsp_list[]{
964 "250-www77.totaalholding.nl Hello "
965 "ec2-18-205-224-193.compute-1.amazonaws.com [18.205.224.193]\r\n"
966 "250-SIZE 52428800\r\n"
967 "250-8BITMIME\r\n"
968 "250-PIPELINING\r\n"
969 "250-X_PIPE_CONNECT\r\n"
970 "250-STARTTLS\r\n"
971 "250 HELP\r\n",
973 "250-HELLO, SAILOR!\r\n"
974 "250-NO-SOLICITING\r\n"
975 "250 8BITMIME\r\n",
977 "250-digilicious.com at your service, localhost. [IPv6:::1]\r\n"
978 "250-SIZE 15728640\r\n"
979 "250-8BITMIME\r\n"
980 "250-STARTTLS\r\n"
981 "250-ENHANCEDSTATUSCODES\r\n"
982 "250-PIPELINING\r\n"
983 "250-BINARYMIME\r\n"
984 "250-CHUNKING\r\n"
985 "250 SMTPUTF8\r\n",
987 "500 5.5.1 command unrecognized: \"EHLO digilicious.com\\r\\n\"\r\n",
989 "250-263xmail at your service\r\n"
990 "250-STARTTLS\r\n"
991 "250-MAE-SMTP\r\n"
992 "250-263.net\r\n" // the '.' is not RFC complaint
993 "250-SIZE 104857600\r\n"
994 "250-ETRN\r\n"
995 "250-ENHANCEDSTATUSCODES\r\n"
996 "250-8BITMIME\r\n"
997 "250 DSN\r\n",
1000 for (auto i : ehlo_rsp_list) {
1001 auto cnn{RFC5321::Connection(0, 1, read_hook)};
1002 auto in{memory_input<>{i, i}};
1003 if (!parse<RFC5321::ehlo_rsp, RFC5321::action /*, tao::pegtl::tracer*/>(
1004 in, cnn)) {
1005 LOG(FATAL) << "Error parsing ehlo response \"" << i << "\"";
1007 if (cnn.ehlo_ok) {
1008 LOG(WARNING) << "ehlo ok";
1010 else {
1011 LOG(WARNING) << "ehlo response was not in the affirmative";
1016 auto get_sender()
1018 if (FLAGS_sender.empty()) {
1019 FLAGS_sender = {[] {
1020 if (!FLAGS_client_id.empty())
1021 return FLAGS_client_id;
1023 auto const id_from_env{getenv("GHSMTP_CLIENT_ID")};
1024 if (id_from_env)
1025 return std::string{id_from_env};
1027 auto const hostname{osutil::get_hostname()};
1028 if (hostname.find('.') != std::string::npos)
1029 return hostname;
1031 LOG(FATAL) << "can't determine my client ID, set GHSMTP_CLIENT_ID maybe";
1032 }()};
1035 auto const sender{Domain{FLAGS_sender}};
1037 if (FLAGS_from.empty()) {
1038 FLAGS_from = "test-it@"s + sender.utf8();
1041 if (FLAGS_to.empty()) {
1042 FLAGS_to = "test-it@"s + sender.utf8();
1045 return sender;
1048 bool is_localhost(DNS::RR const& rr)
1050 if (std::holds_alternative<DNS::RR_MX>(rr)) {
1051 if (iequal(std::get<DNS::RR_MX>(rr).exchange(), "localhost"))
1052 return true;
1054 return false;
1057 bool starts_with(std::string_view str, std::string_view prefix)
1059 if (str.size() >= prefix.size())
1060 if (str.compare(0, prefix.size(), prefix) == 0)
1061 return true;
1062 return false;
1065 bool sts_rec(std::string const& sts_rec)
1067 return starts_with(sts_rec, "v=STSv1");
1070 std::vector<Domain>
1071 get_receivers(DNS::Resolver& res, Mailbox const& to_mbx, bool& enforce_dane)
1073 auto receivers{std::vector<Domain>{}};
1075 // User provided explicit host to receive mail.
1076 if (!FLAGS_mx_host.empty()) {
1077 receivers.emplace_back(FLAGS_mx_host);
1078 return receivers;
1081 // Non-local part is an address literal.
1082 if (to_mbx.domain().is_address_literal()) {
1083 receivers.emplace_back(to_mbx.domain());
1084 return receivers;
1087 // RFC 5321 section 5.1 "Locating the Target Host"
1089 // “The lookup first attempts to locate an MX record associated with
1090 // the name. If a CNAME record is found, the resulting name is
1091 // processed as if it were the initial name.”
1093 // Our (full) resolver will traverse any CNAMEs for us and return
1094 // the CNAME and MX records all together.
1096 auto const& domain = to_mbx.domain().ascii();
1098 auto q_sts{DNS::Query{res, DNS::RR_type::TXT, "_mta-sts."s + domain}};
1099 if (q_sts.has_record()) {
1100 auto sts_records = q_sts.get_strings();
1101 sts_records.erase(std::remove_if(begin(sts_records), end(sts_records),
1102 std::not_fn(sts_rec)),
1103 end(sts_records));
1104 if (size(sts_records) == 1) {
1105 LOG(INFO) << "### This domain implements MTA-STS ###";
1108 else {
1109 LOG(INFO) << "MTA-STS record not found for domain " << domain;
1112 auto q{DNS::Query{res, DNS::RR_type::MX, domain}};
1113 if (q.has_record()) {
1114 if (q.authentic_data()) {
1115 LOG(INFO) << "### MX records authentic for domain " << domain << " ###";
1117 else {
1118 LOG(INFO) << "MX records can't be authenticated for domain " << domain;
1119 enforce_dane = false;
1122 auto mxs{q.get_records()};
1124 mxs.erase(std::remove_if(begin(mxs), end(mxs), is_localhost), end(mxs));
1126 auto const nmx = std::count_if(begin(mxs), end(mxs), [](auto const& rr) {
1127 return std::holds_alternative<DNS::RR_MX>(rr);
1130 if (nmx == 1) {
1131 for (auto const& mx : mxs) {
1132 if (std::holds_alternative<DNS::RR_MX>(mx)) {
1133 // RFC 7505 null MX record
1134 if ((std::get<DNS::RR_MX>(mx).preference() == 0) &&
1135 (std::get<DNS::RR_MX>(mx).exchange().empty() ||
1136 (std::get<DNS::RR_MX>(mx).exchange() == "."))) {
1137 LOG(INFO) << "domain " << domain << " does not accept mail";
1138 return receivers;
1144 if (nmx == 0) {
1145 // implicit MX RR
1146 receivers.emplace_back(domain);
1147 return receivers;
1150 // […] then the sender-SMTP MUST randomize them to spread the load
1151 // across multiple mail exchangers for a specific organization.
1152 std::shuffle(begin(mxs), end(mxs), std::random_device());
1153 std::sort(begin(mxs), end(mxs), [](auto const& a, auto const& b) {
1154 if (std::holds_alternative<DNS::RR_MX>(a) &&
1155 std::holds_alternative<DNS::RR_MX>(b)) {
1156 return std::get<DNS::RR_MX>(a).preference() <
1157 std::get<DNS::RR_MX>(b).preference();
1159 return false;
1162 if (nmx)
1163 LOG(INFO) << "MXs for " << domain << " are:";
1165 for (auto const& mx : mxs) {
1166 if (std::holds_alternative<DNS::RR_MX>(mx)) {
1167 receivers.emplace_back(std::get<DNS::RR_MX>(mx).exchange());
1168 LOG(INFO) << std::setfill(' ') << std::setw(3)
1169 << std::get<DNS::RR_MX>(mx).preference() << " "
1170 << std::get<DNS::RR_MX>(mx).exchange();
1174 return receivers;
1177 auto parse_mailboxes()
1179 auto from_mbx{Mailbox{}};
1180 auto from_in{memory_input<>{FLAGS_from, "from"}};
1181 if (!parse<RFC5322::addr_spec_only, RFC5322::action>(from_in, from_mbx)) {
1182 LOG(FATAL) << "bad From: address syntax <" << FLAGS_from << ">";
1184 LOG(INFO) << " from_mbx == " << from_mbx;
1186 auto local_from{memory_input<>{from_mbx.local_part(), "from.local"}};
1187 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_from);
1189 auto to_mbx{Mailbox{}};
1190 auto to_in{memory_input<>{FLAGS_to, "to"}};
1191 if (!parse<RFC5322::addr_spec_only, RFC5322::action>(to_in, to_mbx)) {
1192 LOG(FATAL) << "bad To: address syntax <" << FLAGS_to << ">";
1194 LOG(INFO) << " to_mbx == " << to_mbx;
1196 auto local_to{memory_input<>{to_mbx.local_part(), "to.local"}};
1197 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_to);
1199 auto smtp_from_mbx{Mailbox{}};
1200 if (!FLAGS_smtp_from.empty()) {
1201 auto smtp_from_in{memory_input<>{FLAGS_smtp_from, "SMTP.from"}};
1202 if (!parse<RFC5321::path_only, RFC5321::action>(smtp_from_in,
1203 smtp_from_mbx)) {
1204 LOG(FATAL) << "bad MAIL FROM: address syntax <" << FLAGS_smtp_from << ">";
1206 LOG(INFO) << " smtp_from_mbx == " << smtp_from_mbx;
1207 auto local_smtp_from{
1208 memory_input<>{smtp_from_mbx.local_part(), "SMTP.from.local"}};
1209 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_smtp_from);
1212 auto smtp_to_mbx{Mailbox{}};
1213 if (!FLAGS_smtp_to.empty()) {
1214 auto smtp_to_in{memory_input<>{FLAGS_smtp_to, "SMTP.to"}};
1215 if (!parse<RFC5321::path_only, RFC5321::action>(smtp_to_in, smtp_to_mbx)) {
1216 LOG(FATAL) << "bad RCPT TO: address syntax <" << FLAGS_smtp_to << ">";
1218 LOG(INFO) << " smtp_to_mbx == " << smtp_to_mbx;
1220 auto local_smtp_to{
1221 memory_input<>{smtp_to_mbx.local_part(), "SMTP.to.local"}};
1222 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_smtp_to);
1225 return std::tuple(from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx);
1228 auto create_eml(Domain const& sender,
1229 std::string const& from,
1230 std::string const& to,
1231 std::vector<content> const& bodies,
1232 bool ext_smtputf8)
1234 auto eml{Eml{}};
1235 auto const date{Now{}};
1236 auto const pill{Pill{}};
1238 auto mid_str = fmt::format("<{}.{}@{}>", date.sec(), pill, sender.ascii());
1239 eml.add_hdr("Message-ID", mid_str.c_str());
1240 eml.add_hdr("Date", date.c_str());
1242 if (!FLAGS_from_name.empty())
1243 eml.add_hdr("From", fmt::format("{} <{}>", FLAGS_from_name, from));
1244 else
1245 eml.add_hdr("From", from);
1247 if (!FLAGS_to_name.empty())
1248 eml.add_hdr("To", fmt::format("{} <{}>", FLAGS_to_name, to));
1249 else
1250 eml.add_hdr("To", to);
1252 eml.add_hdr("Subject", FLAGS_subject);
1254 if (!FLAGS_keywords.empty())
1255 eml.add_hdr("Keywords", FLAGS_keywords);
1257 if (!FLAGS_references.empty())
1258 eml.add_hdr("References", FLAGS_references);
1260 if (!FLAGS_in_reply_to.empty())
1261 eml.add_hdr("In-Reply-To", FLAGS_in_reply_to);
1263 if (!FLAGS_reply_to.empty())
1264 eml.add_hdr("Reply-To", FLAGS_reply_to);
1266 if (!FLAGS_reply_2.empty())
1267 eml.add_hdr("Reply-To", FLAGS_reply_2);
1269 eml.add_hdr("MIME-Version", "1.0");
1270 eml.add_hdr("Content-Language", "en-US");
1272 auto magic{Magic{}}; // to ID buffer contents
1274 if (!FLAGS_content_type.empty()) {
1275 eml.add_hdr("Content-Type", FLAGS_content_type);
1277 else {
1278 eml.add_hdr("Content-Type", magic.buffer(bodies[0]));
1281 if (!FLAGS_content_transfer_encoding.empty()) {
1282 eml.add_hdr("Content-Transfer-Encoding", FLAGS_content_transfer_encoding);
1285 return eml;
1288 void sign_eml(Eml& eml,
1289 std::string const& from_dom,
1290 std::vector<content> const& bodies)
1292 auto const body_type = (bodies[0].type() == data_type::binary)
1293 ? OpenDKIM::sign::body_type::binary
1294 : OpenDKIM::sign::body_type::text;
1296 auto const key_file = FLAGS_dkim_key_file.empty()
1297 ? (FLAGS_selector + ".private")
1298 : FLAGS_dkim_key_file;
1299 std::ifstream keyfs(key_file.c_str());
1300 CHECK(keyfs.good()) << "can't access " << key_file;
1301 std::string key(std::istreambuf_iterator<char>{keyfs}, {});
1302 OpenDKIM::sign dks(key.c_str(), FLAGS_selector.c_str(), from_dom.c_str(),
1303 body_type);
1304 eml.foreach_hdr([&dks](std::string const& name, std::string const& value) {
1305 auto const header = name + ": "s + value;
1306 dks.header(header.c_str());
1308 dks.eoh();
1309 for (auto const& body : bodies) {
1310 dks.body(body);
1312 dks.eom();
1313 eml.add_hdr("DKIM-Signature"s, dks.getsighdr());
1316 template <typename Input>
1317 void do_auth(Input& in, RFC5321::Connection& cnn)
1319 if (FLAGS_username.empty() && FLAGS_password.empty())
1320 return;
1322 auto const auth = cnn.ehlo_params.find("AUTH");
1323 if (auth == end(cnn.ehlo_params)) {
1324 LOG(ERROR) << "server doesn't support AUTH";
1325 fail(in, cnn);
1328 // Perfer PLAIN mechanism.
1329 if (std::find(begin(auth->second), end(auth->second), "PLAIN") !=
1330 end(auth->second)) {
1331 LOG(INFO) << "C: AUTH PLAIN";
1332 auto const tok = fmt::format("\0{}\0{}"s, FLAGS_username, FLAGS_password);
1333 cnn.sock.out() << "AUTH PLAIN " << Base64::enc(tok) << "\r\n" << std::flush;
1334 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1335 if (cnn.reply_code != "235") {
1336 LOG(ERROR) << "AUTH PLAIN returned " << cnn.reply_code;
1337 fail(in, cnn);
1340 // The LOGIN SASL mechanism is obsolete.
1341 else if (std::find(begin(auth->second), end(auth->second), "LOGIN") !=
1342 end(auth->second)) {
1343 LOG(INFO) << "C: AUTH LOGIN";
1344 cnn.sock.out() << "AUTH LOGIN\r\n" << std::flush;
1345 CHECK((parse<RFC5321::auth_login_username>(in)));
1346 cnn.sock.out() << Base64::enc(FLAGS_username) << "\r\n" << std::flush;
1347 CHECK((parse<RFC5321::auth_login_password>(in)));
1348 cnn.sock.out() << Base64::enc(FLAGS_password) << "\r\n" << std::flush;
1349 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1350 if (cnn.reply_code != "235") {
1351 LOG(ERROR) << "AUTH LOGIN returned " << cnn.reply_code;
1352 fail(in, cnn);
1355 else {
1356 LOG(ERROR) << "server doesn't support AUTH methods PLAIN or LOGIN";
1357 fail(in, cnn);
1361 // Do various bad things during the DATA transfer.
1363 template <typename Input>
1364 void bad_daddy(Input& in, RFC5321::Connection& cnn)
1366 LOG(INFO) << "C: DATA";
1367 cnn.sock.out() << "DATA\r\n";
1368 cnn.sock.out() << std::flush;
1370 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1371 if (cnn.reply_code != "354") {
1372 LOG(ERROR) << "DATA returned " << cnn.reply_code;
1373 fail(in, cnn);
1376 // cnn.sock.out() << "\r\nThis ->\n<- is a bare LF!\r\n";
1377 if (FLAGS_bare_lf)
1378 cnn.sock.out() << "\n.\n\r\n";
1380 if (FLAGS_to_the_neck) {
1381 for (;;) {
1382 cnn.sock.out() << "####################"
1383 "####################"
1384 "####################"
1385 << std::flush;
1389 if (FLAGS_long_line) {
1390 for (auto i{0}; i < 10000; ++i) {
1391 cnn.sock.out() << 'X';
1393 cnn.sock.out() << "\r\n" << std::flush;
1396 while (FLAGS_slow_strangle) {
1397 for (auto i{0}; i < 500; ++i) {
1398 cnn.sock.out() << 'X' << std::flush;
1399 sleep(1);
1401 cnn.sock.out() << "\r\n";
1404 // Done!
1405 cnn.sock.out() << ".\r\n" << std::flush;
1406 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1408 LOG(INFO) << "reply_code == " << cnn.reply_code;
1409 CHECK_EQ(cnn.reply_code.at(0), '2');
1411 LOG(INFO) << "C: QUIT";
1412 cnn.sock.out() << "QUIT\r\n" << std::flush;
1413 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1416 bool snd(fs::path config_path,
1417 int fd_in,
1418 int fd_out,
1419 Domain const& sender,
1420 Domain const& receiver,
1421 DNS::RR_collection const& tlsa_rrs,
1422 bool enforce_dane,
1423 Mailbox const& from_mbx,
1424 Mailbox const& to_mbx,
1425 Mailbox const& smtp_from_mbx,
1426 Mailbox const& smtp_to_mbx,
1427 std::vector<content> const& bodies)
1429 auto constexpr read_hook{[]() {}};
1431 auto cnn{RFC5321::Connection(fd_in, fd_out, read_hook)};
1433 auto in{
1434 istream_input<eol::crlf, 1>{cnn.sock.in(), FLAGS_bfr_size, "session"}};
1435 if (!parse<RFC5321::greeting, RFC5321::action>(in, cnn)) {
1436 LOG(WARNING) << "can't parse greeting";
1437 return false;
1440 if (!cnn.greeting_ok) {
1441 LOG(WARNING) << "greeting was not in the affirmative, skipping";
1442 return false;
1445 // try EHLO/HELO
1447 if (FLAGS_use_esmtp) {
1448 LOG(INFO) << "C: EHLO " << sender.ascii();
1450 if (FLAGS_slow_strangle) {
1451 auto ehlo_str = fmt::format("EHLO {}\r\n", sender.ascii());
1452 for (auto ch : ehlo_str) {
1453 cnn.sock.out() << ch << std::flush;
1454 sleep(1);
1457 else {
1458 cnn.sock.out() << "EHLO " << sender.ascii() << "\r\n" << std::flush;
1461 CHECK((parse<RFC5321::ehlo_rsp, RFC5321::action>(in, cnn)));
1462 if (!cnn.ehlo_ok) {
1464 if (FLAGS_force_smtputf8) {
1465 LOG(WARNING) << "ehlo response was not in the affirmative, skipping";
1466 return false;
1469 LOG(WARNING) << "ehlo response was not in the affirmative, trying HELO";
1470 FLAGS_use_esmtp = false;
1474 if (!FLAGS_use_esmtp) {
1475 LOG(INFO) << "C: HELO " << sender.ascii();
1476 cnn.sock.out() << "HELO " << sender.ascii() << "\r\n" << std::flush;
1477 if (!parse<RFC5321::helo_ok_rsp, RFC5321::action>(in, cnn)) {
1478 LOG(WARNING) << "HELO didn't work, skipping";
1479 return false;
1483 // Check extensions
1485 auto bad_dad = FLAGS_bare_lf || FLAGS_slow_strangle || FLAGS_to_the_neck;
1487 if (bad_dad) {
1488 FLAGS_use_chunking = false;
1489 FLAGS_use_size = false;
1492 auto const ext_8bitmime{FLAGS_use_8bitmime && cnn.has_extension("8BITMIME")};
1494 auto const ext_chunking{FLAGS_use_chunking && cnn.has_extension("CHUNKING")};
1496 auto const ext_binarymime{FLAGS_use_binarymime && ext_chunking &&
1497 cnn.has_extension("BINARYMIME")};
1499 auto const ext_deliverby{FLAGS_use_deliverby &&
1500 cnn.has_extension("DELIVERBY")};
1502 auto const ext_pipelining{FLAGS_use_pipelining &&
1503 cnn.has_extension("PIPELINING")};
1505 auto const ext_size{FLAGS_use_size && cnn.has_extension("SIZE")};
1507 auto const ext_smtputf8{FLAGS_use_smtputf8 && cnn.has_extension("SMTPUTF8")};
1509 auto const ext_starttls{FLAGS_use_tls && cnn.has_extension("STARTTLS")};
1511 if (FLAGS_force_smtputf8 && !ext_smtputf8) {
1512 LOG(WARNING) << "no SMTPUTF8, skipping";
1513 return false;
1516 if (ext_starttls) {
1517 LOG(INFO) << "C: STARTTLS";
1518 cnn.sock.out() << "STARTTLS\r\n" << std::flush;
1519 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1521 // LOG(INFO) << "cnn.sock.starttls_client(" << receiver.ascii() << ");";
1522 cnn.sock.starttls_client(config_path, sender.ascii().c_str(),
1523 receiver.ascii().c_str(), tlsa_rrs, enforce_dane);
1525 LOG(INFO) << "TLS: " << cnn.sock.tls_info();
1527 LOG(INFO) << "C: EHLO " << sender.ascii();
1528 cnn.sock.out() << "EHLO " << sender.ascii() << "\r\n" << std::flush;
1529 CHECK((parse<RFC5321::ehlo_rsp, RFC5321::action>(in, cnn)));
1531 else if (FLAGS_require_tls) {
1532 LOG(ERROR) << "No TLS extension, won't send mail in plain text without "
1533 "--require_tls=false.";
1534 LOG(INFO) << "C: QUIT";
1535 cnn.sock.out() << "QUIT\r\n" << std::flush;
1536 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1537 exit(EXIT_FAILURE);
1540 if (FLAGS_noop) {
1541 LOG(INFO) << "C: NOOP";
1542 cnn.sock.out() << "NOOP\r\n" << std::flush;
1545 if (receiver != cnn.server_id) {
1546 LOG(INFO) << "server identifies as " << cnn.server_id;
1549 if (FLAGS_force_smtputf8 && !ext_smtputf8) {
1550 LOG(WARNING) << "does not support SMTPUTF8";
1551 return false;
1554 if (ext_smtputf8 && !ext_8bitmime) {
1555 LOG(ERROR)
1556 << "SMTPUTF8 requires 8BITMIME, see RFC-6531 section 3.1 item 8.";
1557 LOG(INFO) << "C: QUIT";
1558 cnn.sock.out() << "QUIT\r\n" << std::flush;
1559 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1560 exit(EXIT_FAILURE);
1563 auto max_msg_size{0u};
1564 if (ext_size) {
1565 if (!cnn.ehlo_params["SIZE"].empty()) {
1566 char* ep = nullptr;
1567 max_msg_size = strtoul(cnn.ehlo_params["SIZE"][0].c_str(), &ep, 10);
1568 if (ep && (*ep != '\0')) {
1569 LOG(WARNING) << "garbage in SIZE argument: "
1570 << cnn.ehlo_params["SIZE"][0];
1575 auto deliver_by_min{0u};
1576 if (ext_deliverby) {
1577 if (!cnn.ehlo_params["DELIVERBY"].empty()) {
1578 char* ep = nullptr;
1579 deliver_by_min =
1580 strtoul(cnn.ehlo_params["DELIVERBY"][0].c_str(), &ep, 10);
1581 if (ep && (*ep != '\0')) {
1582 LOG(WARNING) << "garbage in DELIVERBY argument: "
1583 << cnn.ehlo_params["DELIVERBY"][0];
1587 do_auth(in, cnn);
1589 in.discard();
1591 auto enc = FLAGS_force_smtputf8 ? Mailbox::domain_encoding::utf8
1592 : Mailbox::domain_encoding::ascii;
1594 std::string from =
1595 from_mbx.empty() ? smtp_from_mbx.as_string(enc) : from_mbx.as_string(enc);
1596 std::string to =
1597 to_mbx.empty() ? smtp_to_mbx.as_string(enc) : to_mbx.as_string(enc);
1599 auto eml{create_eml(sender, from, to, bodies, ext_smtputf8)};
1601 if (FLAGS_use_dkim) {
1602 auto const dom = Mailbox(from).domain().ascii();
1603 sign_eml(eml, dom.c_str(), bodies);
1606 // Get the header as one big string
1607 std::ostringstream hdr_stream;
1608 hdr_stream << eml;
1609 auto const& hdr_str = hdr_stream.str();
1611 // In the case of DATA style transfer, this total_size number is an
1612 // *estimate* only, as line endings may be translated or added
1613 // during transfer. In the BDAT case, this number must be exact.
1615 auto total_size = hdr_str.size();
1616 for (auto const& body : bodies)
1617 total_size += body.size();
1619 if (ext_size && max_msg_size && (total_size > max_msg_size)) {
1620 LOG(ERROR) << "message size " << total_size << " exceeds size limit of "
1621 << max_msg_size;
1622 LOG(INFO) << "C: QUIT";
1623 cnn.sock.out() << "QUIT\r\n" << std::flush;
1624 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1625 exit(EXIT_FAILURE);
1628 std::ostringstream param_stream;
1629 if (FLAGS_huge_size && ext_size) {
1630 // Claim some huge size.
1631 param_stream << " SIZE=" << std::numeric_limits<std::streamsize>::max();
1633 else if (ext_size) {
1634 param_stream << " SIZE=" << total_size;
1637 if (ext_binarymime) {
1638 param_stream << " BODY=BINARYMIME";
1640 else if (ext_8bitmime) {
1641 param_stream << " BODY=8BITMIME";
1644 if (ext_deliverby) {
1645 param_stream << " BY=1200;NT";
1648 if (ext_smtputf8) {
1649 param_stream << " SMTPUTF8";
1652 if (FLAGS_badpipline) {
1653 LOG(INFO) << "C: NOOP NOOP";
1654 cnn.sock.out() << "NOOP\r\nNOOP\r\n" << std::flush;
1657 auto param_str = param_stream.str();
1659 LOG(INFO) << "C: MAIL FROM:<" << from << '>' << param_str;
1660 cnn.sock.out() << "MAIL FROM:<" << from << '>' << param_str << "\r\n";
1661 if (!ext_pipelining) {
1662 check_for_fail(in, cnn, "MAIL FROM");
1665 LOG(INFO) << "C: RCPT TO:<" << to << ">";
1666 cnn.sock.out() << "RCPT TO:<" << to << ">\r\n";
1667 if (!ext_pipelining) {
1668 check_for_fail(in, cnn, "RCPT TO");
1671 if (FLAGS_nosend) {
1672 LOG(INFO) << "C: QUIT";
1673 cnn.sock.out() << "QUIT\r\n" << std::flush;
1674 if (ext_pipelining) {
1675 check_for_fail(in, cnn, "MAIL FROM");
1676 check_for_fail(in, cnn, "RCPT TO");
1678 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1679 LOG(INFO) << "no-sending";
1680 exit(EXIT_SUCCESS);
1683 if (bad_dad) {
1684 if (ext_pipelining) {
1685 cnn.sock.out() << std::flush;
1686 check_for_fail(in, cnn, "MAIL FROM");
1687 check_for_fail(in, cnn, "RCPT TO");
1689 bad_daddy(in, cnn);
1690 return true;
1693 auto msg = std::make_unique<MessageStore>();
1695 // if service is smtp (i.e. sending real mail, not smtp-test)
1696 try {
1697 if (FLAGS_save) {
1698 msg->open(sender.ascii(), total_size * 2, ".Sent");
1699 msg->write(hdr_str.data(), hdr_str.size());
1700 for (auto const& body : bodies) {
1701 msg->write(body.data(), body.size());
1705 catch (std::system_error const& e) {
1706 switch (errno) {
1707 case ENOSPC:
1708 msg->trash();
1709 msg.reset();
1710 LOG(FATAL) << "no space";
1712 default:
1713 msg->trash();
1714 msg.reset();
1715 LOG(ERROR) << "errno==" << errno << ": " << strerror(errno);
1716 LOG(FATAL) << e.what();
1719 catch (std::exception const& e) {
1720 msg->trash();
1721 msg.reset();
1722 LOG(FATAL) << e.what();
1725 if (ext_chunking) {
1726 std::ostringstream bdat_stream;
1727 bdat_stream << "BDAT " << total_size << " LAST";
1728 LOG(INFO) << "C: " << bdat_stream.str();
1730 cnn.sock.out() << bdat_stream.str() << "\r\n";
1731 cnn.sock.out().write(hdr_str.data(), hdr_str.size());
1732 CHECK(cnn.sock.out().good());
1734 for (auto const& body : bodies) {
1735 cnn.sock.out().write(body.data(), body.size());
1736 CHECK(cnn.sock.out().good());
1739 if (FLAGS_pipeline_quit) {
1740 LOG(INFO) << "C: QUIT";
1741 cnn.sock.out() << "QUIT\r\n" << std::flush;
1744 cnn.sock.out() << std::flush;
1745 CHECK(cnn.sock.out().good());
1747 // NOW check returns
1748 if (ext_pipelining) {
1749 check_for_fail(in, cnn, "MAIL FROM");
1750 check_for_fail(in, cnn, "RCPT TO");
1753 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1754 if (cnn.reply_code != "250") {
1755 LOG(ERROR) << "BDAT returned " << cnn.reply_code;
1756 fail(in, cnn);
1759 else {
1760 LOG(INFO) << "C: DATA";
1761 cnn.sock.out() << "DATA\r\n";
1763 // NOW check returns
1764 if (ext_pipelining) {
1765 check_for_fail(in, cnn, "MAIL FROM");
1766 check_for_fail(in, cnn, "RCPT TO");
1768 cnn.sock.out() << std::flush;
1769 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1770 if (cnn.reply_code != "354") {
1771 LOG(ERROR) << "DATA returned " << cnn.reply_code;
1772 fail(in, cnn);
1775 cnn.sock.out() << eml;
1777 for (auto const& body : bodies) {
1778 auto lineno = 0;
1779 auto line{std::string{}};
1780 auto isbody{imemstream{body.data(), body.size()}};
1781 while (std::getline(isbody, line)) {
1782 ++lineno;
1783 if (!cnn.sock.out().good()) {
1784 cnn.sock.log_stats();
1785 LOG(FATAL) << "output no good at line " << lineno;
1787 if (FLAGS_rawdog) {
1788 // This adds a final newline at the end of the file, if no
1789 // line ending was present.
1790 cnn.sock.out() << line << '\n';
1792 else {
1793 // This code converts single LF line endings into CRLF.
1794 // This code does nothing to fix single CR characters not
1795 // part of a CRLF pair.
1797 // This loop adds a CRLF and the end of the transmission if
1798 // the file doesn't already end with one. This is a
1799 // requirement of the SMTP DATA protocol.
1801 if (line.length() && (line.at(0) == '.')) {
1802 cnn.sock.out() << '.';
1804 cnn.sock.out() << line;
1805 if (line.length() && line.back() != '\r')
1806 cnn.sock.out() << '\r';
1807 cnn.sock.out() << '\n';
1811 CHECK(cnn.sock.out().good());
1813 // Done!
1814 if (FLAGS_pipeline_quit) {
1815 LOG(INFO) << "C: QUIT";
1816 cnn.sock.out() << ".\r\nQUIT\r\n" << std::flush;
1818 else {
1819 cnn.sock.out() << ".\r\n" << std::flush;
1821 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1823 if (cnn.reply_code.at(0) == '2') {
1824 if (FLAGS_save) {
1825 msg->deliver();
1827 else {
1828 msg->trash();
1830 LOG(INFO) << "mail was sent successfully";
1832 in.discard();
1834 if (!FLAGS_pipeline_quit) {
1835 LOG(INFO) << "C: QUIT";
1836 cnn.sock.out() << "QUIT\r\n" << std::flush;
1838 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1840 return true;
1843 DNS::RR_collection
1844 get_tlsa_rrs(DNS::Resolver& res, Domain const& domain, uint16_t port)
1846 auto const tlsa = fmt::format("_{}._tcp.{}", port, domain.ascii());
1848 DNS::Query q(res, DNS::RR_type::TLSA, tlsa);
1850 if (q.nx_domain()) {
1851 LOG(INFO) << "TLSA data not found for " << domain << ':' << port;
1854 if (q.bogus_or_indeterminate()) {
1855 LOG(WARNING) << "TLSA data is bogus or indeterminate";
1858 auto tlsa_rrs = q.get_records();
1859 if (!tlsa_rrs.empty()) {
1860 LOG(INFO) << "### TLSA data found for " << domain << ':' << port << " ###";
1863 return tlsa_rrs;
1865 } // namespace
1867 int main(int argc, char* argv[])
1869 std::ios::sync_with_stdio(false);
1871 { // Need to work with either namespace.
1872 using namespace gflags;
1873 using namespace google;
1874 ParseCommandLineFlags(&argc, &argv, true);
1877 auto const config_path = osutil::get_config_dir();
1879 auto sender = get_sender();
1881 if (FLAGS_selftest) {
1882 selftest();
1883 return 0;
1886 auto bodies{std::vector<content>{}};
1887 for (int a = 1; a < argc; ++a) {
1888 if (!fs::exists(argv[a]))
1889 LOG(FATAL) << "can't find mail body part " << argv[a];
1890 bodies.push_back(argv[a]);
1893 if (argc == 1)
1894 bodies.push_back("body.txt");
1896 CHECK_EQ(bodies.size(), 1) << "only one body part for now";
1897 CHECK(!(FLAGS_4 && FLAGS_6)) << "must use /some/ IP version";
1899 if (FLAGS_force_smtputf8)
1900 FLAGS_use_smtputf8 = true;
1902 auto&& [from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx] = parse_mailboxes();
1904 if (to_mbx.domain().empty() && FLAGS_mx_host.empty()) {
1905 LOG(ERROR) << "don't know who to send this mail to";
1906 return 0;
1909 auto const port{osutil::get_port(FLAGS_service.c_str(), "tcp")};
1911 auto res{DNS::Resolver{config_path}};
1912 auto tlsa_rrs{get_tlsa_rrs(res, to_mbx.domain(), port)};
1914 if (FLAGS_pipe) {
1915 return snd(config_path, STDIN_FILENO, STDOUT_FILENO, sender,
1916 to_mbx.domain(), tlsa_rrs, false, from_mbx, to_mbx,
1917 smtp_from_mbx, smtp_to_mbx, bodies)
1918 ? EXIT_SUCCESS
1919 : EXIT_FAILURE;
1922 bool enforce_dane = true;
1923 auto const receivers = get_receivers(res, to_mbx, enforce_dane);
1925 if (receivers.empty()) {
1926 LOG(INFO) << "no place to send this mail";
1927 return EXIT_SUCCESS;
1930 for (auto const& receiver : receivers) {
1931 LOG(INFO) << "trying " << receiver << ":" << FLAGS_service;
1933 if (FLAGS_noconn) {
1934 LOG(INFO) << "skipping";
1935 continue;
1938 auto fd = conn(res, receiver, port);
1939 if (fd == -1) {
1940 LOG(WARNING) << "no connection, skipping";
1941 continue;
1944 // Get our local IP address as "us".
1946 sa::sockaddrs us_addr{};
1947 socklen_t us_addr_len{sizeof us_addr};
1948 char us_addr_str[INET6_ADDRSTRLEN]{'\0'};
1949 std::vector<std::string> fcrdns;
1950 bool private_addr = false;
1952 if (-1 == getsockname(fd, &us_addr.addr, &us_addr_len)) {
1953 // Ignore ENOTSOCK errors from getsockname, useful for testing.
1954 PLOG_IF(WARNING, ENOTSOCK != errno) << "getsockname failed";
1956 else {
1957 switch (us_addr_len) {
1958 case sizeof(sockaddr_in):
1959 PCHECK(inet_ntop(AF_INET, &us_addr.addr_in.sin_addr, us_addr_str,
1960 sizeof us_addr_str) != nullptr);
1961 if (IP4::is_private(us_addr_str))
1962 private_addr = true;
1963 else
1964 fcrdns = DNS::fcrdns4(res, us_addr_str);
1965 break;
1967 case sizeof(sockaddr_in6):
1968 PCHECK(inet_ntop(AF_INET6, &us_addr.addr_in6.sin6_addr, us_addr_str,
1969 sizeof us_addr_str) != nullptr);
1970 if (IP6::is_private(us_addr_str))
1971 private_addr = true;
1972 else
1973 fcrdns = DNS::fcrdns6(res, us_addr_str);
1974 break;
1976 default:
1977 LOG(FATAL) << "bogus address length (" << us_addr_len
1978 << ") returned from getsockname";
1982 if (fcrdns.size()) {
1983 LOG(INFO) << "our names are:";
1984 for (auto const& fc : fcrdns) {
1985 LOG(INFO) << " " << fc;
1989 if (!private_addr) {
1990 // look at from_mbx.domain() and get SPF records
1992 // also look at our ID to get SPF records.
1995 if (to_mbx.domain() == receiver) {
1996 if (snd(config_path, fd, fd, sender, receiver, tlsa_rrs, enforce_dane,
1997 from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx, bodies)) {
1998 return EXIT_SUCCESS;
2001 else {
2002 auto tlsa_rrs_mx{get_tlsa_rrs(res, receiver, port)};
2003 tlsa_rrs_mx.insert(end(tlsa_rrs_mx), begin(tlsa_rrs), end(tlsa_rrs));
2004 if (snd(config_path, fd, fd, sender, receiver, tlsa_rrs_mx, enforce_dane,
2005 from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx, bodies)) {
2006 return EXIT_SUCCESS;
2010 close(fd);
2013 LOG(ERROR) << "we ran out of hosts to try";