/*	$OpenBSD: smtp_session.c,v 1.379 2018/12/21 14:41:41 gilles Exp $	*/

/*
 * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org>
 * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
 * Copyright (c) 2008-2009 Jacek Masiulaniec <jacekm@dobremiasto.net>
 * Copyright (c) 2012 Eric Faurot <eric@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/tree.h>
#include <sys/socket.h>
#include <sys/uio.h>

#include <netinet/in.h>

#include <ctype.h>
#include <errno.h>
#include <event.h>
#include <imsg.h>
#include <limits.h>
#include <inttypes.h>
#include <openssl/ssl.h>
#include <resolv.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <vis.h>

#include "smtpd.h"
#include "log.h"
#include "ssl.h"
#include "rfc5322.h"

#define	SMTP_LINE_MAX			65535
#define	DATA_HIWAT			65535
#define	APPEND_DOMAIN_BUFFER_SIZE	SMTP_LINE_MAX

enum smtp_state {
	STATE_NEW = 0,
	STATE_CONNECTED,
	STATE_TLS,
	STATE_HELO,
	STATE_AUTH_INIT,
	STATE_AUTH_USERNAME,
	STATE_AUTH_PASSWORD,
	STATE_AUTH_FINALIZE,
	STATE_BODY,
	STATE_QUIT,
};

enum session_flags {
	SF_EHLO			= 0x0001,
	SF_8BITMIME		= 0x0002,
	SF_SECURE		= 0x0004,
	SF_AUTHENTICATED	= 0x0008,
	SF_BOUNCE		= 0x0010,
	SF_VERIFIED		= 0x0020,
	SF_BADINPUT		= 0x0080,
};

enum {
	TX_OK = 0,
	TX_ERROR_ENVELOPE,
	TX_ERROR_SIZE,
	TX_ERROR_IO,
	TX_ERROR_LOOP,
	TX_ERROR_MALFORMED,
	TX_ERROR_RESOURCES,
	TX_ERROR_INTERNAL,
};

enum smtp_command {
	CMD_HELO = 0,
	CMD_EHLO,
	CMD_STARTTLS,
	CMD_AUTH,
	CMD_MAIL_FROM,
	CMD_RCPT_TO,
	CMD_DATA,
	CMD_RSET,
	CMD_QUIT,
	CMD_HELP,
	CMD_WIZ,
	CMD_NOOP,
	CMD_COMMIT,
};

struct smtp_rcpt {
	TAILQ_ENTRY(smtp_rcpt)	 entry;
	uint64_t		 evpid;
 	struct mailaddr		 maddr;
	size_t			 destcount;
};

struct smtp_tx {
	struct smtp_session	*session;
	uint32_t		 msgid;

	struct envelope		 evp;
	size_t			 rcptcount;
	size_t			 destcount;
	TAILQ_HEAD(, smtp_rcpt)	 rcpts;

	time_t			 time;
	int			 error;
	size_t			 datain;
	size_t			 odatalen;
	FILE			*ofile;
	struct io		*filter;
	struct rfc5322_parser	*parser;
	int			 rcvcount;
	int			 has_date;
	int			 has_message_id;
};

struct smtp_session {
	uint64_t		 id;
	struct io		*io;
	struct listener		*listener;
	void			*ssl_ctx;
	struct sockaddr_storage	 ss;
	char			 hostname[HOST_NAME_MAX+1];
	char			 smtpname[HOST_NAME_MAX+1];
	int			 fcrdns;

	int			 flags;
	enum smtp_state		 state;

	uint8_t			 banner_sent;
	char			 helo[LINE_MAX];
	char			 cmd[LINE_MAX];
	char			 username[SMTPD_MAXMAILADDRSIZE];

	size_t			 mailcount;
	struct event		 pause;

	struct smtp_tx		*tx;

	enum smtp_command	 last_cmd;
	enum filter_phase	 filter_phase;
	const char		*filter_param;
};

#define ADVERTISE_TLS(s) \
	((s)->listener->flags & F_STARTTLS && !((s)->flags & SF_SECURE))

#define ADVERTISE_AUTH(s) \
	((s)->listener->flags & F_AUTH && (s)->flags & SF_SECURE && \
	 !((s)->flags & SF_AUTHENTICATED))

#define ADVERTISE_EXT_DSN(s) \
	((s)->listener->flags & F_EXT_DSN)

#define	SESSION_FILTERED(s) \
	((s)->listener->flags & F_FILTERED)

#define	SESSION_DATA_FILTERED(s) \
	((s)->listener->flags & F_FILTERED)


static int smtp_mailaddr(struct mailaddr *, char *, int, char **, const char *);
static void smtp_session_init(void);
static void smtp_lookup_servername(struct smtp_session *);
static void smtp_getnameinfo_cb(void *, int, const char *, const char *);
static void smtp_getaddrinfo_cb(void *, int, struct addrinfo *);
static void smtp_connected(struct smtp_session *);
static void smtp_send_banner(struct smtp_session *);
static void smtp_tls_verified(struct smtp_session *);
static void smtp_io(struct io *, int, void *);
static void smtp_enter_state(struct smtp_session *, int);
static void smtp_reply(struct smtp_session *, char *, ...);
static void smtp_command(struct smtp_session *, char *);
static void smtp_rfc4954_auth_plain(struct smtp_session *, char *);
static void smtp_rfc4954_auth_login(struct smtp_session *, char *);
static void smtp_free(struct smtp_session *, const char *);
static const char *smtp_strstate(int);
static void smtp_tls_init(struct smtp_session *);
static int smtp_verify_certificate(struct smtp_session *);
static void smtp_cert_init(struct smtp_session *);
static void smtp_cert_init_cb(void *, int, const char *, const void *, size_t);
static void smtp_cert_verify(struct smtp_session *);
static void smtp_cert_verify_cb(void *, int);
static void smtp_auth_failure_pause(struct smtp_session *);
static void smtp_auth_failure_resume(int, short, void *);

static int  smtp_tx(struct smtp_session *);
static void smtp_tx_free(struct smtp_tx *);
static void smtp_tx_create_message(struct smtp_tx *);
static void smtp_tx_mail_from(struct smtp_tx *, const char *);
static void smtp_tx_rcpt_to(struct smtp_tx *, const char *);
static void smtp_tx_open_message(struct smtp_tx *);
static void smtp_tx_commit(struct smtp_tx *);
static void smtp_tx_rollback(struct smtp_tx *);
static int  smtp_tx_dataline(struct smtp_tx *, const char *);
static int  smtp_tx_filtered_dataline(struct smtp_tx *, const char *);
static void smtp_tx_eom(struct smtp_tx *);
static void smtp_filter_fd(struct smtp_tx *, int);
static int  smtp_message_fd(struct smtp_tx *, int);
static void smtp_message_begin(struct smtp_tx *);
static void smtp_message_end(struct smtp_tx *);
static int  smtp_filter_printf(struct smtp_tx *, const char *, ...);
static int  smtp_message_printf(struct smtp_tx *, const char *, ...);

static int  smtp_check_rset(struct smtp_session *, const char *);
static int  smtp_check_helo(struct smtp_session *, const char *);
static int  smtp_check_ehlo(struct smtp_session *, const char *);
static int  smtp_check_auth(struct smtp_session *s, const char *);
static int  smtp_check_starttls(struct smtp_session *, const char *);
static int  smtp_check_mail_from(struct smtp_session *, const char *);
static int  smtp_check_rcpt_to(struct smtp_session *, const char *);
static int  smtp_check_data(struct smtp_session *, const char *);
static int  smtp_check_noparam(struct smtp_session *, const char *);

static void smtp_filter_phase(enum filter_phase, struct smtp_session *, const char *);

static void smtp_proceed_connected(struct smtp_session *);
static void smtp_proceed_rset(struct smtp_session *, const char *);
static void smtp_proceed_helo(struct smtp_session *, const char *);
static void smtp_proceed_ehlo(struct smtp_session *, const char *);
static void smtp_proceed_auth(struct smtp_session *, const char *);
static void smtp_proceed_starttls(struct smtp_session *, const char *);
static void smtp_proceed_mail_from(struct smtp_session *, const char *);
static void smtp_proceed_rcpt_to(struct smtp_session *, const char *);
static void smtp_proceed_data(struct smtp_session *, const char *);
static void smtp_proceed_noop(struct smtp_session *, const char *);
static void smtp_proceed_help(struct smtp_session *, const char *);
static void smtp_proceed_wiz(struct smtp_session *, const char *);
static void smtp_proceed_quit(struct smtp_session *, const char *);
static void smtp_proceed_commit(struct smtp_session *, const char *);
static void smtp_proceed_rollback(struct smtp_session *, const char *);

static void smtp_filter_begin(struct smtp_session *);
static void smtp_filter_end(struct smtp_session *);
static void smtp_filter_data_begin(struct smtp_session *);
static void smtp_filter_data_end(struct smtp_session *);

static struct {
	int code;
	enum filter_phase filter_phase;
	const char *cmd;

	int (*check)(struct smtp_session *, const char *);
	void (*proceed)(struct smtp_session *, const char *);
} commands[] = {
	{ CMD_HELO,             FILTER_HELO,            "HELO",         smtp_check_helo,        smtp_proceed_helo },
	{ CMD_EHLO,             FILTER_EHLO,            "EHLO",         smtp_check_ehlo,        smtp_proceed_ehlo },
	{ CMD_STARTTLS,         FILTER_STARTTLS,        "STARTTLS",     smtp_check_starttls,    smtp_proceed_starttls },
	{ CMD_AUTH,             FILTER_AUTH,            "AUTH",         smtp_check_auth,        smtp_proceed_auth },
	{ CMD_MAIL_FROM,        FILTER_MAIL_FROM,       "MAIL FROM",    smtp_check_mail_from,   smtp_proceed_mail_from },
	{ CMD_RCPT_TO,          FILTER_RCPT_TO,         "RCPT TO",      smtp_check_rcpt_to,     smtp_proceed_rcpt_to },
	{ CMD_DATA,             FILTER_DATA,            "DATA",         smtp_check_data,        smtp_proceed_data },
	{ CMD_RSET,             FILTER_RSET,            "RSET",         smtp_check_rset,        smtp_proceed_rset },
	{ CMD_QUIT,             FILTER_QUIT,            "QUIT",         smtp_check_noparam,     smtp_proceed_quit },
	{ CMD_NOOP,             FILTER_NOOP,            "NOOP",         smtp_check_noparam,     smtp_proceed_noop },
	{ CMD_HELP,             FILTER_HELP,            "HELP",         smtp_check_noparam,     smtp_proceed_help },
	{ CMD_WIZ,              FILTER_WIZ,             "WIZ",          smtp_check_noparam,     smtp_proceed_wiz },
	{ CMD_COMMIT,  		FILTER_COMMIT,		".",		smtp_check_noparam,	smtp_proceed_commit },
	{ -1,                   0,                      NULL,           NULL },
};

static struct tree wait_lka_helo;
static struct tree wait_lka_mail;
static struct tree wait_lka_rcpt;
static struct tree wait_parent_auth;
static struct tree wait_queue_msg;
static struct tree wait_queue_fd;
static struct tree wait_queue_commit;
static struct tree wait_ssl_init;
static struct tree wait_ssl_verify;
static struct tree wait_filters;
static struct tree wait_filter_fd;

static void
header_append_domain_buffer(char *buffer, char *domain, size_t len)
{
	size_t	i;
	int	escape, quote, comment, bracket;
	int	has_domain, has_bracket, has_group;
	int	pos_bracket, pos_component, pos_insert;
	char	copy[APPEND_DOMAIN_BUFFER_SIZE];

	i = 0;
	escape = quote = comment = bracket = 0;
	has_domain = has_bracket = has_group = 0;
	pos_bracket = pos_insert = pos_component = 0;
	for (i = 0; buffer[i]; ++i) {
		if (buffer[i] == '(' && !escape && !quote)
			comment++;
		if (buffer[i] == '"' && !escape && !comment)
			quote = !quote;
		if (buffer[i] == ')' && !escape && !quote && comment)
			comment--;
		if (buffer[i] == '\\' && !escape && !comment && !quote)
			escape = 1;
		else
			escape = 0;
		if (buffer[i] == '<' && !escape && !comment && !quote && !bracket) {
			bracket++;
			has_bracket = 1;
		}
		if (buffer[i] == '>' && !escape && !comment && !quote && bracket) {
			bracket--;
			pos_bracket = i;
		}
		if (buffer[i] == '@' && !escape && !comment && !quote)
			has_domain = 1;
		if (buffer[i] == ':' && !escape && !comment && !quote)
			has_group = 1;

		/* update insert point if not in comment and not on a whitespace */
		if (!comment && buffer[i] != ')' && !isspace((unsigned char)buffer[i]))
			pos_component = i;
	}

	/* parse error, do not attempt to modify */
	if (escape || quote || comment || bracket)
		return;

	/* domain already present, no need to modify */
	if (has_domain)
		return;

	/* address is group, skip */
	if (has_group)
		return;

	/* there's an address between brackets, just append domain */
	if (has_bracket) {
		pos_bracket--;
		while (isspace((unsigned char)buffer[pos_bracket]))
			pos_bracket--;
		if (buffer[pos_bracket] == '<')
			return;
		pos_insert = pos_bracket + 1;
	}
	else {
		/* otherwise append address to last component */
		pos_insert = pos_component + 1;

		/* empty address */
                if (buffer[pos_component] == '\0' ||
		    isspace((unsigned char)buffer[pos_component]))
                        return;
	}

	if (snprintf(copy, sizeof copy, "%.*s@%s%s",
		(int)pos_insert, buffer,
		domain,
		buffer+pos_insert) >= (int)sizeof copy)
		return;

	memcpy(buffer, copy, len);
}

