summaryrefslogtreecommitdiffstats
path: root/src/postscreen/postscreen_smtpd.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/postscreen/postscreen_smtpd.c1329
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();
+}