don't "domain block" address literals
[ghsmtp.git] / snd.cpp
blob49c6da4b8061f9c21527e11ee010819692961622
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 #include <range/v3/numeric/accumulate.hpp>
11 namespace rng = ranges;
13 DEFINE_uint64(reps, 1, "now many duplicate transactions per connection");
15 // This needs to be at least the length of each string it's trying to match.
16 DEFINE_uint64(bfr_size, 4 * 1024, "parser buffer size");
18 DEFINE_bool(selftest, false, "run a self test");
20 DEFINE_bool(pipeline_quit, false, "pipeline the QUIT command");
21 DEFINE_bool(badpipline, false, "send two NOOPs back-to-back");
22 DEFINE_bool(bare_lf, false, "send a bare LF");
23 DEFINE_bool(huge_size, false, "attempt with huge size");
24 DEFINE_bool(long_line, false, "super long text line");
25 DEFINE_bool(noconn, false, "don't connect to any host");
26 DEFINE_bool(noop, false, "send a NOOP right after EHLO");
27 DEFINE_bool(nosend, false, "don't actually send any mail");
28 DEFINE_bool(pipe, false, "send to stdin/stdout");
29 DEFINE_bool(rawmsg, false, "the body file includes the headers");
30 DEFINE_bool(rawdog,
31 false,
32 "send the body exactly as is, don't fix CRLF issues "
33 "or escape leading dots");
34 DEFINE_bool(require_tls, true, "use STARTTLS or die");
35 DEFINE_bool(save, false, "save mail in .Sent");
36 DEFINE_bool(slow_strangle, false, "super slow mo");
37 DEFINE_bool(to_the_neck, false, "shove data forever");
39 DEFINE_bool(use_8bitmime, true, "use 8BITMIME extension");
40 DEFINE_bool(use_binarymime, true, "use BINARYMIME extension");
41 DEFINE_bool(use_chunking, true, "use CHUNKING extension");
42 DEFINE_bool(use_deliverby, false, "use DELIVERBY extension");
43 DEFINE_bool(use_esmtp, true, "use ESMTP (EHLO)");
44 DEFINE_bool(use_pipelining, true, "use PIPELINING extension");
45 DEFINE_bool(use_prdr, true, "use PRDR extension");
46 DEFINE_bool(use_size, true, "use SIZE extension");
47 DEFINE_bool(use_smtputf8, true, "use SMTPUTF8 extension");
48 DEFINE_bool(use_tls, true, "use STARTTLS extension");
50 // To force it, set if you have UTF8 in the local part of any RFC5321
51 // address.
52 DEFINE_bool(force_smtputf8, false, "force SMTPUTF8 extension");
54 DEFINE_string(sender, "", "FQDN of sending node");
56 DEFINE_string(local_address, "", "local address to bind");
57 DEFINE_string(mx_host, "", "FQDN of receiving node");
58 DEFINE_string(service, "smtp-test", "service name");
59 DEFINE_string(client_id, "", "client name (ID) for EHLO/HELO");
61 DEFINE_string(from, "", "RFC5322 From: address");
62 DEFINE_string(from_name, "", "RFC5322 From: name");
64 DEFINE_string(to, "", "RFC5322 To: address");
65 DEFINE_string(to_name, "", "RFC5322 To: name");
67 DEFINE_string(smtp_from, "", "RFC5321 MAIL FROM address");
68 DEFINE_string(smtp_to, "", "RFC5321 RCPT TO address");
69 DEFINE_string(smtp_to2, "", "second RFC5321 RCPT TO address");
70 DEFINE_string(smtp_to3, "", "third RFC5321 RCPT TO address");
72 DEFINE_string(content_type, "", "RFC5322 Content-Type");
73 DEFINE_string(content_transfer_encoding,
74 "",
75 "RFC5322 Content-Transfer-Encoding");
77 DEFINE_string(subject, "testing one, two, three...", "RFC5322 Subject");
78 DEFINE_string(keywords, "", "RFC5322 Keywords: header");
79 DEFINE_string(references, "", "RFC5322 References: header");
80 DEFINE_string(in_reply_to, "", "RFC5322 In-Reply-To: header");
81 DEFINE_string(reply_to, "", "RFC5322 Reply-To: header");
82 DEFINE_string(reply_2, "", "Second RFC5322 Reply-To: header");
84 DEFINE_bool(4, false, "use only IP version 4");
85 DEFINE_bool(6, false, "use only IP version 6");
87 DEFINE_string(username, "", "AUTH username");
88 DEFINE_string(password, "", "AUTH password");
90 DEFINE_bool(use_dkim, true, "sign with DKIM");
91 DEFINE_bool(bogus_dkim, false, "sign with bogus DKIM");
92 DEFINE_string(selector, "ghsmtp", "DKIM selector");
93 DEFINE_string(dkim_key_file, "", "DKIM key file");
95 #include "Base64.hpp"
96 #include "DNS-fcrdns.hpp"
97 #include "DNS.hpp"
98 #include "Domain.hpp"
99 #include "IP4.hpp"
100 #include "IP6.hpp"
101 #include "Magic.hpp"
102 #include "Mailbox.hpp"
103 #include "MessageStore.hpp"
104 #include "Now.hpp"
105 #include "OpenDKIM.hpp"
106 #include "Pill.hpp"
107 #include "Sock.hpp"
108 #include "fs.hpp"
109 #include "imemstream.hpp"
110 #include "osutil.hpp"
111 #include "sa.hpp"
113 #include <algorithm>
114 #include <fstream>
115 #include <functional>
116 #include <iomanip>
117 #include <iostream>
118 #include <iterator>
119 #include <random>
120 #include <string>
121 #include <string_view>
122 #include <unordered_map>
124 #include <netdb.h>
125 #include <sys/socket.h>
126 #include <sys/types.h>
128 #include <fmt/format.h>
129 #include <fmt/ostream.h>
131 #include <boost/algorithm/string/case_conv.hpp>
133 #include <boost/iostreams/device/mapped_file.hpp>
135 #include <tao/pegtl.hpp>
136 #include <tao/pegtl/contrib/abnf.hpp>
138 using namespace tao::pegtl;
139 using namespace tao::pegtl::abnf;
141 using namespace std::string_literals;
143 namespace Config {
144 constexpr auto read_timeout = std::chrono::minutes(24 * 60);
145 constexpr auto write_timeout = std::chrono::minutes(24 * 60);
146 } // namespace Config
148 // clang-format off
150 namespace chars {
151 struct tail : range<'\x80', '\xBF'> {};
153 struct ch_1 : range<'\x00', '\x7F'> {};
155 struct ch_2 : seq<range<'\xC2', '\xDF'>, tail> {};
157 struct ch_3 : sor<seq<one<'\xE0'>, range<'\xA0', '\xBF'>, tail>,
158 seq<range<'\xE1', '\xEC'>, rep<2, tail>>,
159 seq<one<'\xED'>, range<'\x80', '\x9F'>, tail>,
160 seq<range<'\xEE', '\xEF'>, rep<2, tail>>> {};
162 struct ch_4 : sor<seq<one<'\xF0'>, range<'\x90', '\xBF'>, rep<2, tail>>,
163 seq<range<'\xF1', '\xF3'>, rep<3, tail>>,
164 seq<one<'\xF4'>, range<'\x80', '\x8F'>, rep<2, tail>>> {};
166 struct u8char : sor<ch_1, ch_2, ch_3, ch_4> {};
168 struct non_ascii : sor<ch_2, ch_3, ch_4> {};
170 struct ascii_only : seq<star<ch_1>, eof> {};
172 struct utf8_only : seq<star<u8char>, eof> {};
175 namespace RFC5322 {
177 struct VUCHAR : sor<VCHAR, chars::non_ascii> {};
179 using dot = one<'.'>;
180 using colon = one<':'>;
182 // All 7-bit ASCII except NUL (0), LF (10) and CR (13).
183 struct text_ascii : tao::pegtl::ranges<1, 9, 11, 12, 14, 127> {};
185 // Short lines of ASCII text. LF or CRLF line separators.
186 struct body_ascii : seq<star<seq<rep_max<998, text_ascii>, eol>>,
187 opt<rep_max<998, text_ascii>>, eof> {};
189 struct text_utf8 : sor<text_ascii, chars::non_ascii> {};
191 // Short lines of UTF-8 text. LF or CRLF line separators.
192 struct body_utf8 : seq<star<seq<rep_max<998, text_utf8>, eol>>,
193 opt<rep_max<998, text_utf8>>, eof> {};
195 struct FWS : seq<opt<seq<star<WSP>, eol>>, plus<WSP>> {};
197 struct qtext : sor<one<33>, tao::pegtl::ranges<35, 91, 93, 126>, chars::non_ascii> {};
199 struct quoted_pair : seq<one<'\\'>, sor<VUCHAR, WSP>> {};
201 struct atext : sor<ALPHA, DIGIT,
202 one<'!', '#',
203 '$', '%',
204 '&', '\'',
205 '*', '+',
206 '-', '/',
207 '=', '?',
208 '^', '_',
209 '`', '{',
210 '|', '}',
211 '~'>,
212 chars::non_ascii> {};
214 // ctext is ASCII not '(' or ')' or '\\'
215 struct ctext : sor<tao::pegtl::ranges<33, 39, 42, 91, 93, 126>, chars::non_ascii> {};
217 struct comment;
219 struct ccontent : sor<ctext, quoted_pair, comment> {};
221 struct comment
222 : seq<one<'('>, star<seq<opt<FWS>, ccontent>>, opt<FWS>, one<')'>> {};
224 struct CFWS : sor<seq<plus<seq<opt<FWS>, comment>, opt<FWS>>>, FWS> {};
226 struct qcontent : sor<qtext, quoted_pair> {};
228 // Corrected in errata ID: 3135
229 struct quoted_string
230 : seq<opt<CFWS>,
231 DQUOTE,
232 sor<seq<star<seq<opt<FWS>, qcontent>>, opt<FWS>>, FWS>,
233 DQUOTE,
234 opt<CFWS>> {};
236 // *([FWS] VCHAR) *WSP
237 struct unstructured : seq<star<seq<opt<FWS>, VUCHAR>>, star<WSP>> {};
239 struct atom : seq<opt<CFWS>, plus<atext>, opt<CFWS>> {};
241 struct dot_atom_text : list<plus<atext>, dot> {};
243 struct dot_atom : seq<opt<CFWS>, dot_atom_text, opt<CFWS>> {};
245 struct word : sor<atom, quoted_string> {};
247 struct phrase : plus<word> {};
249 struct local_part : sor<dot_atom, quoted_string> {};
251 // from '!' to '~' excluding 91 92 93 '[' '\\' ']'
253 struct dtext : tao::pegtl::ranges<33, 90, 94, 126> {};
255 struct domain_literal : seq<opt<CFWS>,
256 one<'['>,
257 star<seq<opt<FWS>, dtext>>,
258 opt<FWS>,
259 one<']'>,
260 opt<CFWS>> {};
262 struct domain : sor<dot_atom, domain_literal> {};
264 struct addr_spec : seq<local_part, one<'@'>, domain> {};
266 struct postmaster : TAO_PEGTL_ISTRING("Postmaster") {};
268 struct addr_spec_or_postmaster : sor<addr_spec, postmaster> {};
270 struct addr_spec_only : seq<addr_spec_or_postmaster, eof> {};
272 struct display_name : phrase {};
274 struct display_name_only : seq<display_name, eof> {};
276 // clang-format on
278 // struct name_addr : seq<opt<display_name>, angle_addr> {};
280 // struct mailbox : sor<name_addr, addr_spec> {};
282 template <typename Rule>
283 struct inaction : nothing<Rule> {};
285 template <typename Rule>
286 struct action : nothing<Rule> {};
288 template <>
289 struct action<local_part> {
290 template <typename Input>
291 static void apply(Input const& in, Mailbox& mbx)
293 mbx.set_local(in.string());
297 template <>
298 struct action<domain> {
299 template <typename Input>
300 static void apply(Input const& in, Mailbox& mbx)
302 mbx.set_domain(in.string());
305 } // namespace RFC5322
307 namespace RFC5321 {
309 struct Connection {
310 Sock sock;
312 std::string server_id;
314 std::string ehlo_keyword;
315 std::vector<std::string> ehlo_param;
316 std::unordered_map<std::string, std::vector<std::string>> ehlo_params;
318 std::string reply_code;
320 bool greeting_ok{false};
321 bool ehlo_ok{false};
323 bool has_extension(char const* name) const
325 return ehlo_params.find(name) != end(ehlo_params);
328 Connection(int fd_in, int fd_out, std::function<void(void)> read_hook)
329 : sock(
330 fd_in, fd_out, read_hook, Config::read_timeout, Config::write_timeout)
335 // clang-format off
337 using dot = one<'.'>;
338 using colon = one<':'>;
339 using dash = one<'-'>;
340 using underscore = one<'_'>;
342 struct u_let_dig : sor<ALPHA, DIGIT, chars::non_ascii> {};
344 struct u_ldh_tail : star<sor<seq<plus<one<'-'>>, u_let_dig>, u_let_dig>> {};
346 struct u_label : seq<u_let_dig, u_ldh_tail> {};
348 struct let_dig : sor<ALPHA, DIGIT> {};
350 struct ldh_tail : star<sor<seq<plus<one<'-'>>, let_dig>, let_dig>> {};
352 struct ldh_str : seq<let_dig, ldh_tail> {};
354 struct label : ldh_str {};
356 struct sub_domain : sor<label, u_label> {};
358 struct domain : list<sub_domain, dot> {};
360 struct dec_octet : sor<seq<string<'2','5'>, range<'0','5'>>,
361 seq<one<'2'>, range<'0','4'>, DIGIT>,
362 seq<range<'0', '1'>, rep<2, DIGIT>>,
363 rep_min_max<1, 2, DIGIT>> {};
365 struct IPv4_address_literal
366 : seq<dec_octet, dot, dec_octet, dot, dec_octet, dot, dec_octet> {};
368 struct h16 : rep_min_max<1, 4, HEXDIG> {};
370 struct ls32 : sor<seq<h16, colon, h16>, IPv4_address_literal> {};
372 struct dcolon : two<':'> {};
374 struct IPv6address : sor<seq< rep<6, h16, colon>, ls32>,
375 seq< dcolon, rep<5, h16, colon>, ls32>,
376 seq<opt<h16 >, dcolon, rep<4, h16, colon>, ls32>,
377 seq<opt<h16, opt< colon, h16>>, dcolon, rep<3, h16, colon>, ls32>,
378 seq<opt<h16, rep_opt<2, colon, h16>>, dcolon, rep<2, h16, colon>, ls32>,
379 seq<opt<h16, rep_opt<3, colon, h16>>, dcolon, h16, colon, ls32>,
380 seq<opt<h16, rep_opt<4, colon, h16>>, dcolon, ls32>,
381 seq<opt<h16, rep_opt<5, colon, h16>>, dcolon, h16>,
382 seq<opt<h16, rep_opt<6, colon, h16>>, dcolon >> {};
384 struct IPv6_address_literal : seq<TAO_PEGTL_ISTRING("IPv6:"), IPv6address> {};
386 struct dcontent : tao::pegtl::ranges<33, 90, 94, 126> {};
388 struct standardized_tag : ldh_str {};
390 struct general_address_literal : seq<standardized_tag, colon, plus<dcontent>> {};
392 // See rfc 5321 Section 4.1.3
393 struct address_literal : seq<one<'['>,
394 sor<IPv4_address_literal,
395 IPv6_address_literal,
396 general_address_literal>,
397 one<']'>> {};
400 struct qtextSMTP : sor<tao::pegtl::ranges<32, 33, 35, 91, 93, 126>, chars::non_ascii> {};
401 struct graphic : range<32, 126> {};
402 struct quoted_pairSMTP : seq<one<'\\'>, graphic> {};
403 struct qcontentSMTP : sor<qtextSMTP, quoted_pairSMTP> {};
405 // excluded from atext: "(),.@[]"
406 struct atext : sor<ALPHA, DIGIT,
407 one<'!', '#',
408 '$', '%',
409 '&', '\'',
410 '*', '+',
411 '-', '/',
412 '=', '?',
413 '^', '_',
414 '`', '{',
415 '|', '}',
416 '~'>,
417 chars::non_ascii> {};
418 struct atom : plus<atext> {};
419 struct dot_string : list<atom, dot> {};
420 struct quoted_string : seq<one<'"'>, star<qcontentSMTP>, one<'"'>> {};
421 struct local_part : sor<dot_string, quoted_string> {};
422 struct non_local_part : sor<domain, address_literal> {};
423 struct mailbox : seq<local_part, one<'@'>, non_local_part> {};
425 struct at_domain : seq<one<'@'>, domain> {};
427 struct a_d_l : list<at_domain, one<','>> {};
429 struct path : seq<opt<seq<a_d_l, colon>>, mailbox> {};
431 struct postmaster : TAO_PEGTL_ISTRING("Postmaster") {};
433 struct path_or_postmaster : seq<sor<path, postmaster>, eof> {};
435 // textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
437 // Although not explicit in the grammar of RFC-6531, in practice UTF-8
438 // is used in the replies.
440 // struct textstring : plus<sor<one<9>, range<32, 126>>> {};
442 struct textstring : plus<sor<one<9>, range<32, 126>, chars::non_ascii>> {};
444 struct crap : plus<range<32, 126>> {};
446 struct server_id : sor<domain, address_literal, crap> {};
448 // Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF )
449 // /
450 // ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
451 // *( "220-" [ textstring ] CRLF )
452 // "220" [ SP textstring ] CRLF )
454 struct greeting_ok
455 : sor<seq<TAO_PEGTL_ISTRING("220 "), server_id, opt<textstring>, CRLF>,
456 seq<TAO_PEGTL_ISTRING("220-"), server_id, opt<textstring>, CRLF,
457 star<seq<TAO_PEGTL_ISTRING("220-"), opt<textstring>, CRLF>>,
458 seq<TAO_PEGTL_ISTRING("220"), opt<seq<SP, textstring>>, CRLF>>> {};
460 // Reply-code = %x32-35 %x30-35 %x30-39
462 struct reply_code
463 : seq<range<0x32, 0x35>, range<0x30, 0x35>, range<0x30, 0x39>> {};
465 // Reply-line = *( Reply-code "-" [ textstring ] CRLF )
466 // Reply-code [ SP textstring ] CRLF
468 struct reply_lines
469 : seq<star<seq<reply_code, one<'-'>, opt<textstring>, CRLF>>,
470 seq<reply_code, opt<seq<SP, textstring>>, CRLF>> {};
472 struct greeting
473 : sor<greeting_ok, reply_lines> {};
475 // ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
476 // ; string of any characters other than CR or LF
478 struct ehlo_greet : plus<tao::pegtl::ranges<0, 9, 11, 12, 14, 127>> {};
480 // ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
481 // ; additional syntax of ehlo-params depends on
482 // ; ehlo-keyword
484 // The '.' we also allow in ehlo-keyword since it has been seen in the
485 // wild at least at 263.net.
487 struct ehlo_keyword : seq<sor<ALPHA, DIGIT>, star<sor<ALPHA, DIGIT, dash, dot, underscore>>> {};
489 // ehlo-param = 1*(%d33-126)
490 // ; any CHAR excluding <SP> and all
491 // ; control characters (US-ASCII 0-31 and 127
492 // ; inclusive)
494 struct ehlo_param : plus<range<33, 126>> {};
496 // ehlo-line = ehlo-keyword *( SP ehlo-param )
498 // The AUTH= thing is so common with some servers (postfix) that I
499 // guess we have to accept it.
501 struct ehlo_line
502 : seq<ehlo_keyword, star<seq<sor<SP,one<'='>>, ehlo_param>>> {};
504 // ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF )
505 // /
506 // ( "250-" Domain [ SP ehlo-greet ] CRLF
507 // *( "250-" ehlo-line CRLF )
508 // "250 " ehlo-line CRLF )
510 // The last line having the optional ehlo_line is not strictly correct.
511 // Was added to work with postfix/src/smtpstone/smtp-sink.c.
513 struct ehlo_ok_rsp
514 : sor<seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF>,
516 seq<TAO_PEGTL_ISTRING("250-"), server_id, opt<ehlo_greet>, CRLF,
517 star<seq<TAO_PEGTL_ISTRING("250-"), ehlo_line, CRLF>>,
518 seq<TAO_PEGTL_ISTRING("250 "), opt<ehlo_line>, CRLF>>
519 > {};
521 struct ehlo_rsp
522 : sor<ehlo_ok_rsp, reply_lines> {};
524 struct helo_ok_rsp
525 : seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF> {};
527 struct auth_login_username
528 : seq<TAO_PEGTL_STRING("334 VXNlcm5hbWU6"), CRLF> {};
530 struct auth_login_password
531 : seq<TAO_PEGTL_STRING("334 UGFzc3dvcmQ6"), CRLF> {};
533 // clang-format on
535 template <typename Rule>
536 struct inaction : nothing<Rule> {};
538 template <typename Rule>
539 struct action : nothing<Rule> {};
541 template <>
542 struct action<server_id> {
543 template <typename Input>
544 static void apply(Input const& in, Connection& cnn)
546 cnn.server_id = in.string();
550 template <>
551 struct action<local_part> {
552 template <typename Input>
553 static void apply(Input const& in, Mailbox& mbx)
555 mbx.set_local(in.string());
559 template <>
560 struct action<non_local_part> {
561 template <typename Input>
562 static void apply(Input const& in, Mailbox& mbx)
564 mbx.set_domain(in.string());
568 template <>
569 struct action<greeting_ok> {
570 template <typename Input>
571 static void apply(Input const& in, Connection& cnn)
573 cnn.greeting_ok = true;
574 imemstream stream{begin(in), size(in)};
575 std::string line;
576 while (std::getline(stream, line)) {
577 LOG(INFO) << " S: " << line;
582 template <>
583 struct action<ehlo_ok_rsp> {
584 template <typename Input>
585 static void apply(Input const& in, Connection& cnn)
587 cnn.ehlo_ok = true;
588 imemstream stream{begin(in), size(in)};
589 std::string line;
590 while (std::getline(stream, line)) {
591 LOG(INFO) << " S: " << line;
596 template <>
597 struct action<ehlo_keyword> {
598 template <typename Input>
599 static void apply(Input const& in, Connection& cnn)
601 cnn.ehlo_keyword = in.string();
602 boost::to_upper(cnn.ehlo_keyword);
606 template <>
607 struct action<ehlo_param> {
608 template <typename Input>
609 static void apply(Input const& in, Connection& cnn)
611 cnn.ehlo_param.push_back(in.string());
615 template <>
616 struct action<ehlo_line> {
617 template <typename Input>
618 static void apply(Input const& in, Connection& cnn)
620 cnn.ehlo_params.emplace(std::move(cnn.ehlo_keyword),
621 std::move(cnn.ehlo_param));
625 template <>
626 struct action<reply_lines> {
627 template <typename Input>
628 static void apply(Input const& in, Connection& cnn)
630 imemstream stream{begin(in), size(in)};
631 std::string line;
632 while (std::getline(stream, line)) {
633 LOG(INFO) << " S: " << line;
638 template <>
639 struct action<reply_code> {
640 template <typename Input>
641 static void apply(Input const& in, Connection& cnn)
643 cnn.reply_code = in.string();
646 } // namespace RFC5321
648 namespace {
650 int conn(DNS::Resolver& res, Domain const& node, uint16_t port)
652 auto const use_4{!FLAGS_6};
653 auto const use_6{!FLAGS_4};
655 if (use_6) {
656 auto const fd{socket(AF_INET6, SOCK_STREAM, 0)};
657 PCHECK(fd >= 0) << "socket() failed";
659 if (!FLAGS_local_address.empty()) {
660 auto loc{sockaddr_in6{}};
661 loc.sin6_family = AF_INET6;
662 if (1 != inet_pton(AF_INET6, FLAGS_local_address.c_str(),
663 reinterpret_cast<void*>(&loc.sin6_addr))) {
664 LOG(FATAL) << "can't interpret " << FLAGS_local_address
665 << " as IPv6 address";
667 PCHECK(0 == bind(fd, reinterpret_cast<sockaddr*>(&loc), sizeof(loc)));
670 auto addrs{std::vector<std::string>{}};
672 if (node.is_address_literal()) {
673 if (IP6::is_address(node.ascii())) {
674 addrs.push_back(node.ascii());
676 if (IP6::is_address_literal(node.ascii())) {
677 auto const addr = IP6::as_address(node.ascii());
678 addrs.push_back(std::string(addr.data(), addr.length()));
681 else {
682 addrs = res.get_strings(DNS::RR_type::AAAA, node.ascii());
684 for (auto const& addr : addrs) {
685 auto in6{sockaddr_in6{}};
686 in6.sin6_family = AF_INET6;
687 in6.sin6_port = htons(port);
688 CHECK_EQ(inet_pton(AF_INET6, addr.c_str(),
689 reinterpret_cast<void*>(&in6.sin6_addr)),
691 if (connect(fd, reinterpret_cast<const sockaddr*>(&in6), sizeof(in6))) {
692 PLOG(WARNING) << "connect failed [" << addr << "]:" << port;
693 continue;
696 LOG(INFO) << fd << " connected to [" << addr << "]:" << port;
697 return fd;
700 close(fd);
702 if (use_4) {
703 auto fd{socket(AF_INET, SOCK_STREAM, 0)};
704 PCHECK(fd >= 0) << "socket() failed";
706 if (!FLAGS_local_address.empty()) {
707 auto loc{sockaddr_in{}};
708 loc.sin_family = AF_INET;
709 if (1 != inet_pton(AF_INET, FLAGS_local_address.c_str(),
710 reinterpret_cast<void*>(&loc.sin_addr))) {
711 LOG(FATAL) << "can't interpret " << FLAGS_local_address
712 << " as IPv4 address";
714 LOG(INFO) << "bind " << FLAGS_local_address;
715 PCHECK(0 == bind(fd, reinterpret_cast<sockaddr*>(&loc), sizeof(loc)));
718 auto addrs{std::vector<std::string>{}};
719 if (node.is_address_literal()) {
720 if (IP4::is_address(node.ascii())) {
721 addrs.push_back(node.ascii());
723 if (IP4::is_address_literal(node.ascii())) {
724 auto const addr = IP4::as_address(node.ascii());
725 addrs.push_back(std::string(addr.data(), addr.length()));
728 else {
729 addrs = res.get_strings(DNS::RR_type::A, node.ascii());
731 for (auto const& addr : addrs) {
732 auto in4{sockaddr_in{}};
733 in4.sin_family = AF_INET;
734 in4.sin_port = htons(port);
735 CHECK_EQ(inet_pton(AF_INET, addr.c_str(),
736 reinterpret_cast<void*>(&in4.sin_addr)),
738 if (connect(fd, reinterpret_cast<const sockaddr*>(&in4), sizeof(in4))) {
739 PLOG(WARNING) << "connect failed " << addr << ":" << port;
740 continue;
743 LOG(INFO) << " connected to " << addr << ":" << port;
744 return fd;
747 close(fd);
750 return -1;
753 class Eml {
754 public:
755 void add_hdr(std::string name, std::string value)
757 assert(!name.starts_with("."s)); // Would mess up "dot" stuffing.
758 hdrs_.push_back(std::make_pair(name, value));
761 void foreach_hdr(std::function<void(std::string const& name,
762 std::string const& value)> func)
764 for (auto const& [name, value] : hdrs_) {
765 func(name, value);
769 private:
770 std::vector<std::pair<std::string, std::string>> hdrs_;
772 friend std::ostream& operator<<(std::ostream& os, Eml const& eml)
774 for (auto const& [name, value] : eml.hdrs_) {
775 os << name << ": " << value << "\r\n";
777 // return os << "\r\n"; // end of headers
778 return os /* << "\r\n" */; // end of headers
782 // // clang-format off
783 // char const* const signhdrs[] = {
784 // "From",
786 // "Message-ID",
788 // "Cc",
789 // "Date",
790 // "In-Reply-To",
791 // "References",
792 // "Reply-To",
793 // "Sender",
794 // "Subject",
795 // "To",
797 // "MIME-Version",
798 // "Content-Type",
799 // "Content-Transfer-Encoding",
801 // nullptr
802 // };
803 // clang-format on
805 enum class transfer_encoding {
806 seven_bit,
807 quoted_printable,
808 base64,
809 eight_bit,
810 binary,
813 enum class data_type {
814 ascii, // 7bit, quoted-printable and base64
815 utf8, // 8bit
816 binary, // binary
819 data_type type(std::string_view d)
822 auto in{memory_input<>{d.data(), d.size(), "data"}};
823 if (parse<RFC5322::body_ascii>(in)) {
824 return data_type::ascii;
828 auto in{memory_input<>{d.data(), d.size(), "data"}};
829 if (parse<RFC5322::body_utf8>(in)) {
830 return data_type::utf8;
833 // anything else is
834 return data_type::binary;
837 class content {
838 public:
839 content(char const* path)
840 : path_(path)
842 auto const body_sz{fs::file_size(path_)};
843 CHECK(body_sz) << "no body";
844 file_.open(path_);
845 type_ = ::type(*this);
848 char const* data() const { return file_.data(); }
849 size_t size() const { return file_.size(); }
850 data_type type() const { return type_; }
852 bool empty() const { return size() == 0; }
853 operator std::string_view() const { return std::string_view(data(), size()); }
855 private:
856 data_type type_;
857 fs::path path_;
858 boost::iostreams::mapped_file_source file_;
861 template <typename Input>
862 void fail(Input& in, RFC5321::Connection& cnn)
864 LOG(INFO) << " C: QUIT";
865 cnn.sock.out() << "QUIT\r\n" << std::flush;
866 // we might have a few error replies stacked up if we're pipelining
867 // CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
868 exit(EXIT_FAILURE);
871 template <typename Input>
872 void quit_on_fail(Input& in, RFC5321::Connection& cnn, std::string_view cmd)
874 cnn.sock.out() << std::flush;
875 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
876 if (cnn.reply_code.at(0) != '2') {
877 LOG(ERROR) << cmd << " returned " << cnn.reply_code;
878 fail(in, cnn);
880 in.discard();
883 bool validate_name(const char* flagname, std::string const& value)
885 if (value.empty()) // empty name needs to validate, but
886 return true; // will not be used
887 memory_input<> name_in(value.c_str(), "name");
888 if (!parse<RFC5322::display_name_only, RFC5322::inaction>(name_in)) {
889 LOG(ERROR) << "bad name syntax " << value;
890 return false;
892 return true;
895 DEFINE_validator(from_name, &validate_name);
896 DEFINE_validator(to_name, &validate_name);
898 bool validate_address_RFC5322(const char* flagname, std::string const& value)
900 if (value.empty()) // empty name needs to validate, but
901 return true; // will not be used
902 memory_input<> name_in(value.c_str(), "address");
903 if (!parse<RFC5322::addr_spec_only, RFC5322::inaction>(name_in)) {
904 LOG(ERROR) << "bad address syntax " << value;
905 return false;
907 return true;
910 DEFINE_validator(from, &validate_address_RFC5322);
911 DEFINE_validator(to, &validate_address_RFC5322);
913 bool validate_address_RFC5321(const char* flagname, std::string const& value)
915 if (value.empty()) { // empty name needs to validate, but
916 return true; // will not be used
918 memory_input<> name_in(value.c_str(), "path");
919 if (!parse<RFC5321::path_or_postmaster, RFC5321::inaction>(name_in)) {
920 LOG(ERROR) << "bad RFC-5321 address syntax " << value;
921 return false;
923 return true;
926 DEFINE_validator(smtp_from, &validate_address_RFC5321);
927 DEFINE_validator(smtp_to, &validate_address_RFC5321);
928 DEFINE_validator(smtp_to2, &validate_address_RFC5321);
929 DEFINE_validator(smtp_to3, &validate_address_RFC5321);
931 void selftest()
933 CHECK(validate_name("selftest", ""s));
934 CHECK(validate_name("selftest", "Elmer J Fudd"s));
935 CHECK(validate_name("selftest", "\"Elmer J. Fudd\""s));
936 CHECK(validate_name("selftest", "Elmer! J! Fudd!"s));
938 CHECK(validate_address_RFC5321("selftest", "foo@digilicious.com"s));
939 CHECK(validate_address_RFC5321("selftest", "\"foo\"@digilicious.com"s));
940 CHECK(validate_address_RFC5321(
941 "selftest",
942 "\"very.(),:;<>[]\\\".VERY.\\\"very@\\\\ \\\"very\\\".unusual\"@digilicious.com"s));
944 CHECK(validate_address_RFC5322("selftest", "foo@digilicious.com"s));
945 CHECK(validate_address_RFC5322("selftest", "\"foo\"@digilicious.com"s));
946 CHECK(validate_address_RFC5322(
947 "selftest",
948 "\"very.(),:;<>[]\\\".VERY.\\\"very@\\\\ \\\"very\\\".unusual\"@digilicious.com"s));
950 auto const read_hook{[]() {}};
952 const char* greet_list[]{
953 "220-mtaig-aak03.mx.aol.com ESMTP Internet Inbound\r\n"
954 "220-AOL and its affiliated companies do not\r\n"
955 "220-authorize the use of its proprietary computers and computer\r\n"
956 "220-networks to accept, transmit, or distribute unsolicited bulk\r\n"
957 "220-e-mail sent from the internet.\r\n"
958 "220-Effective immediately:\r\n"
959 "220-AOL may no longer accept connections from IP addresses\r\n"
960 "220 which no do not have reverse-DNS (PTR records) assigned.\r\n",
962 "421 mtaig-maa02.mx.aol.com Service unavailable - try again later\r\n",
965 for (auto const& i : greet_list) {
966 auto cnn{RFC5321::Connection(0, 1, read_hook)};
967 auto in{memory_input<>{i, i}};
968 if (!parse<RFC5321::greeting, RFC5321::action /*, tao::pegtl::tracer*/>(
969 in, cnn)) {
970 LOG(FATAL) << "Error parsing greeting \"" << i << "\"";
972 if (cnn.greeting_ok) {
973 LOG(WARNING) << "greeting ok";
975 else {
976 LOG(WARNING) << "greeting was not in the affirmative";
980 const char* ehlo_rsp_list[]{
981 "250-www77.totaalholding.nl Hello "
982 "ec2-18-205-224-193.compute-1.amazonaws.com [18.205.224.193]\r\n"
983 "250-SIZE 52428800\r\n"
984 "250-8BITMIME\r\n"
985 "250-PIPELINING\r\n"
986 "250-X_PIPE_CONNECT\r\n"
987 "250-STARTTLS\r\n"
988 "250 HELP\r\n",
990 "250-HELLO, SAILOR!\r\n"
991 "250-NO-SOLICITING\r\n"
992 "250 8BITMIME\r\n",
994 "250-digilicious.com at your service, localhost. [IPv6:::1]\r\n"
995 "250-SIZE 15728640\r\n"
996 "250-8BITMIME\r\n"
997 "250-STARTTLS\r\n"
998 "250-ENHANCEDSTATUSCODES\r\n"
999 "250-PIPELINING\r\n"
1000 "250-BINARYMIME\r\n"
1001 "250-CHUNKING\r\n"
1002 "250-SMTPUTF8\r\n"
1003 "250 OK\r\n",
1005 "500 5.5.1 command unrecognized: \"EHLO digilicious.com\\r\\n\"\r\n",
1007 "250-263xmail at your service\r\n"
1008 "250-STARTTLS\r\n"
1009 "250-MAE-SMTP\r\n"
1010 "250-263.net\r\n" // the '.' is not RFC complaint
1011 "250-SIZE 104857600\r\n"
1012 "250-ETRN\r\n"
1013 "250-ENHANCEDSTATUSCODES\r\n"
1014 "250-8BITMIME\r\n"
1015 "250 DSN\r\n",
1018 for (auto const& i : ehlo_rsp_list) {
1019 auto cnn{RFC5321::Connection(0, 1, read_hook)};
1020 auto in{memory_input<>{i, i}};
1021 if (!parse<RFC5321::ehlo_rsp, RFC5321::action /*, tao::pegtl::tracer*/>(
1022 in, cnn)) {
1023 LOG(FATAL) << "Error parsing ehlo response \"" << i << "\"";
1025 if (cnn.ehlo_ok) {
1026 LOG(WARNING) << "ehlo ok";
1028 else {
1029 LOG(WARNING) << "ehlo response was not in the affirmative";
1034 auto get_sender()
1036 if (FLAGS_client_id.empty()) {
1037 FLAGS_client_id = [] {
1038 auto const id_from_env{getenv("GHSMTP_CLIENT_ID")};
1039 if (id_from_env)
1040 return std::string{id_from_env};
1042 auto const hostname{osutil::get_hostname()};
1043 if (hostname.find('.') != std::string::npos)
1044 return hostname;
1046 LOG(FATAL) << "hostname not a FQDN, set GHSMTP_CLIENT_ID maybe?";
1047 }();
1050 if (FLAGS_sender.empty()) {
1051 FLAGS_sender = FLAGS_client_id;
1054 auto const sender{Domain{FLAGS_sender}};
1056 if (FLAGS_from.empty()) {
1057 FLAGS_from = "test-it@"s + sender.utf8();
1060 if (FLAGS_to.empty()) {
1061 FLAGS_to = "test-it@"s + sender.utf8();
1064 return sender;
1067 bool is_localhost(DNS::RR const& rr)
1069 if (std::holds_alternative<DNS::RR_MX>(rr)) {
1070 if (iequal(std::get<DNS::RR_MX>(rr).exchange(), "localhost"))
1071 return true;
1073 return false;
1076 bool starts_with(std::string_view str, std::string_view prefix)
1078 if (str.size() >= prefix.size())
1079 if (str.compare(0, prefix.size(), prefix) == 0)
1080 return true;
1081 return false;
1084 bool sts_rec(std::string const& sts_rec)
1086 return starts_with(sts_rec, "v=STSv1");
1089 std::vector<Domain>
1090 get_receivers(DNS::Resolver& res, Mailbox const& to_mbx, bool& enforce_dane)
1092 auto receivers{std::vector<Domain>{}};
1094 // User provided explicit host to receive mail.
1095 if (!FLAGS_mx_host.empty()) {
1096 receivers.emplace_back(FLAGS_mx_host);
1097 return receivers;
1100 // Non-local part is an address literal.
1101 if (to_mbx.domain().is_address_literal()) {
1102 receivers.emplace_back(to_mbx.domain());
1103 return receivers;
1106 // RFC 5321 section 5.1 "Locating the Target Host"
1108 // “The lookup first attempts to locate an MX record associated with
1109 // the name. If a CNAME record is found, the resulting name is
1110 // processed as if it were the initial name.”
1112 // Our (full) resolver will traverse any CNAMEs for us and return
1113 // the CNAME and MX records all together.
1115 auto const& domain = to_mbx.domain().ascii();
1117 auto q_sts{DNS::Query{res, DNS::RR_type::TXT, "_mta-sts."s + domain}};
1118 if (q_sts.has_record()) {
1119 auto sts_records = q_sts.get_strings();
1120 sts_records.erase(std::remove_if(begin(sts_records), end(sts_records),
1121 std::not_fn(sts_rec)),
1122 end(sts_records));
1123 if (size(sts_records) == 1) {
1124 LOG(INFO) << "### This domain implements MTA-STS ###";
1127 else {
1128 LOG(INFO) << "MTA-STS record not found for domain " << domain;
1131 auto q{DNS::Query{res, DNS::RR_type::MX, domain}};
1132 if (q.has_record()) {
1133 if (q.authentic_data()) {
1134 LOG(INFO) << "### MX records authentic for domain " << domain << " ###";
1136 else {
1137 LOG(INFO) << "MX records can't be authenticated for domain " << domain;
1138 enforce_dane = false;
1141 else {
1142 LOG(INFO) << "no MX records found for domain " << domain;
1144 auto mxs{q.get_records()};
1146 mxs.erase(std::remove_if(begin(mxs), end(mxs), is_localhost), end(mxs));
1148 auto const nmx = std::count_if(begin(mxs), end(mxs), [](auto const& rr) {
1149 return std::holds_alternative<DNS::RR_MX>(rr);
1152 if (nmx == 1) {
1153 for (auto const& mx : mxs) {
1154 if (std::holds_alternative<DNS::RR_MX>(mx)) {
1155 // RFC 7505 null MX record
1156 if ((std::get<DNS::RR_MX>(mx).preference() == 0) &&
1157 (std::get<DNS::RR_MX>(mx).exchange().empty() ||
1158 (std::get<DNS::RR_MX>(mx).exchange() == "."))) {
1159 LOG(INFO) << "domain " << domain << " does not accept mail";
1160 return receivers;
1166 if (nmx == 0) {
1167 // implicit MX RR
1168 receivers.emplace_back(domain);
1169 return receivers;
1172 // […] then the sender-SMTP MUST randomize them to spread the load
1173 // across multiple mail exchangers for a specific organization.
1174 std::shuffle(begin(mxs), end(mxs), std::random_device());
1175 std::sort(begin(mxs), end(mxs), [](auto const& a, auto const& b) {
1176 if (std::holds_alternative<DNS::RR_MX>(a) &&
1177 std::holds_alternative<DNS::RR_MX>(b)) {
1178 return std::get<DNS::RR_MX>(a).preference() <
1179 std::get<DNS::RR_MX>(b).preference();
1181 return false;
1184 if (nmx)
1185 LOG(INFO) << "MXs for " << domain << " are:";
1187 for (auto const& mx : mxs) {
1188 if (std::holds_alternative<DNS::RR_MX>(mx)) {
1189 receivers.emplace_back(std::get<DNS::RR_MX>(mx).exchange());
1190 LOG(INFO) << std::setfill(' ') << std::setw(3)
1191 << std::get<DNS::RR_MX>(mx).preference() << " "
1192 << std::get<DNS::RR_MX>(mx).exchange();
1196 return receivers;
1199 auto parse_mailboxes()
1201 auto from_mbx{Mailbox{}};
1202 auto from_in{memory_input<>{FLAGS_from, "from"}};
1203 if (!parse<RFC5322::addr_spec_only, RFC5322::action>(from_in, from_mbx)) {
1204 LOG(FATAL) << "bad From: address syntax <" << FLAGS_from << ">";
1206 LOG(INFO) << " from_mbx == " << from_mbx;
1208 auto local_from{memory_input<>{from_mbx.local_part(), "from.local"}};
1209 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_from);
1211 auto to_mbx{Mailbox{}};
1212 auto to_in{memory_input<>{FLAGS_to, "to"}};
1213 if (!parse<RFC5322::addr_spec_only, RFC5322::action>(to_in, to_mbx)) {
1214 LOG(FATAL) << "bad To: address syntax <" << FLAGS_to << ">";
1216 LOG(INFO) << " to_mbx == " << to_mbx;
1218 auto local_to{memory_input<>{to_mbx.local_part(), "to.local"}};
1219 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_to);
1221 auto smtp_from_mbx{Mailbox{}};
1222 if (!FLAGS_smtp_from.empty()) {
1223 auto smtp_from_in{memory_input<>{FLAGS_smtp_from, "SMTP.from"}};
1224 if (!parse<RFC5321::path_or_postmaster, RFC5321::action>(smtp_from_in,
1225 smtp_from_mbx)) {
1226 LOG(FATAL) << "bad MAIL FROM: address syntax <" << FLAGS_smtp_from << ">";
1228 LOG(INFO) << " smtp_from_mbx == " << smtp_from_mbx;
1229 auto local_smtp_from{
1230 memory_input<>{smtp_from_mbx.local_part(), "SMTP.from.local"}};
1231 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_smtp_from);
1233 else {
1234 smtp_from_mbx = from_mbx;
1237 auto smtp_to_mbx{Mailbox{}};
1238 if (!FLAGS_smtp_to.empty()) {
1239 auto smtp_to_in{memory_input<>{FLAGS_smtp_to, "SMTP.to"}};
1240 if (!parse<RFC5321::path_or_postmaster, RFC5321::action>(smtp_to_in,
1241 smtp_to_mbx)) {
1242 LOG(FATAL) << "bad RCPT TO: address syntax <" << FLAGS_smtp_to << ">";
1244 LOG(INFO) << " smtp_to_mbx == " << smtp_to_mbx;
1246 auto local_smtp_to{
1247 memory_input<>{smtp_to_mbx.local_part(), "SMTP.to.local"}};
1248 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_smtp_to);
1250 else {
1251 smtp_to_mbx = to_mbx;
1254 auto smtp_to2_mbx{Mailbox{}};
1255 if (!FLAGS_smtp_to2.empty()) {
1256 auto smtp_to2_in{memory_input<>{FLAGS_smtp_to2, "SMTP.to"}};
1257 if (!parse<RFC5321::path_or_postmaster, RFC5321::action>(smtp_to2_in,
1258 smtp_to2_mbx)) {
1259 LOG(FATAL) << "bad RCPT TO: address syntax <" << FLAGS_smtp_to2 << ">";
1261 LOG(INFO) << " smtp_to2_mbx == " << smtp_to2_mbx;
1263 auto local_smtp_to2{
1264 memory_input<>{smtp_to2_mbx.local_part(), "SMTP.to.local"}};
1265 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_smtp_to2);
1268 auto smtp_to3_mbx{Mailbox{}};
1269 if (!FLAGS_smtp_to3.empty()) {
1270 auto smtp_to3_in{memory_input<>{FLAGS_smtp_to3, "SMTP.to"}};
1271 if (!parse<RFC5321::path_or_postmaster, RFC5321::action>(smtp_to3_in,
1272 smtp_to3_mbx)) {
1273 LOG(FATAL) << "bad RCPT TO: address syntax <" << FLAGS_smtp_to3 << ">";
1275 LOG(INFO) << " smtp_to3_mbx == " << smtp_to3_mbx;
1277 auto local_smtp_to3{
1278 memory_input<>{smtp_to3_mbx.local_part(), "SMTP.to.local"}};
1279 FLAGS_force_smtputf8 |= !parse<chars::ascii_only>(local_smtp_to3);
1282 return std::tuple(from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx, smtp_to2_mbx,
1283 smtp_to3_mbx);
1286 auto create_eml(Domain const& sender,
1287 std::string const& from,
1288 std::string const& to,
1289 std::vector<content> const& bodies,
1290 bool ext_smtputf8)
1292 auto eml{Eml{}};
1293 auto const date{Now{}};
1294 auto const pill{Pill{}};
1296 auto mid_str = fmt::format("<{}.{}@{}>", date.sec(), pill.as_string_view(),
1297 sender.ascii());
1298 eml.add_hdr("Message-ID", mid_str.c_str());
1299 eml.add_hdr("Date", date.c_str());
1301 if (!FLAGS_from_name.empty())
1302 eml.add_hdr("From", fmt::format("{} <{}>", FLAGS_from_name, from));
1303 else
1304 eml.add_hdr("From", from);
1306 eml.add_hdr("Subject", FLAGS_subject);
1308 if (!FLAGS_to_name.empty())
1309 eml.add_hdr("To", fmt::format("{} <{}>", FLAGS_to_name, to));
1310 else
1311 eml.add_hdr("To", to);
1313 if (!FLAGS_keywords.empty())
1314 eml.add_hdr("Keywords", FLAGS_keywords);
1316 if (!FLAGS_references.empty())
1317 eml.add_hdr("References", FLAGS_references);
1319 if (!FLAGS_in_reply_to.empty())
1320 eml.add_hdr("In-Reply-To", FLAGS_in_reply_to);
1322 if (!FLAGS_reply_to.empty())
1323 eml.add_hdr("Reply-To", FLAGS_reply_to);
1325 if (!FLAGS_reply_2.empty())
1326 eml.add_hdr("Reply-To", FLAGS_reply_2);
1328 eml.add_hdr("MIME-Version", "1.0");
1329 eml.add_hdr("Content-Language", "en-US");
1331 auto magic{Magic{}}; // to ID buffer contents
1333 if (!FLAGS_content_type.empty()) {
1334 eml.add_hdr("Content-Type", FLAGS_content_type);
1336 else {
1337 eml.add_hdr("Content-Type", magic.buffer(bodies[0]));
1340 if (!FLAGS_content_transfer_encoding.empty()) {
1341 eml.add_hdr("Content-Transfer-Encoding", FLAGS_content_transfer_encoding);
1344 return eml;
1347 void sign_eml(Eml& eml,
1348 std::string const& from_dom,
1349 std::vector<content> const& bodies)
1351 auto const body_type = (bodies[0].type() == data_type::binary)
1352 ? OpenDKIM::sign::body_type::binary
1353 : OpenDKIM::sign::body_type::text;
1355 auto const key_file = FLAGS_dkim_key_file.empty()
1356 ? (FLAGS_selector + ".private")
1357 : FLAGS_dkim_key_file;
1358 std::ifstream keyfs(key_file.c_str());
1359 CHECK(keyfs.good()) << "can't access " << key_file;
1360 std::string key(std::istreambuf_iterator<char>{keyfs}, {});
1361 OpenDKIM::sign dks(key.c_str(), FLAGS_selector.c_str(), from_dom.c_str(),
1362 body_type);
1363 eml.foreach_hdr([&dks](std::string const& name, std::string const& value) {
1364 auto const header = name + ": "s + value;
1365 dks.header(header.c_str());
1367 dks.eoh();
1368 for (auto const& body : bodies) {
1369 dks.body(body);
1371 dks.eom();
1372 eml.add_hdr("DKIM-Signature"s, dks.getsighdr());
1375 template <typename Input>
1376 void do_auth(Input& in, RFC5321::Connection& cnn)
1378 if (FLAGS_username.empty() && FLAGS_password.empty())
1379 return;
1381 auto const auth = cnn.ehlo_params.find("AUTH");
1382 if (auth == end(cnn.ehlo_params)) {
1383 LOG(ERROR) << "server doesn't support AUTH";
1384 fail(in, cnn);
1387 // Perfer PLAIN mechanism.
1388 if (std::find(cbegin(auth->second), cend(auth->second), "PLAIN") !=
1389 cend(auth->second)) {
1390 LOG(INFO) << "C: AUTH PLAIN";
1391 auto const tok = fmt::format("\0{}\0{}", FLAGS_username, FLAGS_password);
1392 cnn.sock.out() << "AUTH PLAIN " << Base64::enc(tok) << "\r\n" << std::flush;
1393 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1394 if (cnn.reply_code != "235") {
1395 LOG(ERROR) << "AUTH PLAIN returned " << cnn.reply_code;
1396 fail(in, cnn);
1399 // The LOGIN SASL mechanism is obsolete.
1400 else if (std::find(begin(auth->second), end(auth->second), "LOGIN") !=
1401 end(auth->second)) {
1402 LOG(INFO) << "C: AUTH LOGIN";
1403 cnn.sock.out() << "AUTH LOGIN\r\n" << std::flush;
1404 CHECK((parse<RFC5321::auth_login_username>(in)));
1405 cnn.sock.out() << Base64::enc(FLAGS_username) << "\r\n" << std::flush;
1406 CHECK((parse<RFC5321::auth_login_password>(in)));
1407 cnn.sock.out() << Base64::enc(FLAGS_password) << "\r\n" << std::flush;
1408 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1409 if (cnn.reply_code != "235") {
1410 LOG(ERROR) << "AUTH LOGIN returned " << cnn.reply_code;
1411 fail(in, cnn);
1414 else {
1415 LOG(ERROR) << "server doesn't support AUTH methods PLAIN or LOGIN";
1416 fail(in, cnn);
1420 // Do various bad things during the DATA transfer.
1422 template <typename Input>
1423 void bad_daddy(Input& in, RFC5321::Connection& cnn)
1425 LOG(INFO) << "C: DATA";
1426 cnn.sock.out() << "DATA\r\n";
1427 cnn.sock.out() << std::flush;
1429 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1430 if (cnn.reply_code != "354") {
1431 LOG(ERROR) << "DATA returned " << cnn.reply_code;
1432 fail(in, cnn);
1435 // cnn.sock.out() << "\r\nThis ->\n<- is a bare LF!\r\n";
1436 if (FLAGS_bare_lf)
1437 cnn.sock.out() << "\n.\n\r\n";
1439 if (FLAGS_to_the_neck) {
1440 for (;;) {
1441 cnn.sock.out() << "####################"
1442 "####################"
1443 "####################"
1444 << std::flush;
1448 if (FLAGS_long_line) {
1449 for (auto i{0}; i < 10000; ++i) {
1450 cnn.sock.out() << 'X';
1452 cnn.sock.out() << "\r\n" << std::flush;
1455 while (FLAGS_slow_strangle) {
1456 for (auto i{0}; i < 500; ++i) {
1457 cnn.sock.out() << 'X' << std::flush;
1458 sleep(1);
1460 cnn.sock.out() << "\r\n";
1463 // Done!
1464 cnn.sock.out() << ".\r\n" << std::flush;
1465 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1467 LOG(INFO) << "reply_code == " << cnn.reply_code;
1468 CHECK_EQ(cnn.reply_code.at(0), '2');
1470 LOG(INFO) << "C: QUIT";
1471 cnn.sock.out() << "QUIT\r\n" << std::flush;
1472 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1475 bool snd(fs::path config_path,
1476 int fd_in,
1477 int fd_out,
1478 Domain const& sender,
1479 Domain const& receiver,
1480 DNS::RR_collection const& tlsa_rrs,
1481 bool enforce_dane,
1482 Mailbox const& from_mbx,
1483 Mailbox const& to_mbx,
1484 Mailbox const& smtp_from_mbx,
1485 Mailbox const& smtp_to_mbx,
1486 Mailbox const& smtp_to2_mbx,
1487 Mailbox const& smtp_to3_mbx,
1488 std::vector<content> const& bodies)
1490 auto constexpr read_hook{[]() {}};
1492 auto cnn{RFC5321::Connection(fd_in, fd_out, read_hook)};
1494 auto in{
1495 istream_input<eol::crlf, 1>{cnn.sock.in(), FLAGS_bfr_size, "session"}};
1496 if (!parse<RFC5321::greeting, RFC5321::action>(in, cnn)) {
1497 LOG(WARNING) << "can't parse greeting";
1499 LOG(WARNING) << " us: " << cnn.sock.us_c_str();
1500 LOG(WARNING) << "them: " << cnn.sock.them_c_str();
1502 cnn.sock.log_stats();
1503 return false;
1506 if (!cnn.greeting_ok) {
1507 LOG(WARNING) << "greeting was not in the affirmative, skipping";
1508 return false;
1511 // try EHLO/HELO
1513 if (FLAGS_use_esmtp) {
1514 LOG(INFO) << "C: EHLO " << FLAGS_client_id;
1516 if (FLAGS_slow_strangle) {
1517 auto ehlo_str = fmt::format("EHLO {}\r\n", FLAGS_client_id);
1518 for (auto ch : ehlo_str) {
1519 cnn.sock.out() << ch << std::flush;
1520 sleep(1);
1523 else {
1524 cnn.sock.out() << "EHLO " << FLAGS_client_id << "\r\n" << std::flush;
1527 CHECK((parse<RFC5321::ehlo_rsp, RFC5321::action>(in, cnn)));
1528 if (!cnn.ehlo_ok) {
1530 if (FLAGS_force_smtputf8) {
1531 LOG(WARNING) << "ehlo response was not in the affirmative, skipping";
1532 return false;
1535 LOG(WARNING) << "ehlo response was not in the affirmative, trying HELO";
1536 FLAGS_use_esmtp = false;
1540 if (!FLAGS_use_esmtp) {
1541 LOG(INFO) << "C: HELO " << sender.ascii();
1542 cnn.sock.out() << "HELO " << sender.ascii() << "\r\n" << std::flush;
1543 if (!parse<RFC5321::helo_ok_rsp, RFC5321::action>(in, cnn)) {
1544 LOG(WARNING) << "HELO didn't work, skipping";
1545 return false;
1549 // Check extensions
1551 auto bad_dad = FLAGS_bare_lf || FLAGS_slow_strangle || FLAGS_to_the_neck;
1553 if (bad_dad) {
1554 FLAGS_use_chunking = false;
1555 FLAGS_use_size = false;
1558 auto const ext_8bitmime{FLAGS_use_8bitmime && cnn.has_extension("8BITMIME")};
1560 auto const ext_chunking{FLAGS_use_chunking && cnn.has_extension("CHUNKING")};
1562 auto const ext_binarymime{FLAGS_use_binarymime && ext_chunking &&
1563 cnn.has_extension("BINARYMIME")};
1565 auto const ext_deliverby{FLAGS_use_deliverby &&
1566 cnn.has_extension("DELIVERBY")};
1568 auto const ext_pipelining{FLAGS_use_pipelining &&
1569 cnn.has_extension("PIPELINING")};
1571 auto const ext_prdr{FLAGS_use_prdr && cnn.has_extension("PRDR")};
1573 auto const ext_size{FLAGS_use_size && cnn.has_extension("SIZE")};
1575 auto const ext_smtputf8{FLAGS_use_smtputf8 && cnn.has_extension("SMTPUTF8")};
1577 auto const ext_starttls{FLAGS_use_tls && cnn.has_extension("STARTTLS")};
1579 if (FLAGS_force_smtputf8 && !ext_smtputf8) {
1580 LOG(WARNING) << "no SMTPUTF8, skipping";
1581 return false;
1584 if (ext_starttls) {
1585 LOG(INFO) << "C: STARTTLS";
1586 cnn.sock.out() << "STARTTLS\r\n" << std::flush;
1587 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1589 LOG(INFO) << "cnn.sock.starttls_client(\"" << receiver.ascii() << "\");";
1590 cnn.sock.starttls_client(config_path, sender.ascii().c_str(),
1591 receiver.ascii().c_str(), tlsa_rrs, enforce_dane);
1593 LOG(INFO) << "TLS: " << cnn.sock.tls_info();
1595 LOG(INFO) << "C: EHLO " << FLAGS_client_id;
1596 cnn.sock.out() << "EHLO " << FLAGS_client_id << "\r\n" << std::flush;
1597 CHECK((parse<RFC5321::ehlo_rsp, RFC5321::action>(in, cnn)));
1599 else if (FLAGS_require_tls) {
1600 LOG(ERROR) << "No TLS extension, won't send mail in plain text without "
1601 "--require_tls=false.";
1602 LOG(INFO) << "C: QUIT";
1603 cnn.sock.out() << "QUIT\r\n" << std::flush;
1604 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1605 exit(EXIT_FAILURE);
1608 if (FLAGS_noop) {
1609 LOG(INFO) << "C: NOOP";
1610 cnn.sock.out() << "NOOP\r\n" << std::flush;
1613 if (receiver != cnn.server_id) {
1614 LOG(INFO) << "server identifies as " << cnn.server_id;
1617 if (FLAGS_force_smtputf8 && !ext_smtputf8) {
1618 LOG(WARNING) << "does not support SMTPUTF8";
1619 return false;
1622 if (ext_smtputf8 && !ext_8bitmime) {
1623 LOG(ERROR)
1624 << "SMTPUTF8 requires 8BITMIME, see RFC-6531 section 3.1 item 8.";
1625 LOG(INFO) << "C: QUIT";
1626 cnn.sock.out() << "QUIT\r\n" << std::flush;
1627 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1628 exit(EXIT_FAILURE);
1631 auto max_msg_size{0u};
1632 if (ext_size) {
1633 if (!cnn.ehlo_params["SIZE"].empty()) {
1634 char* ep = nullptr;
1635 max_msg_size = strtoul(cnn.ehlo_params["SIZE"][0].c_str(), &ep, 10);
1636 if (ep && (*ep != '\0')) {
1637 LOG(WARNING) << "garbage in SIZE argument: "
1638 << cnn.ehlo_params["SIZE"][0];
1643 auto deliver_by_min{0u};
1644 if (ext_deliverby) {
1645 if (!cnn.ehlo_params["DELIVERBY"].empty()) {
1646 char* ep = nullptr;
1647 deliver_by_min =
1648 strtoul(cnn.ehlo_params["DELIVERBY"][0].c_str(), &ep, 10);
1649 if (ep && (*ep != '\0')) {
1650 LOG(WARNING) << "garbage in DELIVERBY argument: "
1651 << cnn.ehlo_params["DELIVERBY"][0];
1655 if (deliver_by_min) {
1656 LOG(INFO) << "DELIVERBY " << deliver_by_min;
1658 do_auth(in, cnn);
1660 in.discard();
1662 auto enc = FLAGS_force_smtputf8 ? Mailbox::domain_encoding::utf8
1663 : Mailbox::domain_encoding::ascii;
1665 std::string from =
1666 from_mbx.empty() ? smtp_from_mbx.as_string(enc) : from_mbx.as_string(enc);
1667 std::string to =
1668 to_mbx.empty() ? smtp_to_mbx.as_string(enc) : to_mbx.as_string(enc);
1670 auto eml{create_eml(sender, from, to, bodies, ext_smtputf8)};
1672 if (FLAGS_use_dkim) {
1673 auto const dom = Mailbox(from).domain().ascii();
1674 sign_eml(eml, dom.c_str(), bodies);
1676 else if (FLAGS_bogus_dkim) {
1677 eml.add_hdr("DKIM-Signature", "");
1680 // Get the header as one big string
1681 std::ostringstream hdr_stream;
1682 hdr_stream << eml;
1683 if (!FLAGS_rawmsg)
1684 hdr_stream << "\r\n";
1685 auto hdr_str = hdr_stream.view();
1687 // In the case of DATA style transfer, this total_size number is an
1688 // *estimate* only, as line endings may be translated or added
1689 // during transfer. In the BDAT case, this number must be exact.
1691 auto const total_size = rng::accumulate(
1692 bodies, hdr_str.size(),
1693 [](size_t s, const content& body) { return s + body.size(); });
1695 // auto total_size = hdr_str.size();
1696 // for (auto const& body : bodies)
1697 // total_size += body.size();
1699 if (ext_size && max_msg_size && (total_size > max_msg_size)) {
1700 LOG(ERROR) << "message size " << total_size << " exceeds size limit of "
1701 << max_msg_size;
1702 LOG(INFO) << "C: QUIT";
1703 cnn.sock.out() << "QUIT\r\n" << std::flush;
1704 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1705 exit(EXIT_FAILURE);
1708 std::ostringstream param_stream;
1709 if (FLAGS_huge_size && ext_size) {
1710 // Claim some huge size.
1711 param_stream << " SIZE=" << std::numeric_limits<std::streamsize>::max();
1713 else if (ext_size) {
1714 param_stream << " SIZE=" << total_size;
1717 if (ext_binarymime) {
1718 param_stream << " BODY=BINARYMIME";
1720 else if (ext_8bitmime) {
1721 param_stream << " BODY=8BITMIME";
1724 if (ext_prdr && (!smtp_to2_mbx.empty() || !smtp_to3_mbx.empty())) {
1725 param_stream << " PRDR";
1728 if (ext_deliverby) {
1729 param_stream << " BY=1200;NT";
1732 if (ext_smtputf8) {
1733 param_stream << " SMTPUTF8";
1736 if (FLAGS_badpipline) {
1737 LOG(INFO) << "C: NOOP NOOP";
1738 cnn.sock.out() << "NOOP\r\nNOOP\r\n" << std::flush;
1741 bool rcpt_to_ok = false;
1742 bool rcpt_to2_ok = false;
1743 bool rcpt_to3_ok = false;
1745 auto param_str = param_stream.str();
1747 for (auto count = 0UL; count < FLAGS_reps; ++count) {
1749 LOG(INFO) << "C: MAIL FROM:<" << smtp_from_mbx.as_string(enc) << '>'
1750 << param_str;
1751 cnn.sock.out() << "MAIL FROM:<" << smtp_from_mbx.as_string(enc) << '>'
1752 << param_str << "\r\n";
1753 if (!ext_pipelining) {
1754 quit_on_fail(in, cnn, "MAIL FROM");
1757 LOG(INFO) << "C: RCPT TO:<" << smtp_to_mbx.as_string(enc) << ">";
1758 cnn.sock.out() << "RCPT TO:<" << smtp_to_mbx.as_string(enc) << ">\r\n";
1759 if (!ext_pipelining) {
1760 // check RCPT TO #1
1761 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1762 rcpt_to_ok = cnn.reply_code.at(0) == '2';
1765 if (!smtp_to2_mbx.empty()) {
1766 LOG(INFO) << "C: RCPT TO:<" << smtp_to2_mbx.as_string(enc) << ">";
1767 cnn.sock.out() << "RCPT TO:<" << smtp_to2_mbx.as_string(enc) << ">\r\n";
1768 if (!ext_pipelining) {
1769 // check RCPT TO #2
1770 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1771 rcpt_to2_ok = cnn.reply_code.at(0) == '2';
1775 if (!smtp_to3_mbx.empty()) {
1776 LOG(INFO) << "C: RCPT TO:<" << smtp_to3_mbx.as_string(enc) << ">";
1777 cnn.sock.out() << "RCPT TO:<" << smtp_to3_mbx.as_string(enc) << ">\r\n";
1778 if (!ext_pipelining) {
1779 // check RCPT TO #3
1780 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1781 rcpt_to3_ok = cnn.reply_code.at(0) == '2';
1785 if (FLAGS_nosend) {
1786 if (ext_pipelining) {
1787 quit_on_fail(in, cnn, "MAIL FROM");
1788 if (rcpt_to_ok)
1789 quit_on_fail(in, cnn, "RCPT TO");
1790 if (rcpt_to2_ok)
1791 quit_on_fail(in, cnn, "RCPT TO");
1792 if (rcpt_to3_ok)
1793 quit_on_fail(in, cnn, "RCPT TO");
1795 LOG(INFO) << "C: QUIT";
1796 cnn.sock.out() << "QUIT\r\n" << std::flush;
1797 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1798 LOG(INFO) << "no-sending";
1799 exit(EXIT_SUCCESS);
1802 if (bad_dad) {
1803 if (ext_pipelining) {
1804 cnn.sock.out() << std::flush;
1805 quit_on_fail(in, cnn, "MAIL FROM");
1806 quit_on_fail(in, cnn, "RCPT TO");
1808 bad_daddy(in, cnn);
1809 return true;
1812 auto msg = std::make_unique<MessageStore>();
1814 // if service is smtp (i.e. sending real mail, not smtp-test)
1815 try {
1816 if (FLAGS_save) {
1817 msg->open(sender.ascii(), total_size * 2, ".Sent");
1818 msg->write(hdr_str.data(), hdr_str.size());
1819 for (auto const& body : bodies) {
1820 msg->write(body.data(), body.size());
1824 catch (std::system_error const& e) {
1825 switch (errno) {
1826 case ENOSPC:
1827 msg->trash();
1828 msg.reset();
1829 LOG(FATAL) << "no space";
1830 [[fallthrough]];
1832 default:
1833 msg->trash();
1834 msg.reset();
1835 LOG(ERROR) << "errno==" << errno << ": " << strerror(errno);
1836 LOG(FATAL) << e.what();
1839 catch (std::exception const& e) {
1840 msg->trash();
1841 msg.reset();
1842 LOG(FATAL) << e.what();
1845 if (ext_chunking) {
1847 if (ext_pipelining) {
1848 quit_on_fail(in, cnn, "MAIL FROM");
1850 // check RCPT TO #1
1851 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1852 rcpt_to_ok = cnn.reply_code.at(0) == '2';
1854 if (!smtp_to2_mbx.empty()) {
1855 // check RCPT TO #2
1856 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1857 rcpt_to2_ok = cnn.reply_code.at(0) == '2';
1860 if (!smtp_to3_mbx.empty()) {
1861 // check RCPT TO #3
1862 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1863 rcpt_to3_ok = cnn.reply_code.at(0) == '2';
1866 // If all RCPT TOs failed, we give up.
1867 if (!(rcpt_to_ok || rcpt_to2_ok || rcpt_to3_ok)) {
1868 fail(in, cnn);
1872 std::ostringstream bdat_stream;
1873 bdat_stream << "BDAT " << total_size << " LAST";
1874 LOG(INFO) << "C: " << bdat_stream.str();
1876 cnn.sock.out() << bdat_stream.str() << "\r\n";
1877 cnn.sock.out().write(hdr_str.data(), hdr_str.size());
1878 CHECK(cnn.sock.out().good());
1880 for (auto const& body : bodies) {
1881 cnn.sock.out().write(body.data(), body.size());
1882 CHECK(cnn.sock.out().good());
1885 // Done sending data
1886 if (FLAGS_pipeline_quit) {
1887 LOG(INFO) << "C: QUIT";
1888 cnn.sock.out() << "QUIT\r\n" << std::flush;
1890 else {
1891 cnn.sock.out() << std::flush;
1894 // Not CHUNKING
1895 else {
1896 LOG(INFO) << "C: DATA";
1897 cnn.sock.out() << "DATA\r\n";
1899 // Now check returns, after DATA
1900 if (ext_pipelining) {
1901 quit_on_fail(in, cnn, "MAIL FROM");
1903 // check RCPT TO #1
1904 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1905 rcpt_to_ok = cnn.reply_code.at(0) == '2';
1907 if (!smtp_to2_mbx.empty()) {
1908 // check RCPT TO #2
1909 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1910 rcpt_to2_ok = cnn.reply_code.at(0) == '2';
1913 if (!smtp_to3_mbx.empty()) {
1914 // check RCPT TO #3
1915 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1916 rcpt_to3_ok = cnn.reply_code.at(0) == '2';
1919 // If all RCPT TOs failed, we give up.
1920 if (!(rcpt_to_ok || rcpt_to2_ok || rcpt_to3_ok)) {
1921 fail(in, cnn);
1924 else {
1925 cnn.sock.out() << std::flush;
1928 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1929 if (cnn.reply_code != "354") {
1930 LOG(ERROR) << "DATA/BDAT #1 returned " << cnn.reply_code;
1931 fail(in, cnn);
1934 cnn.sock.out() << eml;
1935 if (!FLAGS_rawmsg)
1936 cnn.sock.out() << "\r\n";
1938 for (auto const& body : bodies) {
1939 auto isbody{imemstream{body.data(), body.size()}};
1940 auto line{std::string{}};
1941 for (auto lineno = 0; std::getline(isbody, line); ++lineno) {
1942 if (!cnn.sock.out().good()) {
1943 cnn.sock.log_stats();
1944 LOG(FATAL) << "output no good at line " << lineno;
1946 if (FLAGS_rawdog) {
1947 // This adds a final newline at the end of the file, if no
1948 // line ending was present.
1949 cnn.sock.out() << line << '\n';
1951 else {
1952 // "dot" stuffing:
1953 if (line.length() && (line.at(0) == '.')) {
1954 cnn.sock.out() << '.';
1957 // This code converts single LF line endings into CRLF.
1958 // This code does nothing to fix single CR characters not
1959 // part of a CRLF pair.
1961 // This loop adds a CRLF and the end of the transmission if
1962 // the file doesn't already end with one. This is a
1963 // requirement of the SMTP DATA protocol.
1965 cnn.sock.out() << line;
1966 if (line.length() && line.back() != '\r')
1967 cnn.sock.out() << '\r';
1968 cnn.sock.out() << '\n';
1972 CHECK(cnn.sock.out().good());
1974 // Done sending data
1975 if (FLAGS_pipeline_quit) {
1976 LOG(INFO) << "C: QUIT";
1977 cnn.sock.out() << ".\r\nQUIT\r\n" << std::flush;
1979 else {
1980 cnn.sock.out() << ".\r\n" << std::flush;
1984 auto success{false};
1985 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1987 // Now, either DATA or BDATs lets check replies
1988 if (ext_prdr) {
1989 if (cnn.reply_code == "353") {
1990 if (rcpt_to_ok) {
1991 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1992 success = cnn.reply_code.length() && cnn.reply_code.at(0) == '2';
1994 if (rcpt_to2_ok) {
1995 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
1996 success = success &&
1997 (cnn.reply_code.length() && cnn.reply_code.at(0) == '2');
1999 if (rcpt_to3_ok) {
2000 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
2001 success = success &&
2002 (cnn.reply_code.length() && cnn.reply_code.at(0) == '2');
2005 else {
2006 // last (and useless?) response.
2007 LOG(INFO) << "DATA/BDAT #2 returned " << cnn.reply_code;
2008 success = cnn.reply_code.length() && cnn.reply_code.at(0) == '2';
2011 else {
2012 LOG(INFO) << "DATA/BDAT #3 returned " << cnn.reply_code;
2013 success = cnn.reply_code.length() && cnn.reply_code.at(0) == '2';
2016 if (success) {
2017 if (FLAGS_save) {
2018 msg->deliver();
2020 else {
2021 msg->trash();
2023 LOG(INFO) << "all mail was sent successfully";
2025 else {
2026 LOG(INFO) << "some mail was *NOT* sent successfully";
2029 in.discard();
2030 } // FLAGS_reps
2032 if (!FLAGS_pipeline_quit) {
2033 LOG(INFO) << "C: QUIT";
2034 cnn.sock.out() << "QUIT\r\n" << std::flush;
2036 CHECK((parse<RFC5321::reply_lines, RFC5321::action>(in, cnn)));
2038 return true;
2041 DNS::RR_collection
2042 get_tlsa_rrs(DNS::Resolver& res, Domain const& domain, uint16_t port)
2044 auto const tlsa = fmt::format("_{}._tcp.{}", port, domain.ascii());
2046 DNS::Query q(res, DNS::RR_type::TLSA, tlsa);
2048 if (q.nx_domain()) {
2049 LOG(INFO) << "TLSA data not found for " << domain << ':' << port;
2052 if (q.bogus_or_indeterminate()) {
2053 LOG(WARNING) << "TLSA data is bogus or indeterminate";
2056 if (q.authentic_data()) {
2057 LOG(INFO) << "### TLSA records authentic for domain " << domain << " ###";
2059 else {
2060 LOG(INFO) << "TLSA records can't be authenticated for domain " << domain;
2063 auto tlsa_rrs = q.get_records();
2064 if (!tlsa_rrs.empty()) {
2065 LOG(INFO) << "### TLSA data found for " << domain << ':' << port << " ###";
2068 return tlsa_rrs;
2070 } // namespace
2072 int main(int argc, char* argv[])
2074 std::ios::sync_with_stdio(false);
2076 { // Need to work with either namespace.
2077 using namespace gflags;
2078 using namespace google;
2079 ParseCommandLineFlags(&argc, &argv, true);
2082 auto const config_path = osutil::get_config_dir();
2084 auto sender = get_sender();
2086 if (FLAGS_selftest) {
2087 selftest();
2088 return 0;
2091 auto bodies{std::vector<content>{}};
2092 for (int a = 1; a < argc; ++a) {
2093 if (!fs::exists(argv[a]))
2094 LOG(FATAL) << "can't find mail body part " << argv[a];
2095 bodies.push_back(argv[a]);
2098 if (argc == 1)
2099 bodies.push_back("body.txt");
2101 CHECK_EQ(bodies.size(), 1) << "only one body part for now";
2102 CHECK(!(FLAGS_4 && FLAGS_6)) << "must use /some/ IP version";
2104 if (FLAGS_force_smtputf8)
2105 FLAGS_use_smtputf8 = true;
2107 auto&& [from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx, smtp_to2_mbx,
2108 smtp_to3_mbx] = parse_mailboxes();
2110 if (smtp_to_mbx.domain().empty() && smtp_to2_mbx.domain().empty() &&
2111 smtp_to3_mbx.domain().empty()) {
2112 smtp_to_mbx = to_mbx;
2115 if (smtp_to_mbx.domain().empty() && FLAGS_mx_host.empty()) {
2116 LOG(ERROR) << "don't know who to send this mail to";
2117 return 0;
2120 if (!smtp_to2_mbx.domain().empty() &&
2121 smtp_to2_mbx.domain() != smtp_to_mbx.domain()) {
2122 LOG(ERROR) << "can't send to both " << smtp_to_mbx.domain() << " and "
2123 << smtp_to2_mbx.domain();
2124 return 0;
2127 if (!smtp_to3_mbx.domain().empty() &&
2128 smtp_to3_mbx.domain() != smtp_to_mbx.domain()) {
2129 LOG(ERROR) << "can't send to both " << smtp_to_mbx.domain() << " and "
2130 << smtp_to3_mbx.domain();
2131 return 0;
2134 auto const port{osutil::get_port(FLAGS_service.c_str(), "tcp")};
2136 auto res{DNS::Resolver{config_path}};
2137 auto tlsa_rrs{get_tlsa_rrs(res, smtp_to_mbx.domain(), port)};
2139 if (FLAGS_pipe) {
2140 return snd(config_path, STDIN_FILENO, STDOUT_FILENO, sender,
2141 smtp_to_mbx.domain(), tlsa_rrs, false, from_mbx, to_mbx,
2142 smtp_from_mbx, smtp_to_mbx, smtp_to2_mbx, smtp_to3_mbx, bodies)
2143 ? EXIT_SUCCESS
2144 : EXIT_FAILURE;
2147 bool enforce_dane = true;
2148 auto const receivers = get_receivers(res, smtp_to_mbx, enforce_dane);
2150 if (receivers.empty()) {
2151 LOG(INFO) << "no place to send this mail";
2152 return EXIT_SUCCESS;
2155 for (auto const& receiver : receivers) {
2156 LOG(INFO) << "trying " << receiver << ":" << FLAGS_service;
2158 if (FLAGS_noconn) {
2159 LOG(INFO) << "skipping";
2160 continue;
2163 auto fd = conn(res, receiver, port);
2164 if (fd == -1) {
2165 LOG(WARNING) << "no connection, skipping";
2166 continue;
2169 // Get our local IP address as "us".
2171 sa::sockaddrs us_addr{};
2172 socklen_t us_addr_len{sizeof us_addr};
2173 char us_addr_str[INET6_ADDRSTRLEN]{'\0'};
2174 std::vector<std::string> fcrdns;
2175 bool private_addr = false;
2177 if (-1 == getsockname(fd, &us_addr.addr, &us_addr_len)) {
2178 // Ignore ENOTSOCK errors from getsockname, useful for testing.
2179 PLOG_IF(WARNING, ENOTSOCK != errno) << "getsockname failed";
2181 else {
2182 switch (us_addr_len) {
2183 case sizeof(sockaddr_in):
2184 PCHECK(inet_ntop(AF_INET, &us_addr.addr_in.sin_addr, us_addr_str,
2185 sizeof us_addr_str) != nullptr);
2186 if (IP4::is_private(us_addr_str))
2187 private_addr = true;
2188 else
2189 fcrdns = DNS::fcrdns4(res, us_addr_str);
2190 break;
2192 case sizeof(sockaddr_in6):
2193 PCHECK(inet_ntop(AF_INET6, &us_addr.addr_in6.sin6_addr, us_addr_str,
2194 sizeof us_addr_str) != nullptr);
2195 if (IP6::is_private(us_addr_str))
2196 private_addr = true;
2197 else
2198 fcrdns = DNS::fcrdns6(res, us_addr_str);
2199 break;
2201 default:
2202 LOG(FATAL) << "bogus address length (" << us_addr_len
2203 << ") returned from getsockname";
2207 if (fcrdns.size()) {
2208 LOG(INFO) << "our names are:";
2209 for (auto const& fc : fcrdns) {
2210 LOG(INFO) << " " << fc;
2214 if (!private_addr) {
2215 // look at from_mbx.domain() and get SPF records
2217 // also look at our ID to get SPF records.
2220 if (smtp_to_mbx.domain() == receiver) {
2221 if (snd(config_path, fd, fd, sender, receiver, tlsa_rrs, enforce_dane,
2222 from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx, smtp_to2_mbx,
2223 smtp_to3_mbx, bodies)) {
2224 return EXIT_SUCCESS;
2227 else {
2228 auto tlsa_rrs_mx{get_tlsa_rrs(res, receiver, port)};
2229 tlsa_rrs_mx.insert(end(tlsa_rrs_mx), begin(tlsa_rrs), end(tlsa_rrs));
2230 if (snd(config_path, fd, fd, sender, receiver, tlsa_rrs_mx, enforce_dane,
2231 from_mbx, to_mbx, smtp_from_mbx, smtp_to_mbx, smtp_to2_mbx,
2232 smtp_to3_mbx, bodies)) {
2233 return EXIT_SUCCESS;
2237 close(fd);
2240 LOG(ERROR) << "we ran out of hosts to try";