7 #include "imemstream.hpp"
11 #include <gflags/gflags.h>
13 // This needs to be at least the length of each string it's trying to match.
14 DEFINE_uint64(pbfr_size
, 4 * 1024, "parser buffer size");
16 DEFINE_bool(use_esmtp
, true, "use ESMTP (EHLO)");
18 DEFINE_string(local_address
, "", "local address to bind");
20 #include <boost/algorithm/string/case_conv.hpp>
22 #include <tao/pegtl.hpp>
23 #include <tao/pegtl/contrib/abnf.hpp>
25 using namespace tao::pegtl
;
26 using namespace tao::pegtl::abnf
;
28 using namespace std::string_literals
;
36 struct tail
: range
<'\x80', '\xBF'> {};
38 struct ch_1
: range
<'\x00', '\x7F'> {};
40 struct ch_2
: seq
<range
<'\xC2', '\xDF'>, tail
> {};
42 struct ch_3
: sor
<seq
<one
<'\xE0'>, range
<'\xA0', '\xBF'>, tail
>,
43 seq
<range
<'\xE1', '\xEC'>, rep
<2, tail
>>,
44 seq
<one
<'\xED'>, range
<'\x80', '\x9F'>, tail
>,
45 seq
<range
<'\xEE', '\xEF'>, rep
<2, tail
>>> {};
47 struct ch_4
: sor
<seq
<one
<'\xF0'>, range
<'\x90', '\xBF'>, rep
<2, tail
>>,
48 seq
<range
<'\xF1', '\xF3'>, rep
<3, tail
>>,
49 seq
<one
<'\xF4'>, range
<'\x80', '\x8F'>, rep
<2, tail
>>> {};
51 struct u8char
: sor
<ch_1
, ch_2
, ch_3
, ch_4
> {};
53 struct non_ascii
: sor
<ch_2
, ch_3
, ch_4
> {};
55 struct ascii_only
: seq
<star
<ch_1
>, eof
> {};
57 struct utf8_only
: seq
<star
<u8char
>, eof
> {};
65 using colon
= one
<':'>;
66 using dash
= one
<'-'>;
68 struct u_let_dig
: sor
<ALPHA
, DIGIT
, chars::non_ascii
> {};
70 struct u_ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, u_let_dig
>, u_let_dig
>> {};
72 struct u_label
: seq
<u_let_dig
, u_ldh_tail
> {};
74 struct let_dig
: sor
<ALPHA
, DIGIT
> {};
76 struct ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, let_dig
>, let_dig
>> {};
78 struct ldh_str
: seq
<let_dig
, ldh_tail
> {};
80 struct label
: ldh_str
{};
82 struct sub_domain
: sor
<label
, u_label
> {};
84 struct domain
: list
<sub_domain
, dot
> {};
86 struct dec_octet
: sor
<seq
<string
<'2','5'>, range
<'0','5'>>,
87 seq
<one
<'2'>, range
<'0','4'>, DIGIT
>,
88 seq
<range
<'0', '1'>, rep
<2, DIGIT
>>,
89 rep_min_max
<1, 2, DIGIT
>> {};
91 struct IPv4_address_literal
92 : seq
<dec_octet
, dot
, dec_octet
, dot
, dec_octet
, dot
, dec_octet
> {};
94 struct h16
: rep_min_max
<1, 4, HEXDIG
> {};
96 struct ls32
: sor
<seq
<h16
, colon
, h16
>, IPv4_address_literal
> {};
98 struct dcolon
: two
<':'> {};
100 struct IPv6address
: sor
<seq
< rep
<6, h16
, colon
>, ls32
>,
101 seq
< dcolon
, rep
<5, h16
, colon
>, ls32
>,
102 seq
<opt
<h16
>, dcolon
, rep
<4, h16
, colon
>, ls32
>,
103 seq
<opt
<h16
, opt
< colon
, h16
>>, dcolon
, rep
<3, h16
, colon
>, ls32
>,
104 seq
<opt
<h16
, rep_opt
<2, colon
, h16
>>, dcolon
, rep
<2, h16
, colon
>, ls32
>,
105 seq
<opt
<h16
, rep_opt
<3, colon
, h16
>>, dcolon
, h16
, colon
, ls32
>,
106 seq
<opt
<h16
, rep_opt
<4, colon
, h16
>>, dcolon
, ls32
>,
107 seq
<opt
<h16
, rep_opt
<5, colon
, h16
>>, dcolon
, h16
>,
108 seq
<opt
<h16
, rep_opt
<6, colon
, h16
>>, dcolon
>> {};
110 struct IPv6_address_literal
: seq
<TAO_PEGTL_ISTRING("IPv6:"), IPv6address
> {};
112 struct dcontent
: ranges
<33, 90, 94, 126> {};
114 struct standardized_tag
: ldh_str
{};
116 struct general_address_literal
: seq
<standardized_tag
, colon
, plus
<dcontent
>> {};
118 // See rfc 5321 Section 4.1.3
119 struct address_literal
: seq
<one
<'['>,
120 sor
<IPv4_address_literal
,
121 IPv6_address_literal
,
122 general_address_literal
>,
126 struct qtextSMTP
: sor
<ranges
<32, 33, 35, 91, 93, 126>, chars::non_ascii
> {};
127 struct graphic
: range
<32, 126> {};
128 struct quoted_pairSMTP
: seq
<one
<'\\'>, graphic
> {};
129 struct qcontentSMTP
: sor
<qtextSMTP
, quoted_pairSMTP
> {};
131 // excluded from atext: "(),.@[]"
132 struct atext
: sor
<ALPHA
, DIGIT
,
143 chars::non_ascii
> {};
144 struct atom
: plus
<atext
> {};
145 struct dot_string
: list
<atom
, dot
> {};
146 struct quoted_string
: seq
<one
<'"'>, star
<qcontentSMTP
>, one
<'"'>> {};
147 struct local_part
: sor
<dot_string
, quoted_string
> {};
148 struct non_local_part
: sor
<domain
, address_literal
> {};
149 struct mailbox
: seq
<local_part
, one
<'@'>, non_local_part
> {};
151 struct at_domain
: seq
<one
<'@'>, domain
> {};
153 struct a_d_l
: list
<at_domain
, one
<','>> {};
155 struct path
: seq
<opt
<seq
<a_d_l
, colon
>>, mailbox
> {};
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 )
172 // ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
173 // *( "220-" [ textstring ] CRLF )
174 // "220 " [ textstring ] CRLF )
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
185 : seq
<range
<0x32, 0x35>, range
<0x30, 0x35>, range
<0x30, 0x39>> {};
187 // Reply-line = *( Reply-code "-" [ textstring ] CRLF )
188 // Reply-code [ SP textstring ] CRLF
191 : seq
<star
<seq
<reply_code
, one
<'-'>, opt
<textstring
>, CRLF
>>,
192 seq
<reply_code
, opt
<seq
<SP
, textstring
>>, CRLF
>> {};
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
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
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.
224 : seq
<ehlo_keyword
, star
<seq
<sor
<SP
,one
<'='>>, ehlo_param
>>> {};
226 // ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF )
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.
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
>>
244 : sor
<ehlo_ok_rsp
, reply_lines
> {};
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
> {};
257 template <typename Rule
>
258 struct inaction
: nothing
<Rule
> {
261 template <typename Rule
>
262 struct action
: nothing
<Rule
> {
266 struct action
<server_id
> {
267 template <typename Input
>
268 static void apply(Input
const& in
, Connection
& conn
)
270 conn
.server_id
= in
.string();
275 struct action
<local_part
> {
276 template <typename Input
>
277 static void apply(Input
const& in
, Mailbox
& mbx
)
279 mbx
.set_local(in
.string());
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());
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
)};
300 while (std::getline(stream
, line
)) {
301 LOG(INFO
) << "S: " << line
;
307 struct action
<ehlo_ok_rsp
> {
308 template <typename Input
>
309 static void apply(Input
const& in
, Connection
& conn
)
312 imemstream stream
{begin(in
), size(in
)};
314 while (std::getline(stream
, line
)) {
315 LOG(INFO
) << "S: " << line
;
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
);
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());
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
));
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
)};
356 while (std::getline(stream
, line
)) {
357 LOG(INFO
) << "S: " << line
;
363 struct action
<reply_code
> {
364 template <typename Input
>
365 static void apply(Input
const& in
, Connection
& conn
)
367 conn
.reply_code
= in
.string();
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"))
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
);
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
),
410 std::count_if(begin(mx_recs
), end(mx_recs
), [](auto const& rr
) {
411 return std::holds_alternative
<DNS::RR_MX
>(rr
);
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";
429 // domain must have address record
430 mxs
.emplace_back(dom
);
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();
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();
459 int conn(DNS::Resolver
& res
, Domain
const& node
, uint16_t port
)
461 int fd
= socket(AF_INET
, SOCK_STREAM
, 0);
462 PCHECK(fd
>= 0) << "socket() failed";
464 if (!FLAGS_local_address
.empty()) {
465 auto loc
{sockaddr_in
{}};
466 loc
.sin_family
= AF_INET
;
467 if (1 != inet_pton(AF_INET
, FLAGS_local_address
.c_str(),
468 reinterpret_cast<void*>(&loc
.sin_addr
))) {
469 LOG(FATAL
) << "can't interpret " << FLAGS_local_address
470 << " as IPv4 address";
472 PCHECK(0 == bind(fd
, reinterpret_cast<sockaddr
*>(&loc
), sizeof(loc
)));
475 auto addrs
{std::vector
<std::string
>{}};
476 if (node
.is_address_literal()) {
477 if (IP4::is_address(node
.ascii())) {
478 addrs
.push_back(node
.ascii());
480 if (IP4::is_address_literal(node
.ascii())) {
481 auto const addr
= IP4::as_address(node
.ascii());
482 addrs
.push_back(std::string(addr
.data(), addr
.length()));
486 addrs
= res
.get_strings(DNS::RR_type::A
, node
.ascii());
488 for (auto addr
: addrs
) {
489 auto in4
{sockaddr_in
{}};
490 in4
.sin_family
= AF_INET
;
491 in4
.sin_port
= htons(port
);
492 CHECK_EQ(inet_pton(AF_INET
, addr
.c_str(),
493 reinterpret_cast<void*>(&in4
.sin_addr
)),
495 if (connect(fd
, reinterpret_cast<const sockaddr
*>(&in4
), sizeof(in4
))) {
496 PLOG(WARNING
) << "connect failed " << addr
<< ":" << port
;
500 // LOG(INFO) << fd << " connected to " << addr << ":" << port;
508 std::optional
<std::unique_ptr
<SMTP::Connection
>>
509 open_session(DNS::Resolver
& res
,
510 fs::path config_path
,
515 auto const port
{osutil::get_port(service
, "tcp")};
517 int fd
= conn(res
, mx
, port
);
519 LOG(WARNING
) << mx
<< " no connection";
523 // Listen for greeting
525 auto constexpr read_hook
{[]() {}};
526 auto conn
= std::make_unique
<SMTP::Connection
>(fd
, fd
, read_hook
);
529 istream_input
<eol::crlf
, 1>{conn
->sock
.in(), FLAGS_pbfr_size
, "session"};
530 if (!parse
<SMTP::greeting
, SMTP::action
>(in
, *conn
)) {
531 LOG(WARNING
) << "greeting was unrecognizable";
535 if (!conn
->greeting_ok
) {
536 LOG(WARNING
) << "greeting was not in the affirmative";
543 auto use_esmtp
= FLAGS_use_esmtp
;
545 LOG(INFO
) << "C: EHLO " << sender
.ascii();
546 conn
->sock
.out() << "EHLO " << sender
.ascii() << "\r\n" << std::flush
;
547 if (!parse
<SMTP::ehlo_rsp
, SMTP::action
>(in
, *conn
) || !conn
->ehlo_ok
) {
548 LOG(WARNING
) << "EHLO response was unrecognizable, trying HELO";
553 LOG(INFO
) << "C: HELO " << sender
.ascii();
554 conn
->sock
.out() << "HELO " << sender
.ascii() << "\r\n" << std::flush
;
555 if (!parse
<SMTP::helo_ok_rsp
, SMTP::action
>(in
, *conn
)) {
556 LOG(WARNING
) << "HELO response was unrecognizable";
564 if (conn
->has_extension("STARTTLS")) {
565 LOG(INFO
) << "C: STARTTLS";
566 conn
->sock
.out() << "STARTTLS\r\n" << std::flush
;
567 if (!parse
<SMTP::reply_lines
, SMTP::action
>(in
, *conn
)) {
568 LOG(WARNING
) << "STARTTLS response was unrecognizable";
573 DNS::RR_collection tlsa_rrs
; // FIXME
574 if (!conn
->sock
.starttls_client(config_path
, sender
.ascii().c_str(),
575 mx
.ascii().c_str(), tlsa_rrs
, false)) {
576 LOG(WARNING
) << "failed to STARTTLS";
582 return std::optional
<std::unique_ptr
<SMTP::Connection
>>(std::move(conn
));
585 bool do_mail_from(SMTP::Connection
& conn
, Mailbox from
)
587 std::ostringstream param_stream
;
588 // param_stream << " SIZE=" << total_size;
590 if (conn
.has_extension("BINARYMIME")) {
591 param_stream
<< " BODY=BINARYMIME";
593 else if (conn
.has_extension("8BITMIME")) {
594 param_stream
<< " BODY=8BITMIME";
597 if (conn
.has_extension("SMTPUTF8")) {
598 param_stream
<< " SMTPUTF8";
601 auto const param_str
= param_stream
.str();
603 LOG(INFO
) << "C: MAIL FROM:<" << from
<< '>' << param_str
;
604 conn
.sock
.out() << "MAIL FROM:<" << from
<< '>' << param_str
<< "\r\n"
606 auto in
{istream_input
<eol::crlf
, 1>{conn
.sock
.in(), FLAGS_pbfr_size
,
608 if (!parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
)) {
609 LOG(ERROR
) << "MAIL FROM: reply unparseable";
612 if (conn
.reply_code
.at(0) != '2') {
613 LOG(WARNING
) << "MAIL FROM: negative reply " << conn
.reply_code
;
617 conn
.mail_from
= from
;
621 bool do_rcpt_to(SMTP::Connection
& conn
, Mailbox from
, Mailbox to
)
623 if (conn
.mail_from
!= from
) {
624 do_mail_from(conn
, from
);
627 if (std::find(begin(conn
.rcpt_to
), end(conn
.rcpt_to
), to
) !=
629 LOG(INFO
) << to
<< " already in recpt_to list of " << conn
.server_id
;
633 LOG(INFO
) << "C: RCPT TO:<" << to
<< '>';
634 conn
.sock
.out() << "RCPT TO:<" << to
<< ">\r\n" << std::flush
;
636 istream_input
<eol::crlf
, 1>{conn
.sock
.in(), FLAGS_pbfr_size
, "rcpt_to"}};
637 if (!parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
)) {
638 LOG(ERROR
) << "RCPT TO: reply unparseable";
641 if (conn
.reply_code
.at(0) != '2') {
642 LOG(WARNING
) << "RCPT TO: negative reply " << conn
.reply_code
;
646 conn
.rcpt_to
.emplace_back(to
);
650 bool do_data(SMTP::Connection
& conn
, std::istream
& is
)
652 LOG(INFO
) << "C: DATA";
653 conn
.sock
.out() << "DATA\r\n" << std::flush
;
654 auto in
{istream_input
<eol::crlf
, 1>{conn
.sock
.in(), FLAGS_pbfr_size
, "data"}};
655 if (!parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
)) {
656 LOG(ERROR
) << "DATA command reply unparseable";
659 if (conn
.reply_code
!= "354") {
660 LOG(ERROR
) << "DATA returned " << conn
.reply_code
;
665 auto line
{std::string
{}};
667 while (std::getline(is
, line
)) {
669 if (!conn
.sock
.out().good()) {
670 conn
.sock
.log_stats();
671 LOG(ERROR
) << "output no good at line " << lineno
;
674 if (line
.length() && (line
.at(0) == '.')) {
675 conn
.sock
.out() << '.';
677 conn
.sock
.out() << line
;
678 if (line
.back() != '\r') {
679 LOG(WARNING
) << "bare new line in message body at line " << lineno
;
680 conn
.sock
.out() << '\r';
682 conn
.sock
.out() << '\n';
684 if (!conn
.sock
.out().good()) {
685 LOG(ERROR
) << "socket error of some sort after DATA";
690 conn
.sock
.out() << ".\r\n" << std::flush
;
692 if (!parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
)) {
693 LOG(ERROR
) << "DATA reply unparseable";
697 LOG(INFO
) << "reply_code == " << conn
.reply_code
;
698 return conn
.reply_code
.at(0) == '2';
701 bool do_bdat(SMTP::Connection
& conn
, std::istream
& is
)
703 auto bdat_error
= false;
704 std::streamsize
const bfr_size
= 1024 * 1024;
705 iobuffer
<char> bfr(bfr_size
);
708 istream_input
<eol::crlf
, 1>{conn
.sock
.in(), FLAGS_pbfr_size
, "bdat"};
710 is
.read(bfr
.data(), bfr_size
);
711 auto const size_read
= is
.gcount();
713 conn
.sock
.out() << "BDAT " << size_read
<< "\r\n";
714 LOG(INFO
) << "C: BDAT " << size_read
;
716 conn
.sock
.out().write(bfr
.data(), size_read
);
717 conn
.sock
.out() << std::flush
;
719 if (!parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
)) {
720 LOG(ERROR
) << "BDAT reply unparseable";
724 if (conn
.reply_code
!= "250") {
725 LOG(ERROR
) << "BDAT returned " << conn
.reply_code
;
731 conn
.sock
.out() << "BDAT 0 LAST\r\n" << std::flush
;
732 LOG(INFO
) << "C: BDAT 0 LAST";
734 CHECK((parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
)));
735 if (conn
.reply_code
!= "250") {
736 LOG(ERROR
) << "BDAT 0 LAST returned " << conn
.reply_code
;
743 bool do_send(SMTP::Connection
& conn
, std::istream
& is
)
745 if (conn
.has_extension("CHUNKING"))
746 return do_bdat(conn
, is
);
747 return do_data(conn
, is
);
750 bool do_rset(SMTP::Connection
& conn
)
752 LOG(INFO
) << "C: RSET";
753 conn
.sock
.out() << "RSET\r\n" << std::flush
;
755 istream_input
<eol::crlf
, 1>{conn
.sock
.in(), FLAGS_pbfr_size
, "rset"};
756 return parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
);
759 bool do_quit(SMTP::Connection
& conn
)
761 LOG(INFO
) << "C: QUIT";
762 conn
.sock
.out() << "QUIT\r\n" << std::flush
;
764 istream_input
<eol::crlf
, 1>{conn
.sock
.in(), FLAGS_pbfr_size
, "quit"};
765 return parse
<SMTP::reply_lines
, SMTP::action
>(in
, conn
);
770 Send::Send(fs::path config_path
, char const* service
)
771 : config_path_(config_path
)
776 bool Send::mail_from(Mailbox
const& mailbox
)
778 mail_from_
= mailbox
;
780 for (auto& [mx
, conn
] : exchangers_
) {
781 conn
->mail_from
.clear();
782 conn
->rcpt_to
.clear();
787 bool Send::rcpt_to(DNS::Resolver
& res
,
789 std::string
& error_msg
)
791 if (mail_from_
.empty()) {
792 LOG(WARNING
) << "sequence error, must have MAIL FROM: before RCPT TO:";
796 // Check for existing receivers entry.
797 if (auto rec
= receivers_
.find(to
.domain()); rec
!= receivers_
.end()) {
798 if (auto ex
= exchangers_
.find(rec
->second
); ex
!= exchangers_
.end()) {
799 auto& conn
= ex
->second
;
800 LOG(INFO
) << "### found existing receiver";
801 LOG(INFO
) << "### do_rcpt_to(" << mail_from_
<< ", " << to
<< ");";
802 return do_rcpt_to(*conn
, mail_from_
, to
);
804 LOG(ERROR
) << "found a receiver but not an exchanger "
805 << "from == " << mail_from_
<< " to == " << to
;
809 // Get a connection to an MX for this domain
810 std::vector
<Domain
> mxs
= get_mxs(res
, to
.domain());
812 for (auto& mx
: mxs
) {
813 // Check for existing connection.
814 if (auto ex
= exchangers_
.find(mx
); ex
!= exchangers_
.end()) {
815 LOG(INFO
) << "### found existing connection to " << mx
;
816 receivers_
.emplace(to
.domain(), mx
);
817 auto& conn
= ex
->second
;
818 LOG(INFO
) << "### do_rcpt_to(" << mail_from_
<< ", " << to
<< ");";
819 return do_rcpt_to(*conn
, mail_from_
, to
);
821 // Open new connection.
822 if (auto new_conn
= open_session(res
, config_path_
, mail_from_
.domain(), mx
,
825 LOG(INFO
) << "### opened new connection to " << mx
;
826 exchangers_
.emplace(mx
, std::move(*new_conn
));
827 auto ex
= exchangers_
.find(mx
);
828 CHECK(ex
!= exchangers_
.end());
829 receivers_
.emplace(to
.domain(), mx
);
830 auto& conn
= ex
->second
;
831 LOG(INFO
) << "### do_rcpt_to(" << mail_from_
<< ", " << to
<< ");";
832 return do_rcpt_to(*conn
, mail_from_
, to
);
836 LOG(WARNING
) << "ran out of mail exchangers for " << to
;
840 bool Send::send(std::string_view msg_input
)
842 // FIXME this should be done in parallel
843 for (auto& [dom
, conn
] : exchangers_
) {
844 if (!conn
->rcpt_to
.empty()) {
846 auto is
{imemstream
{msg_input
.data(), msg_input
.length()}};
847 if (!do_send(*conn
, is
)) {
848 LOG(WARNING
) << "failed to send to " << conn
->server_id
;
853 LOG(INFO
) << "no receivers, skipping MX " << dom
;
855 conn
->mail_from
.clear();
856 conn
->rcpt_to
.clear();
863 for (auto& [dom
, conn
] : exchangers_
) {
864 if (!conn
->rcpt_to
.empty()) {
865 if (!do_rset(*conn
)) {
866 LOG(WARNING
) << "failed to rset " << conn
->server_id
;
869 conn
->mail_from
.clear();
870 conn
->rcpt_to
.clear();
876 for (auto& [dom
, conn
] : exchangers_
) {
878 conn
->sock
.close_fds();