static void
header_address_rewrite_buffer(char *buffer, const char *address, size_t len)
{
	size_t	i;
	int	address_len;
	int	escape, quote, comment, bracket;
	int	has_bracket, has_group;
	int	pos_bracket_beg, pos_bracket_end, pos_component_beg, pos_component_end;
	int	insert_beg, insert_end;
	char	copy[APPEND_DOMAIN_BUFFER_SIZE];

	escape = quote = comment = bracket = 0;
	has_bracket = has_group = 0;
	pos_bracket_beg = pos_bracket_end = pos_component_beg = pos_component_end = 0;
	for (i = 0; buffer[i]; ++i) {
		if (buffer[i] == '(' && !escape && !quote)
			comment++;
		if (buffer[i] == '"' && !escape && !comment)
			quote = !quote;
		if (buffer[i] == ')' && !escape && !quote && comment)
			comment--;
		if (buffer[i] == '\\' && !escape && !comment && !quote)
			escape = 1;
		else
			escape = 0;
		if (buffer[i] == '<' && !escape && !comment && !quote && !bracket) {
			bracket++;
			has_bracket = 1;
			pos_bracket_beg = i+1;
		}
		if (buffer[i] == '>' && !escape && !comment && !quote && bracket) {
			bracket--;
			pos_bracket_end = i;
		}
		if (buffer[i] == ':' && !escape && !comment && !quote)
			has_group = 1;

		/* update insert point if not in comment and not on a whitespace */
		if (!comment && buffer[i] != ')' && !isspace((unsigned char)buffer[i]))
			pos_component_end = i;
	}

	/* parse error, do not attempt to modify */
	if (escape || quote || comment || bracket)
		return;

	/* address is group, skip */
	if (has_group)
		return;

	/* there's an address between brackets, just replace everything brackets */
	if (has_bracket) {
		insert_beg = pos_bracket_beg;
		insert_end = pos_bracket_end;
	}
	else {
		if (pos_component_end == 0)
			pos_component_beg = 0;
		else {
			for (pos_component_beg = pos_component_end; pos_component_beg >= 0; --pos_component_beg)
				if (buffer[pos_component_beg] == ')' || isspace(buffer[pos_component_beg]))
					break;
			pos_component_beg += 1;
			pos_component_end += 1;
		}
		insert_beg = pos_component_beg;
		insert_end = pos_component_end;
	}

	/* check that masquerade won' t overflow */
	address_len = strlen(address);
	if (strlen(buffer) - (insert_end - insert_beg) + address_len >= len)
		return;

	(void)strlcpy(copy, buffer, sizeof copy);
	(void)strlcpy(copy+insert_beg, address, sizeof (copy) - insert_beg);
	(void)strlcat(copy, buffer+insert_end, sizeof (copy));
	memcpy(buffer, copy, len);
}

static void
header_domain_append_callback(struct smtp_tx *tx, const char *hdr,
    const char *val)
{
	size_t			i, j, linelen;
	int			escape, quote, comment, skip;
	char			buffer[APPEND_DOMAIN_BUFFER_SIZE];
	const char *line, *end;

	if (smtp_message_printf(tx, "%s:", hdr) == -1)
		return;

	j = 0;
	escape = quote = comment = skip = 0;
	memset(buffer, 0, sizeof buffer);

	for (line = val; line; line = end) {
		end = strchr(line, '\n');
		if (end) {
			linelen = end - line;
			end++;
		}
		else
			linelen = strlen(line);

		for (i = 0; i < linelen; ++i) {
			if (line[i] == '(' && !escape && !quote)
				comment++;
			if (line[i] == '"' && !escape && !comment)
				quote = !quote;
			if (line[i] == ')' && !escape && !quote && comment)
				comment--;
			if (line[i] == '\\' && !escape && !comment && !quote)
				escape = 1;
			else
				escape = 0;

			/* found a separator, buffer contains a full address */
			if (line[i] == ',' && !escape && !quote && !comment) {
				if (!skip && j + strlen(tx->session->listener->hostname) + 1 < sizeof buffer) {
					header_append_domain_buffer(buffer, tx->session->listener->hostname, sizeof buffer);
					if (tx->session->flags & SF_AUTHENTICATED &&
					    tx->session->listener->sendertable[0] &&
					    tx->session->listener->flags & F_MASQUERADE &&
					    !(strcasecmp(hdr, "From")))
						header_address_rewrite_buffer(buffer, mailaddr_to_text(&tx->evp.sender),
						    sizeof buffer);
				}
				if (smtp_message_printf(tx, "%s,", buffer) == -1)
					return;
				j = 0;
				skip = 0;
				memset(buffer, 0, sizeof buffer);
			}
			else {
				if (skip) {
					if (smtp_message_printf(tx, "%c", line[i]) == -1)
						return;
				}
				else {
					buffer[j++] = line[i];
					if (j == sizeof (buffer) - 1) {
						if (smtp_message_printf(tx, "%s", buffer) == -1)
							return;
						skip = 1;
						j = 0;
						memset(buffer, 0, sizeof buffer);
					}
				}
			}
		}
		if (skip) {
			if (smtp_message_printf(tx, "\n") == -1)
				return;
		}
		else {
			buffer[j++] = '\n';
			if (j == sizeof (buffer) - 1) {
				if (smtp_message_printf(tx, "%s", buffer) == -1)
					return;
				skip = 1;
				j = 0;
				memset(buffer, 0, sizeof buffer);
			}
		}
	}

	/* end of header, if buffer is not empty we'll process it */
	if (buffer[0]) {
		if (j + strlen(tx->session->listener->hostname) + 1 < sizeof buffer) {
			header_append_domain_buffer(buffer, tx->session->listener->hostname, sizeof buffer);
			if (tx->session->flags & SF_AUTHENTICATED &&
			    tx->session->listener->sendertable[0] &&
			    tx->session->listener->flags & F_MASQUERADE &&
			    !(strcasecmp(hdr, "From")))
				header_address_rewrite_buffer(buffer, mailaddr_to_text(&tx->evp.sender),
				    sizeof buffer);
		}
		smtp_message_printf(tx, "%s", buffer);
	}
}

static void
smtp_session_init(void)
{
	static int	init = 0;

	if (!init) {
		tree_init(&wait_lka_helo);
		tree_init(&wait_lka_mail);
		tree_init(&wait_lka_rcpt);
		tree_init(&wait_parent_auth);
		tree_init(&wait_queue_msg);
		tree_init(&wait_queue_fd);
		tree_init(&wait_queue_commit);
		tree_init(&wait_ssl_init);
		tree_init(&wait_ssl_verify);
		tree_init(&wait_filters);
		tree_init(&wait_filter_fd);
		init = 1;
	}
}

int
smtp_session(struct listener *listener, int sock,
    const struct sockaddr_storage *ss, const char *hostname, struct io *io)
{
	struct smtp_session	*s;

	smtp_session_init();

	if ((s = calloc(1, sizeof(*s))) == NULL)
		return (-1);

	s->id = generate_uid();
	s->listener = listener;
	memmove(&s->ss, ss, sizeof(*ss));

	if (io != NULL)
		s->io = io;
	else
		s->io = io_new();

	io_set_callback(s->io, smtp_io, s);
	io_set_fd(s->io, sock);
	io_set_timeout(s->io, SMTPD_SESSION_TIMEOUT * 1000);
	io_set_write(s->io);

	s->state = STATE_NEW;

	(void)strlcpy(s->smtpname, listener->hostname, sizeof(s->smtpname));

	log_trace(TRACE_SMTP, "smtp: %p: connected to listener %p "
	    "[hostname=%s, port=%d, tag=%s]", s, listener,
	    listener->hostname, ntohs(listener->port), listener->tag);

	/* For local enqueueing, the hostname is already set */
	if (hostname) {
		s->flags |= SF_AUTHENTICATED;
		/* A bit of a hack */
		if (!strcmp(hostname, "localhost"))
			s->flags |= SF_BOUNCE;
		(void)strlcpy(s->hostname, hostname, sizeof(s->hostname));
		s->fcrdns = 1;
		smtp_lookup_servername(s);
	} else {
		resolver_getnameinfo((struct sockaddr *)&s->ss, NI_NAMEREQD,
		    smtp_getnameinfo_cb, s);
	}

	/* session may have been freed by now */

	return (0);
}

static void
smtp_getnameinfo_cb(void *arg, int gaierrno, const char *host, const char *serv)
{
	struct smtp_session *s = arg;
	struct addrinfo hints;

	if (gaierrno) {
		log_warnx("getnameinfo: %s: %s", ss_to_text(&s->ss),
		    gai_strerror(gaierrno));
		(void)strlcpy(s->hostname, "<unknown>", sizeof(s->hostname));

		if (gaierrno == EAI_NODATA || gaierrno == EAI_NONAME)
			s->fcrdns = 0;
		else
			s->fcrdns = -1;

		smtp_lookup_servername(s);
		return;
	}

	(void)strlcpy(s->hostname, host, sizeof(s->hostname));

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = s->ss.ss_family;
	hints.ai_socktype = SOCK_STREAM;
	resolver_getaddrinfo(s->hostname, NULL, &hints, smtp_getaddrinfo_cb, s);
}

static void
smtp_getaddrinfo_cb(void *arg, int gaierrno, struct addrinfo *ai0)
{
	struct smtp_session *s = arg;
	struct addrinfo *ai;
	char fwd[64], rev[64];

	if (gaierrno) {
		log_warnx("getaddrinfo: %s: %s", s->hostname,
		    gai_strerror(gaierrno));

		if (gaierrno == EAI_NODATA || gaierrno == EAI_NONAME)
			s->fcrdns = 0;
		else
			s->fcrdns = -1;
	}
	else {
		strlcpy(rev, ss_to_text(&s->ss), sizeof(rev));
		for (ai = ai0; ai; ai = ai->ai_next) {
			strlcpy(fwd, sa_to_text(ai->ai_addr), sizeof(fwd));
			if (!strcmp(fwd, rev)) {
				s->fcrdns = 1;
				break;
			}
		}
		freeaddrinfo(ai0);
	}

	smtp_lookup_servername(s);
}

