better with obs forms
[ghsmtp.git] / Send.cpp
blob37f38cd7add6951c8d0864254770d2ee6d093556
1 #include "Send.hpp"
3 #include <random>
5 #include "IP4.hpp"
6 #include "IP6.hpp"
7 #include "imemstream.hpp"
8 #include "message.hpp"
9 #include "osutil.hpp"
11 #include <fmt/format.h>
13 #include <gflags/gflags.h>
15 // This needs to be at least the length of each string it's trying to match.
16 DEFINE_uint64(pbfr_size, 4 * 1024, "parser buffer size");
18 DEFINE_bool(use_esmtp, true, "use ESMTP (EHLO)");
20 DEFINE_string(local_address, "", "local address to bind");
22 #include <boost/algorithm/string/case_conv.hpp>
24 #include <tao/pegtl.hpp>
25 #include <tao/pegtl/contrib/abnf.hpp>
27 using namespace tao::pegtl;
28 using namespace tao::pegtl::abnf;
30 using namespace std::string_literals;
32 using std::begin;
33 using std::end;
35 // clang-format off
37 namespace chars {
38 struct tail : range<'\x80', '\xBF'> {};
40 struct ch_1 : range<'\x00', '\x7F'> {};
42 struct ch_2 : seq<range<'\xC2', '\xDF'>, tail> {};
44 struct ch_3 : sor<seq<one<'\xE0'>, range<'\xA0', '\xBF'>, tail>,
45 seq<range<'\xE1', '\xEC'>, rep<2, tail>>,
46 seq<one<'\xED'>, range<'\x80', '\x9F'>, tail>,
47 seq<range<'\xEE', '\xEF'>, rep<2, tail>>> {};
49 struct ch_4 : sor<seq<one<'\xF0'>, range<'\x90', '\xBF'>, rep<2, tail>>,
50 seq<range<'\xF1', '\xF3'>, rep<3, tail>>,
51 seq<one<'\xF4'>, range<'\x80', '\x8F'>, rep<2, tail>>> {};
53 struct u8char : sor<ch_1, ch_2, ch_3, ch_4> {};
55 struct non_ascii : sor<ch_2, ch_3, ch_4> {};
57 struct ascii_only : seq<star<ch_1>, eof> {};
59 struct utf8_only : seq<star<u8char>, eof> {};
62 namespace SMTP {
64 // clang-format off
66 using dot = one<'.'>;
67 using colon = one<':'>;
68 using dash = one<'-'>;
70 struct u_let_dig : sor<ALPHA, DIGIT, chars::non_ascii> {};
72 struct u_ldh_tail : star<sor<seq<plus<one<'-'>>, u_let_dig>, u_let_dig>> {};
74 struct u_label : seq<u_let_dig, u_ldh_tail> {};
76 struct let_dig : sor<ALPHA, DIGIT> {};
78 struct ldh_tail : star<sor<seq<plus<one<'-'>>, let_dig>, let_dig>> {};
80 struct ldh_str : seq<let_dig, ldh_tail> {};
82 struct label : ldh_str {};
84 struct sub_domain : sor<label, u_label> {};
86 struct domain : list<sub_domain, dot> {};
88 struct dec_octet : sor<seq<string<'2','5'>, range<'0','5'>>,
89 seq<one<'2'>, range<'0','4'>, DIGIT>,
90 seq<range<'0', '1'>, rep<2, DIGIT>>,
91 rep_min_max<1, 2, DIGIT>> {};
93 struct IPv4_address_literal
94 : seq<dec_octet, dot, dec_octet, dot, dec_octet, dot, dec_octet> {};
96 struct h16 : rep_min_max<1, 4, HEXDIG> {};
98 struct ls32 : sor<seq<h16, colon, h16>, IPv4_address_literal> {};
100 struct dcolon : two<':'> {};
102 struct IPv6address : sor<seq< rep<6, h16, colon>, ls32>,
103 seq< dcolon, rep<5, h16, colon>, ls32>,
104 seq<opt<h16 >, dcolon, rep<4, h16, colon>, ls32>,
105 seq<opt<h16, opt< colon, h16>>, dcolon, rep<3, h16, colon>, ls32>,
106 seq<opt<h16, rep_opt<2, colon, h16>>, dcolon, rep<2, h16, colon>, ls32>,
107 seq<opt<h16, rep_opt<3, colon, h16>>, dcolon, h16, colon, ls32>,
108 seq<opt<h16, rep_opt<4, colon, h16>>, dcolon, ls32>,
109 seq<opt<h16, rep_opt<5, colon, h16>>, dcolon, h16>,
110 seq<opt<h16, rep_opt<6, colon, h16>>, dcolon >> {};
112 struct IPv6_address_literal : seq<TAO_PEGTL_ISTRING("IPv6:"), IPv6address> {};
114 struct dcontent : ranges<33, 90, 94, 126> {};
116 struct standardized_tag : ldh_str {};
118 struct general_address_literal : seq<standardized_tag, colon, plus<dcontent>> {};
120 // See rfc 5321 Section 4.1.3
121 struct address_literal : seq<one<'['>,
122 sor<IPv4_address_literal,
123 IPv6_address_literal,
124 general_address_literal>,
125 one<']'>> {};
128 struct qtextSMTP : sor<ranges<32, 33, 35, 91, 93, 126>, chars::non_ascii> {};
129 struct graphic : range<32, 126> {};
130 struct quoted_pairSMTP : seq<one<'\\'>, graphic> {};
131 struct qcontentSMTP : sor<qtextSMTP, quoted_pairSMTP> {};
133 // excluded from atext: "(),.@[]"
134 struct atext : sor<ALPHA, DIGIT,
135 one<'!', '#',
136 '$', '%',
137 '&', '\'',
138 '*', '+',
139 '-', '/',
140 '=', '?',
141 '^', '_',
142 '`', '{',
143 '|', '}',
144 '~'>,
145 chars::non_ascii> {};
146 struct atom : plus<atext> {};
147 struct dot_string : list<atom, dot> {};
148 struct quoted_string : seq<one<'"'>, plus<qcontentSMTP>, one<'"'>> {};
149 struct local_part : sor<dot_string, quoted_string> {};
150 struct non_local_part : sor<domain, address_literal> {};
151 struct mailbox : seq<local_part, one<'@'>, non_local_part> {};
153 struct at_domain : seq<one<'@'>, domain> {};
155 struct path : seq<one<'<'>, mailbox, one<'>'>> {};
157 struct path_only : seq<path, eof> {};
159 // textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
161 // Although not explicit in the grammar of RFC-6531, in practice UTF-8
162 // is used in the replys.
164 // struct textstring : plus<sor<one<9>, range<32, 126>>> {};
166 struct textstring : plus<sor<one<9>, range<32, 126>, chars::non_ascii>> {};
168 struct server_id : sor<domain, address_literal> {};
170 // Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF )
171 // /
172 // ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
173 // *( "220-" [ textstring ] CRLF )
174 // "220 " [ textstring ] CRLF )
176 struct greeting_ok
177 : sor<seq<TAO_PEGTL_ISTRING("220 "), server_id, opt<textstring>, CRLF>,
178 seq<TAO_PEGTL_ISTRING("220-"), server_id, opt<textstring>, CRLF,
179 star<seq<TAO_PEGTL_ISTRING("220-"), opt<textstring>, CRLF>>,
180 seq<TAO_PEGTL_ISTRING("220 "), opt<textstring>, CRLF>>> {};
182 // Reply-code = %x32-35 %x30-35 %x30-39
184 struct reply_code
185 : seq<range<0x32, 0x35>, range<0x30, 0x35>, range<0x30, 0x39>> {};
187 // Reply-line = *( Reply-code "-" [ textstring ] CRLF )
188 // Reply-code [ SP textstring ] CRLF
190 struct reply_lines
191 : seq<star<seq<reply_code, one<'-'>, opt<textstring>, CRLF>>,
192 seq<reply_code, opt<seq<SP, textstring>>, CRLF>> {};
194 struct greeting
195 : sor<greeting_ok, reply_lines> {};
197 // ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
198 // ; string of any characters other than CR or LF
200 struct ehlo_greet : plus<ranges<0, 9, 11, 12, 14, 127>> {};
202 // ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
203 // ; additional syntax of ehlo-params depends on
204 // ; ehlo-keyword
206 // The '.' we also allow in ehlo-keyword since it has been seen in the
207 // wild at least at 263.net.
209 struct ehlo_keyword : seq<sor<ALPHA, DIGIT>, star<sor<ALPHA, DIGIT, dash, dot>>> {};
211 // ehlo-param = 1*(%d33-126)
212 // ; any CHAR excluding <SP> and all
213 // ; control characters (US-ASCII 0-31 and 127
214 // ; inclusive)
216 struct ehlo_param : plus<range<33, 126>> {};
218 // ehlo-line = ehlo-keyword *( SP ehlo-param )
220 // The AUTH= thing is so common with some servers (postfix) that I
221 // guess we have to accept it.
223 struct ehlo_line
224 : seq<ehlo_keyword, star<seq<sor<SP,one<'='>>, ehlo_param>>> {};
226 // ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF )
227 // /
228 // ( "250-" Domain [ SP ehlo-greet ] CRLF
229 // *( "250-" ehlo-line CRLF )
230 // "250 " ehlo-line CRLF )
232 // The last line having the optional ehlo_line is not strictly correct.
233 // Was added to work with postfix/src/smtpstone/smtp-sink.c.
235 struct ehlo_ok_rsp
236 : sor<seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF>,
238 seq<TAO_PEGTL_ISTRING("250-"), server_id, opt<ehlo_greet>, CRLF,
239 star<seq<TAO_PEGTL_ISTRING("250-"), ehlo_line, CRLF>>,
240 seq<TAO_PEGTL_ISTRING("250 "), opt<ehlo_line>, CRLF>>
241 > {};
243 struct ehlo_rsp
244 : sor<ehlo_ok_rsp, reply_lines> {};
246 struct helo_ok_rsp
247 : seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF> {};
249 struct auth_login_username
250 : seq<TAO_PEGTL_STRING("334 VXNlcm5hbWU6"), CRLF> {};
252 struct auth_login_password
253 : seq<TAO_PEGTL_STRING("334 UGFzc3dvcmQ6"), CRLF> {};
255 // clang-format on
257 template <typename Rule>
258 struct inaction : nothing<Rule> {
261 template <typename Rule>
262 struct action : nothing<Rule> {
265 template <>
266 struct action<server_id> {
267 template <typename Input>
268 static void apply(Input const& in, Connection& conn)
270 conn.server_id = in.string();
274 template <>
275 struct action<local_part> {
276 template <typename Input>
277 static void apply(Input const& in, Mailbox& mbx)
279 mbx.set_local(in.string());
283 template <>
284 struct action<non_local_part> {
285 template <typename Input>
286 static void apply(Input const& in, Mailbox& mbx)
288 mbx.set_domain(in.string());
292 template <>
293 struct action<greeting_ok> {
294 template <typename Input>
295 static void apply(Input const& in, Connection& conn)
297 conn.greeting_ok = true;
298 imemstream stream{begin(in), size(in)};
299 std::string line;
300 while (std::getline(stream, line)) {
301 LOG(INFO) << "S: " << line;
306 template <>
307 struct action<ehlo_ok_rsp> {
308 template <typename Input>
309 static void apply(Input const& in, Connection& conn)
311 conn.ehlo_ok = true;
312 imemstream stream{begin(in), size(in)};
313 std::string line;
314 while (std::getline(stream, line)) {
315 LOG(INFO) << "S: " << line;
320 template <>
321 struct action<ehlo_keyword> {
322 template <typename Input>
323 static void apply(Input const& in, Connection& conn)
325 conn.ehlo_keyword = in.string();
326 boost::to_upper(conn.ehlo_keyword);
330 template <>
331 struct action<ehlo_param> {
332 template <typename Input>
333 static void apply(Input const& in, Connection& conn)
335 conn.ehlo_param.push_back(in.string());
339 template <>
340 struct action<ehlo_line> {
341 template <typename Input>
342 static void apply(Input const& in, Connection& conn)
344 conn.ehlo_params.emplace(std::move(conn.ehlo_keyword),
345 std::move(conn.ehlo_param));
349 template <>
350 struct action<reply_lines> {
351 template <typename Input>
352 static void apply(Input const& in, Connection& conn)
354 imemstream stream{begin(in), size(in)};
355 std::string line;
356 while (std::getline(stream, line)) {
357 LOG(INFO) << "S: " << line;
362 template <>
363 struct action<reply_code> {
364 template <typename Input>
365 static void apply(Input const& in, Connection& conn)
367 conn.reply_code = in.string();
370 } // namespace SMTP
372 namespace {
373 bool is_localhost(DNS::RR const& rr)
375 if (std::holds_alternative<DNS::RR_MX>(rr)) {
376 if (iequal(std::get<DNS::RR_MX>(rr).exchange(), "localhost"))
377 return true;
379 return false;
382 std::vector<Domain> get_mxs(DNS::Resolver& res, Domain const& domain)
384 auto mxs{std::vector<Domain>{}};
386 // Non-local part is an address literal.
387 if (domain.is_address_literal()) {
388 mxs.emplace_back(domain);
389 return mxs;
392 // RFC 5321 section 5.1 "Locating the Target Host"
394 // “The lookup first attempts to locate an MX record associated with
395 // the name. If a CNAME record is found, the resulting name is
396 // processed as if it were the initial name.”
398 // Our (full) resolver will traverse any CNAMEs for us and return
399 // the CNAME and MX records all together.
401 auto const& dom = domain.ascii();
403 auto q{DNS::Query{res, DNS::RR_type::MX, dom}};
404 auto mx_recs{q.get_records()};
406 mx_recs.erase(std::remove_if(begin(mx_recs), end(mx_recs), is_localhost),
407 end(mx_recs));
409 auto const nmx =
410 std::count_if(begin(mx_recs), end(mx_recs), [](auto const& rr) {
411 return std::holds_alternative<DNS::RR_MX>(rr);
414 if (nmx == 1) {
415 for (auto const& mx : mx_recs) {
416 if (std::holds_alternative<DNS::RR_MX>(mx)) {
417 // RFC 7505 null MX record
418 if ((std::get<DNS::RR_MX>(mx).preference() == 0) &&
419 (std::get<DNS::RR_MX>(mx).exchange().empty() ||
420 (std::get<DNS::RR_MX>(mx).exchange() == "."))) {
421 LOG(WARNING) << "domain " << dom << " does not accept mail";
422 return mxs;
428 if (nmx == 0) {
429 // domain must have address record
430 mxs.emplace_back(dom);
431 return mxs;
434 // […] then the sender-SMTP MUST randomize them to spread the load
435 // across multiple mail exchangers for a specific organization.
436 std::shuffle(begin(mx_recs), end(mx_recs), std::random_device());
437 std::sort(begin(mx_recs), end(mx_recs), [](auto const& a, auto const& b) {
438 if (std::holds_alternative<DNS::RR_MX>(a) &&
439 std::holds_alternative<DNS::RR_MX>(b)) {
440 return std::get<DNS::RR_MX>(a).preference() <
441 std::get<DNS::RR_MX>(b).preference();
443 return false;
446 LOG(INFO) << "MXs for " << domain << " are:";
447 for (auto const& mx : mx_recs) {
448 if (std::holds_alternative<DNS::RR_MX>(mx)) {
449 mxs.emplace_back(std::get<DNS::RR_MX>(mx).exchange());
450 LOG(INFO) << std::setfill(' ') << std::setw(3)
451 << std::get<DNS::RR_MX>(mx).preference() << " "
452 << std::get<DNS::RR_MX>(mx).exchange();
456 for (auto const& mx : mxs) {
457 if (mx.is_address_literal()) {
458 LOG(WARNING) << "MX record for " << dom
459 << " contains address literal: " << mx;
463 return mxs;
466 int conn(DNS::Resolver& res, Domain const& node, uint16_t port)
468 int fd = socket(AF_INET, SOCK_STREAM, 0);
469 PCHECK(fd >= 0) << "socket() failed";
471 if (!FLAGS_local_address.empty()) {
472 auto loc{sockaddr_in{}};
473 loc.sin_family = AF_INET;
474 if (1 != inet_pton(AF_INET, FLAGS_local_address.c_str(),
475 reinterpret_cast<void*>(&loc.sin_addr))) {
476 LOG(FATAL) << "can't interpret " << FLAGS_local_address
477 << " as IPv4 address";
479 PCHECK(0 == bind(fd, reinterpret_cast<sockaddr*>(&loc), sizeof(loc)));
482 auto addrs{std::vector<std::string>{}};
483 if (node.is_address_literal()) {
484 if (IP4::is_address(node.ascii())) {
485 addrs.push_back(node.ascii());
487 if (IP4::is_address_literal(node.ascii())) {
488 auto const addr = IP4::as_address(node.ascii());
489 addrs.push_back(std::string(addr.data(), addr.length()));
492 else {
493 addrs = res.get_strings(DNS::RR_type::A, node.ascii());
495 for (auto addr : addrs) {
496 auto in4{sockaddr_in{}};
497 in4.sin_family = AF_INET;
498 in4.sin_port = htons(port);
499 CHECK_EQ(inet_pton(AF_INET, addr.c_str(),
500 reinterpret_cast<void*>(&in4.sin_addr)),
502 if (connect(fd, reinterpret_cast<const sockaddr*>(&in4), sizeof(in4))) {
503 PLOG(WARNING) << "connect failed " << addr << ":" << port;
504 continue;
507 // LOG(INFO) << fd << " connected to " << addr << ":" << port;
508 return fd;
511 close(fd);
512 return -1;
515 std::optional<std::unique_ptr<SMTP::Connection>>
516 open_session(DNS::Resolver& res,
517 fs::path config_path,
518 Domain sender,
519 Domain mx,
520 char const* service)
522 auto const port{osutil::get_port(service, "tcp")};
524 int fd = conn(res, mx, port);
525 if (fd == -1) {
526 LOG(WARNING) << mx << " no connection";
527 return {};
530 // Listen for greeting
532 auto constexpr read_hook{[]() {}};
533 auto conn = std::make_unique<SMTP::Connection>(fd, fd, read_hook);
535 auto in =
536 istream_input<eol::crlf, 1>{conn->sock.in(), FLAGS_pbfr_size, "session"};
537 if (!parse<SMTP::greeting, SMTP::action>(in, *conn)) {
538 LOG(WARNING) << "greeting was unrecognizable";
539 close(fd);
540 return {};
542 if (!conn->greeting_ok) {
543 LOG(WARNING) << "greeting was not in the affirmative";
544 close(fd);
545 return {};
548 // EHLO/HELO
550 auto use_esmtp = FLAGS_use_esmtp;
551 if (use_esmtp) {
552 LOG(INFO) << "C: EHLO " << sender.ascii();
553 conn->sock.out() << "EHLO " << sender.ascii() << "\r\n" << std::flush;
554 if (!parse<SMTP::ehlo_rsp, SMTP::action>(in, *conn) || !conn->ehlo_ok) {
555 LOG(WARNING) << "EHLO response was unrecognizable, trying HELO";
556 use_esmtp = false;
559 if (!use_esmtp) {
560 LOG(INFO) << "C: HELO " << sender.ascii();
561 conn->sock.out() << "HELO " << sender.ascii() << "\r\n" << std::flush;
562 if (!parse<SMTP::helo_ok_rsp, SMTP::action>(in, *conn)) {
563 LOG(ERROR) << "HELO response was unrecognizable";
564 close(fd);
565 return {};
569 // STARTTLS
571 if (conn->has_extension("STARTTLS")) {
572 LOG(INFO) << "C: STARTTLS";
573 conn->sock.out() << "STARTTLS\r\n" << std::flush;
574 if (!parse<SMTP::reply_lines, SMTP::action>(in, *conn)) {
575 LOG(ERROR) << "STARTTLS response was unrecognizable";
576 close(fd);
577 return {};
580 DNS::RR_collection tlsa_rrs; // FIXME
581 if (!conn->sock.starttls_client(config_path, sender.ascii().c_str(),
582 mx.ascii().c_str(), tlsa_rrs, false)) {
583 LOG(WARNING) << "failed to STARTTLS";
584 close(fd);
585 return {};
588 LOG(INFO) << "C: EHLO " << sender.ascii();
589 conn->sock.out() << "EHLO " << sender.ascii() << "\r\n" << std::flush;
590 if (!parse<SMTP::ehlo_rsp, SMTP::action>(in, *conn) || !conn->ehlo_ok) {
591 LOG(WARNING) << "EHLO response was unrecognizable, trying HELO";
592 close(fd);
593 return {};
597 return std::optional<std::unique_ptr<SMTP::Connection>>(std::move(conn));
600 std::string from_params(SMTP::Connection& conn)
602 std::ostringstream param_stream;
603 // param_stream << " SIZE=" << total_size;
605 if (conn.has_extension("BINARYMIME")) {
606 param_stream << " BODY=BINARYMIME";
608 else if (conn.has_extension("8BITMIME")) {
609 param_stream << " BODY=8BITMIME";
612 if (conn.has_extension("SMTPUTF8")) {
613 param_stream << " SMTPUTF8";
616 return param_stream.str();
619 bool do_reply_lines(SMTP::Connection& conn,
620 std::string_view info,
621 std::string& error_msg)
623 auto in{istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size,
624 "mail_from"}};
625 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
626 LOG(ERROR) << info << ": reply unparseable";
627 error_msg =
628 "432 4.3.0 Recipient's incoming mail queue has been stopped\r\n";
629 return false;
631 if (conn.reply_code.at(0) == '5') {
632 LOG(WARNING) << info << ": negative reply " << conn.reply_code;
633 error_msg = "554 5.5.4 Permanent error\r\n";
634 return false;
636 if (conn.reply_code.at(0) != '2') {
637 LOG(WARNING) << info << ": negative reply " << conn.reply_code;
638 error_msg =
639 "432 4.3.0 Recipient's incoming mail queue has been stopped\r\n";
640 return false;
643 return true;
646 bool do_mail_from(SMTP::Connection& conn,
647 Mailbox mail_from,
648 std::string& error_msg)
650 auto const param_str = from_params(conn);
652 LOG(INFO) << "C: MAIL FROM:<" << mail_from << '>' << param_str;
653 conn.sock.out() << "MAIL FROM:<" << mail_from << '>' << param_str << "\r\n";
655 conn.sock.out() << std::flush;
657 return do_reply_lines(conn, "MAIL FROM", error_msg);
660 bool do_rcpt_to(SMTP::Connection& conn, Mailbox rcpt_to, std::string& error_msg)
662 LOG(INFO) << "C: RCPT TO:<" << rcpt_to << '>';
663 conn.sock.out() << "RCPT TO:<" << rcpt_to << ">\r\n";
665 conn.sock.out() << std::flush;
667 return do_reply_lines(conn, "RCPT TO", error_msg);
670 bool mail_from_rcpt_to_pipelined(SMTP::Connection& conn,
671 Mailbox mail_from,
672 Mailbox rcpt_to,
673 std::string& error_msg)
675 auto const param_str = from_params(conn);
677 LOG(INFO) << "C: MAIL FROM:<" << mail_from << '>' << param_str;
678 conn.sock.out() << "MAIL FROM:<" << mail_from << '>' << param_str << "\r\n";
680 LOG(INFO) << "C: RCPT TO:<" << rcpt_to << '>';
681 conn.sock.out() << "RCPT TO:<" << rcpt_to << ">\r\n";
683 conn.sock.out() << std::flush;
685 return do_reply_lines(conn, "MAIL FROM", error_msg) &&
686 do_reply_lines(conn, "RCPT TO", error_msg);
689 bool do_mail_from_rcpt_to(SMTP::Connection& conn,
690 Mailbox mail_from,
691 Mailbox rcpt_to,
692 std::string& error_msg)
694 if (conn.has_extension("PIPELINING"))
695 return mail_from_rcpt_to_pipelined(conn, mail_from, rcpt_to, error_msg);
697 if (!do_mail_from(conn, mail_from, error_msg)) {
698 LOG(ERROR) << "MAIL FROM: failed";
699 return false;
701 if (!do_rcpt_to(conn, rcpt_to, error_msg)) {
702 LOG(ERROR) << "RCPT TO: failed";
703 return false;
706 return true;
709 bool do_data(SMTP::Connection& conn, std::istream& is)
711 LOG(INFO) << "C: DATA";
712 conn.sock.out() << "DATA\r\n" << std::flush;
713 auto in{istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "data"}};
714 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
715 LOG(ERROR) << "DATA command reply unparseable";
716 return false;
718 if (conn.reply_code != "354") {
719 LOG(ERROR) << "DATA returned " << conn.reply_code;
720 return false;
723 auto lineno = 0;
724 auto line{std::string{}};
726 while (std::getline(is, line)) {
727 ++lineno;
728 if (!conn.sock.out().good()) {
729 conn.sock.log_stats();
730 LOG(ERROR) << "output no good at line " << lineno;
731 return false;
733 if (line.length() && (line.at(0) == '.')) {
734 conn.sock.out() << '.';
736 conn.sock.out() << line;
737 if (line.back() != '\r') {
738 LOG(WARNING) << "bare new line in message body at line " << lineno;
739 conn.sock.out() << '\r';
741 conn.sock.out() << '\n';
743 if (!conn.sock.out().good()) {
744 LOG(ERROR) << "socket error of some sort after DATA";
745 return false;
748 // Done!
749 conn.sock.out() << ".\r\n" << std::flush;
751 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
752 LOG(ERROR) << "DATA reply unparseable";
753 return false;
756 LOG(INFO) << "reply_code == " << conn.reply_code;
757 return conn.reply_code.at(0) == '2';
760 bool do_bdat(SMTP::Connection& conn, std::istream& is)
762 auto bdat_error = false;
763 std::streamsize const bfr_size = 1024 * 1024;
764 iobuffer<char> bfr(bfr_size);
766 auto in =
767 istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "bdat"};
768 while (!is.eof()) {
769 is.read(bfr.data(), bfr_size);
770 auto const size_read = is.gcount();
772 conn.sock.out() << "BDAT " << size_read << "\r\n";
773 LOG(INFO) << "C: BDAT " << size_read;
775 conn.sock.out().write(bfr.data(), size_read);
776 conn.sock.out() << std::flush;
778 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
779 LOG(ERROR) << "BDAT reply unparseable";
780 bdat_error = true;
781 break;
783 if (conn.reply_code != "250") {
784 LOG(ERROR) << "BDAT returned " << conn.reply_code;
785 bdat_error = true;
786 break;
790 conn.sock.out() << "BDAT 0 LAST\r\n" << std::flush;
791 LOG(INFO) << "C: BDAT 0 LAST";
793 CHECK((parse<SMTP::reply_lines, SMTP::action>(in, conn)));
794 if (conn.reply_code != "250") {
795 LOG(ERROR) << "BDAT 0 LAST returned " << conn.reply_code;
796 return false;
799 return !bdat_error;
802 bool do_send(SMTP::Connection& conn, std::istream& is)
804 if (conn.has_extension("CHUNKING"))
805 return do_bdat(conn, is);
806 return do_data(conn, is);
809 bool do_rset(SMTP::Connection& conn)
811 LOG(INFO) << "C: RSET";
812 conn.sock.out() << "RSET\r\n" << std::flush;
813 auto in =
814 istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "rset"};
815 return parse<SMTP::reply_lines, SMTP::action>(in, conn);
818 bool do_quit(SMTP::Connection& conn)
820 LOG(INFO) << "C: QUIT";
821 conn.sock.out() << "QUIT\r\n" << std::flush;
822 auto in =
823 istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "quit"};
824 return parse<SMTP::reply_lines, SMTP::action>(in, conn);
827 } // namespace
829 Send::Send(fs::path config_path, char const* service)
830 : config_path_(config_path)
831 , service_(service)
835 bool Send::mail_from_rcpt_to(DNS::Resolver& res,
836 Mailbox const& mail_from,
837 Mailbox const& rcpt_to,
838 std::string& error_msg)
840 if (conn_) {
841 conn_->sock.close_fds();
842 conn_.reset(nullptr);
844 // Get a connection to an MX for this domain
845 std::vector<Domain> mxs = get_mxs(res, rcpt_to.domain());
846 CHECK(!mxs.empty());
847 for (auto& mx : mxs) {
848 LOG(INFO) << "### trying " << mx;
849 // Open new connection.
850 if (auto new_conn = open_session(res, config_path_, mail_from.domain(), mx,
851 service_.c_str());
852 new_conn) {
853 LOG(INFO) << "### opened new connection to " << mx;
854 conn_ = std::move(*new_conn);
855 return do_mail_from_rcpt_to(*conn_, mail_from, rcpt_to, error_msg);
859 LOG(WARNING) << "ran out of mail exchangers for " << rcpt_to;
860 error_msg = "432 4.3.0 Recipient's incoming mail queue has been stopped\r\n";
861 return false;
864 bool Send::send(std::string_view msg_input)
866 if (!conn_) {
867 return false;
869 auto is{imemstream{msg_input.data(), msg_input.length()}};
870 if (!do_send(*conn_, is)) {
871 LOG(WARNING) << "failed to send to " << conn_->server_id;
872 return false;
874 return true;
877 void Send::rset()
879 if (!conn_) {
880 return;
882 if (!do_rset(*conn_)) {
883 LOG(WARNING) << "failed to rset " << conn_->server_id;
887 void Send::quit()
889 if (!conn_) {
890 return;
892 if (!do_quit(*conn_)) {
893 LOG(WARNING) << "failed to quit " << conn_->server_id;
895 conn_->sock.close_fds();