1 #include <gflags/gflags.h>
5 // This needs to be at least the length of each string it's trying to match.
6 DEFINE_uint64(bfr_size
, 4 * 1024, "parser buffer size");
8 DEFINE_uint64(max_xfer_size
, 64 * 1024, "maximum BDAT transfer size");
12 #include "Session.hpp"
15 #include "iobuffer.hpp"
23 #include <tao/pegtl.hpp>
24 #include <tao/pegtl/contrib/abnf.hpp>
26 using namespace tao::pegtl
;
27 using namespace tao::pegtl::abnf
;
37 std::pair
<std::string
, std::string
> param
;
38 std::unordered_map
<std::string
, std::string
> parameters
;
40 std::streamsize chunk_size
;
42 Ctx(fs::path config_path
, std::function
<void(void)> read_hook
)
43 : session(config_path
, read_hook
)
50 struct UTF8_tail
: range
<'\x80', '\xBF'> {};
52 struct UTF8_1
: range
<'\x00', '\x7F'> {};
54 struct UTF8_2
: seq
<range
<'\xC2', '\xDF'>, UTF8_tail
> {};
56 struct UTF8_3
: sor
<seq
<one
<'\xE0'>, range
<'\xA0', '\xBF'>, UTF8_tail
>,
57 seq
<range
<'\xE1', '\xEC'>, rep
<2, UTF8_tail
>>,
58 seq
<one
<'\xED'>, range
<'\x80', '\x9F'>, UTF8_tail
>,
59 seq
<range
<'\xEE', '\xEF'>, rep
<2, UTF8_tail
>>> {};
61 struct UTF8_4
: sor
<seq
<one
<'\xF0'>, range
<'\x90', '\xBF'>, rep
<2, UTF8_tail
>>,
62 seq
<range
<'\xF1', '\xF3'>, rep
<3, UTF8_tail
>>,
63 seq
<one
<'\xF4'>, range
<'\x80', '\x8F'>, rep
<2, UTF8_tail
>>> {};
65 struct UTF8_non_ascii
: sor
<UTF8_2
, UTF8_3
, UTF8_4
> {};
67 struct quoted_pair
: seq
<one
<'\\'>, sor
<VCHAR
, WSP
>> {};
70 using colon
= one
<':'>;
71 using dash
= one
<'-'>;
73 struct u_let_dig
: sor
<ALPHA
, DIGIT
, UTF8_non_ascii
> {};
75 struct u_ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, u_let_dig
>, u_let_dig
>> {};
77 struct u_label
: seq
<u_let_dig
, u_ldh_tail
> {};
79 struct let_dig
: sor
<ALPHA
, DIGIT
> {};
81 struct ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, let_dig
>, let_dig
>> {};
83 struct ldh_str
: seq
<let_dig
, ldh_tail
> {};
85 // struct label : seq<let_dig, opt<ldh_str>> {};
87 struct sub_domain
: u_label
{};
89 struct domain
: list_tail
<sub_domain
, dot
> {};
91 struct dec_octet
: sor
<seq
<string
<'2','5'>, range
<'0','5'>>,
92 seq
<one
<'2'>, range
<'0','4'>, DIGIT
>,
93 seq
<range
<'0', '1'>, rep
<2, DIGIT
>>,
94 rep_min_max
<1, 2, DIGIT
>> {};
96 struct IPv4_address_literal
97 : seq
<dec_octet
, dot
, dec_octet
, dot
, dec_octet
, dot
, dec_octet
> {};
99 struct h16
: rep_min_max
<1, 4, HEXDIG
> {};
101 struct ls32
: sor
<seq
<h16
, colon
, h16
>, IPv4_address_literal
> {};
103 struct dcolon
: two
<':'> {};
105 struct IPv6address
: sor
<seq
< rep
<6, h16
, colon
>, ls32
>,
106 seq
< dcolon
, rep
<5, h16
, colon
>, ls32
>,
107 seq
<opt
<h16
>, dcolon
, rep
<4, h16
, colon
>, ls32
>,
108 seq
<opt
<h16
, opt
< colon
, h16
>>, dcolon
, rep
<3, h16
, colon
>, ls32
>,
109 seq
<opt
<h16
, rep_opt
<2, colon
, h16
>>, dcolon
, rep
<2, h16
, colon
>, ls32
>,
110 seq
<opt
<h16
, rep_opt
<3, colon
, h16
>>, dcolon
, h16
, colon
, ls32
>,
111 seq
<opt
<h16
, rep_opt
<4, colon
, h16
>>, dcolon
, ls32
>,
112 seq
<opt
<h16
, rep_opt
<5, colon
, h16
>>, dcolon
, h16
>,
113 seq
<opt
<h16
, rep_opt
<6, colon
, h16
>>, dcolon
>> {};
115 struct IPv6_address_literal
: seq
<TAO_PEGTL_ISTRING("IPv6:"), IPv6address
> {};
117 struct dcontent
: ranges
<33, 90, 94, 126> {};
119 struct standardized_tag
: ldh_str
{};
121 struct general_address_literal
: seq
<standardized_tag
, colon
, plus
<dcontent
>> {};
123 // See rfc 5321 Section 4.1.3
124 struct address_literal
: seq
<one
<'['>,
125 sor
<IPv4_address_literal
,
126 IPv6_address_literal
,
127 general_address_literal
>,
130 struct at_domain
: seq
<one
<'@'>, domain
> {};
132 struct a_d_l
: list
<at_domain
, one
<','>> {};
134 // The qtextSMTP rule explained: it's ASCII...
135 // excluding all the control chars below SPACE
136 // 34 '"' the double quote
137 // 92 '\\' the back slash
140 // 32-33: ' ' and '!'
141 // 35-91: "#$%&'()*+,-./" 0-9 ":;<=>?@" A-Z '['
142 // 93-126: "]^_`" a-z "{|}~"
144 struct qtextSMTP
: sor
<ranges
<32, 33, 35, 91, 93, 126>, UTF8_non_ascii
> {};
146 struct graphic
: range
<32, 126> {};
148 struct quoted_pairSMTP
: seq
<one
<'\\'>, graphic
> {};
150 struct qcontentSMTP
: sor
<qtextSMTP
, quoted_pairSMTP
> {};
152 struct quoted_string
: seq
<one
<'"'>, star
<qcontentSMTP
>, one
<'"'>> {};
154 // excluded from atext are the “specials”: "()<>[]:;@\\,."
156 struct atext
: sor
<ALPHA
, DIGIT
,
169 struct atom
: plus
<atext
> {};
171 struct dot_string
: list
<atom
, dot
> {};
173 struct local_part
: sor
<dot_string
, quoted_string
> {};
175 struct non_local_part
: sor
<domain
, address_literal
> {};
177 struct mailbox
: seq
<local_part
, one
<'@'>, non_local_part
> {};
179 struct path
: seq
<one
<'<'>, seq
<opt
<seq
<a_d_l
, colon
>>, mailbox
, one
<'>'>>> {};
181 struct bounce_path
: TAO_PEGTL_ISTRING("<>") {};
183 struct reverse_path
: sor
<path
, bounce_path
> {};
185 struct magic_postmaster
: TAO_PEGTL_ISTRING("<Postmaster>") {};
187 struct forward_path
: sor
<path
, magic_postmaster
> {};
189 struct esmtp_keyword
: seq
<sor
<ALPHA
, DIGIT
>, star
<sor
<ALPHA
, DIGIT
, dash
>>> {};
191 struct esmtp_value
: plus
<sor
<range
<33, 60>, range
<62, 126>, UTF8_non_ascii
>> {};
193 struct esmtp_param
: seq
<esmtp_keyword
, opt
<seq
<one
<'='>, esmtp_value
>>> {};
195 struct mail_parameters
: list
<esmtp_param
, SP
> {};
197 struct rcpt_parameters
: list
<esmtp_param
, SP
> {};
199 struct string
: sor
<quoted_string
, atom
> {};
201 struct helo
: seq
<TAO_PEGTL_ISTRING("HELO"),
203 sor
<domain
, address_literal
>,
206 struct ehlo
: seq
<TAO_PEGTL_ISTRING("EHLO"),
208 sor
<domain
, address_literal
>,
211 struct mail_from
: seq
<TAO_PEGTL_ISTRING("MAIL"),
212 TAO_PEGTL_ISTRING(" FROM:"),
213 opt
<SP
>, // common enough error, we'll allow it
215 opt
<seq
<SP
, mail_parameters
>>,
218 struct rcpt_to
: seq
<TAO_PEGTL_ISTRING("RCPT"),
219 TAO_PEGTL_ISTRING(" TO:"),
220 opt
<SP
>, // common error
222 opt
<seq
<SP
, rcpt_parameters
>>,
225 struct chunk_size
: plus
<DIGIT
> {};
227 struct last
: TAO_PEGTL_ISTRING("LAST") {};
229 struct bdat
: seq
<TAO_PEGTL_ISTRING("BDAT"), SP
, chunk_size
, CRLF
> {};
231 struct bdat_last
: seq
<TAO_PEGTL_ISTRING("BDAT"), SP
, chunk_size
, SP
, last
, CRLF
> {};
233 struct data
: seq
<TAO_PEGTL_ISTRING("DATA"), CRLF
> {};
235 struct data_end
: seq
<dot
, CRLF
> {};
237 struct data_blank
: CRLF
{};
239 // Rules for strict RFC adherence:
241 // RFC 5321 text line length section 4.5.3.1.6.
243 : seq
<one
<'.'>, rep_min_max
<1, 998, not_one
<'\r', '\n'>>, CRLF
> {};
245 struct data_plain
: seq
<rep_min_max
<1, 998, not_one
<'\r', '\n'>>, CRLF
> {};
247 // But let's accept real-world crud, up to a point...
249 struct anything_else
: seq
<star
<not_one
<'\n'>>, one
<'\n'>> {};
251 // This particular crud will trigger an error return with the "no bare
254 struct not_data_end
: seq
<dot
, LF
> {};
256 struct data_line
: sor
<at
<data_end
>,
264 struct data_grammar
: until
<data_end
, data_line
> {};
266 struct rset
: seq
<TAO_PEGTL_ISTRING("RSET"), CRLF
> {};
268 struct noop
: seq
<TAO_PEGTL_ISTRING("NOOP"), opt
<seq
<SP
, string
>>, CRLF
> {};
270 struct vrfy
: seq
<TAO_PEGTL_ISTRING("VRFY"), opt
<seq
<SP
, string
>>, CRLF
> {};
272 struct help
: seq
<TAO_PEGTL_ISTRING("HELP"), opt
<seq
<SP
, string
>>, CRLF
> {};
275 : seq
<TAO_PEGTL_ISTRING("STAR"), TAO_PEGTL_ISTRING("TTLS"), CRLF
> {};
277 struct quit
: seq
<TAO_PEGTL_ISTRING("QUIT"), CRLF
> {};
281 // base64-char = ALPHA / DIGIT / "+" / "/"
284 struct base64_char
: sor
<ALPHA
, DIGIT
, one
<'+', '/'>> {};
286 // base64-terminal = (2base64-char "==") / (3base64-char "=")
288 struct base64_terminal
: sor
<seq
<rep
<2, base64_char
>, TAO_PEGTL_ISTRING("==")>,
289 seq
<rep
<3, base64_char
>, one
<'='>>
292 // base64 = base64-terminal /
293 // ( 1*(4base64-char) [base64-terminal] )
295 struct base64
: sor
<base64_terminal
,
296 seq
<plus
<rep
<4, base64_char
>>,
297 opt
<base64_terminal
>>
300 // initial-response= base64 / "="
302 struct initial_response
: sor
<base64
, one
<'='>> {};
304 // cancel-response = "*"
306 struct cancel_response
: one
<'*'> {};
308 struct UPPER_ALPHA
: range
<'A', 'Z'> {};
310 using HYPHEN
= one
<'-'>;
311 using UNDERSCORE
= one
<'_'>;
313 struct mech_char
: sor
<UPPER_ALPHA
, DIGIT
, HYPHEN
, UNDERSCORE
> {};
314 struct sasl_mech
: rep_min_max
<1, 20, mech_char
> {};
316 // auth-command = "AUTH" SP sasl-mech [SP initial-response]
317 // *(CRLF [base64]) [CRLF cancel-response]
319 // ;; <sasl-mech> is defined in RFC 4422
321 struct auth
: seq
<TAO_PEGTL_ISTRING("AUTH"), SP
, sasl_mech
,
322 opt
<seq
<SP
, initial_response
>>,
323 // star<CRLF, opt<base64>>,
324 // opt<seq<CRLF, cancel_response>>,
328 struct bogus_cmd_short
: seq
<rep_min_max
<0, 3, not_one
<'\r', '\n'>>, CRLF
> {};
329 struct bogus_cmd_long
: seq
<rep_min_max
<4, 1000, not_one
<'\r', '\n'>>, CRLF
> {};
331 // commands in size order
333 struct any_cmd
: seq
<sor
<bogus_cmd_short
,
352 struct grammar
: plus
<any_cmd
> {};
356 template <typename Rule
>
357 struct action
: nothing
<Rule
> {
360 template <typename Rule
>
361 struct data_action
: nothing
<Rule
> {
365 struct action
<esmtp_keyword
> {
366 template <typename Input
>
367 static void apply(Input
const& in
, Ctx
& ctx
)
369 ctx
.param
.first
= in
.string();
374 struct action
<bogus_cmd_short
> {
375 template <typename Input
>
376 static void apply(Input
const& in
, Ctx
& ctx
)
378 LOG(INFO
) << "bogus_cmd_short";
379 ctx
.session
.cmd_unrecognized(in
.string());
384 struct action
<bogus_cmd_long
> {
385 template <typename Input
>
386 static void apply(Input
const& in
, Ctx
& ctx
)
388 LOG(INFO
) << "bogus_cmd_long";
389 ctx
.session
.cmd_unrecognized(in
.string());
394 struct action
<anything_else
> {
395 template <typename Input
>
396 static void apply(Input
const& in
, Ctx
& ctx
)
398 LOG(INFO
) << "anything_else";
399 ctx
.session
.cmd_unrecognized(in
.string());
404 struct action
<esmtp_value
> {
405 template <typename Input
>
406 static void apply(Input
const& in
, Ctx
& ctx
)
408 ctx
.param
.second
= in
.string();
413 struct action
<esmtp_param
> {
414 template <typename Input
>
415 static void apply(Input
const& in
, Ctx
& ctx
)
417 ctx
.parameters
.insert(ctx
.param
);
418 ctx
.param
.first
.clear();
419 ctx
.param
.second
.clear();
424 struct action
<local_part
> {
425 template <typename Input
>
426 static void apply(Input
const& in
, Ctx
& ctx
)
428 ctx
.mb_loc
= in
.string();
429 // RFC 5321, section 4.5.3.1.1.
430 if (ctx
.mb_loc
.length() > 64) {
431 LOG(WARNING
) << "local part too long " << ctx
.mb_loc
;
437 struct action
<non_local_part
> {
438 template <typename Input
>
439 static void apply(Input
const& in
, Ctx
& ctx
)
441 ctx
.mb_dom
= in
.string();
442 // RFC 5321, section 4.5.3.1.2.
443 if (ctx
.mb_dom
.length() > 255) {
444 LOG(WARNING
) << "domain name or number too long " << ctx
.mb_dom
;
450 struct action
<magic_postmaster
> {
451 static void apply0(Ctx
& ctx
)
453 ctx
.mb_loc
= std::string
{"Postmaster"};
459 struct action
<helo
> {
460 template <typename Input
>
461 static void apply(Input
const& in
, Ctx
& ctx
)
463 auto const b
= begin(in
) + 5; // +5 for the length of "HELO "
464 auto const e
= std::find(b
, end(in
) - 2, ' '); // -2 for the CRLF
465 ctx
.session
.helo(std::string_view(b
, e
- b
));
470 struct action
<ehlo
> {
471 template <typename Input
>
472 static void apply(Input
const& in
, Ctx
& ctx
)
474 auto const b
= begin(in
) + 5; // +5 for the length of "EHLO "
475 auto const e
= std::find(b
, end(in
) - 2, ' '); // -2 for the CRLF
476 ctx
.session
.ehlo(std::string_view(b
, e
- b
));
481 struct action
<mail_from
> {
482 static void apply0(Ctx
& ctx
)
484 ctx
.session
.mail_from(Mailbox
{ctx
.mb_loc
, ctx
.mb_dom
}, ctx
.parameters
);
487 ctx
.parameters
.clear();
492 struct action
<rcpt_to
> {
493 static void apply0(Ctx
& ctx
)
495 ctx
.session
.rcpt_to(Mailbox
{ctx
.mb_loc
, ctx
.mb_dom
}, ctx
.parameters
);
498 ctx
.parameters
.clear();
503 struct action
<chunk_size
> {
504 template <typename Input
>
505 static void apply(Input
const& in
, Ctx
& ctx
)
507 ctx
.chunk_size
= std::strtoull(in
.string().c_str(), nullptr, 10);
511 void bdat_act(Ctx
& ctx
, bool last
)
513 auto status_returned
= false;
515 if (!ctx
.session
.bdat_start(ctx
.chunk_size
))
516 status_returned
= true;
518 auto to_xfer
= ctx
.chunk_size
;
520 auto const bfr_size
{std::min(to_xfer
, std::streamsize(FLAGS_max_xfer_size
))};
521 iobuffer
<char> bfr(bfr_size
);
524 auto const xfer_sz
{std::min(to_xfer
, bfr_size
)};
526 ctx
.session
.in().read(bfr
.data(), xfer_sz
);
527 if (!ctx
.session
.in()) {
528 LOG(ERROR
) << "attempt to read " << xfer_sz
<< " octets but only got "
529 << ctx
.session
.in().gcount();
530 if (ctx
.session
.maxed_out()) {
531 LOG(ERROR
) << "input maxed out";
532 if (!status_returned
)
533 ctx
.session
.bdat_size_error();
535 else if (ctx
.session
.timed_out()) {
536 LOG(ERROR
) << "input timed out";
537 if (!status_returned
)
538 ctx
.session
.bdat_io_error();
540 else if (ctx
.session
.in().eof()) {
541 LOG(ERROR
) << "EOF in BDAT";
542 if (!status_returned
)
543 ctx
.session
.bdat_io_error();
546 LOG(ERROR
) << "I/O error in BDAT";
547 if (!status_returned
)
548 ctx
.session
.bdat_io_error();
552 if (!ctx
.session
.msg_write(bfr
.data(), xfer_sz
)) {
553 if (!status_returned
)
554 ctx
.session
.bdat_size_error();
555 status_returned
= true;
561 if (!status_returned
) {
562 ctx
.session
.bdat_done(ctx
.chunk_size
, last
);
567 struct action
<bdat
> {
568 static void apply0(Ctx
& ctx
)
570 bdat_act(ctx
, false);
575 struct action
<bdat_last
> {
576 static void apply0(Ctx
& ctx
)
583 struct data_action
<data_end
> {
584 static void apply0(Ctx
& ctx
) { ctx
.session
.data_done(); }
588 struct data_action
<data_blank
> {
589 static void apply0(Ctx
& ctx
)
591 constexpr char CRLF
[]{'\r', '\n'};
592 ctx
.session
.msg_write(CRLF
, sizeof(CRLF
));
597 struct data_action
<data_plain
> {
598 template <typename Input
>
599 static void apply(Input
const& in
, Ctx
& ctx
)
601 auto const len
{end(in
) - begin(in
)};
602 ctx
.session
.msg_write(begin(in
), len
);
607 struct data_action
<data_dot
> {
608 template <typename Input
>
609 static void apply(Input
const& in
, Ctx
& ctx
)
611 auto const len
{end(in
) - begin(in
) - 1};
612 ctx
.session
.msg_write(begin(in
) + 1, len
);
617 struct data_action
<not_data_end
> {
618 static void apply0(Ctx
& ctx
) __attribute__((noreturn
))
620 ctx
.session
.bare_lf();
625 struct data_action
<anything_else
> {
626 template <typename Input
>
627 static void apply(Input
const& in
, Ctx
& ctx
)
629 LOG(WARNING
) << "garbage in data stream: \"" << esc(in
.string()) << "\"";
630 auto const len
{end(in
) - begin(in
)};
632 ctx
.session
.msg_write(begin(in
), len
);
634 LOG(WARNING
) << "line too long at " << len
<< " octets";
640 struct action
<data
> {
641 template <typename Input
>
642 static void apply(Input
const& in
, Ctx
& ctx
)
644 if (ctx
.session
.data_start()) {
645 auto din
= istream_input
<eol::crlf
, 1>(ctx
.session
.in(), FLAGS_bfr_size
,
648 if (!parse_nested
<RFC5321::data_grammar
, RFC5321::data_action
>(in
, din
,
650 ctx
.session
.log_stats();
651 if (!(ctx
.session
.maxed_out() || ctx
.session
.timed_out())) {
652 ctx
.session
.error("bad DATA syntax");
657 catch (parse_error
const& e
) {
658 LOG(WARNING
) << e
.what();
659 ctx
.session
.error("unable to parse DATA stream");
661 catch (std::exception
const& e
) {
662 LOG(WARNING
) << e
.what();
663 ctx
.session
.error("unknown problem in DATA stream");
670 struct action
<rset
> {
671 static void apply0(Ctx
& ctx
) { ctx
.session
.rset(); }
674 template <typename Input
>
675 std::string_view
get_string_view(Input
const& in
)
677 auto const b
= begin(in
) + 4;
678 auto const len
= end(in
) - b
;
679 auto str
= std::string_view(b
, len
);
680 if (str
.front() == ' ')
681 str
.remove_prefix(1);
686 struct action
<noop
> {
687 template <typename Input
>
688 static void apply(Input
const& in
, Ctx
& ctx
)
690 auto const str
= get_string_view(in
);
691 ctx
.session
.noop(str
);
696 struct action
<vrfy
> {
697 template <typename Input
>
698 static void apply(Input
const& in
, Ctx
& ctx
)
700 auto const str
= get_string_view(in
);
701 ctx
.session
.vrfy(str
);
706 struct action
<help
> {
707 template <typename Input
>
708 static void apply(Input
const& in
, Ctx
& ctx
)
710 auto const str
= get_string_view(in
);
711 ctx
.session
.help(str
);
716 struct action
<starttls
> {
717 static void apply0(Ctx
& ctx
) { ctx
.session
.starttls(); }
721 struct action
<quit
> {
722 static void apply0(Ctx
& ctx
) __attribute__((noreturn
)) { ctx
.session
.quit(); }
726 struct action
<auth
> {
727 static void apply0(Ctx
& ctx
) __attribute__((noreturn
)) { ctx
.session
.auth(); }
729 } // namespace RFC5321
731 void timeout(int signum
) __attribute__((noreturn
));
732 void timeout(int signum
)
734 const char errmsg
[] = "421 4.4.2 time-out\r\n";
735 write(STDOUT_FILENO
, errmsg
, sizeof errmsg
- 1);
739 int main(int argc
, char* argv
[])
741 std::ios::sync_with_stdio(false);
743 { // Need to work with either namespace.
744 using namespace gflags
;
745 using namespace google
;
746 ParseCommandLineFlags(&argc
, &argv
, true);
749 // Set timeout signal handler to limit total run time.
750 struct sigaction sact
{
752 PCHECK(sigemptyset(&sact
.sa_mask
) == 0);
754 sact
.sa_handler
= timeout
;
755 PCHECK(sigaction(SIGALRM
, &sact
, nullptr) == 0);
757 auto const log_dir
{getenv("GOOGLE_LOG_DIR")};
760 fs::create_directories(log_dir
, ec
);
763 google::InitGoogleLogging(argv
[0]);
765 std::unique_ptr
<RFC5321::Ctx
> ctx
;
767 // Don't wait for STARTTLS to fail if no cert.
768 auto const config_path
= osutil::get_config_dir();
769 auto const certs
= osutil::list_directory(config_path
, Config::cert_fn_re
);
770 CHECK_GE(certs
.size(), 1) << "no certs found";
772 auto const read_hook
{[&ctx
]() { ctx
->session
.flush(); }};
773 ctx
= std::make_unique
<RFC5321::Ctx
>(config_path
, read_hook
);
775 ctx
->session
.greeting();
777 istream_input
<eol::crlf
, 1> in
{ctx
->session
.in(), FLAGS_bfr_size
, "session"};
781 ret
= !parse
<RFC5321::grammar
, RFC5321::action
>(in
, *ctx
);
783 catch (std::exception
const& e
) {
784 LOG(WARNING
) << e
.what();
787 if (ctx
->session
.maxed_out()) {
788 ctx
->session
.max_out();
790 else if (ctx
->session
.timed_out()) {
791 ctx
->session
.time_out();
794 // ctx->session.error("session end without QUIT command from client");