void
smtp_session_imsg(struct mproc *p, struct imsg *imsg)
{
	struct ca_cert_resp_msg       	*resp_ca_cert;
	struct ca_vrfy_resp_msg       	*resp_ca_vrfy;
	struct smtp_session		*s;
	struct smtp_rcpt		*rcpt;
	void				*ssl;
	char				 user[LOGIN_NAME_MAX];
	struct msg			 m;
	const char			*line, *helo;
	uint64_t			 reqid, evpid;
	uint32_t			 msgid;
	int				 status, success;
	void				*ssl_ctx;
	int                              filter_response;
	const char                      *filter_param;
	uint8_t                          i;

	switch (imsg->hdr.type) {

	case IMSG_SMTP_CHECK_SENDER:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &status);
		m_end(&m);
		s = tree_xpop(&wait_lka_mail, reqid);
		switch (status) {
		case LKA_OK:
			smtp_tx_create_message(s->tx);
			break;

		case LKA_PERMFAIL:
			smtp_tx_free(s->tx);
			smtp_reply(s, "%d %s", 530, "Sender rejected");
			break;
		case LKA_TEMPFAIL:
			smtp_tx_free(s->tx);
			smtp_reply(s, "421 %s: Temporary Error",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
			break;
		}
		return;

	case IMSG_SMTP_EXPAND_RCPT:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &status);
		m_get_string(&m, &line);
		m_end(&m);
		s = tree_xpop(&wait_lka_rcpt, reqid);
		switch (status) {
		case LKA_OK:
			fatalx("unexpected ok");
		case LKA_PERMFAIL:
			smtp_reply(s, "%s", line);
			break;
		case LKA_TEMPFAIL:
			smtp_reply(s, "%s", line);
		}
		return;

	case IMSG_SMTP_LOOKUP_HELO:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		s = tree_xpop(&wait_lka_helo, reqid);
		m_get_int(&m, &status);
		if (status == LKA_OK) {
			m_get_string(&m, &helo);
			(void)strlcpy(s->smtpname, helo, sizeof(s->smtpname));
		}
		m_end(&m);
		smtp_connected(s);
		return;

	case IMSG_SMTP_MESSAGE_CREATE:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		s = tree_xpop(&wait_queue_msg, reqid);
		if (success) {
			m_get_msgid(&m, &msgid);
			s->tx->msgid = msgid;
			s->tx->evp.id = msgid_to_evpid(msgid);
			s->tx->rcptcount = 0;
			report_smtp_tx_begin("smtp-in", s->id, s->tx->msgid);
			smtp_reply(s, "250 %s: Ok",
			    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
		} else {
			smtp_tx_free(s->tx);
			smtp_reply(s, "421 %s: Temporary Error",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
			smtp_enter_state(s, STATE_QUIT);
		}
		m_end(&m);
		return;

	case IMSG_SMTP_MESSAGE_OPEN:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		m_end(&m);

		s = tree_xpop(&wait_queue_fd, reqid);
		if (!success || imsg->fd == -1) {
			if (imsg->fd != -1)
				close(imsg->fd);
			smtp_reply(s, "421 %s: Temporary Error",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
			smtp_enter_state(s, STATE_QUIT);
			return;
		}

		log_debug("smtp: %p: fd %d from queue", s, imsg->fd);

		if (smtp_message_fd(s->tx, imsg->fd)) {
			if (!SESSION_DATA_FILTERED(s))
				smtp_message_begin(s->tx);
			else
				smtp_filter_data_begin(s);
		}
		return;

	case IMSG_FILTER_SMTP_DATA_BEGIN:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		m_end(&m);

		s = tree_xpop(&wait_filter_fd, reqid);
		if (!success || imsg->fd == -1) {
			if (imsg->fd != -1)
				close(imsg->fd);
			smtp_reply(s, "421 %s: Temporary Error",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
			smtp_enter_state(s, STATE_QUIT);
			return;
		}

		log_debug("smtp: %p: fd %d from lka", s, imsg->fd);

		smtp_filter_fd(s->tx, imsg->fd);
		smtp_message_begin(s->tx);
		return;

	case IMSG_QUEUE_ENVELOPE_SUBMIT:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		s = tree_xget(&wait_lka_rcpt, reqid);
		if (success) {
			m_get_evpid(&m, &evpid);
			s->tx->evp.id = evpid;
			s->tx->destcount++;
			report_smtp_tx_envelope("smtp-in", s->id, s->tx->msgid, evpid);
		}
		else
			s->tx->error = TX_ERROR_ENVELOPE;
		m_end(&m);
		return;

	case IMSG_QUEUE_ENVELOPE_COMMIT:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		m_end(&m);
		if (!success)
			fatalx("commit evp failed: not supposed to happen");
		s = tree_xpop(&wait_lka_rcpt, reqid);
		if (s->tx->error) {
			/*
			 * If an envelope failed, we can't cancel the last
			 * RCPT only so we must cancel the whole transaction
			 * and close the connection.
			 */
			smtp_reply(s, "421 %s: Temporary failure",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
			smtp_enter_state(s, STATE_QUIT);
		}
		else {
			rcpt = xcalloc(1, sizeof(*rcpt));
			rcpt->evpid = s->tx->evp.id;
			rcpt->destcount = s->tx->destcount;
			rcpt->maddr = s->tx->evp.rcpt;
			TAILQ_INSERT_TAIL(&s->tx->rcpts, rcpt, entry);

			s->tx->destcount = 0;
			s->tx->rcptcount++;
			smtp_reply(s, "250 %s %s: Recipient ok",
			    esc_code(ESC_STATUS_OK, ESC_DESTINATION_ADDRESS_VALID),
			    esc_description(ESC_DESTINATION_ADDRESS_VALID));
		}
		return;

	case IMSG_SMTP_MESSAGE_COMMIT:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		m_end(&m);
		s = tree_xpop(&wait_queue_commit, reqid);
		if (!success) {
			smtp_tx_free(s->tx);
			smtp_reply(s, "421 %s: Temporary failure",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
			smtp_enter_state(s, STATE_QUIT);
			return;
		}

		smtp_reply(s, "250 %s: %08x Message accepted for delivery",
		    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS),
		    s->tx->msgid);

		log_info("%016"PRIx64" smtp message "
		    "msgid=%08x size=%zu nrcpt=%zu proto=%s",
		    s->id,
		    s->tx->msgid,
		    s->tx->odatalen,
		    s->tx->rcptcount,
		    s->flags & SF_EHLO ? "ESMTP" : "SMTP");
		TAILQ_FOREACH(rcpt, &s->tx->rcpts, entry) {
			log_info("%016"PRIx64" smtp envelope "
			    "evpid=%016"PRIx64" from=<%s%s%s> to=<%s%s%s>",
			    s->id,
			    rcpt->evpid,
			    s->tx->evp.sender.user,
			    s->tx->evp.sender.user[0] == '\0' ? "" : "@",
			    s->tx->evp.sender.domain,
			    rcpt->maddr.user,
			    rcpt->maddr.user[0] == '\0' ? "" : "@",
			    rcpt->maddr.domain);
		}
		smtp_tx_free(s->tx);
		s->mailcount++;
		smtp_enter_state(s, STATE_HELO);
		return;

	case IMSG_SMTP_AUTHENTICATE:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &success);
		m_end(&m);

		s = tree_xpop(&wait_parent_auth, reqid);
		strnvis(user, s->username, sizeof user, VIS_WHITE | VIS_SAFE);
		if (success == LKA_OK) {
			log_info("%016"PRIx64" smtp "
			    "authentication user=%s "
			    "result=ok",
			    s->id, user);
			s->flags |= SF_AUTHENTICATED;
			smtp_reply(s, "235 %s: Authentication succeeded",
			    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
		}
		else if (success == LKA_PERMFAIL) {
			log_info("%016"PRIx64" smtp "
			    "authentication user=%s "
			    "result=permfail",
			    s->id, user);
			smtp_auth_failure_pause(s);
			return;
		}
		else if (success == LKA_TEMPFAIL) {
			log_info("%016"PRIx64" smtp "
			    "authentication user=%s "
			    "result=tempfail",
			    s->id, user);
			smtp_reply(s, "421 %s: Temporary failure",
			    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
		}
		else
			fatalx("bad lka response");

		smtp_enter_state(s, STATE_HELO);
		return;

	case IMSG_SMTP_TLS_INIT:
		resp_ca_cert = imsg->data;
		s = tree_xpop(&wait_ssl_init, resp_ca_cert->reqid);

		if (resp_ca_cert->status == CA_FAIL) {
			log_info("%016"PRIx64" smtp disconnected "
			    "reason=ca-failure",
			    s->id);
			smtp_free(s, "CA failure");
			return;
		}

		resp_ca_cert = xmemdup(imsg->data, sizeof *resp_ca_cert);
		resp_ca_cert->cert = xstrdup((char *)imsg->data +
		    sizeof *resp_ca_cert);
		ssl_ctx = dict_get(env->sc_ssl_dict, resp_ca_cert->name);
		ssl = ssl_smtp_init(ssl_ctx, s->listener->flags & F_TLS_VERIFY);
		io_set_read(s->io);
		io_start_tls(s->io, ssl);

		freezero(resp_ca_cert->cert, resp_ca_cert->cert_len);
		free(resp_ca_cert);
		return;

	case IMSG_SMTP_TLS_VERIFY:
		resp_ca_vrfy = imsg->data;
		s = tree_xpop(&wait_ssl_verify, resp_ca_vrfy->reqid);

		if (resp_ca_vrfy->status == CA_OK)
			s->flags |= SF_VERIFIED;
		else if (s->listener->flags & F_TLS_VERIFY) {
			log_info("%016"PRIx64" smtp "
			    "disconnected reason=cert-check-failed",
			    s->id);
			smtp_free(s, "SSL certificate check failed");
			return;
		}
		smtp_tls_verified(s);
		io_resume(s->io, IO_IN);
		return;

	case IMSG_FILTER_SMTP_PROTOCOL:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_int(&m, &filter_response);
		if (filter_response != FILTER_PROCEED)
			m_get_string(&m, &filter_param);
		else
			filter_param = NULL;
		m_end(&m);

		s = tree_xpop(&wait_filters, reqid);

		switch (filter_response) {
		case FILTER_REJECT:
		case FILTER_DISCONNECT:
			if (!valid_smtp_response(filter_param) ||
			    (filter_param[0] != '4' && filter_param[0] != '5'))
				filter_param = "421 Internal server error";
			if (!strncmp(filter_param, "421", 3))
				filter_response = FILTER_DISCONNECT;

			report_smtp_filter_response("smtp-in", s->id, s->filter_phase,
			    filter_response, filter_param);

			smtp_reply(s, "%s", filter_param);

			if (filter_response == FILTER_DISCONNECT)
				smtp_enter_state(s, STATE_QUIT);
			else if (s->filter_phase == FILTER_COMMIT)
				smtp_proceed_rollback(s, NULL);
			break;

		case FILTER_PROCEED:
			filter_param = s->filter_param;
			/* fallthrough*/

		case FILTER_REWRITE:
			report_smtp_filter_response("smtp-in", s->id, s->filter_phase,
			    filter_response,
			    filter_param == s->filter_param ? NULL : filter_param);
			if (s->filter_phase == FILTER_CONNECT) {
				smtp_proceed_connected(s);
				return;
			}
			for (i = 0; i < nitems(commands); ++i)
				if (commands[i].filter_phase == s->filter_phase) {
					if (filter_response == FILTER_REWRITE)
						if (!commands[i].check(s, filter_param))
							break;
					commands[i].proceed(s, filter_param);
					break;
				}
			break;
		}
		return;
	}

	log_warnx("smtp_session_imsg: unexpected %s imsg",
	    imsg_to_str(imsg->hdr.type));
	fatalx(NULL);
}

static void
smtp_tls_verified(struct smtp_session *s)
{
	X509 *x;

	x = SSL_get_peer_certificate(io_ssl(s->io));
	if (x) {
		log_info("%016"PRIx64" smtp "
		    "client-cert-check result=\"%s\"",
		    s->id,
		    (s->flags & SF_VERIFIED) ? "success" : "failure");
		X509_free(x);
	}

	if (s->listener->flags & F_SMTPS) {
		stat_increment("smtp.smtps", 1);
		io_set_write(s->io);
		smtp_send_banner(s);
	}
	else {
		stat_increment("smtp.tls", 1);
		smtp_enter_state(s, STATE_HELO);
	}
}

static void
smtp_io(struct io *io, int evt, void *arg)
{
	struct smtp_session    *s = arg;
	char		       *line;
	size_t			len;
	int			eom;

	log_trace(TRACE_IO, "smtp: %p: %s %s", s, io_strevent(evt),
	    io_strio(io));

	switch (evt) {

	case IO_TLSREADY:
		log_info("%016"PRIx64" smtp tls ciphers=%s",
		    s->id, ssl_to_text(io_ssl(s->io)));

		report_smtp_link_tls("smtp-in", s->id, ssl_to_text(io_ssl(s->io)));

		s->flags |= SF_SECURE;
		s->helo[0] = '\0';

		smtp_cert_verify(s);
		break;

	case IO_DATAIN:
	    nextline:
		line = io_getline(s->io, &len);
		if ((line == NULL && io_datalen(s->io) >= SMTP_LINE_MAX) ||
		    (line && len >= SMTP_LINE_MAX)) {
			s->flags |= SF_BADINPUT;
			smtp_reply(s, "500 %s: Line too long",
			    esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_STATUS));
			smtp_enter_state(s, STATE_QUIT);
			io_set_write(io);
			return;
		}

		/* No complete line received */
		if (line == NULL)
			return;

		/* Message body */
		eom = 0;
		if (s->state == STATE_BODY) {
			eom = (s->tx->filter == NULL) ?
			    smtp_tx_dataline(s->tx, line) :
			    smtp_tx_filtered_dataline(s->tx, line);
			if (eom == 0)
				goto nextline;
		}

		/* Pipelining not supported */
		if (io_datalen(s->io)) {
			s->flags |= SF_BADINPUT;
			smtp_reply(s, "500 %s %s: Pipelining not supported",
			    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
			    esc_description(ESC_INVALID_COMMAND));
			smtp_enter_state(s, STATE_QUIT);
			io_set_write(io);
			return;
		}

		if (eom) {
			io_set_write(io);
			if (s->tx->filter == NULL)
				smtp_tx_eom(s->tx);
			return;
		}

		/* Must be a command */
		if (strlcpy(s->cmd, line, sizeof(s->cmd)) >= sizeof(s->cmd)) {
			s->flags |= SF_BADINPUT;
			smtp_reply(s, "500 %s: Command line too long",
			    esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_STATUS));
			smtp_enter_state(s, STATE_QUIT);
			io_set_write(io);
			return;
		}
		io_set_write(io);
		smtp_command(s, line);
		break;

	case IO_LOWAT:
		if (s->state == STATE_QUIT) {
			log_info("%016"PRIx64" smtp disconnected "
			    "reason=quit",
			    s->id);
			smtp_free(s, "done");
			break;
		}

		/* Wait for the client to start tls */
		if (s->state == STATE_TLS) {
			smtp_cert_init(s);
			break;
		}

		io_set_read(io);
		break;

	case IO_TIMEOUT:
		log_info("%016"PRIx64" smtp disconnected "
		    "reason=timeout",
		    s->id);
		smtp_free(s, "timeout");
		break;

	case IO_DISCONNECTED:
		log_info("%016"PRIx64" smtp disconnected "
		    "reason=disconnect",
		    s->id);
		smtp_free(s, "disconnected");
		break;

	case IO_ERROR:
		log_info("%016"PRIx64" smtp disconnected "
		    "reason=\"io-error: %s\"",
		    s->id, io_error(io));
		smtp_free(s, "IO error");
		break;

	default:
		fatalx("smtp_io()");
	}
}

static void
smtp_command(struct smtp_session *s, char *line)
{
	char			       *args;
	int				cmd, i;

	log_trace(TRACE_SMTP, "smtp: %p: <<< %s", s, line);
	report_smtp_protocol_client("smtp-in", s->id, line);

	/*
	 * These states are special.
	 */
	if (s->state == STATE_AUTH_INIT) {
		smtp_rfc4954_auth_plain(s, line);
		return;
	}
	if (s->state == STATE_AUTH_USERNAME || s->state == STATE_AUTH_PASSWORD) {
		smtp_rfc4954_auth_login(s, line);
		return;
	}

	/*
	 * Unlike other commands, "mail from" and "rcpt to" contain a
	 * space in the command name.
	 */
	if (strncasecmp("mail from:", line, 10) == 0 ||
	    strncasecmp("rcpt to:", line, 8) == 0)
		args = strchr(line, ':');
	else
		args = strchr(line, ' ');

	if (args) {
		*args++ = '\0';
		while (isspace((unsigned char)*args))
			args++;
	}

	cmd = -1;
	for (i = 0; commands[i].code != -1; i++)
		if (!strcasecmp(line, commands[i].cmd)) {
			cmd = commands[i].code;
			break;
		}

	s->last_cmd = cmd;
	switch (cmd) {
	/*
	 * INIT
	 */
	case CMD_HELO:
		if (!smtp_check_helo(s, args))
			break;
		smtp_filter_phase(FILTER_HELO, s, args);
		break;

	case CMD_EHLO:
		if (!smtp_check_ehlo(s, args))
			break;
		smtp_filter_phase(FILTER_EHLO, s, args);
		break;

	/*
	 * SETUP
	 */
	case CMD_STARTTLS:
		if (!smtp_check_starttls(s, args))
			break;

		smtp_filter_phase(FILTER_STARTTLS, s, NULL);
		break;

	case CMD_AUTH:
		if (!smtp_check_auth(s, args))
			break;
		smtp_filter_phase(FILTER_AUTH, s, args);
		break;

	case CMD_MAIL_FROM:
		if (!smtp_check_mail_from(s, args))
			break;
		smtp_filter_phase(FILTER_MAIL_FROM, s, args);
		break;

	/*
	 * TRANSACTION
	 */
	case CMD_RCPT_TO:
		if (!smtp_check_rcpt_to(s, args))
			break;
		smtp_filter_phase(FILTER_RCPT_TO, s, args);
		break;

	case CMD_RSET:
		if (!smtp_check_rset(s, args))
			break;
		smtp_filter_phase(FILTER_RSET, s, NULL);
		break;

	case CMD_DATA:
		if (!smtp_check_data(s, args))
			break;
		smtp_filter_phase(FILTER_DATA, s, NULL);
		break;

	/*
	 * ANY
	 */
	case CMD_QUIT:
		if (!smtp_check_noparam(s, args))
			break;		
		smtp_filter_phase(FILTER_QUIT, s, NULL);
		break;

	case CMD_NOOP:
		if (!smtp_check_noparam(s, args))
			break;		
		smtp_filter_phase(FILTER_NOOP, s, NULL);
		break;

	case CMD_HELP:
		if (!smtp_check_noparam(s, args))
			break;		
		smtp_proceed_help(s, NULL);
		break;

	case CMD_WIZ:
		if (!smtp_check_noparam(s, args))
			break;		
		smtp_proceed_wiz(s, NULL);
		break;

	default:
		smtp_reply(s, "500 %s %s: Command unrecognized",
			    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
			    esc_description(ESC_INVALID_COMMAND));
		break;
	}
}

static int
smtp_check_rset(struct smtp_session *s, const char *args)
{
	if (!smtp_check_noparam(s, args))
		return 0;

	if (s->helo[0] == '\0') {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}
	return 1;
}

static int
smtp_check_helo(struct smtp_session *s, const char *args)
{
	if (!s->banner_sent) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->helo[0]) {
		smtp_reply(s, "503 %s %s: Already identified",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (args == NULL) {
		smtp_reply(s, "501 %s %s: HELO requires domain name",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (!valid_domainpart(args)) {
		smtp_reply(s, "501 %s %s: Invalid domain name",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
		    esc_description(ESC_INVALID_COMMAND_ARGUMENTS));
		return 0;
	}

	return 1;
}

static int
smtp_check_ehlo(struct smtp_session *s, const char *args)
{
	if (!s->banner_sent) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->helo[0]) {
		smtp_reply(s, "503 %s %s: Already identified",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (args == NULL) {
		smtp_reply(s, "501 %s %s: EHLO requires domain name",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (!valid_domainpart(args)) {
		smtp_reply(s, "501 %s %s: Invalid domain name",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
		    esc_description(ESC_INVALID_COMMAND_ARGUMENTS));
		return 0;
	}

	return 1;
}

static int
smtp_check_auth(struct smtp_session *s, const char *args)
{
	if (s->helo[0] == '\0' || s->tx) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->flags & SF_AUTHENTICATED) {
		smtp_reply(s, "503 %s %s: Already authenticated",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (!ADVERTISE_AUTH(s)) {
		smtp_reply(s, "503 %s %s: Command not supported",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (args == NULL) {
		smtp_reply(s, "501 %s %s: No parameters given",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
		    esc_description(ESC_INVALID_COMMAND_ARGUMENTS));
		return 0;
	}

	return 1;
}

static int
smtp_check_starttls(struct smtp_session *s, const char *args)
{
	if (s->helo[0] == '\0' || s->tx) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (!(s->listener->flags & F_STARTTLS)) {
		smtp_reply(s, "503 %s %s: Command not supported",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->flags & SF_SECURE) {
		smtp_reply(s, "503 %s %s: Channel already secured",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (args != NULL) {
		smtp_reply(s, "501 %s %s: No parameters allowed",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
		    esc_description(ESC_INVALID_COMMAND_ARGUMENTS));
		return 0;
	}

	return 1;
}

static int
smtp_check_mail_from(struct smtp_session *s, const char *args)
{
	if (s->helo[0] == '\0' || s->tx) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->listener->flags & F_STARTTLS_REQUIRE &&
	    !(s->flags & SF_SECURE)) {
		smtp_reply(s,
		    "530 %s %s: Must issue a STARTTLS command first",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->listener->flags & F_AUTH_REQUIRE &&
	    !(s->flags & SF_AUTHENTICATED)) {
		smtp_reply(s,
		    "530 %s %s: Must issue an AUTH command first",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->mailcount >= env->sc_session_max_mails) {
		/* we can pretend we had too many recipients */
		smtp_reply(s, "452 %s %s: Too many messages sent",
		    esc_code(ESC_STATUS_TEMPFAIL, ESC_TOO_MANY_RECIPIENTS),
		    esc_description(ESC_TOO_MANY_RECIPIENTS));
		return 0;
	}

	if (!smtp_tx(s)) {
		smtp_reply(s, "421 %s: Temporary Error",
		    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
		smtp_enter_state(s, STATE_QUIT);
		return 0;
	}

	return 1;
}

static int
smtp_check_rcpt_to(struct smtp_session *s, const char *args)
{
	if (s->tx == NULL) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	return 1;
}

static int
smtp_check_data(struct smtp_session *s, const char *args)
{
	if (!smtp_check_noparam(s, args))
		return 0;

	if (s->tx == NULL) {
		smtp_reply(s, "503 %s %s: Command not allowed at this point.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
		    esc_description(ESC_INVALID_COMMAND));
		return 0;
	}

	if (s->tx->rcptcount == 0) {
		smtp_reply(s, "503 %s %s: No recipient specified",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
		    esc_description(ESC_INVALID_COMMAND_ARGUMENTS));
		return 0;
	}

	return 1;
}

static int
smtp_check_noparam(struct smtp_session *s, const char *args)
{
	if (args != NULL) {
		smtp_reply(s, "500 %s %s: command does not accept arguments.",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
		    esc_description(ESC_INVALID_COMMAND_ARGUMENTS));
		return 0;
	}
	return 1;
}

static void
smtp_query_filters(enum filter_phase phase, struct smtp_session *s, const char *args)
{
	m_create(p_lka, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_lka, s->id);
	m_add_int(p_lka, phase);
	m_add_string(p_lka, args);
	m_close(p_lka);
	tree_xset(&wait_filters, s->id, s);
}

static void
smtp_filter_begin(struct smtp_session *s)
{
	if (!SESSION_FILTERED(s))
		return;

	m_create(p_lka, IMSG_FILTER_SMTP_BEGIN, 0, 0, -1);
	m_add_id(p_lka, s->id);
	m_add_string(p_lka, s->listener->filter_name);
	m_add_sockaddr(p_lka, (struct sockaddr *)&s->ss);
	m_add_sockaddr(p_lka, (struct sockaddr *)&s->listener->ss);
	m_add_string(p_lka, s->hostname);
	m_add_int(p_lka, s->fcrdns);
	m_close(p_lka);
}

static void
smtp_filter_end(struct smtp_session *s)
{
	if (!SESSION_FILTERED(s))
		return;

	m_create(p_lka, IMSG_FILTER_SMTP_END, 0, 0, -1);
	m_add_id(p_lka, s->id);
	m_close(p_lka);
}

static void
smtp_filter_data_begin(struct smtp_session *s)
{
	if (!SESSION_FILTERED(s))
		return;

	m_create(p_lka, IMSG_FILTER_SMTP_DATA_BEGIN, 0, 0, -1);
	m_add_id(p_lka, s->id);
	m_close(p_lka);
	tree_xset(&wait_filter_fd, s->id, s);
}

static void
smtp_filter_data_end(struct smtp_session *s)
{
	if (!SESSION_FILTERED(s))
		return;

	if (s->tx->filter == NULL)
		return;

	io_free(s->tx->filter);
	s->tx->filter = NULL;

	m_create(p_lka, IMSG_FILTER_SMTP_DATA_END, 0, 0, -1);
	m_add_id(p_lka, s->id);
	m_close(p_lka);
}

static void
smtp_filter_phase(enum filter_phase phase, struct smtp_session *s, const char *param)
{
	uint8_t i;

	s->filter_phase = phase;
	s->filter_param = param;

	if (SESSION_FILTERED(s)) {
		smtp_query_filters(phase, s, param ? param : "");
		return;
	}

	if (s->filter_phase == FILTER_CONNECT) {
		smtp_proceed_connected(s);
		return;
	}

	for (i = 0; i < nitems(commands); ++i)
		if (commands[i].filter_phase == s->filter_phase) {
			commands[i].proceed(s, param);
			break;
		}
}

static void
smtp_proceed_rset(struct smtp_session *s, const char *args)
{
	if (s->tx) {
		if (s->tx->msgid)
			smtp_tx_rollback(s->tx);
		smtp_tx_free(s->tx);
	}

	smtp_reply(s, "250 %s: Reset state",
	    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
}

static void
smtp_proceed_helo(struct smtp_session *s, const char *args)
{
	(void)strlcpy(s->helo, args, sizeof(s->helo));
	s->flags &= SF_SECURE | SF_AUTHENTICATED | SF_VERIFIED;

	report_smtp_link_identify("smtp-in", s->id, s->helo);

	smtp_enter_state(s, STATE_HELO);
	smtp_reply(s, "250 %s Hello %s [%s], pleased to meet you",
	    s->smtpname,
	    s->helo,
	    ss_to_text(&s->ss));
}

static void
smtp_proceed_ehlo(struct smtp_session *s, const char *args)
{
	(void)strlcpy(s->helo, args, sizeof(s->helo));
	s->flags &= SF_SECURE | SF_AUTHENTICATED | SF_VERIFIED;
	s->flags |= SF_EHLO;
	s->flags |= SF_8BITMIME;

	report_smtp_link_identify("smtp-in", s->id, s->helo);

	smtp_enter_state(s, STATE_HELO);
	smtp_reply(s, "250-%s Hello %s [%s], pleased to meet you",
	    s->smtpname,
	    s->helo,
	    ss_to_text(&s->ss));

	smtp_reply(s, "250-8BITMIME");
	smtp_reply(s, "250-ENHANCEDSTATUSCODES");
	smtp_reply(s, "250-SIZE %zu", env->sc_maxsize);
	if (ADVERTISE_EXT_DSN(s))
		smtp_reply(s, "250-DSN");
	if (ADVERTISE_TLS(s))
		smtp_reply(s, "250-STARTTLS");
	if (ADVERTISE_AUTH(s))
		smtp_reply(s, "250-AUTH PLAIN LOGIN");
	smtp_reply(s, "250 HELP");
}

static void
smtp_proceed_auth(struct smtp_session *s, const char *args)
{
	char tmp[SMTP_LINE_MAX];
	char *eom, *method;

	(void)strlcpy(tmp, args, sizeof tmp);

	method = tmp;
	eom = strchr(tmp, ' ');
	if (eom == NULL)
		eom = strchr(tmp, '\t');
	if (eom != NULL)
		*eom++ = '\0';
	if (strcasecmp(method, "PLAIN") == 0)
		smtp_rfc4954_auth_plain(s, eom);
	else if (strcasecmp(method, "LOGIN") == 0)
		smtp_rfc4954_auth_login(s, eom);
	else
		smtp_reply(s, "504 %s %s: AUTH method \"%s\" not supported",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_SECURITY_FEATURES_NOT_SUPPORTED),
		    esc_description(ESC_SECURITY_FEATURES_NOT_SUPPORTED),
		    method);
}

static void
smtp_proceed_starttls(struct smtp_session *s, const char *args)
{
	smtp_reply(s, "220 %s: Ready to start TLS",
	    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
	smtp_enter_state(s, STATE_TLS);
}

static void
smtp_proceed_mail_from(struct smtp_session *s, const char *args)
{
	smtp_tx_mail_from(s->tx, args);
}

static void
smtp_proceed_rcpt_to(struct smtp_session *s, const char *args)
{
	smtp_tx_rcpt_to(s->tx, args);
}

static void
smtp_proceed_data(struct smtp_session *s, const char *args)
{
	smtp_tx_open_message(s->tx);
}

static void
smtp_proceed_quit(struct smtp_session *s, const char *args)
{
	smtp_reply(s, "221 %s: Bye",
	    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
	smtp_enter_state(s, STATE_QUIT);
}

static void
smtp_proceed_noop(struct smtp_session *s, const char *args)
{
	smtp_reply(s, "250 %s: Ok",
	    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
}

static void
smtp_proceed_help(struct smtp_session *s, const char *args)
{
	smtp_reply(s, "214- This is " SMTPD_NAME);
	smtp_reply(s, "214- To report bugs in the implementation, "
	    "please contact bugs@openbsd.org");
	smtp_reply(s, "214- with full details");
	smtp_reply(s, "214 %s: End of HELP info",
	    esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS));
}

static void
smtp_proceed_wiz(struct smtp_session *s, const char *args)
{
	smtp_reply(s, "500 %s %s: this feature is not supported yet ;-)",
	    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND),
	    esc_description(ESC_INVALID_COMMAND));
}

static void
smtp_proceed_commit(struct smtp_session *s, const char *args)
{
	smtp_message_end(s->tx);
}

static void
smtp_proceed_rollback(struct smtp_session *s, const char *args)
{
	struct smtp_tx *tx;

	tx = s->tx;

	fclose(tx->ofile);
	tx->ofile = NULL;

	smtp_tx_rollback(tx);
	smtp_tx_free(tx);
	smtp_enter_state(s, STATE_HELO);
}

static void
smtp_rfc4954_auth_plain(struct smtp_session *s, char *arg)
{
	char		 buf[1024], *user, *pass;
	int		 len;

	switch (s->state) {
	case STATE_HELO:
		if (arg == NULL) {
			smtp_enter_state(s, STATE_AUTH_INIT);
			smtp_reply(s, "334 ");
			return;
		}
		smtp_enter_state(s, STATE_AUTH_INIT);
		/* FALLTHROUGH */

	case STATE_AUTH_INIT:
		/* String is not NUL terminated, leave room. */
		if ((len = base64_decode(arg, (unsigned char *)buf,
			    sizeof(buf) - 1)) == -1)
			goto abort;
		/* buf is a byte string, NUL terminate. */
		buf[len] = '\0';

		/*
		 * Skip "foo" in "foo\0user\0pass", if present.
		 */
		user = memchr(buf, '\0', len);
		if (user == NULL || user >= buf + len - 2)
			goto abort;
		user++; /* skip NUL */
		if (strlcpy(s->username, user, sizeof(s->username))
		    >= sizeof(s->username))
			goto abort;

		pass = memchr(user, '\0', len - (user - buf));
		if (pass == NULL || pass >= buf + len - 2)
			goto abort;
		pass++; /* skip NUL */

		m_create(p_lka,  IMSG_SMTP_AUTHENTICATE, 0, 0, -1);
		m_add_id(p_lka, s->id);
		m_add_string(p_lka, s->listener->authtable);
		m_add_string(p_lka, user);
		m_add_string(p_lka, pass);
		m_close(p_lka);
		tree_xset(&wait_parent_auth, s->id, s);
		return;

	default:
		fatal("smtp_rfc4954_auth_plain: unknown state");
	}

abort:
	smtp_reply(s, "501 %s %s: Syntax error",
	    esc_code(ESC_STATUS_PERMFAIL, ESC_SYNTAX_ERROR),
	    esc_description(ESC_SYNTAX_ERROR));
	smtp_enter_state(s, STATE_HELO);
}

static void
smtp_rfc4954_auth_login(struct smtp_session *s, char *arg)
{
	char		buf[LINE_MAX];

	switch (s->state) {
	case STATE_HELO:
		smtp_enter_state(s, STATE_AUTH_USERNAME);
		if (arg != NULL && *arg != '\0') {
			smtp_rfc4954_auth_login(s, arg);
			return;
		}
		smtp_reply(s, "334 VXNlcm5hbWU6");
		return;

	case STATE_AUTH_USERNAME:
		memset(s->username, 0, sizeof(s->username));
		if (base64_decode(arg, (unsigned char *)s->username,
				  sizeof(s->username) - 1) == -1)
			goto abort;

		smtp_enter_state(s, STATE_AUTH_PASSWORD);
		smtp_reply(s, "334 UGFzc3dvcmQ6");
		return;

	case STATE_AUTH_PASSWORD:
		memset(buf, 0, sizeof(buf));
		if (base64_decode(arg, (unsigned char *)buf,
				  sizeof(buf)-1) == -1)
			goto abort;

		m_create(p_lka,  IMSG_SMTP_AUTHENTICATE, 0, 0, -1);
		m_add_id(p_lka, s->id);
		m_add_string(p_lka, s->listener->authtable);
		m_add_string(p_lka, s->username);
		m_add_string(p_lka, buf);
		m_close(p_lka);
		tree_xset(&wait_parent_auth, s->id, s);
		return;

	default:
		fatal("smtp_rfc4954_auth_login: unknown state");
	}

abort:
	smtp_reply(s, "501 %s %s: Syntax error",
	    esc_code(ESC_STATUS_PERMFAIL, ESC_SYNTAX_ERROR),
	    esc_description(ESC_SYNTAX_ERROR));
	smtp_enter_state(s, STATE_HELO);
}

static void
smtp_lookup_servername(struct smtp_session *s)
{
	if (s->listener->hostnametable[0]) {
		m_create(p_lka, IMSG_SMTP_LOOKUP_HELO, 0, 0, -1);
		m_add_id(p_lka, s->id);
		m_add_string(p_lka, s->listener->hostnametable);
		m_add_sockaddr(p_lka, (struct sockaddr*)&s->ss);
		m_close(p_lka);
		tree_xset(&wait_lka_helo, s->id, s);
		return;
	}

	smtp_connected(s);
}

static void
smtp_connected(struct smtp_session *s)
{
	smtp_enter_state(s, STATE_CONNECTED);

	log_info("%016"PRIx64" smtp connected address=%s host=%s",
	    s->id, ss_to_text(&s->ss), s->hostname);

	smtp_filter_begin(s);

	report_smtp_link_connect("smtp-in", s->id, s->hostname, s->fcrdns, &s->ss,
	    &s->listener->ss);

	smtp_filter_phase(FILTER_CONNECT, s, ss_to_text(&s->ss));
}

static void
smtp_proceed_connected(struct smtp_session *s)
{
	if (s->listener->flags & F_SMTPS)
		smtp_cert_init(s);
	else
		smtp_send_banner(s);
}

static void
smtp_send_banner(struct smtp_session *s)
{
	smtp_reply(s, "220 %s ESMTP %s", s->smtpname, SMTPD_NAME);
	s->banner_sent = 1;
}

void
smtp_enter_state(struct smtp_session *s, int newstate)
{
	log_trace(TRACE_SMTP, "smtp: %p: %s -> %s", s,
	    smtp_strstate(s->state),
	    smtp_strstate(newstate));

	s->state = newstate;
}

static void
smtp_reply(struct smtp_session *s, char *fmt, ...)
{
	va_list	 ap;
	int	 n;
	char	 buf[LINE_MAX], tmp[LINE_MAX];

	va_start(ap, fmt);
	n = vsnprintf(buf, sizeof buf, fmt, ap);
	va_end(ap);
	if (n == -1 || n >= LINE_MAX)
		fatalx("smtp_reply: line too long");
	if (n < 4)
		fatalx("smtp_reply: response too short");

	log_trace(TRACE_SMTP, "smtp: %p: >>> %s", s, buf);

	switch (buf[0]) {
	case '2':
		if (s->last_cmd == CMD_MAIL_FROM)
			report_smtp_tx_mail("smtp-in", s->id, s->tx->msgid, s->cmd + 10, 1);
		else if (s->last_cmd == CMD_RCPT_TO)
			report_smtp_tx_rcpt("smtp-in", s->id, s->tx->msgid, s->cmd + 8, 1);
		break;
	case '3':
		if (s->last_cmd == CMD_DATA)
			report_smtp_tx_data("smtp-in", s->id, s->tx->msgid, 1);
		break;
	case '5':
	case '4':
		/* do not report smtp_tx_mail/smtp_tx_rcpt errors
		 * if they happened outside of a transaction.
		 */
		if (s->tx) {
			if (s->last_cmd == CMD_MAIL_FROM)
				report_smtp_tx_mail("smtp-in", s->id, s->tx->msgid,
				    s->cmd + 10, buf[0] == '4' ? -1 : 0);
			else if (s->last_cmd == CMD_RCPT_TO)
				report_smtp_tx_rcpt("smtp-in", s->id,
				    s->tx->msgid, s->cmd + 8, buf[0] == '4' ? -1 : 0);
			else if (s->last_cmd == CMD_DATA && s->tx->rcptcount)
				report_smtp_tx_data("smtp-in", s->id, s->tx->msgid,
				    buf[0] == '4' ? -1 : 0);
		}

		if (s->flags & SF_BADINPUT) {
			log_info("%016"PRIx64" smtp "
			    "bad-input result=\"%.*s\"",
			    s->id, n, buf);
		}
		else if (s->state == STATE_AUTH_INIT) {
			log_info("%016"PRIx64" smtp "
			    "failed-command "
			    "command=\"AUTH PLAIN (...)\" result=\"%.*s\"",
			    s->id, n, buf);
		}
		else if (s->state == STATE_AUTH_USERNAME) {
			log_info("%016"PRIx64" smtp "
			    "failed-command "
			    "command=\"AUTH LOGIN (username)\" result=\"%.*s\"",
			    s->id, n, buf);
		}
		else if (s->state == STATE_AUTH_PASSWORD) {
			log_info("%016"PRIx64" smtp "
			    "failed-command "
			    "command=\"AUTH LOGIN (password)\" result=\"%.*s\"",
			    s->id, n, buf);
		}
		else {
			strnvis(tmp, s->cmd, sizeof tmp, VIS_SAFE | VIS_CSTYLE);
			log_info("%016"PRIx64" smtp "
			    "failed-command command=\"%s\" "
			    "result=\"%.*s\"",
			    s->id, tmp, n, buf);
		}
		break;
	}

	io_xprintf(s->io, "%s\r\n", buf);
	report_smtp_protocol_server("smtp-in", s->id, buf);
}

static void
smtp_free(struct smtp_session *s, const char * reason)
{
	if (s->tx) {
		if (s->tx->msgid)
			smtp_tx_rollback(s->tx);
		smtp_tx_free(s->tx);
	}

	report_smtp_link_disconnect("smtp-in", s->id);
	smtp_filter_end(s);

	if (s->flags & SF_SECURE && s->listener->flags & F_SMTPS)
		stat_decrement("smtp.smtps", 1);
	if (s->flags & SF_SECURE && s->listener->flags & F_STARTTLS)
		stat_decrement("smtp.tls", 1);

	io_free(s->io);
	free(s);

	smtp_collect();
}

static int
smtp_mailaddr(struct mailaddr *maddr, char *line, int mailfrom, char **args,
    const char *domain)
{
	char   *p, *e;

	if (line == NULL)
		return (0);

	if (*line != '<')
		return (0);

	e = strchr(line, '>');
	if (e == NULL)
		return (0);
	*e++ = '\0';
	while (*e == ' ')
		e++;
	*args = e;

	if (!text_to_mailaddr(maddr, line + 1))
		return (0);

	p = strchr(maddr->user, ':');
	if (p != NULL) {
		p++;
		memmove(maddr->user, p, strlen(p) + 1);
	}

	if (!valid_localpart(maddr->user) ||
	    !valid_domainpart(maddr->domain)) {
		/* accept empty return-path in MAIL FROM, required for bounces */
		if (mailfrom && maddr->user[0] == '\0' && maddr->domain[0] == '\0')
			return (1);

		/* no user-part, reject */
		if (maddr->user[0] == '\0')
			return (0);

		/* no domain, local user */
		if (maddr->domain[0] == '\0') {
			(void)strlcpy(maddr->domain, domain,
			    sizeof(maddr->domain));
			return (1);
		}
		return (0);
	}

	return (1);
}

static void
smtp_tls_init(struct smtp_session *s)
{
	struct ca_cert_req_msg req_ca_cert;

	req_ca_cert.reqid = s->id;
	if (s->listener->pki_name[0]) {
		(void)strlcpy(req_ca_cert.name, s->listener->pki_name,
		    sizeof req_ca_cert.name);
		req_ca_cert.fallback = 0;
	}
	else {
		(void)strlcpy(req_ca_cert.name, s->smtpname,
		    sizeof req_ca_cert.name);
		req_ca_cert.fallback = 1;
	}
	m_compose(p_lka, IMSG_SMTP_TLS_INIT, 0, 0, -1,
	    &req_ca_cert, sizeof(req_ca_cert));
	tree_xset(&wait_ssl_init, s->id, s);
}

static int
smtp_verify_certificate(struct smtp_session *s)
{
#define MAX_CERTS	16
#define MAX_CERT_LEN	(MAX_IMSGSIZE - (IMSG_HEADER_SIZE + sizeof(req_ca_vrfy)))
	struct ca_vrfy_req_msg	req_ca_vrfy;
	struct iovec		iov[2];
	X509		       *x;
	STACK_OF(X509)	       *xchain;
	const char	       *name;
	unsigned char	       *cert_der[MAX_CERTS];
	int			cert_len[MAX_CERTS];
	int			i, cert_count, res;

	res = 0;
	memset(cert_der, 0, sizeof(cert_der));
	memset(&req_ca_vrfy, 0, sizeof req_ca_vrfy);

	/* Send the client certificate */
	if (s->listener->ca_name[0]) {
		name = s->listener->ca_name;
		req_ca_vrfy.fallback = 0;
	}
	else {
		name = s->smtpname;
		req_ca_vrfy.fallback = 1;
	}

	if (strlcpy(req_ca_vrfy.name, name, sizeof req_ca_vrfy.name)
	    >= sizeof req_ca_vrfy.name)
		return 0;

	x = SSL_get_peer_certificate(io_ssl(s->io));
	if (x == NULL)
		return 0;
	xchain = SSL_get_peer_cert_chain(io_ssl(s->io));

	/*
	 * Client provided a certificate and possibly a certificate chain.
	 * SMTP can't verify because it does not have the information that
	 * it needs, instead it will pass the certificate and chain to the
	 * lookup process and wait for a reply.
	 *
	 */

	cert_len[0] = i2d_X509(x, &cert_der[0]);
	X509_free(x);

	if (cert_len[0] < 0) {
		log_warnx("warn: failed to encode certificate");
		goto end;
	}
	log_debug("debug: certificate 0: len=%d", cert_len[0]);
	if (cert_len[0] > (int)MAX_CERT_LEN) {
		log_warnx("warn: certificate too long");
		goto end;
	}

	if (xchain) {
		cert_count = sk_X509_num(xchain);
		log_debug("debug: certificate chain len: %d", cert_count);
		if (cert_count >= MAX_CERTS) {
			log_warnx("warn: certificate chain too long");
			goto end;
		}
	}
	else
		cert_count = 0;

	for (i = 0; i < cert_count; ++i) {
		x = sk_X509_value(xchain, i);
		cert_len[i+1] = i2d_X509(x, &cert_der[i+1]);
		if (cert_len[i+1] < 0) {
			log_warnx("warn: failed to encode certificate");
			goto end;
		}
		log_debug("debug: certificate %i: len=%d", i+1, cert_len[i+1]);
		if (cert_len[i+1] > (int)MAX_CERT_LEN) {
			log_warnx("warn: certificate too long");
			goto end;
		}
	}

	tree_xset(&wait_ssl_verify, s->id, s);

	/* Send the client certificate */
	req_ca_vrfy.reqid = s->id;
	req_ca_vrfy.cert_len = cert_len[0];
	req_ca_vrfy.n_chain = cert_count;
	iov[0].iov_base = &req_ca_vrfy;
	iov[0].iov_len = sizeof(req_ca_vrfy);
	iov[1].iov_base = cert_der[0];
	iov[1].iov_len = cert_len[0];
	m_composev(p_lka, IMSG_SMTP_TLS_VERIFY_CERT, 0, 0, -1,
	    iov, nitems(iov));

	memset(&req_ca_vrfy, 0, sizeof req_ca_vrfy);
	req_ca_vrfy.reqid = s->id;

	/* Send the chain, one cert at a time */
	for (i = 0; i < cert_count; ++i) {
		req_ca_vrfy.cert_len = cert_len[i+1];
		iov[1].iov_base = cert_der[i+1];
		iov[1].iov_len  = cert_len[i+1];
		m_composev(p_lka, IMSG_SMTP_TLS_VERIFY_CHAIN, 0, 0, -1,
		    iov, nitems(iov));
	}

	/* Tell lookup process that it can start verifying, we're done */
	memset(&req_ca_vrfy, 0, sizeof req_ca_vrfy);
	req_ca_vrfy.reqid = s->id;
	m_compose(p_lka, IMSG_SMTP_TLS_VERIFY, 0, 0, -1,
	    &req_ca_vrfy, sizeof req_ca_vrfy);

	res = 1;

    end:
	for (i = 0; i < MAX_CERTS; ++i)
		free(cert_der[i]);

	return res;
}

static void
smtp_cert_init(struct smtp_session *s)
{
	const char *name;
	int fallback;

	if (s->listener->pki_name[0]) {
		name = s->listener->pki_name;
		fallback = 0;
	}
	else {
		name = s->smtpname;
		fallback = 1;
	}

	if (cert_init(name, fallback, smtp_cert_init_cb, s))
		tree_xset(&wait_ssl_init, s->id, s);
}

static void
smtp_cert_init_cb(void *arg, int status, const char *name, const void *cert,
    size_t cert_len)
{
	struct smtp_session *s = arg;
	void *ssl, *ssl_ctx;

	tree_pop(&wait_ssl_init, s->id);

	if (status == CA_FAIL) {
		log_info("%016"PRIx64" smtp disconnected "
		    "reason=ca-failure",
		    s->id);
		smtp_free(s, "CA failure");
		return;
	}

	ssl_ctx = dict_get(env->sc_ssl_dict, name);
	ssl = ssl_smtp_init(ssl_ctx, s->listener->flags & F_TLS_VERIFY);
	io_set_read(s->io);
	io_start_tls(s->io, ssl);
}

static void
smtp_cert_verify(struct smtp_session *s)
{
	const char *name;
	int fallback;

	if (s->listener->ca_name[0]) {
		name = s->listener->ca_name;
		fallback = 0;
	}
	else {
		name = s->smtpname;
		fallback = 1;
	}

	if (cert_verify(io_ssl(s->io), name, fallback, smtp_cert_verify_cb, s)) {
		tree_xset(&wait_ssl_verify, s->id, s);
		io_pause(s->io, IO_IN);
	}
}

static void
smtp_cert_verify_cb(void *arg, int status)
{
	struct smtp_session *s = arg;
	const char *reason = NULL;
	int resume;

	resume = tree_pop(&wait_ssl_verify, s->id) != NULL;

	switch (status) {
	case CERT_OK:
		reason = "cert-ok";
		s->flags |= SF_VERIFIED;
		break;
	case CERT_NOCA:
		reason = "no-ca";
		break;
	case CERT_NOCERT:
		reason = "no-client-cert";
		break;
	case CERT_INVALID:
		reason = "cert-invalid";
		break;
	default:
		reason = "cert-check-failed";
		break;
	}

	log_debug("smtp: %p: smtp_cert_verify_cb: %s", s, reason);

	if (!(s->flags & SF_VERIFIED) && (s->listener->flags & F_TLS_VERIFY)) {
		log_info("%016"PRIx64" smtp disconnected "
		    " reason=%s", s->id,
		    reason);
		smtp_free(s, "SSL certificate check failed");
		return;
	}

	smtp_tls_verified(s);
	if (resume)
		io_resume(s->io, IO_IN);
}

static void
smtp_auth_failure_resume(int fd, short event, void *p)
{
	struct smtp_session *s = p;

	smtp_reply(s, "535 Authentication failed");
	smtp_enter_state(s, STATE_HELO);
}

static void
smtp_auth_failure_pause(struct smtp_session *s)
{
	struct timeval	tv;

	tv.tv_sec = 0;
	tv.tv_usec = arc4random_uniform(1000000);
	log_trace(TRACE_SMTP, "smtp: timing-attack protection triggered, "
	    "will defer answer for %lu microseconds", tv.tv_usec);
	evtimer_set(&s->pause, smtp_auth_failure_resume, s);
	evtimer_add(&s->pause, &tv);
}

static int
smtp_tx(struct smtp_session *s)
{
	struct smtp_tx *tx;

	tx = calloc(1, sizeof(*tx));
	if (tx == NULL)
		return 0;

	TAILQ_INIT(&tx->rcpts);

	s->tx = tx;
	tx->session = s;

	/* setup the envelope */
	tx->evp.ss = s->ss;
	(void)strlcpy(tx->evp.tag, s->listener->tag, sizeof(tx->evp.tag));
	(void)strlcpy(tx->evp.smtpname, s->smtpname, sizeof(tx->evp.smtpname));
	(void)strlcpy(tx->evp.hostname, s->hostname, sizeof tx->evp.hostname);
	(void)strlcpy(tx->evp.helo, s->helo, sizeof(tx->evp.helo));

	if (s->flags & SF_BOUNCE)
		tx->evp.flags |= EF_BOUNCE;
	if (s->flags & SF_AUTHENTICATED)
		tx->evp.flags |= EF_AUTHENTICATED;

	if ((tx->parser = rfc5322_parser_new()) == NULL) {
		free(tx);
		return 0;
	}

	return 1;
}

static void
smtp_tx_free(struct smtp_tx *tx)
{
	struct smtp_rcpt *rcpt;

	rfc5322_free(tx->parser);

	while ((rcpt = TAILQ_FIRST(&tx->rcpts))) {
		TAILQ_REMOVE(&tx->rcpts, rcpt, entry);
		free(rcpt);
	}

	if (tx->ofile)
		fclose(tx->ofile);

	tx->session->tx = NULL;

	free(tx);
}

static void
smtp_tx_mail_from(struct smtp_tx *tx, const char *line)
{
	char *opt;
	char *copy;
	char tmp[SMTP_LINE_MAX];

	(void)strlcpy(tmp, line, sizeof tmp);
	copy = tmp;  

	if (smtp_mailaddr(&tx->evp.sender, copy, 1, &copy,
		tx->session->smtpname) == 0) {
		smtp_reply(tx->session, "553 %s: Sender address syntax error",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_ADDRESS_STATUS));
		smtp_tx_free(tx);
		return;
	}

	while ((opt = strsep(&copy, " "))) {
		if (*opt == '\0')
			continue;

		if (strncasecmp(opt, "AUTH=", 5) == 0)
			log_debug("debug: smtp: AUTH in MAIL FROM command");
		else if (strncasecmp(opt, "SIZE=", 5) == 0)
			log_debug("debug: smtp: SIZE in MAIL FROM command");
		else if (strcasecmp(opt, "BODY=7BIT") == 0)
			/* XXX only for this transaction */
			tx->session->flags &= ~SF_8BITMIME;
		else if (strcasecmp(opt, "BODY=8BITMIME") == 0)
			;
		else if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "RET=", 4) == 0) {
			opt += 4;
			if (strcasecmp(opt, "HDRS") == 0)
				tx->evp.dsn_ret = DSN_RETHDRS;
			else if (strcasecmp(opt, "FULL") == 0)
				tx->evp.dsn_ret = DSN_RETFULL;
		} else if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "ENVID=", 6) == 0) {
			opt += 6;
			if (strlcpy(tx->evp.dsn_envid, opt, sizeof(tx->evp.dsn_envid))
			    >= sizeof(tx->evp.dsn_envid)) {
				smtp_reply(tx->session,
				    "503 %s %s: option too large, truncated: %s",
				    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
				    esc_description(ESC_INVALID_COMMAND_ARGUMENTS), opt);
				smtp_tx_free(tx);
				return;
			}
		} else {
			smtp_reply(tx->session, "503 %s %s: Unsupported option %s",
			    esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS),
			    esc_description(ESC_INVALID_COMMAND_ARGUMENTS), opt);
			smtp_tx_free(tx);
			return;
		}
	}

	/* only check sendertable if defined and user has authenticated */
	if (tx->session->flags & SF_AUTHENTICATED &&
	    tx->session->listener->sendertable[0]) {
		m_create(p_lka, IMSG_SMTP_CHECK_SENDER, 0, 0, -1);
		m_add_id(p_lka, tx->session->id);
		m_add_string(p_lka, tx->session->listener->sendertable);
		m_add_string(p_lka, tx->session->username);
		m_add_mailaddr(p_lka, &tx->evp.sender);
		m_close(p_lka);
		tree_xset(&wait_lka_mail, tx->session->id, tx->session);
	}
	else
		smtp_tx_create_message(tx);
}

static void
smtp_tx_create_message(struct smtp_tx *tx)
{
	m_create(p_queue, IMSG_SMTP_MESSAGE_CREATE, 0, 0, -1);
	m_add_id(p_queue, tx->session->id);
	m_close(p_queue);
	tree_xset(&wait_queue_msg, tx->session->id, tx->session);
}

static void
smtp_tx_rcpt_to(struct smtp_tx *tx, const char *line)
{
	char *opt, *p;
	char *copy;
	char tmp[SMTP_LINE_MAX];

	(void)strlcpy(tmp, line, sizeof tmp);
	copy = tmp; 

	if (tx->rcptcount >= env->sc_session_max_rcpt) {
		smtp_reply(tx->session, "451 %s %s: Too many recipients",
		    esc_code(ESC_STATUS_TEMPFAIL, ESC_TOO_MANY_RECIPIENTS),
		    esc_description(ESC_TOO_MANY_RECIPIENTS));
		return;
	}

	if (smtp_mailaddr(&tx->evp.rcpt, copy, 0, &copy,
	    tx->session->smtpname) == 0) {
		smtp_reply(tx->session,
		    "501 %s: Recipient address syntax error",
		    esc_code(ESC_STATUS_PERMFAIL,
		        ESC_BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX));
		return;
	}

	while ((opt = strsep(&copy, " "))) {
		if (*opt == '\0')
			continue;

		if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "NOTIFY=", 7) == 0) {
			opt += 7;
			while ((p = strsep(&opt, ","))) {
				if (strcasecmp(p, "SUCCESS") == 0)
					tx->evp.dsn_notify |= DSN_SUCCESS;
				else if (strcasecmp(p, "FAILURE") == 0)
					tx->evp.dsn_notify |= DSN_FAILURE;
				else if (strcasecmp(p, "DELAY") == 0)
					tx->evp.dsn_notify |= DSN_DELAY;
				else if (strcasecmp(p, "NEVER") == 0)
					tx->evp.dsn_notify |= DSN_NEVER;
			}

			if (tx->evp.dsn_notify & DSN_NEVER &&
			    tx->evp.dsn_notify & (DSN_SUCCESS | DSN_FAILURE |
			    DSN_DELAY)) {
				smtp_reply(tx->session,
				    "553 NOTIFY option NEVER cannot be"
				    " combined with other options");
				return;
			}
		} else if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "ORCPT=", 6) == 0) {
			opt += 6;
			if (!text_to_mailaddr(&tx->evp.dsn_orcpt, opt)) {
				smtp_reply(tx->session,
				    "553 ORCPT address syntax error");
				return;
			}
		} else {
			smtp_reply(tx->session, "503 Unsupported option %s", opt);
			return;
		}
	}

	m_create(p_lka, IMSG_SMTP_EXPAND_RCPT, 0, 0, -1);
	m_add_id(p_lka, tx->session->id);
	m_add_envelope(p_lka, &tx->evp);
	m_close(p_lka);
	tree_xset(&wait_lka_rcpt, tx->session->id, tx->session);
}

static void
smtp_tx_open_message(struct smtp_tx *tx)
{
	m_create(p_queue, IMSG_SMTP_MESSAGE_OPEN, 0, 0, -1);
	m_add_id(p_queue, tx->session->id);
	m_add_msgid(p_queue, tx->msgid);
	m_close(p_queue);
	tree_xset(&wait_queue_fd, tx->session->id, tx->session);
}

static void
smtp_tx_commit(struct smtp_tx *tx)
{
	m_create(p_queue, IMSG_SMTP_MESSAGE_COMMIT, 0, 0, -1);
	m_add_id(p_queue, tx->session->id);
	m_add_msgid(p_queue, tx->msgid);
	m_close(p_queue);
	tree_xset(&wait_queue_commit, tx->session->id, tx->session);
	report_smtp_tx_commit("smtp-in", tx->session->id, tx->msgid, tx->odatalen);
	smtp_filter_data_end(tx->session);
}

static void
smtp_tx_rollback(struct smtp_tx *tx)
{
	m_create(p_queue, IMSG_SMTP_MESSAGE_ROLLBACK, 0, 0, -1);
	m_add_msgid(p_queue, tx->msgid);
	m_close(p_queue);
	report_smtp_tx_rollback("smtp-in", tx->session->id, tx->msgid);
	smtp_filter_data_end(tx->session);
}

static int
smtp_tx_dataline(struct smtp_tx *tx, const char *line)
{
	struct rfc5322_result res;
	int r;

	log_trace(TRACE_SMTP, "<<< [MSG] %s", line);

	if (!strcmp(line, ".")) {
		report_smtp_protocol_client("smtp-in", tx->session->id, ".");
		log_trace(TRACE_SMTP, "<<< [EOM]");
		if (tx->error)
			return 1;
		line = NULL;
	}
	else {
		/* ignore data line if an error is set */
		if (tx->error)
			return 0;

		/* escape lines starting with a '.' */
		if (line[0] == '.')
			line += 1;

		/* account for newline */
		tx->datain += strlen(line) + 1;
		if (tx->datain > env->sc_maxsize) {
			tx->error = TX_ERROR_SIZE;
			return 0;
		}
	}

	if (rfc5322_push(tx->parser, line) == -1) {
		log_warnx("failed to push dataline");
		tx->error = TX_ERROR_INTERNAL;
		return 0;
	}

	for(;;) {
		r = rfc5322_next(tx->parser, &res);
		switch (r) {
		case -1:
			if (errno == ENOMEM)
				tx->error = TX_ERROR_INTERNAL;
			else
				tx->error = TX_ERROR_MALFORMED;
			return 0;

		case RFC5322_NONE:
			/* Need more data */
			return 0;

		case RFC5322_HEADER_START:
			/* ignore bcc */
			if (!strcasecmp("Bcc", res.hdr))
				continue;

			if (!strcasecmp("To", res.hdr) ||
			    !strcasecmp("Cc", res.hdr) ||
			    !strcasecmp("From", res.hdr)) {
				rfc5322_unfold_header(tx->parser);
				continue;
			}

			if (!strcasecmp("Received", res.hdr)) {
				if (++tx->rcvcount >= MAX_HOPS_COUNT) {
					log_warnx("warn: loop detected");
					tx->error = TX_ERROR_LOOP;
					return 0;
				}
			}
			else if (!tx->has_date && !strcasecmp("Date", res.hdr))
				tx->has_date = 1;
			else if (!tx->has_message_id &&
			    !strcasecmp("Message-Id", res.hdr))
				tx->has_message_id = 1;

			smtp_message_printf(tx, "%s:%s\n", res.hdr, res.value);
			break;

		case RFC5322_HEADER_CONT:

			if (!strcasecmp("Bcc", res.hdr) ||
			    !strcasecmp("To", res.hdr) ||
			    !strcasecmp("Cc", res.hdr) ||
			    !strcasecmp("From", res.hdr))
				continue;

			smtp_message_printf(tx, "%s\n", res.value);
			break;

		case RFC5322_HEADER_END:
			if (!strcasecmp("To", res.hdr) ||
			    !strcasecmp("Cc", res.hdr) ||
			    !strcasecmp("From", res.hdr))
				header_domain_append_callback(tx, res.hdr,
				    res.value);
			break;

		case RFC5322_END_OF_HEADERS:
			if (tx->session->listener->local ||
			    tx->session->listener->port == 587) {

				if (!tx->has_date) {
					log_debug("debug: %p: adding Date", tx);
					smtp_message_printf(tx, "Date: %s\n",
					    time_to_text(tx->time));
				}

				if (!tx->has_message_id) {
					log_debug("debug: %p: adding Message-ID", tx);
					smtp_message_printf(tx,
					    "Message-ID: <%016"PRIx64"@%s>\n",
					    generate_uid(),
					    tx->session->listener->hostname);
				}
			}
			break;

		case RFC5322_BODY_START:
		case RFC5322_BODY:
			smtp_message_printf(tx, "%s\n", res.value);
			break;

		case RFC5322_END_OF_MESSAGE:
			return 1;

		default:
			fatalx("%s", __func__);
		}
	}
}

static int
smtp_tx_filtered_dataline(struct smtp_tx *tx, const char *line)
{
	if (!strcmp(line, ".")) {
		if (tx->error)
			return 1;
		line = NULL;
	}
	else {
		/* ignore data line if an error is set */
		if (tx->error)
			return 0;

		/* escape lines starting with a '.' */
		if (line[0] == '.')
			line += 1;

		/* account for newline */
		tx->datain += strlen(line) + 1;
		if (tx->datain > env->sc_maxsize) {
			tx->error = TX_ERROR_SIZE;
			return 0;
		}
	}
	io_printf(tx->filter, "%s\r\n", line ? line : ".");
	return line ? 0 : 1;
}

static void
smtp_tx_eom(struct smtp_tx *tx)
{
	smtp_filter_phase(FILTER_COMMIT, tx->session, NULL);
}

static int
smtp_message_fd(struct smtp_tx *tx, int fd)
{
	struct smtp_session *s;

	s = tx->session;

	log_debug("smtp: %p: message fd %d", s, fd);

	if ((tx->ofile = fdopen(fd, "w")) == NULL) {
		close(fd);
		smtp_reply(s, "421 %s: Temporary Error",
		    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
		smtp_enter_state(s, STATE_QUIT);
		return 0;
	}
	return 1;
}

static void
filter_session_io(struct io *io, int evt, void *arg)
{
	struct smtp_tx*tx = arg;
	char*line = NULL;
	ssize_t len;

	log_trace(TRACE_IO, "filter session io (smtp): %p: %s %s", tx, io_strevent(evt),
	    io_strio(io));

	switch (evt) {
	case IO_DATAIN:
	nextline:
		line = io_getline(tx->filter, &len);
		/* No complete line received */
		if (line == NULL)
			return;

		if (smtp_tx_dataline(tx, line)) {
			smtp_tx_eom(tx);
			return;
		}

		goto nextline;
	}
}

static void
smtp_filter_fd(struct smtp_tx *tx, int fd)
{
	struct smtp_session *s;

	s = tx->session;

	log_debug("smtp: %p: filter fd %d", s, fd);

	tx->filter = io_new();
	io_set_fd(tx->filter, fd);
	io_set_callback(tx->filter, filter_session_io, tx);
}

static void
smtp_message_begin(struct smtp_tx *tx)
{
	struct smtp_session *s;
	X509 *x;
	int	(*m_printf)(struct smtp_tx *, const char *, ...);

	m_printf = smtp_message_printf;
	if (tx->filter)
		m_printf = smtp_filter_printf;

	s = tx->session;

	log_debug("smtp: %p: message begin", s);

	smtp_reply(s, "354 Enter mail, end with \".\""
	    " on a line by itself");	
	
	m_printf(tx, "Received: ");
	if (!(s->listener->flags & F_MASK_SOURCE)) {
		m_printf(tx, "from %s (%s [%s])",
		    s->helo,
		    s->hostname,
		    ss_to_text(&s->ss));
	}
	m_printf(tx, "\n\tby %s (%s) with %sSMTP%s%s id %08x",
	    s->smtpname,
	    SMTPD_NAME,
	    s->flags & SF_EHLO ? "E" : "",
	    s->flags & SF_SECURE ? "S" : "",
	    s->flags & SF_AUTHENTICATED ? "A" : "",
	    tx->msgid);

	if (s->flags & SF_SECURE) {
		x = SSL_get_peer_certificate(io_ssl(s->io));
		m_printf(tx, " (%s:%s:%d:%s)",
		    SSL_get_version(io_ssl(s->io)),
		    SSL_get_cipher_name(io_ssl(s->io)),
		    SSL_get_cipher_bits(io_ssl(s->io), NULL),
		    (s->flags & SF_VERIFIED) ? "YES" : (x ? "FAIL" : "NO"));
		X509_free(x);

		if (s->listener->flags & F_RECEIVEDAUTH) {
			m_printf(tx, " auth=%s",
			    s->username[0] ? "yes" : "no");
			if (s->username[0])
				m_printf(tx, " user=%s", s->username);
		}
	}

	if (tx->rcptcount == 1) {
		m_printf(tx, "\n\tfor <%s@%s>",
		    tx->evp.rcpt.user,
		    tx->evp.rcpt.domain);
	}

	m_printf(tx, ";\n\t%s\n", time_to_text(time(&tx->time)));

	smtp_enter_state(s, STATE_BODY);
}

static void
smtp_message_end(struct smtp_tx *tx)
{
	struct smtp_session *s;

	s = tx->session;

	log_debug("debug: %p: end of message, error=%d", s, tx->error);

	fclose(tx->ofile);
	tx->ofile = NULL;

	switch(tx->error) {
	case TX_OK:
		smtp_tx_commit(tx);
		return;		

	case TX_ERROR_SIZE:
		smtp_reply(s, "554 %s %s: Transaction failed, message too big",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_MESSAGE_TOO_BIG_FOR_SYSTEM),
		    esc_description(ESC_MESSAGE_TOO_BIG_FOR_SYSTEM));
		break;

	case TX_ERROR_LOOP:
		smtp_reply(s, "500 %s %s: Loop detected",
		   esc_code(ESC_STATUS_PERMFAIL, ESC_ROUTING_LOOP_DETECTED),
		   esc_description(ESC_ROUTING_LOOP_DETECTED));
		break;

	case TX_ERROR_MALFORMED:
		smtp_reply(s, "550 %s %s: Message is not RFC 2822 compliant",
		    esc_code(ESC_STATUS_PERMFAIL, ESC_DELIVERY_NOT_AUTHORIZED_MESSAGE_REFUSED),
		    esc_description(ESC_DELIVERY_NOT_AUTHORIZED_MESSAGE_REFUSED));
		break;

	case TX_ERROR_IO:
	case TX_ERROR_RESOURCES:
		smtp_reply(s, "421 %s: Temporary Error",
		    esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS));
		break;

	default:
		/* fatal? */
		smtp_reply(s, "421 Internal server error");
	}

	smtp_tx_rollback(tx);
	smtp_tx_free(tx);
	smtp_enter_state(s, STATE_HELO);
}

static int
smtp_filter_printf(struct smtp_tx *tx, const char *fmt, ...)
{
	va_list	ap;
	int	len;

	if (tx->error)
		return -1;

	va_start(ap, fmt);
	len = io_vprintf(tx->filter, fmt, ap);
	va_end(ap);

	if (len < 0) {
		log_warn("smtp-in: session %016"PRIx64": vfprintf", tx->session->id);
		tx->error = TX_ERROR_IO;
	}
	else
		tx->odatalen += len;

	return len;
}

static int
smtp_message_printf(struct smtp_tx *tx, const char *fmt, ...)
{
	va_list	ap;
	int	len;

	if (tx->error)
		return -1;

	va_start(ap, fmt);
	len = vfprintf(tx->ofile, fmt, ap);
	va_end(ap);

	if (len < 0) {
		log_warn("smtp-in: session %016"PRIx64": vfprintf", tx->session->id);
		tx->error = TX_ERROR_IO;
	}
	else
		tx->odatalen += len;

	return len;
}

#define CASE(x) case x : return #x

const char *
smtp_strstate(int state)
{
	static char	buf[32];

	switch (state) {
	CASE(STATE_NEW);
	CASE(STATE_CONNECTED);
	CASE(STATE_TLS);
	CASE(STATE_HELO);
	CASE(STATE_AUTH_INIT);
	CASE(STATE_AUTH_USERNAME);
	CASE(STATE_AUTH_PASSWORD);
	CASE(STATE_AUTH_FINALIZE);
	CASE(STATE_BODY);
	CASE(STATE_QUIT);
	default:
		(void)snprintf(buf, sizeof(buf), "STATE_??? (%d)", state);
		return (buf);
	}
}