diff options
Diffstat (limited to 'src/postscreen/postscreen_smtpd.c')
-rw-r--r-- | src/postscreen/postscreen_smtpd.c | 1329 |
1 files changed, 1329 insertions, 0 deletions
diff --git a/src/postscreen/postscreen_smtpd.c b/src/postscreen/postscreen_smtpd.c new file mode 100644 index 0000000..027bf3a --- /dev/null +++ b/src/postscreen/postscreen_smtpd.c @@ -0,0 +1,1329 @@ +/*++ +/* NAME +/* postscreen_smtpd 3 +/* SUMMARY +/* postscreen built-in SMTP server engine +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void psc_smtpd_pre_jail_init(void) +/* +/* void psc_smtpd_init(void) +/* +/* void psc_smtpd_tests(state) +/* PSC_STATE *state; +/* +/* void PSC_SMTPD_X21(state, final_reply) +/* PSC_STATE *state; +/* const char *final_reply; +/* DESCRIPTION +/* psc_smtpd_pre_jail_init() performs one-time per-process +/* initialization during the "before chroot" execution phase. +/* +/* psc_smtpd_init() performs one-time per-process initialization. +/* +/* psc_smtpd_tests() starts up an SMTP server engine for deep +/* protocol tests and for collecting helo/sender/recipient +/* information. +/* +/* PSC_SMTPD_X21() redirects the SMTP client to an SMTP server +/* engine, which sends the specified final reply at the first +/* legitimate opportunity without doing any protocol tests. +/* +/* Unlike the Postfix SMTP server, this engine does not announce +/* PIPELINING support. This exposes spambots that pipeline +/* their commands anyway. Like the Postfix SMTP server, this +/* engine will accept input with bare newline characters. To +/* pass the "pipelining" and "bare newline" test, the client +/* has to properly speak SMTP all the way to the RCPT TO +/* command. These tests fail if the client violates the protocol +/* at any stage. +/* +/* No support is announced for AUTH, XCLIENT or XFORWARD. +/* Clients that need this should be whitelisted or should talk +/* directly to the submission service. +/* +/* The engine rejects RCPT TO and VRFY commands with the +/* state->rcpt_reply response which depends on program history, +/* rejects ETRN with a generic response, and closes the +/* connection after QUIT. +/* +/* Since this engine defers or rejects all non-junk commands, +/* there is no point maintaining separate counters for "error" +/* commands and "junk" commands. Instead, the engine maintains +/* a per-session command counter, and terminates the session +/* with a 421 reply when the command count exceeds the limit. +/* +/* We limit the command count, as well as the total time to +/* receive a command. This limits the time per client more +/* effectively than would be possible with read() timeouts. +/* +/* There is no concern about getting blocked on output. The +/* psc_send() routine uses non-blocking output, and discards +/* output that the client is not willing to receive. +/* PROTOCOL INSPECTION VERSUS CONTENT INSPECTION +/* The goal of postscreen is to keep spambots away from Postfix. +/* To recognize spambots, postscreen measures properties of +/* the client IP address and of the client SMTP protocol +/* implementation. These client properties don't change with +/* each delivery attempt. Therefore it is possible to make a +/* long-term decision after a single measurement. For example, +/* allow a good client to skip the DNSBL test for 24 hours, +/* or to skip the pipelining test for one week. +/* +/* If postscreen were to measure properties of message content +/* (MIME compliance, etc.) then it would measure properties +/* that may change with each delivery attempt. Here, it would +/* be wrong to make a long-term decision after a single +/* measurement. Instead, postscreen would need to develop a +/* ranking based on the content of multiple messages from the +/* same client. +/* +/* Many spambots avoid spamming the same site repeatedly. +/* Thus, postscreen must make decisions after a single +/* measurement. Message content is not a good indicator for +/* making long-term decisions after single measurements, and +/* that is why postscreen does not inspect message content. +/* REJECTING RCPT TO VERSUS SENDING LIVE SOCKETS TO SMTPD(8) +/* When post-handshake protocol tests are enabled, postscreen +/* rejects the RCPT TO command from a good client, and forces +/* it to deliver mail in a later session. This is why +/* post-handshake protocol tests have a longer expiration time +/* than pre-handshake tests. +/* +/* Instead, postscreen could send the network socket to smtpd(8) +/* and ship the session history (including TLS and other SMTP +/* or non-SMTP attributes) as auxiliary data. The Postfix SMTP +/* server would then use new code to replay the session history, +/* and would use existing code to validate the client, helo, +/* sender and recipient address. +/* +/* Such an approach would increase the implementation and +/* maintenance effort, because: +/* +/* 1) New replay code would be needed in smtpd(8), such that +/* the HELO, EHLO, and MAIL command handlers can delay their +/* error responses until the RCPT TO reply. +/* +/* 2) postscreen(8) would have to implement more of smtpd(8)'s +/* syntax checks, to avoid confusing delayed "syntax error" +/* and other error responses syntax error responses while +/* replaying history. +/* +/* 3) New code would be needed in postscreen(8) and smtpd(8) +/* to send and receive the session history (including TLS and +/* other SMTP or non-SMTP attributes) as auxiliary data while +/* sending the network socket from postscreen(8) to smtpd(8). +/* REJECTING RCPT TO VERSUS PROXYING LIVE SESSIONS TO SMTPD(8) +/* An alternative would be to proxy the session history to a +/* real Postfix SMTP process, presumably passing TLS and other +/* attributes via an extended XCLIENT implementation. That +/* would require all the work described in 2) above, plus +/* duplication of all the features of the smtpd(8) TLS engine, +/* plus additional XCLIENT support for a lot more attributes. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <string.h> +#include <ctype.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include <msg.h> +#include <stringops.h> +#include <mymalloc.h> +#include <iostuff.h> +#include <vstring.h> + +/* Global library. */ + +#include <mail_params.h> +#include <mail_proto.h> +#include <is_header.h> +#include <string_list.h> +#include <maps.h> +#include <ehlo_mask.h> +#include <lex_822.h> + +/* TLS library. */ + +#include <tls.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Plan for future body processing. See smtp-sink.c. For now, we have no + * per-session push-back except for the single-character push-back that + * VSTREAM guarantees after we read one character. + */ +#define PSC_SMTPD_HAVE_PUSH_BACK(state) (0) +#define PSC_SMTPD_PUSH_BACK_CHAR(state, ch) \ + vstream_ungetc((state)->smtp_client_stream, (ch)) +#define PSC_SMTPD_NEXT_CHAR(state) \ + VSTREAM_GETC((state)->smtp_client_stream) + +#define PSC_SMTPD_BUFFER_EMPTY(state) \ + (!PSC_SMTPD_HAVE_PUSH_BACK(state) \ + && vstream_peek((state)->smtp_client_stream) <= 0) + +#define PSC_SMTPD_PEEK_DATA(state) \ + vstream_peek_data((state)->smtp_client_stream) +#define PSC_SMTPD_PEEK_LEN(state) \ + vstream_peek((state)->smtp_client_stream) + + /* + * Dynamic reply strings. To minimize overhead we format these once. + */ +static char *psc_smtpd_greeting; /* smtp banner */ +static char *psc_smtpd_helo_reply; /* helo reply */ +static char *psc_smtpd_ehlo_reply_plain;/* multi-line ehlo reply, non-TLS */ +static char *psc_smtpd_ehlo_reply_tls; /* multi-line ehlo reply, with TLS */ +static char *psc_smtpd_timeout_reply; /* timeout reply */ +static char *psc_smtpd_421_reply; /* generic final_reply value */ + + /* + * Forward declaration, needed by PSC_CLEAR_EVENT_REQUEST. + */ +static void psc_smtpd_time_event(int, void *); +static void psc_smtpd_read_event(int, void *); + + /* + * Encapsulation. The STARTTLS, EHLO and AUTH command handlers temporarily + * suspend SMTP command events, send an asynchronous proxy request, and + * resume SMTP command events after receiving the asynchrounous proxy + * response (the EHLO handler must asynchronously talk to the auth server + * before it can announce the SASL mechanism list; the list can depend on + * the client IP address and on the presence on TLS encryption). + */ +#define PSC_RESUME_SMTP_CMD_EVENTS(state) do { \ + PSC_READ_EVENT_REQUEST2(vstream_fileno((state)->smtp_client_stream), \ + psc_smtpd_read_event, psc_smtpd_time_event, \ + (void *) (state), PSC_EFF_CMD_TIME_LIMIT); \ + if (!PSC_SMTPD_BUFFER_EMPTY(state)) \ + psc_smtpd_read_event(EVENT_READ, (void *) state); \ + } while (0) + +#define PSC_SUSPEND_SMTP_CMD_EVENTS(state) \ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno((state)->smtp_client_stream), \ + psc_smtpd_time_event, (void *) (state)); + + /* + * Make control characters and other non-text visible. + */ +#define PSC_SMTPD_ESCAPE_TEXT(dest, src, src_len, max_len) do { \ + ssize_t _s_len = (src_len); \ + ssize_t _m_len = (max_len); \ + (void) escape((dest), (src), _s_len < _m_len ? _s_len : _m_len); \ + } while (0) + + /* + * Command parser support. + */ +#define PSC_SMTPD_NEXT_TOKEN(ptr) mystrtok(&(ptr), " ") + + /* + * EHLO keyword filter + */ +static MAPS *psc_ehlo_discard_maps; +static int psc_ehlo_discard_mask; + + /* + * Command editing filter. + */ +static DICT *psc_cmd_filter; + + /* + * Encapsulation. We must not forget turn off input/timer events when we + * terminate the SMTP protocol engine. + * + * It would be safer to turn off input/timer events after each event, and to + * turn on input/timer events again when we want more input. But experience + * with the Postfix smtp-source and smtp-sink tools shows that this would + * noticeably increase the run-time cost. + */ +#define PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, event, reply) do { \ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno((state)->smtp_client_stream), \ + (event), (void *) (state)); \ + PSC_DROP_SESSION_STATE((state), (reply)); \ + } while (0) + +#define PSC_CLEAR_EVENT_HANGUP(state, event) do { \ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno((state)->smtp_client_stream), \ + (event), (void *) (state)); \ + psc_hangup_event(state); \ + } while (0) + +/* psc_helo_cmd - record HELO and respond */ + +static int psc_helo_cmd(PSC_STATE *state, char *args) +{ + char *helo_name = PSC_SMTPD_NEXT_TOKEN(args); + + /* + * smtpd(8) incompatibility: we ignore extra words; smtpd(8) saves them. + */ + if (helo_name == 0) + return (PSC_SEND_REPLY(state, "501 Syntax: HELO hostname\r\n")); + + PSC_STRING_UPDATE(state->helo_name, helo_name); + PSC_STRING_RESET(state->sender); + /* Don't downgrade state->protocol, in case some test depends on this. */ + return (PSC_SEND_REPLY(state, psc_smtpd_helo_reply)); +} + +/* psc_smtpd_format_ehlo_reply - format EHLO response */ + +static void psc_smtpd_format_ehlo_reply(VSTRING *buf, int discard_mask + /* , const char *sasl_mechanism_list */ ) +{ + const char *myname = "psc_smtpd_format_ehlo_reply"; + int saved_len = 0; + + if (msg_verbose) + msg_info("%s: discard_mask %s", myname, str_ehlo_mask(discard_mask)); + +#define PSC_EHLO_APPEND(save, buf, fmt) do { \ + (save) = LEN(buf); \ + vstring_sprintf_append((buf), (fmt)); \ + } while (0) + +#define PSC_EHLO_APPEND1(save, buf, fmt, arg1) do { \ + (save) = LEN(buf); \ + vstring_sprintf_append((buf), (fmt), (arg1)); \ + } while (0) + + vstring_sprintf(psc_temp, "250-%s\r\n", var_myhostname); + if ((discard_mask & EHLO_MASK_SIZE) == 0) { + if (var_message_limit) + PSC_EHLO_APPEND1(saved_len, psc_temp, "250-SIZE %lu\r\n", + (unsigned long) var_message_limit); + else + PSC_EHLO_APPEND(saved_len, psc_temp, "250-SIZE\r\n"); + } + if ((discard_mask & EHLO_MASK_VRFY) == 0 && var_disable_vrfy_cmd == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-VRFY\r\n"); + if ((discard_mask & EHLO_MASK_ETRN) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-ETRN\r\n"); + if ((discard_mask & EHLO_MASK_STARTTLS) == 0 && var_psc_use_tls) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-STARTTLS\r\n"); +#ifdef TODO_SASL_AUTH + if ((discard_mask & EHLO_MASK_AUTH) == 0 && sasl_mechanism_list + && (!var_psc_tls_auth_only || (discard_mask & EHLO_MASK_STARTTLS))) { + PSC_EHLO_APPEND1(saved_len, psc_temp, "AUTH %s", sasl_mechanism_list); + if (var_broken_auth_clients) + PSC_EHLO_APPEND1(saved_len, psc_temp, "AUTH=%s", sasl_mechanism_list); + } +#endif + if ((discard_mask & EHLO_MASK_ENHANCEDSTATUSCODES) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-ENHANCEDSTATUSCODES\r\n"); + if ((discard_mask & EHLO_MASK_8BITMIME) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-8BITMIME\r\n"); + if ((discard_mask & EHLO_MASK_DSN) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-DSN\r\n"); + /* Fix 20140708: announce SMTPUTF8. */ + if (var_smtputf8_enable && (discard_mask & EHLO_MASK_SMTPUTF8) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-SMTPUTF8\r\n"); + if ((discard_mask & EHLO_MASK_CHUNKING) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-CHUNKING\r\n"); + STR(psc_temp)[saved_len + 3] = ' '; +} + +/* psc_ehlo_cmd - record EHLO and respond */ + +static int psc_ehlo_cmd(PSC_STATE *state, char *args) +{ + char *helo_name = PSC_SMTPD_NEXT_TOKEN(args); + const char *ehlo_words; + int discard_mask; + char *reply; + + /* + * smtpd(8) incompatibility: we ignore extra words; smtpd(8) saves them. + */ + if (helo_name == 0) + return (PSC_SEND_REPLY(state, "501 Syntax: EHLO hostname\r\n")); + + PSC_STRING_UPDATE(state->helo_name, helo_name); + PSC_STRING_RESET(state->sender); + state->protocol = MAIL_PROTO_ESMTP; + + /* + * smtpd(8) compatibility: dynamic reply filtering. + */ + if (psc_ehlo_discard_maps != 0 + && (ehlo_words = psc_maps_find(psc_ehlo_discard_maps, + state->smtp_client_addr, 0)) != 0 + && (discard_mask = ehlo_mask(ehlo_words)) != psc_ehlo_discard_mask) { + if (discard_mask && !(discard_mask & EHLO_MASK_SILENT)) + msg_info("[%s]%s: discarding EHLO keywords: %s", + PSC_CLIENT_ADDR_PORT(state), str_ehlo_mask(discard_mask)); + if (state->flags & PSC_STATE_FLAG_USING_TLS) + discard_mask |= EHLO_MASK_STARTTLS; + psc_smtpd_format_ehlo_reply(psc_temp, discard_mask); + reply = STR(psc_temp); + state->ehlo_discard_mask = discard_mask; + } else if (psc_ehlo_discard_maps && psc_ehlo_discard_maps->error) { + msg_fatal("%s lookup error for %s", + psc_ehlo_discard_maps->title, state->smtp_client_addr); + } else if (state->flags & PSC_STATE_FLAG_USING_TLS) { + reply = psc_smtpd_ehlo_reply_tls; + state->ehlo_discard_mask = psc_ehlo_discard_mask | EHLO_MASK_STARTTLS; + } else { + reply = psc_smtpd_ehlo_reply_plain; + state->ehlo_discard_mask = psc_ehlo_discard_mask; + } + return (PSC_SEND_REPLY(state, reply)); +} + +/* psc_starttls_resume - resume the SMTP protocol after tlsproxy activation */ + +static void psc_starttls_resume(int unused_event, void *context) +{ + const char *myname = "psc_starttls_resume"; + PSC_STATE *state = (PSC_STATE *) context; + + /* + * Reset SMTP server state if STARTTLS was successful. + */ + if (state->flags & PSC_STATE_FLAG_USING_TLS) { + /* Purge the push-back buffer, when implemented. */ + PSC_STRING_RESET(state->helo_name); + PSC_STRING_RESET(state->sender); +#ifdef TODO_SASL_AUTH + /* Reset SASL AUTH state. Dovecot responses may change. */ +#endif + } + + /* + * Resume read/timeout events. If we still have unread input, resume the + * command processor immediately. + */ + PSC_RESUME_SMTP_CMD_EVENTS(state); +} + +/* psc_starttls_cmd - activate the tlsproxy server */ + +static int psc_starttls_cmd(PSC_STATE *state, char *args) +{ + const char *myname = "psc_starttls_cmd"; + + /* + * smtpd(8) incompatibility: we can't send a 4XX reply that TLS is + * unavailable when tlsproxy(8) detects the problem too late. + */ + if (PSC_SMTPD_NEXT_TOKEN(args) != 0) + return (PSC_SEND_REPLY(state, "501 Syntax: EHLO hostname\r\n")); + if (state->flags & PSC_STATE_FLAG_USING_TLS) + return (PSC_SEND_REPLY(state, + "554 5.5.1 Error: TLS already active\r\n")); + if (var_psc_use_tls == 0 || (state->ehlo_discard_mask & EHLO_MASK_STARTTLS)) + return (PSC_SEND_REPLY(state, + "502 5.5.1 Error: command not implemented\r\n")); + + /* + * Suspend the SMTP protocol until psc_starttls_resume() is called. + */ + PSC_SUSPEND_SMTP_CMD_EVENTS(state); + psc_starttls_open(state, psc_starttls_resume); + return (0); +} + +/* psc_extract_addr - extract MAIL/RCPT address, unquoted form */ + +static char *psc_extract_addr(VSTRING *result, const char *string) +{ + const unsigned char *cp = (const unsigned char *) string; + char *addr; + char *colon; + int stop_at; + int inquote = 0; + + /* + * smtpd(8) incompatibility: we allow more invalid address forms, and we + * don't validate recipients. We are not going to deliver them so we + * won't have to worry about deliverability. This may have to change when + * we pass the socket to a real SMTP server and replay message envelope + * commands. + */ + + /* Skip SP characters. */ + while (*cp && *cp == ' ') + cp++; + + /* Choose the terminator for <addr> or bare addr. */ + if (*cp == '<') { + cp++; + stop_at = '>'; + } else { + stop_at = ' '; + } + + /* Skip to terminator or end. */ + VSTRING_RESET(result); + for ( /* void */ ; *cp; cp++) { + if (!inquote && *cp == stop_at) + break; + if (*cp == '"') { + inquote = !inquote; + } else { + if (*cp == '\\' && *++cp == 0) + break; + VSTRING_ADDCH(result, *cp); + } + } + VSTRING_TERMINATE(result); + + /* + * smtpd(8) compatibility: truncate deprecated route address form. This + * is primarily to simplify logfile analysis. + */ + addr = STR(result); + if (*addr == '@' && (colon = strchr(addr, ':')) != 0) + addr = colon + 1; + return (addr); +} + +/* psc_mail_cmd - record MAIL and respond */ + +static int psc_mail_cmd(PSC_STATE *state, char *args) +{ + char *colon; + char *addr; + + /* + * smtpd(8) incompatibility: we never reject the sender, and we ignore + * additional arguments. + */ + if (var_psc_helo_required && state->helo_name == 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: send HELO/EHLO first\r\n")); + if (state->sender != 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: nested MAIL command\r\n")); + if (args == 0 || (colon = strchr(args, ':')) == 0) + return (PSC_SEND_REPLY(state, + "501 5.5.4 Syntax: MAIL FROM:<address>\r\n")); + if ((addr = psc_extract_addr(psc_temp, colon + 1)) == 0) + return (PSC_SEND_REPLY(state, + "501 5.1.7 Bad sender address syntax\r\n")); + PSC_STRING_UPDATE(state->sender, addr); + return (PSC_SEND_REPLY(state, "250 2.1.0 Ok\r\n")); +} + +/* psc_soften_reply - copy and soft-bounce a reply */ + +static char *psc_soften_reply(const char *reply) +{ + static VSTRING *buf = 0; + + if (buf == 0) + buf = vstring_alloc(100); + vstring_strcpy(buf, reply); + if (reply[0] == '5') + STR(buf)[0] = '4'; + if (reply[4] == '5') + STR(buf)[4] = '4'; + return (STR(buf)); +} + +/* psc_rcpt_cmd record RCPT and respond */ + +static int psc_rcpt_cmd(PSC_STATE *state, char *args) +{ + char *colon; + char *addr; + + /* + * smtpd(8) incompatibility: we reject all recipients, and ignore + * additional arguments. + */ + if (state->sender == 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: need MAIL command\r\n")); + if (args == 0 || (colon = strchr(args, ':')) == 0) + return (PSC_SEND_REPLY(state, + "501 5.5.4 Syntax: RCPT TO:<address>\r\n")); + if ((addr = psc_extract_addr(psc_temp, colon + 1)) == 0) + return (PSC_SEND_REPLY(state, + "501 5.1.3 Bad recipient address syntax\r\n")); + msg_info("NOQUEUE: reject: RCPT from [%s]:%s: %.*s; " + "from=<%s>, to=<%s>, proto=%s, helo=<%s>", + PSC_CLIENT_ADDR_PORT(state), + (int) strlen(state->rcpt_reply) - 2, + var_soft_bounce == 0 ? state->rcpt_reply : + psc_soften_reply(state->rcpt_reply), + state->sender, addr, state->protocol, + state->helo_name ? state->helo_name : ""); + return (PSC_SEND_REPLY(state, state->rcpt_reply)); +} + +/* psc_data_cmd - respond to DATA and disconnect */ + +static int psc_data_cmd(PSC_STATE *state, char *args) +{ + const char myname[] = "psc_data_cmd"; + + /* + * smtpd(8) incompatibility: postscreen(8) drops the connection, instead + * of waiting for the next command. Justification: postscreen(8) should + * never see DATA from a legitimate client, because 1) the server rejects + * every recipient, and 2) the server does not announce PIPELINING. + */ + msg_info("DATA without valid RCPT from [%s]:%s", + PSC_CLIENT_ADDR_PORT(state)); + if (PSC_SMTPD_NEXT_TOKEN(args) != 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "501 5.5.4 Syntax: DATA\r\n"); + else if (state->sender == 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "503 5.5.1 Error: need RCPT command\r\n"); + else + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "554 5.5.1 Error: no valid recipients\r\n"); + /* Caution: state is now a dangling pointer. */ + return (0); +} + +/* psc_bdat_cmd - respond to BDAT and disconnect */ + +static int psc_bdat_cmd(PSC_STATE *state, char *args) +{ + const char *myname = "psc_bdat_cmd"; + + /* + * smtpd(8) incompatibility: postscreen(8) drops the connection, instead + * of reading the entire BDAT chunk and staying in sync with the client. + * Justification: postscreen(8) should never see BDAT from a legitimate + * client, because 1) the server rejects every recipient, and 2) the + * server does not announce PIPELINING. + */ + msg_info("BDAT without valid RCPT from [%s]:%s", + PSC_CLIENT_ADDR_PORT(state)); + if (state->ehlo_discard_mask & EHLO_MASK_CHUNKING) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "502 5.5.1 Error: command not implemented\r\n"); + else if (PSC_SMTPD_NEXT_TOKEN(args) == 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "501 5.5.4 Syntax: BDAT count [LAST]\r\n"); + else if (state->sender == 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "554 5.5.1 Error: need RCPT command\r\n"); + else + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "554 5.5.1 Error: no valid recipients\r\n"); + /* Caution: state is now a dangling pointer. */ + return (0); +} + +/* psc_rset_cmd - reset, send 250 OK */ + +static int psc_rset_cmd(PSC_STATE *state, char *unused_args) +{ + PSC_STRING_RESET(state->sender); + return (PSC_SEND_REPLY(state, "250 2.0.0 Ok\r\n")); +} + +/* psc_noop_cmd - respond to something */ + +static int psc_noop_cmd(PSC_STATE *state, char *unused_args) +{ + return (PSC_SEND_REPLY(state, "250 2.0.0 Ok\r\n")); +} + +/* psc_vrfy_cmd - respond to VRFY */ + +static int psc_vrfy_cmd(PSC_STATE *state, char *args) +{ + + /* + * smtpd(8) incompatibility: we reject all requests, and ignore + * additional arguments. + */ + if (PSC_SMTPD_NEXT_TOKEN(args) == 0) + return (PSC_SEND_REPLY(state, + "501 5.5.4 Syntax: VRFY address\r\n")); + if (var_psc_disable_vrfy) + return (PSC_SEND_REPLY(state, + "502 5.5.1 VRFY command is disabled\r\n")); + return (PSC_SEND_REPLY(state, state->rcpt_reply)); +} + +/* psc_etrn_cmd - reset, send 250 OK */ + +static int psc_etrn_cmd(PSC_STATE *state, char *args) +{ + + /* + * smtpd(8) incompatibility: we reject all requests, and ignore + * additional arguments. + */ + if (var_psc_helo_required && state->helo_name == 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: send HELO/EHLO first\r\n")); + if (PSC_SMTPD_NEXT_TOKEN(args) == 0) + return (PSC_SEND_REPLY(state, + "500 Syntax: ETRN domain\r\n")); + return (PSC_SEND_REPLY(state, "458 Unable to queue messages\r\n")); +} + +/* psc_quit_cmd - respond to QUIT and disconnect */ + +static int psc_quit_cmd(PSC_STATE *state, char *unused_args) +{ + const char *myname = "psc_quit_cmd"; + + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + "221 2.0.0 Bye\r\n"); + /* Caution: state is now a dangling pointer. */ + return (0); +} + +/* psc_smtpd_time_event - handle per-session time limit */ + +static void psc_smtpd_time_event(int event, void *context) +{ + const char *myname = "psc_smtpd_time_event"; + PSC_STATE *state = (PSC_STATE *) context; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d event %d on smtp socket %d from [%s]:%s flags=%s", + myname, psc_post_queue_length, psc_check_queue_length, + event, vstream_fileno(state->smtp_client_stream), + state->smtp_client_addr, state->smtp_client_port, + psc_print_state_flags(state->flags, myname)); + + msg_info("COMMAND TIME LIMIT from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), state->where); + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + psc_smtpd_timeout_reply); +} + + /* + * The table of all SMTP commands that we know. + */ +typedef struct { + const char *name; + int (*action) (PSC_STATE *, char *); + int flags; /* see below */ +} PSC_SMTPD_COMMAND; + +#define PSC_SMTPD_CMD_FLAG_NONE (0) /* no flags (i.e. disabled) */ +#define PSC_SMTPD_CMD_FLAG_ENABLE (1<<0) /* command is enabled */ +#define PSC_SMTPD_CMD_FLAG_DESTROY (1<<1) /* dangling pointer alert */ +#define PSC_SMTPD_CMD_FLAG_PRE_TLS (1<<2) /* allowed with mandatory TLS */ +#define PSC_SMTPD_CMD_FLAG_SUSPEND (1<<3) /* suspend command engine */ +#define PSC_SMTPD_CMD_FLAG_HAS_PAYLOAD (1<<4) /* command has payload */ + +static const PSC_SMTPD_COMMAND command_table[] = { + "HELO", psc_helo_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS, + "EHLO", psc_ehlo_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS, + "STARTTLS", psc_starttls_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS | PSC_SMTPD_CMD_FLAG_SUSPEND, + "XCLIENT", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_NONE, + "XFORWARD", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_NONE, + "AUTH", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_NONE, + "MAIL", psc_mail_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "RCPT", psc_rcpt_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "DATA", psc_data_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_DESTROY, + /* ".", psc_dot_cmd, PSC_SMTPD_CMD_FLAG_NONE, */ + "BDAT", psc_bdat_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_DESTROY | PSC_SMTPD_CMD_FLAG_HAS_PAYLOAD, + "RSET", psc_rset_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "NOOP", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS, + "VRFY", psc_vrfy_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "ETRN", psc_etrn_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "QUIT", psc_quit_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_DESTROY | PSC_SMTPD_CMD_FLAG_PRE_TLS, + 0, +}; + +/* psc_smtpd_read_event - pseudo responder */ + +static void psc_smtpd_read_event(int event, void *context) +{ + const char *myname = "psc_smtpd_read_event"; + PSC_STATE *state = (PSC_STATE *) context; + time_t *expire_time = state->client_info->expire_time; + int ch; + struct cmd_trans { + int state; + int want; + int next_state; + }; + const char *saved_where; + +#define PSC_SMTPD_CMD_ST_ANY 0 +#define PSC_SMTPD_CMD_ST_CR 1 +#define PSC_SMTPD_CMD_ST_CR_LF 2 + + static const struct cmd_trans cmd_trans[] = { + PSC_SMTPD_CMD_ST_ANY, '\r', PSC_SMTPD_CMD_ST_CR, + PSC_SMTPD_CMD_ST_CR, '\n', PSC_SMTPD_CMD_ST_CR_LF, + 0, 0, 0, + }; + const struct cmd_trans *transp; + char *cmd_buffer_ptr; + char *command; + const PSC_SMTPD_COMMAND *cmdp; + int write_stat; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d event %d on smtp socket %d from [%s]:%s flags=%s", + myname, psc_post_queue_length, psc_check_queue_length, + event, vstream_fileno(state->smtp_client_stream), + state->smtp_client_addr, state->smtp_client_port, + psc_print_state_flags(state->flags, myname)); + + /* + * Basic liveness requirements. + * + * Drain all input in the VSTREAM buffer, otherwise this socket will not + * receive further read event notification until the client disconnects! + * + * To suspend this loop temporarily before the buffer is drained, use the + * PSC_SUSPEND_SMTP_CMD_EVENTS() and PSC_RESUME_SMTP_CMD_EVENTS() macros, + * and set the PSC_SMTPD_CMD_FLAG_SUSPEND flag in the command table. + * + * Don't try to read input before it has arrived, otherwise we would starve + * the pseudo threads of other sessions. Get out of here as soon as the + * VSTREAM read buffer dries up. Do not look for more input in kernel + * buffers. That input wasn't likely there when psc_smtpd_read_event() + * was called. Also, yielding the pseudo thread will improve fairness for + * other pseudo threads. + */ + + /* + * Note: on entry into this function the VSTREAM buffer may or may not be + * empty, so we test the "no more input" condition at the bottom of the + * loops. + */ + for (;;) { + + /* + * Read one command line, possibly one fragment at a time. + */ + for (;;) { + + if ((ch = PSC_SMTPD_NEXT_CHAR(state)) == VSTREAM_EOF) { + PSC_CLEAR_EVENT_HANGUP(state, psc_smtpd_time_event); + return; + } + + /* + * Sanity check. We don't want to store infinitely long commands. + */ + if (state->read_state == PSC_SMTPD_CMD_ST_ANY + && VSTRING_LEN(state->cmd_buffer) >= var_line_limit) { + msg_info("COMMAND LENGTH LIMIT from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), state->where); + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + psc_smtpd_421_reply); + return; + } + VSTRING_ADDCH(state->cmd_buffer, ch); + + /* + * Try to match the current character desired by the state + * machine. If that fails, try to restart the machine with a + * match for its first state. Like smtpd(8), we understand lines + * ending in <CR><LF> and bare <LF>. Unlike smtpd(8), we may + * treat lines ending in bare <LF> as an offense. + */ + for (transp = cmd_trans; transp->state != state->read_state; transp++) + if (transp->want == 0) + msg_panic("%s: command_read: unknown state: %d", + myname, state->read_state); + if (ch == transp->want) + state->read_state = transp->next_state; + else if (ch == cmd_trans[0].want) + state->read_state = cmd_trans[0].next_state; + else + state->read_state = PSC_SMTPD_CMD_ST_ANY; + if (state->read_state == PSC_SMTPD_CMD_ST_CR_LF) { + vstring_truncate(state->cmd_buffer, + VSTRING_LEN(state->cmd_buffer) - 2); + break; + } + + /* + * Bare newline test. + */ + if (ch == '\n') { + if ((state->flags & PSC_STATE_MASK_BARLF_TODO_SKIP) + == PSC_STATE_FLAG_BARLF_TODO) { + PSC_SMTPD_ESCAPE_TEXT(psc_temp, STR(state->cmd_buffer), + VSTRING_LEN(state->cmd_buffer) - 1, 100); + msg_info("BARE NEWLINE from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), STR(psc_temp)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_BARLF_FAIL); + PSC_UNPASS_SESSION_STATE(state, PSC_STATE_FLAG_BARLF_PASS); + expire_time[PSC_TINDX_BARLF] = PSC_TIME_STAMP_DISABLED; /* XXX */ + /* Skip this test for the remainder of this session. */ + PSC_SKIP_SESSION_STATE(state, "bare newline test", + PSC_STATE_FLAG_BARLF_SKIP); + switch (psc_barlf_action) { + case PSC_ACT_DROP: + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "521 5.5.1 Protocol error\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, + PSC_STATE_FLAG_BARLF_FAIL); + /* Temporarily whitelist until something expires. */ + PSC_PASS_SESSION_STATE(state, "bare newline test", + PSC_STATE_FLAG_BARLF_PASS); + expire_time[PSC_TINDX_BARLF] = event_time() + psc_min_ttl; + break; + default: + msg_panic("%s: unknown bare_newline action value %d", + myname, psc_barlf_action); + } + } + vstring_truncate(state->cmd_buffer, + VSTRING_LEN(state->cmd_buffer) - 1); + break; + } + + /* + * Yield this pseudo thread when the VSTREAM buffer is empty in + * the middle of a command. + * + * XXX Do not reset the read timeout. The entire command must be + * received within the time limit. + */ + if (PSC_SMTPD_BUFFER_EMPTY(state)) + return; + } + + /* + * Terminate the command buffer, and apply the last-resort command + * editing workaround. + */ + VSTRING_TERMINATE(state->cmd_buffer); + if (psc_cmd_filter != 0) { + const char *cp; + + for (cp = STR(state->cmd_buffer); *cp && IS_SPACE_TAB(*cp); cp++) + /* void */ ; + if ((cp = psc_dict_get(psc_cmd_filter, cp)) != 0) { + msg_info("[%s]:%s: replacing command \"%.100s\" with \"%.100s\"", + state->smtp_client_addr, state->smtp_client_port, + STR(state->cmd_buffer), cp); + vstring_strcpy(state->cmd_buffer, cp); + } else if (psc_cmd_filter->error != 0) { + msg_fatal("%s:%s lookup error for \"%.100s\"", + psc_cmd_filter->type, psc_cmd_filter->name, + STR(state->cmd_buffer)); + } + } + + /* + * Reset the command buffer write pointer and state machine in + * preparation for the next command. For this to work as expected, + * VSTRING_RESET() must be non-destructive. We just can't ask for the + * VSTRING_LEN() and vstring_end() results. + */ + state->read_state = PSC_SMTPD_CMD_ST_ANY; + VSTRING_RESET(state->cmd_buffer); + + /* + * Process the command line. + * + * Caution: some command handlers terminate the session and destroy the + * session state structure. When this happens we must leave the SMTP + * engine to avoid a dangling pointer problem. + */ + cmd_buffer_ptr = STR(state->cmd_buffer); + if (msg_verbose) + msg_info("< [%s]:%s: %s", state->smtp_client_addr, + state->smtp_client_port, cmd_buffer_ptr); + + /* Parse the command name. */ + if ((command = PSC_SMTPD_NEXT_TOKEN(cmd_buffer_ptr)) == 0) + command = ""; + + /* + * The non-SMTP, PIPELINING and command COUNT tests depend on the + * client command handler. + * + * Caution: cmdp->name and cmdp->action may be null on loop exit. + */ + saved_where = state->where; + state->where = PSC_SMTPD_CMD_UNIMPL; + for (cmdp = command_table; cmdp->name != 0; cmdp++) { + if (strcasecmp(command, cmdp->name) == 0) { + state->where = cmdp->name; + break; + } + } + + if ((state->flags & PSC_STATE_FLAG_SMTPD_X21) + && cmdp->action != psc_quit_cmd) { + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + state->final_reply); + return; + } + /* Non-SMTP command test. */ + if ((state->flags & PSC_STATE_MASK_NSMTP_TODO_SKIP) + == PSC_STATE_FLAG_NSMTP_TODO && cmdp->name == 0 + && (is_header(command) + /* Ignore forbid_cmds lookup errors. Non-critical feature. */ + || (*var_psc_forbid_cmds + && string_list_match(psc_forbid_cmds, command)))) { + printable(command, '?'); + PSC_SMTPD_ESCAPE_TEXT(psc_temp, cmd_buffer_ptr, + strlen(cmd_buffer_ptr), 100); + msg_info("NON-SMTP COMMAND from [%s]:%s after %s: %.100s %s", + PSC_CLIENT_ADDR_PORT(state), saved_where, + command, STR(psc_temp)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_NSMTP_FAIL); + PSC_UNPASS_SESSION_STATE(state, PSC_STATE_FLAG_NSMTP_PASS); + expire_time[PSC_TINDX_NSMTP] = PSC_TIME_STAMP_DISABLED; /* XXX */ + /* Skip this test for the remainder of this SMTP session. */ + PSC_SKIP_SESSION_STATE(state, "non-smtp test", + PSC_STATE_FLAG_NSMTP_SKIP); + switch (psc_nsmtp_action) { + case PSC_ACT_DROP: + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "521 5.7.0 Error: I can break rules, too. Goodbye.\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, + PSC_STATE_FLAG_NSMTP_FAIL); + /* Temporarily whitelist until something else expires. */ + PSC_PASS_SESSION_STATE(state, "non-smtp test", + PSC_STATE_FLAG_NSMTP_PASS); + expire_time[PSC_TINDX_NSMTP] = event_time() + psc_min_ttl; + break; + default: + msg_panic("%s: unknown non_smtp_command action value %d", + myname, psc_nsmtp_action); + } + } + /* Command PIPELINING test. */ + if ((cmdp->flags & PSC_SMTPD_CMD_FLAG_HAS_PAYLOAD) == 0 + && (state->flags & PSC_STATE_MASK_PIPEL_TODO_SKIP) + == PSC_STATE_FLAG_PIPEL_TODO && !PSC_SMTPD_BUFFER_EMPTY(state)) { + printable(command, '?'); + PSC_SMTPD_ESCAPE_TEXT(psc_temp, PSC_SMTPD_PEEK_DATA(state), + PSC_SMTPD_PEEK_LEN(state), 100); + msg_info("COMMAND PIPELINING from [%s]:%s after %.100s: %s", + PSC_CLIENT_ADDR_PORT(state), command, STR(psc_temp)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_PIPEL_FAIL); + PSC_UNPASS_SESSION_STATE(state, PSC_STATE_FLAG_PIPEL_PASS); + expire_time[PSC_TINDX_PIPEL] = PSC_TIME_STAMP_DISABLED; /* XXX */ + /* Skip this test for the remainder of this SMTP session. */ + PSC_SKIP_SESSION_STATE(state, "pipelining test", + PSC_STATE_FLAG_PIPEL_SKIP); + switch (psc_pipel_action) { + case PSC_ACT_DROP: + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "521 5.5.1 Protocol error\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, + PSC_STATE_FLAG_PIPEL_FAIL); + /* Temporarily whitelist until something else expires. */ + PSC_PASS_SESSION_STATE(state, "pipelining test", + PSC_STATE_FLAG_PIPEL_PASS); + expire_time[PSC_TINDX_PIPEL] = event_time() + psc_min_ttl; + break; + default: + msg_panic("%s: unknown pipelining action value %d", + myname, psc_pipel_action); + } + } + + /* + * The following tests don't pass until the client gets all the way + * to the RCPT TO command. However, the client can still fail these + * tests with some later command. + */ + if (cmdp->action == psc_rcpt_cmd) { + if ((state->flags & PSC_STATE_MASK_BARLF_TODO_PASS_FAIL) + == PSC_STATE_FLAG_BARLF_TODO) { + PSC_PASS_SESSION_STATE(state, "bare newline test", + PSC_STATE_FLAG_BARLF_PASS); + /* XXX Reset to PSC_TIME_STAMP_DISABLED on failure. */ + expire_time[PSC_TINDX_BARLF] = event_time() + var_psc_barlf_ttl; + } + if ((state->flags & PSC_STATE_MASK_NSMTP_TODO_PASS_FAIL) + == PSC_STATE_FLAG_NSMTP_TODO) { + PSC_PASS_SESSION_STATE(state, "non-smtp test", + PSC_STATE_FLAG_NSMTP_PASS); + /* XXX Reset to PSC_TIME_STAMP_DISABLED on failure. */ + expire_time[PSC_TINDX_NSMTP] = event_time() + var_psc_nsmtp_ttl; + } + if ((state->flags & PSC_STATE_MASK_PIPEL_TODO_PASS_FAIL) + == PSC_STATE_FLAG_PIPEL_TODO) { + PSC_PASS_SESSION_STATE(state, "pipelining test", + PSC_STATE_FLAG_PIPEL_PASS); + /* XXX Reset to PSC_TIME_STAMP_DISABLED on failure. */ + expire_time[PSC_TINDX_PIPEL] = event_time() + var_psc_pipel_ttl; + } + } + /* Command COUNT limit test. */ + if (++state->command_count > var_psc_cmd_count + && cmdp->action != psc_quit_cmd) { + msg_info("COMMAND COUNT LIMIT from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), saved_where); + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + psc_smtpd_421_reply); + return; + } + /* Finally, execute the command. */ + if (cmdp->name == 0 || (cmdp->flags & PSC_SMTPD_CMD_FLAG_ENABLE) == 0) { + write_stat = PSC_SEND_REPLY(state, + "502 5.5.2 Error: command not recognized\r\n"); + } else if (var_psc_enforce_tls + && (state->flags & PSC_STATE_FLAG_USING_TLS) == 0 + && (cmdp->flags & PSC_SMTPD_CMD_FLAG_PRE_TLS) == 0) { + write_stat = PSC_SEND_REPLY(state, + "530 5.7.0 Must issue a STARTTLS command first\r\n"); + } else { + write_stat = cmdp->action(state, cmd_buffer_ptr); + if (cmdp->flags & PSC_SMTPD_CMD_FLAG_DESTROY) + return; + } + + /* + * Terminate the session after a write error. + */ + if (write_stat < 0) { + PSC_CLEAR_EVENT_HANGUP(state, psc_smtpd_time_event); + return; + } + + /* + * We're suspended, waiting for some external event to happen. + * Hopefully, someone will call us back to process the remainder of + * the pending input, otherwise we could hang. + */ + if (cmdp->flags & PSC_SMTPD_CMD_FLAG_SUSPEND) + return; + + /* + * Reset the command read timeout before reading the next command. + */ + event_request_timer(psc_smtpd_time_event, (void *) state, + PSC_EFF_CMD_TIME_LIMIT); + + /* + * Yield this pseudo thread when the VSTREAM buffer is empty. + */ + if (PSC_SMTPD_BUFFER_EMPTY(state)) + return; + } +} + +/* psc_smtpd_tests - per-session deep protocol test initialization */ + +void psc_smtpd_tests(PSC_STATE *state) +{ + static char *myname = "psc_smtpd_tests"; + + /* + * Report errors and progress in the context of this test. + */ + PSC_BEGIN_TESTS(state, "tests after SMTP handshake"); + + /* + * Initialize per-session state that is used only by the dummy engine: + * the command read buffer and the command read state machine. + */ + state->cmd_buffer = vstring_alloc(100); + state->read_state = PSC_SMTPD_CMD_ST_ANY; + + /* + * Disable all after-220 tests when we need to reply with 421 and hang up + * after reading the next SMTP client command. + * + * Opportunistically make postscreen more useful, by turning on all + * after-220 tests when a bad client failed a before-220 test. + * + * Otherwise, only apply the explicitly-configured after-220 tests. + */ + if (state->flags & PSC_STATE_FLAG_SMTPD_X21) { + state->flags &= ~PSC_STATE_MASK_SMTPD_TODO; + } else if (state->flags & PSC_STATE_MASK_ANY_FAIL) { + state->flags |= PSC_STATE_MASK_SMTPD_TODO; + } + + /* + * Send no SMTP banner to pregreeting clients. This eliminates a lot of + * "NON-SMTP COMMAND" events, and improves sender/recipient logging. + */ + if ((state->flags & PSC_STATE_FLAG_PREGR_FAIL) == 0 + && PSC_SEND_REPLY(state, psc_smtpd_greeting) != 0) { + psc_hangup_event(state); + return; + } + + /* + * Wait for the client to respond. + */ + PSC_READ_EVENT_REQUEST2(vstream_fileno(state->smtp_client_stream), + psc_smtpd_read_event, psc_smtpd_time_event, + (void *) state, PSC_EFF_CMD_TIME_LIMIT); +} + +/* psc_smtpd_init - per-process deep protocol test initialization */ + +void psc_smtpd_init(void) +{ + + /* + * Initialize the server banner. + */ + vstring_sprintf(psc_temp, "220 %s\r\n", var_smtpd_banner); + psc_smtpd_greeting = mystrdup(STR(psc_temp)); + + /* + * Initialize the HELO reply. + */ + vstring_sprintf(psc_temp, "250 %s\r\n", var_myhostname); + psc_smtpd_helo_reply = mystrdup(STR(psc_temp)); + + /* + * STARTTLS support. Note the complete absence of #ifdef USE_TLS + * throughout the postscreen(8) source code. If Postfix is built without + * TLS support, then the TLS proxy will simply report that TLS is not + * available, and conventional error handling will take care of the + * issue. + * + * Legacy code copied from smtpd(8). The pre-fabricated EHLO reply depends + * on this. + */ + if (*var_psc_tls_level) { + switch (tls_level_lookup(var_psc_tls_level)) { + default: + msg_fatal("Invalid TLS level \"%s\"", var_psc_tls_level); + /* NOTREACHED */ + break; + case TLS_LEV_SECURE: + case TLS_LEV_VERIFY: + case TLS_LEV_FPRINT: + msg_warn("%s: unsupported TLS level \"%s\", using \"encrypt\"", + VAR_PSC_TLS_LEVEL, var_psc_tls_level); + /* FALLTHROUGH */ + case TLS_LEV_ENCRYPT: + var_psc_enforce_tls = var_psc_use_tls = 1; + break; + case TLS_LEV_MAY: + var_psc_enforce_tls = 0; + var_psc_use_tls = 1; + break; + case TLS_LEV_NONE: + var_psc_enforce_tls = var_psc_use_tls = 0; + break; + } + } + var_psc_use_tls = var_psc_use_tls || var_psc_enforce_tls; +#ifdef TODO_SASL_AUTH + var_psc_tls_auth_only = var_psc_tls_auth_only || var_psc_enforce_tls; +#endif + + /* + * Initialize the EHLO reply. Once for plaintext sessions, and once for + * TLS sessions. + */ + psc_smtpd_format_ehlo_reply(psc_temp, psc_ehlo_discard_mask); + psc_smtpd_ehlo_reply_plain = mystrdup(STR(psc_temp)); + + psc_smtpd_format_ehlo_reply(psc_temp, + psc_ehlo_discard_mask | EHLO_MASK_STARTTLS); + psc_smtpd_ehlo_reply_tls = mystrdup(STR(psc_temp)); + + /* + * Initialize the 421 timeout reply. + */ + vstring_sprintf(psc_temp, "421 4.4.2 %s Error: timeout exceeded\r\n", + var_myhostname); + psc_smtpd_timeout_reply = mystrdup(STR(psc_temp)); + + /* + * Initialize the generic 421 reply. + */ + vstring_sprintf(psc_temp, "421 %s Service unavailable - try again later\r\n", + var_myhostname); + psc_smtpd_421_reply = mystrdup(STR(psc_temp)); + + /* + * Initialize the reply footer. + */ + if (*var_psc_rej_footer || *var_psc_rej_ftr_maps) + psc_expand_init(); +} + +/* psc_smtpd_pre_jail_init - per-process deep protocol test initialization */ + +void psc_smtpd_pre_jail_init(void) +{ + + /* + * Determine what server ESMTP features to suppress, typically to avoid + * inter-operability problems. We do the default filter here, and + * determine client-dependent filtering on the fly. + * + * XXX Bugger. This means we have to restart when the table changes! + */ + if (*var_psc_ehlo_dis_maps) + psc_ehlo_discard_maps = maps_create(VAR_PSC_EHLO_DIS_MAPS, + var_psc_ehlo_dis_maps, + DICT_FLAG_LOCK); + psc_ehlo_discard_mask = ehlo_mask(var_psc_ehlo_dis_words); + + /* + * Last-resort command editing support. + */ + if (*var_psc_cmd_filter) + psc_cmd_filter = dict_open(var_psc_cmd_filter, O_RDONLY, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); + + /* + * SMTP server reply footer. + */ + if (*var_psc_rej_ftr_maps) + pcs_send_pre_jail_init(); +} |