/*++
/* 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 allowlisted 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>
#include <info_log_addr_form.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 asynchronous 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 (ENFORCING_SIZE_LIMIT(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 5.5.4 Syntax: STARTTLS\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"));
#ifdef USE_TLS
    if (!psc_tls_ready)
	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);
#else
    return (PSC_SEND_REPLY(state,
			   "502 5.5.1 Error: command not implemented\r\n"));
#endif
}

/* 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),
	     info_log_addr_form_sender(state->sender),
	     info_log_addr_form_recipient(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 MAIL 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. Note: at this point, state->cmd_buffer is
	     * not null-terminated and may contain embedded null bytes.
	     */
	    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 allowlist 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;
	}

	/*
	 * Avoid complaints from Postfix maps about malformed content. Note:
	 * this will stop at the first null byte, just like the code that
	 * parses the command name or command arguments.
	 */
#define PSC_BAD_UTF8(str) \
	(var_smtputf8_enable && !valid_utf8_stringz(str))

	/*
	 * Terminate the command buffer, and apply the last-resort command
	 * editing workaround.
	 */
	VSTRING_TERMINATE(state->cmd_buffer);
	if (psc_cmd_filter != 0 && !PSC_BAD_UTF8(STR(state->cmd_buffer))) {
	    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)
		|| PSC_BAD_UTF8(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 allowlist 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 allowlist 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. This affects the EHLO greeting.
     */
    psc_tls_pre_jail();

    /*
     * 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_tls_ready ? 0 : EHLO_MASK_STARTTLS));
    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();
}
