allow
[ghsmtp.git] / Send.cpp
blob79ed3fb19cebc39d81e4fe57388a388c8d91b90b
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<'"'>, star<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 a_d_l : list<at_domain, one<','>> {};
157 struct path : seq<opt<seq<a_d_l, colon>>, mailbox> {};
159 struct path_only : seq<path, eof> {};
161 // textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
163 // Although not explicit in the grammar of RFC-6531, in practice UTF-8
164 // is used in the replys.
166 // struct textstring : plus<sor<one<9>, range<32, 126>>> {};
168 struct textstring : plus<sor<one<9>, range<32, 126>, chars::non_ascii>> {};
170 struct server_id : sor<domain, address_literal> {};
172 // Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF )
173 // /
174 // ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
175 // *( "220-" [ textstring ] CRLF )
176 // "220 " [ textstring ] CRLF )
178 struct greeting_ok
179 : sor<seq<TAO_PEGTL_ISTRING("220 "), server_id, opt<textstring>, CRLF>,
180 seq<TAO_PEGTL_ISTRING("220-"), server_id, opt<textstring>, CRLF,
181 star<seq<TAO_PEGTL_ISTRING("220-"), opt<textstring>, CRLF>>,
182 seq<TAO_PEGTL_ISTRING("220 "), opt<textstring>, CRLF>>> {};
184 // Reply-code = %x32-35 %x30-35 %x30-39
186 struct reply_code
187 : seq<range<0x32, 0x35>, range<0x30, 0x35>, range<0x30, 0x39>> {};
189 // Reply-line = *( Reply-code "-" [ textstring ] CRLF )
190 // Reply-code [ SP textstring ] CRLF
192 struct reply_lines
193 : seq<star<seq<reply_code, one<'-'>, opt<textstring>, CRLF>>,
194 seq<reply_code, opt<seq<SP, textstring>>, CRLF>> {};
196 struct greeting
197 : sor<greeting_ok, reply_lines> {};
199 // ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
200 // ; string of any characters other than CR or LF
202 struct ehlo_greet : plus<ranges<0, 9, 11, 12, 14, 127>> {};
204 // ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
205 // ; additional syntax of ehlo-params depends on
206 // ; ehlo-keyword
208 // The '.' we also allow in ehlo-keyword since it has been seen in the
209 // wild at least at 263.net.
211 struct ehlo_keyword : seq<sor<ALPHA, DIGIT>, star<sor<ALPHA, DIGIT, dash, dot>>> {};
213 // ehlo-param = 1*(%d33-126)
214 // ; any CHAR excluding <SP> and all
215 // ; control characters (US-ASCII 0-31 and 127
216 // ; inclusive)
218 struct ehlo_param : plus<range<33, 126>> {};
220 // ehlo-line = ehlo-keyword *( SP ehlo-param )
222 // The AUTH= thing is so common with some servers (postfix) that I
223 // guess we have to accept it.
225 struct ehlo_line
226 : seq<ehlo_keyword, star<seq<sor<SP,one<'='>>, ehlo_param>>> {};
228 // ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF )
229 // /
230 // ( "250-" Domain [ SP ehlo-greet ] CRLF
231 // *( "250-" ehlo-line CRLF )
232 // "250 " ehlo-line CRLF )
234 // The last line having the optional ehlo_line is not strictly correct.
235 // Was added to work with postfix/src/smtpstone/smtp-sink.c.
237 struct ehlo_ok_rsp
238 : sor<seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF>,
240 seq<TAO_PEGTL_ISTRING("250-"), server_id, opt<ehlo_greet>, CRLF,
241 star<seq<TAO_PEGTL_ISTRING("250-"), ehlo_line, CRLF>>,
242 seq<TAO_PEGTL_ISTRING("250 "), opt<ehlo_line>, CRLF>>
243 > {};
245 struct ehlo_rsp
246 : sor<ehlo_ok_rsp, reply_lines> {};
248 struct helo_ok_rsp
249 : seq<TAO_PEGTL_ISTRING("250 "), server_id, opt<ehlo_greet>, CRLF> {};
251 struct auth_login_username
252 : seq<TAO_PEGTL_STRING("334 VXNlcm5hbWU6"), CRLF> {};
254 struct auth_login_password
255 : seq<TAO_PEGTL_STRING("334 UGFzc3dvcmQ6"), CRLF> {};
257 // clang-format on
259 template <typename Rule>
260 struct inaction : nothing<Rule> {
263 template <typename Rule>
264 struct action : nothing<Rule> {
267 template <>
268 struct action<server_id> {
269 template <typename Input>
270 static void apply(Input const& in, Connection& conn)
272 conn.server_id = in.string();
276 template <>
277 struct action<local_part> {
278 template <typename Input>
279 static void apply(Input const& in, Mailbox& mbx)
281 mbx.set_local(in.string());
285 template <>
286 struct action<non_local_part> {
287 template <typename Input>
288 static void apply(Input const& in, Mailbox& mbx)
290 mbx.set_domain(in.string());
294 template <>
295 struct action<greeting_ok> {
296 template <typename Input>
297 static void apply(Input const& in, Connection& conn)
299 conn.greeting_ok = true;
300 imemstream stream{begin(in), size(in)};
301 std::string line;
302 while (std::getline(stream, line)) {
303 LOG(INFO) << "S: " << line;
308 template <>
309 struct action<ehlo_ok_rsp> {
310 template <typename Input>
311 static void apply(Input const& in, Connection& conn)
313 conn.ehlo_ok = true;
314 imemstream stream{begin(in), size(in)};
315 std::string line;
316 while (std::getline(stream, line)) {
317 LOG(INFO) << "S: " << line;
322 template <>
323 struct action<ehlo_keyword> {
324 template <typename Input>
325 static void apply(Input const& in, Connection& conn)
327 conn.ehlo_keyword = in.string();
328 boost::to_upper(conn.ehlo_keyword);
332 template <>
333 struct action<ehlo_param> {
334 template <typename Input>
335 static void apply(Input const& in, Connection& conn)
337 conn.ehlo_param.push_back(in.string());
341 template <>
342 struct action<ehlo_line> {
343 template <typename Input>
344 static void apply(Input const& in, Connection& conn)
346 conn.ehlo_params.emplace(std::move(conn.ehlo_keyword),
347 std::move(conn.ehlo_param));
351 template <>
352 struct action<reply_lines> {
353 template <typename Input>
354 static void apply(Input const& in, Connection& conn)
356 imemstream stream{begin(in), size(in)};
357 std::string line;
358 while (std::getline(stream, line)) {
359 LOG(INFO) << "S: " << line;
364 template <>
365 struct action<reply_code> {
366 template <typename Input>
367 static void apply(Input const& in, Connection& conn)
369 conn.reply_code = in.string();
372 } // namespace SMTP
374 namespace {
375 bool is_localhost(DNS::RR const& rr)
377 if (std::holds_alternative<DNS::RR_MX>(rr)) {
378 if (iequal(std::get<DNS::RR_MX>(rr).exchange(), "localhost"))
379 return true;
381 return false;
384 std::vector<Domain> get_mxs(DNS::Resolver& res, Domain const& domain)
386 auto mxs{std::vector<Domain>{}};
388 // Non-local part is an address literal.
389 if (domain.is_address_literal()) {
390 mxs.emplace_back(domain);
391 return mxs;
394 // RFC 5321 section 5.1 "Locating the Target Host"
396 // “The lookup first attempts to locate an MX record associated with
397 // the name. If a CNAME record is found, the resulting name is
398 // processed as if it were the initial name.”
400 // Our (full) resolver will traverse any CNAMEs for us and return
401 // the CNAME and MX records all together.
403 auto const& dom = domain.ascii();
405 auto q{DNS::Query{res, DNS::RR_type::MX, dom}};
406 auto mx_recs{q.get_records()};
408 mx_recs.erase(std::remove_if(begin(mx_recs), end(mx_recs), is_localhost),
409 end(mx_recs));
411 auto const nmx =
412 std::count_if(begin(mx_recs), end(mx_recs), [](auto const& rr) {
413 return std::holds_alternative<DNS::RR_MX>(rr);
416 if (nmx == 1) {
417 for (auto const& mx : mx_recs) {
418 if (std::holds_alternative<DNS::RR_MX>(mx)) {
419 // RFC 7505 null MX record
420 if ((std::get<DNS::RR_MX>(mx).preference() == 0) &&
421 (std::get<DNS::RR_MX>(mx).exchange().empty() ||
422 (std::get<DNS::RR_MX>(mx).exchange() == "."))) {
423 LOG(WARNING) << "domain " << dom << " does not accept mail";
424 return mxs;
430 if (nmx == 0) {
431 // domain must have address record
432 mxs.emplace_back(dom);
433 return mxs;
436 // […] then the sender-SMTP MUST randomize them to spread the load
437 // across multiple mail exchangers for a specific organization.
438 std::shuffle(begin(mx_recs), end(mx_recs), std::random_device());
439 std::sort(begin(mx_recs), end(mx_recs), [](auto const& a, auto const& b) {
440 if (std::holds_alternative<DNS::RR_MX>(a) &&
441 std::holds_alternative<DNS::RR_MX>(b)) {
442 return std::get<DNS::RR_MX>(a).preference() <
443 std::get<DNS::RR_MX>(b).preference();
445 return false;
448 LOG(INFO) << "MXs for " << domain << " are:";
449 for (auto const& mx : mx_recs) {
450 if (std::holds_alternative<DNS::RR_MX>(mx)) {
451 mxs.emplace_back(std::get<DNS::RR_MX>(mx).exchange());
452 LOG(INFO) << std::setfill(' ') << std::setw(3)
453 << std::get<DNS::RR_MX>(mx).preference() << " "
454 << std::get<DNS::RR_MX>(mx).exchange();
458 for (auto const& mx : mxs) {
459 if (mx.is_address_literal()) {
460 LOG(WARNING) << "MX record for " << dom
461 << " contains address literal: " << mx;
465 return mxs;
468 int conn(DNS::Resolver& res, Domain const& node, uint16_t port)
470 int fd = socket(AF_INET, SOCK_STREAM, 0);
471 PCHECK(fd >= 0) << "socket() failed";
473 if (!FLAGS_local_address.empty()) {
474 auto loc{sockaddr_in{}};
475 loc.sin_family = AF_INET;
476 if (1 != inet_pton(AF_INET, FLAGS_local_address.c_str(),
477 reinterpret_cast<void*>(&loc.sin_addr))) {
478 LOG(FATAL) << "can't interpret " << FLAGS_local_address
479 << " as IPv4 address";
481 PCHECK(0 == bind(fd, reinterpret_cast<sockaddr*>(&loc), sizeof(loc)));
484 auto addrs{std::vector<std::string>{}};
485 if (node.is_address_literal()) {
486 if (IP4::is_address(node.ascii())) {
487 addrs.push_back(node.ascii());
489 if (IP4::is_address_literal(node.ascii())) {
490 auto const addr = IP4::as_address(node.ascii());
491 addrs.push_back(std::string(addr.data(), addr.length()));
494 else {
495 addrs = res.get_strings(DNS::RR_type::A, node.ascii());
497 for (auto addr : addrs) {
498 auto in4{sockaddr_in{}};
499 in4.sin_family = AF_INET;
500 in4.sin_port = htons(port);
501 CHECK_EQ(inet_pton(AF_INET, addr.c_str(),
502 reinterpret_cast<void*>(&in4.sin_addr)),
504 if (connect(fd, reinterpret_cast<const sockaddr*>(&in4), sizeof(in4))) {
505 PLOG(WARNING) << "connect failed " << addr << ":" << port;
506 continue;
509 // LOG(INFO) << fd << " connected to " << addr << ":" << port;
510 return fd;
513 close(fd);
514 return -1;
517 std::optional<std::unique_ptr<SMTP::Connection>>
518 open_session(DNS::Resolver& res,
519 fs::path config_path,
520 Domain sender,
521 Domain mx,
522 char const* service)
524 auto const port{osutil::get_port(service, "tcp")};
526 int fd = conn(res, mx, port);
527 if (fd == -1) {
528 LOG(WARNING) << mx << " no connection";
529 return {};
532 // Listen for greeting
534 auto constexpr read_hook{[]() {}};
535 auto conn = std::make_unique<SMTP::Connection>(fd, fd, read_hook);
537 auto in =
538 istream_input<eol::crlf, 1>{conn->sock.in(), FLAGS_pbfr_size, "session"};
539 if (!parse<SMTP::greeting, SMTP::action>(in, *conn)) {
540 LOG(WARNING) << "greeting was unrecognizable";
541 close(fd);
542 return {};
544 if (!conn->greeting_ok) {
545 LOG(WARNING) << "greeting was not in the affirmative";
546 close(fd);
547 return {};
550 // EHLO/HELO
552 auto use_esmtp = FLAGS_use_esmtp;
553 if (use_esmtp) {
554 LOG(INFO) << "C: EHLO " << sender.ascii();
555 conn->sock.out() << "EHLO " << sender.ascii() << "\r\n" << std::flush;
556 if (!parse<SMTP::ehlo_rsp, SMTP::action>(in, *conn) || !conn->ehlo_ok) {
557 LOG(WARNING) << "EHLO response was unrecognizable, trying HELO";
558 use_esmtp = false;
561 if (!use_esmtp) {
562 LOG(INFO) << "C: HELO " << sender.ascii();
563 conn->sock.out() << "HELO " << sender.ascii() << "\r\n" << std::flush;
564 if (!parse<SMTP::helo_ok_rsp, SMTP::action>(in, *conn)) {
565 LOG(ERROR) << "HELO response was unrecognizable";
566 close(fd);
567 return {};
571 // STARTTLS
573 if (conn->has_extension("STARTTLS")) {
574 LOG(INFO) << "C: STARTTLS";
575 conn->sock.out() << "STARTTLS\r\n" << std::flush;
576 if (!parse<SMTP::reply_lines, SMTP::action>(in, *conn)) {
577 LOG(ERROR) << "STARTTLS response was unrecognizable";
578 close(fd);
579 return {};
582 DNS::RR_collection tlsa_rrs; // FIXME
583 if (!conn->sock.starttls_client(config_path, sender.ascii().c_str(),
584 mx.ascii().c_str(), tlsa_rrs, false)) {
585 LOG(WARNING) << "failed to STARTTLS";
586 close(fd);
587 return {};
590 LOG(INFO) << "C: EHLO " << sender.ascii();
591 conn->sock.out() << "EHLO " << sender.ascii() << "\r\n" << std::flush;
592 if (!parse<SMTP::ehlo_rsp, SMTP::action>(in, *conn) || !conn->ehlo_ok) {
593 LOG(WARNING) << "EHLO response was unrecognizable, trying HELO";
594 close(fd);
595 return {};
599 return std::optional<std::unique_ptr<SMTP::Connection>>(std::move(conn));
602 std::string from_params(SMTP::Connection& conn)
604 std::ostringstream param_stream;
605 // param_stream << " SIZE=" << total_size;
607 if (conn.has_extension("BINARYMIME")) {
608 param_stream << " BODY=BINARYMIME";
610 else if (conn.has_extension("8BITMIME")) {
611 param_stream << " BODY=8BITMIME";
614 if (conn.has_extension("SMTPUTF8")) {
615 param_stream << " SMTPUTF8";
618 return param_stream.str();
621 bool do_reply_lines(SMTP::Connection& conn,
622 std::string_view info,
623 std::string& error_msg)
625 auto in{istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size,
626 "mail_from"}};
627 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
628 LOG(ERROR) << info << ": reply unparseable";
629 error_msg =
630 "432 4.3.0 Recipient's incoming mail queue has been stopped\r\n";
631 return false;
633 if (conn.reply_code.at(0) == '5') {
634 LOG(WARNING) << info << ": negative reply " << conn.reply_code;
635 error_msg = "554 5.5.4 Permanent error\r\n";
636 return false;
638 if (conn.reply_code.at(0) != '2') {
639 LOG(WARNING) << info << ": negative reply " << conn.reply_code;
640 error_msg =
641 "432 4.3.0 Recipient's incoming mail queue has been stopped\r\n";
642 return false;
645 return true;
648 bool do_mail_from(SMTP::Connection& conn,
649 Mailbox mail_from,
650 std::string& error_msg)
652 auto const param_str = from_params(conn);
654 LOG(INFO) << "C: MAIL FROM:<" << mail_from << '>' << param_str;
655 conn.sock.out() << "MAIL FROM:<" << mail_from << '>' << param_str << "\r\n";
657 conn.sock.out() << std::flush;
659 return do_reply_lines(conn, "MAIL FROM", error_msg);
662 bool do_rcpt_to(SMTP::Connection& conn, Mailbox rcpt_to, std::string& error_msg)
664 LOG(INFO) << "C: RCPT TO:<" << rcpt_to << '>';
665 conn.sock.out() << "RCPT TO:<" << rcpt_to << ">\r\n";
667 conn.sock.out() << std::flush;
669 return do_reply_lines(conn, "RCPT TO", error_msg);
672 bool mail_from_rcpt_to_pipelined(SMTP::Connection& conn,
673 Mailbox mail_from,
674 Mailbox rcpt_to,
675 std::string& error_msg)
677 auto const param_str = from_params(conn);
679 LOG(INFO) << "C: MAIL FROM:<" << mail_from << '>' << param_str;
680 conn.sock.out() << "MAIL FROM:<" << mail_from << '>' << param_str << "\r\n";
682 LOG(INFO) << "C: RCPT TO:<" << rcpt_to << '>';
683 conn.sock.out() << "RCPT TO:<" << rcpt_to << ">\r\n";
685 conn.sock.out() << std::flush;
687 return do_reply_lines(conn, "MAIL FROM", error_msg) &&
688 do_reply_lines(conn, "RCPT TO", error_msg);
691 bool do_mail_from_rcpt_to(SMTP::Connection& conn,
692 Mailbox mail_from,
693 Mailbox rcpt_to,
694 std::string& error_msg)
696 if (conn.has_extension("PIPELINING"))
697 return mail_from_rcpt_to_pipelined(conn, mail_from, rcpt_to, error_msg);
699 if (!do_mail_from(conn, mail_from, error_msg)) {
700 LOG(ERROR) << "MAIL FROM: failed";
701 return false;
703 if (!do_rcpt_to(conn, rcpt_to, error_msg)) {
704 LOG(ERROR) << "RCPT TO: failed";
705 return false;
708 return true;
711 bool do_data(SMTP::Connection& conn, std::istream& is)
713 LOG(INFO) << "C: DATA";
714 conn.sock.out() << "DATA\r\n" << std::flush;
715 auto in{istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "data"}};
716 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
717 LOG(ERROR) << "DATA command reply unparseable";
718 return false;
720 if (conn.reply_code != "354") {
721 LOG(ERROR) << "DATA returned " << conn.reply_code;
722 return false;
725 auto lineno = 0;
726 auto line{std::string{}};
728 while (std::getline(is, line)) {
729 ++lineno;
730 if (!conn.sock.out().good()) {
731 conn.sock.log_stats();
732 LOG(ERROR) << "output no good at line " << lineno;
733 return false;
735 if (line.length() && (line.at(0) == '.')) {
736 conn.sock.out() << '.';
738 conn.sock.out() << line;
739 if (line.back() != '\r') {
740 LOG(WARNING) << "bare new line in message body at line " << lineno;
741 conn.sock.out() << '\r';
743 conn.sock.out() << '\n';
745 if (!conn.sock.out().good()) {
746 LOG(ERROR) << "socket error of some sort after DATA";
747 return false;
750 // Done!
751 conn.sock.out() << ".\r\n" << std::flush;
753 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
754 LOG(ERROR) << "DATA reply unparseable";
755 return false;
758 LOG(INFO) << "reply_code == " << conn.reply_code;
759 return conn.reply_code.at(0) == '2';
762 bool do_bdat(SMTP::Connection& conn, std::istream& is)
764 auto bdat_error = false;
765 std::streamsize const bfr_size = 1024 * 1024;
766 iobuffer<char> bfr(bfr_size);
768 auto in =
769 istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "bdat"};
770 while (!is.eof()) {
771 is.read(bfr.data(), bfr_size);
772 auto const size_read = is.gcount();
774 conn.sock.out() << "BDAT " << size_read << "\r\n";
775 LOG(INFO) << "C: BDAT " << size_read;
777 conn.sock.out().write(bfr.data(), size_read);
778 conn.sock.out() << std::flush;
780 if (!parse<SMTP::reply_lines, SMTP::action>(in, conn)) {
781 LOG(ERROR) << "BDAT reply unparseable";
782 bdat_error = true;
783 break;
785 if (conn.reply_code != "250") {
786 LOG(ERROR) << "BDAT returned " << conn.reply_code;
787 bdat_error = true;
788 break;
792 conn.sock.out() << "BDAT 0 LAST\r\n" << std::flush;
793 LOG(INFO) << "C: BDAT 0 LAST";
795 CHECK((parse<SMTP::reply_lines, SMTP::action>(in, conn)));
796 if (conn.reply_code != "250") {
797 LOG(ERROR) << "BDAT 0 LAST returned " << conn.reply_code;
798 return false;
801 return !bdat_error;
804 bool do_send(SMTP::Connection& conn, std::istream& is)
806 if (conn.has_extension("CHUNKING"))
807 return do_bdat(conn, is);
808 return do_data(conn, is);
811 bool do_rset(SMTP::Connection& conn)
813 LOG(INFO) << "C: RSET";
814 conn.sock.out() << "RSET\r\n" << std::flush;
815 auto in =
816 istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "rset"};
817 return parse<SMTP::reply_lines, SMTP::action>(in, conn);
820 bool do_quit(SMTP::Connection& conn)
822 LOG(INFO) << "C: QUIT";
823 conn.sock.out() << "QUIT\r\n" << std::flush;
824 auto in =
825 istream_input<eol::crlf, 1>{conn.sock.in(), FLAGS_pbfr_size, "quit"};
826 return parse<SMTP::reply_lines, SMTP::action>(in, conn);
829 } // namespace
831 Send::Send(fs::path config_path, char const* service)
832 : config_path_(config_path)
833 , service_(service)
837 bool Send::mail_from_rcpt_to(DNS::Resolver& res,
838 Mailbox const& mail_from,
839 Mailbox const& rcpt_to,
840 std::string& error_msg)
842 if (conn_) {
843 conn_->sock.close_fds();
844 conn_.reset(nullptr);
846 // Get a connection to an MX for this domain
847 std::vector<Domain> mxs = get_mxs(res, rcpt_to.domain());
848 CHECK(!mxs.empty());
849 for (auto& mx : mxs) {
850 LOG(INFO) << "### trying " << mx;
851 // Open new connection.
852 if (auto new_conn = open_session(res, config_path_, mail_from.domain(), mx,
853 service_.c_str());
854 new_conn) {
855 LOG(INFO) << "### opened new connection to " << mx;
856 conn_ = std::move(*new_conn);
857 return do_mail_from_rcpt_to(*conn_, mail_from, rcpt_to, error_msg);
861 LOG(WARNING) << "ran out of mail exchangers for " << rcpt_to;
862 error_msg = "432 4.3.0 Recipient's incoming mail queue has been stopped\r\n";
863 return false;
866 bool Send::send(std::string_view msg_input)
868 if (!conn_) {
869 return false;
871 auto is{imemstream{msg_input.data(), msg_input.length()}};
872 if (!do_send(*conn_, is)) {
873 LOG(WARNING) << "failed to send to " << conn_->server_id;
874 return false;
876 return true;
879 void Send::rset()
881 if (!conn_) {
882 return;
884 if (!do_rset(*conn_)) {
885 LOG(WARNING) << "failed to rset " << conn_->server_id;
889 void Send::quit()
891 if (!conn_) {
892 return;
894 if (!do_quit(*conn_)) {
895 LOG(WARNING) << "failed to quit " << conn_->server_id;
897 conn_->sock.close_fds();