clean up block lists
[ghsmtp.git] / smtp.cpp
blob1e476758039c6526f448b455ac7a0dfde88d321b
1 #include <gflags/gflags.h>
2 namespace gflags {
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");
10 #include <fstream>
12 #include "Session.hpp"
13 #include "esc.hpp"
14 #include "fs.hpp"
15 #include "iobuffer.hpp"
16 #include "osutil.hpp"
18 #include <cstdlib>
19 #include <memory>
21 #include <signal.h>
23 #include <tao/pegtl.hpp>
24 #include <tao/pegtl/contrib/abnf.hpp>
26 using namespace tao::pegtl;
27 using namespace tao::pegtl::abnf;
29 namespace RFC5321 {
31 struct Ctx {
32 Session session;
34 std::string mb_loc;
35 std::string mb_dom;
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)
48 // clang-format off
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>> {};
69 using dot = one<'.'>;
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>,
128 one<']'>> {};
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
138 // DEL
139 // So including:
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,
157 one<'!', '#',
158 '$', '%',
159 '&', '\'',
160 '*', '+',
161 '-', '/',
162 '=', '?',
163 '^', '_',
164 '`', '{',
165 '|', '}',
166 '~'>,
167 UTF8_non_ascii> {};
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>,
204 CRLF> {};
206 struct ehlo : seq<TAO_PEGTL_ISTRING("EHLO"),
208 sor<domain, address_literal>,
209 CRLF> {};
211 struct mail_from : seq<TAO_PEGTL_ISTRING("MAIL"),
212 TAO_PEGTL_ISTRING(" FROM:"),
213 opt<SP>, // common enough error, we'll allow it
214 reverse_path,
215 opt<seq<SP, mail_parameters>>,
216 CRLF> {};
218 struct rcpt_to : seq<TAO_PEGTL_ISTRING("RCPT"),
219 TAO_PEGTL_ISTRING(" TO:"),
220 opt<SP>, // common error
221 forward_path,
222 opt<seq<SP, rcpt_parameters>>,
223 CRLF> {};
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.
242 struct data_dot
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
252 // LF" message.
254 struct not_data_end : seq<dot, LF> {};
256 struct data_line : sor<at<data_end>,
257 seq<sor<data_blank,
258 data_dot,
259 data_plain,
260 not_data_end,
261 anything_else>,
262 discard>> {};
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> {};
274 struct starttls
275 : seq<TAO_PEGTL_ISTRING("STAR"), TAO_PEGTL_ISTRING("TTLS"), CRLF> {};
277 struct quit : seq<TAO_PEGTL_ISTRING("QUIT"), CRLF> {};
279 // Anti-AUTH support
281 // base64-char = ALPHA / DIGIT / "+" / "/"
282 // ;; Case-sensitive
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<'='>>
290 > {};
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>>
298 > {};
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]
318 // CRLF
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>>,
325 CRLF> {};
326 // bad commands:
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,
334 data,
335 quit,
336 rset,
337 noop,
338 vrfy,
339 help,
340 helo,
341 ehlo,
342 bdat,
343 bdat_last,
344 starttls,
345 rcpt_to,
346 mail_from,
347 auth,
348 bogus_cmd_long,
349 anything_else>,
350 discard> {};
352 struct grammar : plus<any_cmd> {};
354 // clang-format on
356 template <typename Rule>
357 struct action : nothing<Rule> {
360 template <typename Rule>
361 struct data_action : nothing<Rule> {
364 template <>
365 struct action<esmtp_keyword> {
366 template <typename Input>
367 static void apply(Input const& in, Ctx& ctx)
369 ctx.param.first = in.string();
373 template <>
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());
383 template <>
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());
393 template <>
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());
403 template <>
404 struct action<esmtp_value> {
405 template <typename Input>
406 static void apply(Input const& in, Ctx& ctx)
408 ctx.param.second = in.string();
412 template <>
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();
423 template <>
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;
436 template <>
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;
449 template <>
450 struct action<magic_postmaster> {
451 static void apply0(Ctx& ctx)
453 ctx.mb_loc = std::string{"Postmaster"};
454 ctx.mb_dom.clear();
458 template <>
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));
469 template <>
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));
480 template <>
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);
485 ctx.mb_loc.clear();
486 ctx.mb_dom.clear();
487 ctx.parameters.clear();
491 template <>
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);
496 ctx.mb_loc.clear();
497 ctx.mb_dom.clear();
498 ctx.parameters.clear();
502 template <>
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);
523 while (to_xfer) {
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();
545 else {
546 LOG(ERROR) << "I/O error in BDAT";
547 if (!status_returned)
548 ctx.session.bdat_io_error();
550 return;
552 if (!ctx.session.msg_write(bfr.data(), xfer_sz)) {
553 if (!status_returned)
554 ctx.session.bdat_size_error();
555 status_returned = true;
558 to_xfer -= xfer_sz;
561 if (!status_returned) {
562 ctx.session.bdat_done(ctx.chunk_size, last);
566 template <>
567 struct action<bdat> {
568 static void apply0(Ctx& ctx)
570 bdat_act(ctx, false);
574 template <>
575 struct action<bdat_last> {
576 static void apply0(Ctx& ctx)
578 bdat_act(ctx, true);
582 template <>
583 struct data_action<data_end> {
584 static void apply0(Ctx& ctx) { ctx.session.data_done(); }
587 template <>
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));
596 template <>
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);
606 template <>
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);
616 template <>
617 struct data_action<not_data_end> {
618 static void apply0(Ctx& ctx) __attribute__((noreturn))
620 ctx.session.bare_lf();
624 template <>
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)};
631 if (len)
632 ctx.session.msg_write(begin(in), len);
633 if (len > 1000) {
634 LOG(WARNING) << "line too long at " << len << " octets";
639 template <>
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,
646 "data");
647 try {
648 if (!parse_nested<RFC5321::data_grammar, RFC5321::data_action>(in, din,
649 ctx)) {
650 ctx.session.log_stats();
651 if (!(ctx.session.maxed_out() || ctx.session.timed_out())) {
652 ctx.session.error("bad DATA syntax");
655 return;
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");
669 template <>
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);
682 return str;
685 template <>
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);
695 template <>
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);
705 template <>
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);
715 template <>
716 struct action<starttls> {
717 static void apply0(Ctx& ctx) { ctx.session.starttls(); }
720 template <>
721 struct action<quit> {
722 static void apply0(Ctx& ctx) __attribute__((noreturn)) { ctx.session.quit(); }
725 template <>
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);
736 _Exit(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);
753 sact.sa_flags = 0;
754 sact.sa_handler = timeout;
755 PCHECK(sigaction(SIGALRM, &sact, nullptr) == 0);
757 auto const log_dir{getenv("GOOGLE_LOG_DIR")};
758 if (log_dir) {
759 error_code ec;
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"};
779 int ret = 0;
780 try {
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();
793 // else {
794 // ctx->session.error("session end without QUIT command from client");
795 // }
797 return ret;