summaryrefslogtreecommitdiffstats
path: root/src/smtp/smtp_proto.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/smtp/smtp_proto.c')
-rw-r--r--src/smtp/smtp_proto.c2469
1 files changed, 2469 insertions, 0 deletions
diff --git a/src/smtp/smtp_proto.c b/src/smtp/smtp_proto.c
new file mode 100644
index 0000000..a43a326
--- /dev/null
+++ b/src/smtp/smtp_proto.c
@@ -0,0 +1,2469 @@
+/*++
+/* NAME
+/* smtp_proto 3
+/* SUMMARY
+/* client SMTP/LMTP protocol
+/* SYNOPSIS
+/* #include "smtp.h"
+/*
+/* int smtp_helo(state)
+/* SMTP_STATE *state;
+/*
+/* int smtp_xfer(state)
+/* SMTP_STATE *state;
+/*
+/* int smtp_rset(state)
+/* SMTP_STATE *state;
+/*
+/* int smtp_quit(state)
+/* SMTP_STATE *state;
+/* DESCRIPTION
+/* In the subsequent text, SMTP implies LMTP.
+/* This module implements the client side of the SMTP protocol.
+/*
+/* smtp_helo() performs the initial handshake with the SMTP server.
+/* When TLS is enabled, this includes STARTTLS negotiations.
+/*
+/* smtp_xfer() sends message envelope information followed by the
+/* message data, and finishes the SMTP conversation. These operations
+/* are combined in one function, in order to implement SMTP pipelining.
+/* Recipients are marked as "done" in the mail queue file when
+/* bounced or delivered. The message delivery status is updated
+/* accordingly.
+/*
+/* smtp_rset() sends a single RSET command and waits for the
+/* response. In case of a negative reply it sets the
+/* CANT_RSET_THIS_SESSION flag.
+/*
+/* smtp_quit() sends a single QUIT command and waits for the
+/* response if configured to do so. It always turns off connection
+/* caching.
+/* DIAGNOSTICS
+/* smtp_helo(), smtp_xfer(), smtp_rset() and smtp_quit() return
+/* 0 in case of success, -1 in case of failure. For smtp_xfer(),
+/* smtp_rset() and smtp_quit(), success means the ability to
+/* perform an SMTP conversation, not necessarily the ability
+/* to deliver mail, or the achievement of server happiness.
+/*
+/* In case of a rejected or failed connection, a connection
+/* is marked as "bad, do not cache". Otherwise, connection
+/* caching may be turned off (without being marked "bad") at
+/* the discretion of the code that implements the individual
+/* protocol steps.
+/*
+/* Warnings: corrupt message file. A corrupt message is marked
+/* as "corrupt" by changing its queue file permissions.
+/* BUGS
+/* Some SMTP servers will abort when the number of recipients
+/* for one message exceeds their capacity. This behavior violates
+/* the SMTP protocol.
+/* The only way around this is to limit the number of recipients
+/* per transaction to an artificially-low value.
+/* SEE ALSO
+/* smtp(3h) internal data structures
+/* smtp_chat(3) query/reply SMTP support
+/* smtp_trouble(3) error handlers
+/* 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
+/*
+/* Pipelining code in cooperation with:
+/* Jon Ribbens
+/* Oaktree Internet Solutions Ltd.,
+/* Internet House,
+/* Canal Basin,
+/* Coventry,
+/* CV1 4LY, United Kingdom.
+/*
+/* Connection caching in cooperation with:
+/* Victor Duchovni
+/* Morgan Stanley
+/*
+/* TLS support originally by:
+/* Lutz Jaenicke
+/* BTU Cottbus
+/* Allgemeine Elektrotechnik
+/* Universitaetsplatz 3-4
+/* D-03044 Cottbus, Germany
+/*--*/
+
+/* System library. */
+
+#include <sys_defs.h>
+#include <sys/stat.h>
+#include <sys/socket.h> /* shutdown(2) */
+#include <netinet/in.h> /* ntohs() */
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h> /* 44BSD stdarg.h uses abort() */
+#include <stdarg.h>
+#include <time.h>
+
+#ifdef STRCASECMP_IN_STRINGS_H
+#include <strings.h>
+#endif
+
+/* Utility library. */
+
+#include <msg.h>
+#include <vstring.h>
+#include <vstream.h>
+#include <vstring_vstream.h>
+#include <stringops.h>
+#include <mymalloc.h>
+#include <iostuff.h>
+#include <split_at.h>
+#include <name_code.h>
+#include <name_mask.h>
+
+/* Global library. */
+
+#include <mail_params.h>
+#include <smtp_stream.h>
+#include <mail_queue.h>
+#include <recipient_list.h>
+#include <deliver_request.h>
+#include <defer.h>
+#include <bounce.h>
+#include <record.h>
+#include <rec_type.h>
+#include <off_cvt.h>
+#include <mark_corrupt.h>
+#include <quote_821_local.h>
+#include <quote_822_local.h>
+#include <mail_proto.h>
+#include <mime_state.h>
+#include <ehlo_mask.h>
+#include <maps.h>
+#include <tok822.h>
+#include <mail_addr_map.h>
+#include <ext_prop.h>
+#include <namadr_list.h>
+#include <match_parent_style.h>
+#include <lex_822.h>
+#include <dsn_mask.h>
+#include <xtext.h>
+#include <uxtext.h>
+#include <smtputf8.h>
+
+/* Application-specific. */
+
+#include "smtp.h"
+#include "smtp_sasl.h"
+
+ /*
+ * Sender and receiver state. A session does not necessarily go through a
+ * linear progression, but states are guaranteed to not jump backwards.
+ * Normal sessions go from MAIL->RCPT->DATA->DOT->QUIT->LAST. The states
+ * MAIL, RCPT, and DATA may also be followed by ABORT->QUIT->LAST.
+ *
+ * When connection caching is enabled, the QUIT state is suppressed. Normal
+ * sessions proceed as MAIL->RCPT->DATA->DOT->LAST, while aborted sessions
+ * end with ABORT->LAST. The connection is left open for a limited time. An
+ * RSET probe should be sent before attempting to reuse an open connection
+ * for a new transaction.
+ *
+ * The code to send an RSET probe is a special case with its own initial state
+ * and with its own dedicated state transitions. The session proceeds as
+ * RSET->LAST. This code is kept inside the main protocol engine for
+ * consistent error handling and error reporting. It is not to be confused
+ * with the code that sends RSET to abort a mail transaction in progress.
+ *
+ * The code to send QUIT without message delivery transaction jumps into the
+ * main state machine. If this introduces complications, then we should
+ * introduce a second QUIT state with its own dedicated state transitions,
+ * just like we did for RSET probes.
+ *
+ * By default, the receiver skips the QUIT response. Some SMTP servers
+ * disconnect after responding to ".", and some SMTP servers wait before
+ * responding to QUIT.
+ *
+ * Client states that are associated with sending mail (up to and including
+ * SMTP_STATE_DOT) must have smaller numerical values than the non-sending
+ * states (SMTP_STATE_ABORT .. SMTP_STATE_LAST).
+ */
+#define SMTP_STATE_XFORWARD_NAME_ADDR 0
+#define SMTP_STATE_XFORWARD_PROTO_HELO 1
+#define SMTP_STATE_MAIL 2
+#define SMTP_STATE_RCPT 3
+#define SMTP_STATE_DATA 4
+#define SMTP_STATE_DOT 5
+#define SMTP_STATE_ABORT 6
+#define SMTP_STATE_RSET 7
+#define SMTP_STATE_QUIT 8
+#define SMTP_STATE_LAST 9
+
+int *xfer_timeouts[SMTP_STATE_LAST] = {
+ &var_smtp_xfwd_tmout, /* name/addr */
+ &var_smtp_xfwd_tmout, /* helo/proto */
+ &var_smtp_mail_tmout,
+ &var_smtp_rcpt_tmout,
+ &var_smtp_data0_tmout,
+ &var_smtp_data2_tmout,
+ &var_smtp_rset_tmout,
+ &var_smtp_rset_tmout,
+ &var_smtp_quit_tmout,
+};
+
+char *xfer_states[SMTP_STATE_LAST] = {
+ "sending XFORWARD name/address",
+ "sending XFORWARD protocol/helo_name",
+ "sending MAIL FROM",
+ "sending RCPT TO",
+ "sending DATA command",
+ "sending end of data -- message may be sent more than once",
+ "sending final RSET",
+ "sending RSET probe",
+ "sending QUIT",
+};
+
+char *xfer_request[SMTP_STATE_LAST] = {
+ "XFORWARD name/address command",
+ "XFORWARD helo/protocol command",
+ "MAIL FROM command",
+ "RCPT TO command",
+ "DATA command",
+ "end of DATA command",
+ "final RSET command",
+ "RSET probe",
+ "QUIT command",
+};
+
+ /*
+ * Note: MIME downgrade never happens for mail that must be delivered with
+ * SMTPUTF8 (the sender requested SMTPUTF8, AND the delivery request
+ * involves at least one UTF-8 envelope address or header value.
+ */
+#define SMTP_MIME_DOWNGRADE(session, request) \
+ (var_disable_mime_oconv == 0 \
+ && (session->features & SMTP_FEATURE_8BITMIME) == 0 \
+ && strcmp(request->encoding, MAIL_ATTR_ENC_7BIT) != 0)
+
+#ifdef USE_TLS
+
+static int smtp_start_tls(SMTP_STATE *);
+
+#endif
+
+ /*
+ * Call-back information for header/body checks. We don't provide call-backs
+ * for actions that change the message delivery time or destination.
+ */
+static void smtp_hbc_logger(void *, const char *, const char *, const char *, const char *);
+static void smtp_text_out(void *, int, const char *, ssize_t, off_t);
+
+HBC_CALL_BACKS smtp_hbc_callbacks[1] = {
+ smtp_hbc_logger,
+ smtp_text_out,
+};
+
+static int smtp_vrfy_tgt;
+
+/* smtp_vrfy_init - initialize */
+
+void smtp_vrfy_init(void)
+{
+ static const NAME_CODE vrfy_init_table[] = {
+ SMTP_VRFY_TGT_RCPT, SMTP_STATE_RCPT,
+ SMTP_VRFY_TGT_DATA, SMTP_STATE_DATA,
+ 0,
+ };
+
+ if ((smtp_vrfy_tgt = name_code(vrfy_init_table, NAME_CODE_FLAG_NONE,
+ var_smtp_vrfy_tgt)) == 0)
+ msg_fatal("bad protocol stage: \"%s = %s\"",
+ VAR_SMTP_VRFY_TGT, var_smtp_vrfy_tgt);
+}
+
+/* smtp_helo - perform initial handshake with SMTP server */
+
+int smtp_helo(SMTP_STATE *state)
+{
+ const char *myname = "smtp_helo";
+ SMTP_SESSION *session = state->session;
+ DELIVER_REQUEST *request = state->request;
+ SMTP_ITERATOR *iter = state->iterator;
+ SMTP_RESP *resp;
+ SMTP_RESP fake;
+ int except;
+ char *lines;
+ char *words;
+ char *word;
+ int n;
+ static const NAME_CODE xforward_features[] = {
+ XFORWARD_NAME, SMTP_FEATURE_XFORWARD_NAME,
+ XFORWARD_ADDR, SMTP_FEATURE_XFORWARD_ADDR,
+ XFORWARD_PORT, SMTP_FEATURE_XFORWARD_PORT,
+ XFORWARD_PROTO, SMTP_FEATURE_XFORWARD_PROTO,
+ XFORWARD_HELO, SMTP_FEATURE_XFORWARD_HELO,
+ XFORWARD_IDENT, SMTP_FEATURE_XFORWARD_IDENT,
+ XFORWARD_DOMAIN, SMTP_FEATURE_XFORWARD_DOMAIN,
+ 0, 0,
+ };
+ const char *ehlo_words;
+ int discard_mask;
+ static const NAME_MASK pix_bug_table[] = {
+ PIX_BUG_DISABLE_ESMTP, SMTP_FEATURE_PIX_NO_ESMTP,
+ PIX_BUG_DELAY_DOTCRLF, SMTP_FEATURE_PIX_DELAY_DOTCRLF,
+ 0,
+ };
+ const char *pix_bug_words;
+ const char *pix_bug_source;
+ int pix_bug_mask;
+
+#ifdef USE_TLS
+ int saved_features = session->features;
+ int tls_helo_status;
+
+#endif
+ const char *NOCLOBBER where;
+
+ /*
+ * Skip the plaintext SMTP handshake when connecting in SMTPS mode.
+ */
+#ifdef USE_TLS
+ if (var_smtp_tls_wrappermode
+ && (state->misc_flags & SMTP_MISC_FLAG_IN_STARTTLS) == 0) {
+ /* XXX Mix-up of per-session and per-request flags. */
+ state->misc_flags |= SMTP_MISC_FLAG_IN_STARTTLS;
+ smtp_stream_setup(state->session->stream, var_smtp_starttls_tmout,
+ var_smtp_rec_deadline);
+ tls_helo_status = smtp_start_tls(state);
+ state->misc_flags &= ~SMTP_MISC_FLAG_IN_STARTTLS;
+ return (tls_helo_status);
+ }
+#endif
+
+ /*
+ * Prepare for disaster.
+ */
+ smtp_stream_setup(state->session->stream, var_smtp_helo_tmout,
+ var_smtp_rec_deadline);
+ if ((except = vstream_setjmp(state->session->stream)) != 0)
+ return (smtp_stream_except(state, except, where));
+
+ /*
+ * If not recursing after STARTTLS, examine the server greeting banner
+ * and decide if we are going to send EHLO as the next command.
+ */
+ if (var_smtp_tls_wrappermode
+ || (state->misc_flags & SMTP_MISC_FLAG_IN_STARTTLS) == 0) {
+
+ /*
+ * Read and parse the server's SMTP greeting banner.
+ */
+ where = "receiving the initial server greeting";
+ switch ((resp = smtp_chat_resp(session))->code / 100) {
+ case 2:
+ break;
+ case 5:
+ if (var_smtp_skip_5xx_greeting)
+ STR(resp->dsn_buf)[0] = '4';
+ /* FALLTHROUGH */
+ default:
+ return (smtp_site_fail(state, STR(iter->host), resp,
+ "host %s refused to talk to me: %s",
+ session->namaddr,
+ translit(resp->str, "\n", " ")));
+ }
+
+ /*
+ * If the policy table specifies a bogus TLS security level, fail
+ * now.
+ */
+#ifdef USE_TLS
+ if (state->tls->level == TLS_LEV_INVALID)
+ /* Warning is already logged. */
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.0"),
+ "client TLS configuration problem"));
+#endif
+
+ /*
+ * XXX Some PIX firewall versions require flush before ".<CR><LF>" so
+ * it does not span a packet boundary. This hurts performance so it
+ * is not on by default.
+ */
+ if (resp->str[strspn(resp->str, "20 *\t\n")] == 0) {
+ /* Best effort only. Ignore errors. */
+ if (smtp_pix_bug_maps != 0
+ && (pix_bug_words =
+ maps_find(smtp_pix_bug_maps,
+ STR(iter->addr), 0)) != 0) {
+ pix_bug_source = VAR_LMTP_SMTP(PIX_BUG_MAPS);
+ } else {
+ pix_bug_words = var_smtp_pix_bug_words;
+ pix_bug_source = VAR_LMTP_SMTP(PIX_BUG_WORDS);
+ }
+ if (*pix_bug_words) {
+ pix_bug_mask = name_mask_opt(pix_bug_source, pix_bug_table,
+ pix_bug_words,
+ NAME_MASK_ANY_CASE | NAME_MASK_IGNORE);
+ if ((pix_bug_mask & SMTP_FEATURE_PIX_DELAY_DOTCRLF)
+ && request->msg_stats.incoming_arrival.tv_sec
+ > vstream_ftime(state->session->stream) - var_smtp_pix_thresh)
+ pix_bug_mask &= ~SMTP_FEATURE_PIX_DELAY_DOTCRLF;
+ msg_info("%s: enabling PIX workarounds: %s for %s",
+ request->queue_id,
+ str_name_mask("pix workaround bitmask",
+ pix_bug_table, pix_bug_mask),
+ session->namaddrport);
+ session->features |= pix_bug_mask;
+ }
+ }
+
+ /*
+ * See if we are talking to ourself. This should not be possible with
+ * the way we implement DNS lookups. However, people are known to
+ * sometimes screw up the naming service. And, mailer loops are still
+ * possible when our own mailer routing tables are mis-configured.
+ */
+ words = resp->str;
+ (void) mystrtok(&words, "- \t\n");
+ for (n = 0; (word = mystrtok(&words, " \t\n")) != 0; n++) {
+ if (n == 0 && strcasecmp(word, var_myhostname) == 0) {
+ if (state->misc_flags & SMTP_MISC_FLAG_LOOP_DETECT)
+ msg_warn("host %s greeted me with my own hostname %s",
+ session->namaddrport, var_myhostname);
+ } else if (strcasecmp(word, "ESMTP") == 0)
+ session->features |= SMTP_FEATURE_ESMTP;
+ }
+ if (smtp_mode) {
+ if (var_smtp_always_ehlo
+ && (session->features & SMTP_FEATURE_PIX_NO_ESMTP) == 0)
+ session->features |= SMTP_FEATURE_ESMTP;
+ if (var_smtp_never_ehlo
+ || (session->features & SMTP_FEATURE_PIX_NO_ESMTP) != 0)
+ session->features &= ~SMTP_FEATURE_ESMTP;
+ } else {
+ session->features |= SMTP_FEATURE_ESMTP;
+ }
+ }
+
+ /*
+ * If recursing after STARTTLS, there is no server greeting banner.
+ * Always send EHLO as the next command.
+ */
+ else {
+ session->features |= SMTP_FEATURE_ESMTP;
+ }
+
+ /*
+ * Return the compliment. Fall back to SMTP if our ESMTP recognition
+ * heuristic failed.
+ */
+ if (smtp_mode) {
+ where = "performing the EHLO handshake";
+ if (session->features & SMTP_FEATURE_ESMTP) {
+ smtp_chat_cmd(session, "EHLO %s", var_smtp_helo_name);
+ if ((resp = smtp_chat_resp(session))->code / 100 != 2) {
+ if (resp->code == 421)
+ return (smtp_site_fail(state, STR(iter->host), resp,
+ "host %s refused to talk to me: %s",
+ session->namaddr,
+ translit(resp->str, "\n", " ")));
+ else
+ session->features &= ~SMTP_FEATURE_ESMTP;
+ }
+ }
+ if ((session->features & SMTP_FEATURE_ESMTP) == 0) {
+ where = "performing the HELO handshake";
+ smtp_chat_cmd(session, "HELO %s", var_smtp_helo_name);
+ if ((resp = smtp_chat_resp(session))->code / 100 != 2)
+ return (smtp_site_fail(state, STR(iter->host), resp,
+ "host %s refused to talk to me: %s",
+ session->namaddr,
+ translit(resp->str, "\n", " ")));
+ }
+ } else {
+ where = "performing the LHLO handshake";
+ smtp_chat_cmd(session, "LHLO %s", var_smtp_helo_name);
+ if ((resp = smtp_chat_resp(session))->code / 100 != 2)
+ return (smtp_site_fail(state, STR(iter->host), resp,
+ "host %s refused to talk to me: %s",
+ session->namaddr,
+ translit(resp->str, "\n", " ")));
+ }
+
+ /*
+ * No early returns allowed, to ensure consistent handling of TLS and
+ * SASL policies.
+ */
+ if (session->features & SMTP_FEATURE_ESMTP) {
+
+ /*
+ * Determine what server EHLO keywords to ignore, typically to avoid
+ * inter-operability problems.
+ */
+ if (smtp_ehlo_dis_maps == 0
+ || (ehlo_words = maps_find(smtp_ehlo_dis_maps,
+ STR(iter->addr), 0)) == 0)
+ ehlo_words = var_smtp_ehlo_dis_words;
+ if (smtp_ehlo_dis_maps && smtp_ehlo_dis_maps->error) {
+ msg_warn("%s: %s map lookup error for %s",
+ session->state->request->queue_id,
+ smtp_ehlo_dis_maps->title, STR(iter->addr));
+ vstream_longjmp(session->stream, SMTP_ERR_DATA);
+ }
+ discard_mask = ehlo_mask(ehlo_words);
+ if (discard_mask && !(discard_mask & EHLO_MASK_SILENT))
+ msg_info("discarding EHLO keywords: %s",
+ str_ehlo_mask(discard_mask));
+
+ /*
+ * Pick up some useful features offered by the SMTP server. XXX Until
+ * we have a portable routine to convert from string to off_t with
+ * proper overflow detection, ignore the message size limit
+ * advertised by the SMTP server. Otherwise, we might do the wrong
+ * thing when the server advertises a really huge message size limit.
+ *
+ * XXX Allow for "code (SP|-) ehlo-keyword (SP|=) ehlo-param...",
+ * because MicroSoft implemented AUTH based on an old draft.
+ */
+ lines = resp->str;
+ for (n = 0; (words = mystrtok(&lines, "\n")) != 0; /* see below */ ) {
+ if (mystrtok(&words, "- ")
+ && (word = mystrtok(&words, " \t=")) != 0) {
+ if (n == 0) {
+ if (session->helo != 0)
+ myfree(session->helo);
+
+ /*
+ * XXX: Keep the original case: we don't expect a single
+ * SMTP server to randomly change the case of its helo
+ * response. If different capitalization is detected, we
+ * should assume disjoint TLS caches.
+ */
+ session->helo = mystrdup(word);
+ if (strcasecmp(word, var_myhostname) == 0
+ && (state->misc_flags & SMTP_MISC_FLAG_LOOP_DETECT) != 0) {
+ msg_warn("host %s replied to HELO/EHLO"
+ " with my own hostname %s",
+ session->namaddrport, var_myhostname);
+ if (session->features & SMTP_FEATURE_BEST_MX)
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "5.4.6"),
+ "mail for %s loops back to myself",
+ request->nexthop));
+ else
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.4.6"),
+ "mail for %s loops back to myself",
+ request->nexthop));
+ }
+ } else if (strcasecmp(word, "8BITMIME") == 0) {
+ if ((discard_mask & EHLO_MASK_8BITMIME) == 0)
+ session->features |= SMTP_FEATURE_8BITMIME;
+ } else if (strcasecmp(word, "PIPELINING") == 0) {
+ if ((discard_mask & EHLO_MASK_PIPELINING) == 0)
+ session->features |= SMTP_FEATURE_PIPELINING;
+ } else if (strcasecmp(word, "XFORWARD") == 0) {
+ if ((discard_mask & EHLO_MASK_XFORWARD) == 0)
+ while ((word = mystrtok(&words, " \t")) != 0)
+ session->features |=
+ name_code(xforward_features,
+ NAME_CODE_FLAG_NONE, word);
+ } else if (strcasecmp(word, "SIZE") == 0) {
+ if ((discard_mask & EHLO_MASK_SIZE) == 0) {
+ session->features |= SMTP_FEATURE_SIZE;
+ if ((word = mystrtok(&words, " \t")) != 0) {
+ if (!alldig(word))
+ msg_warn("bad EHLO SIZE limit \"%s\" from %s",
+ word, session->namaddrport);
+ else
+ session->size_limit = off_cvt_string(word);
+ }
+ }
+#ifdef USE_TLS
+ } else if (strcasecmp(word, "STARTTLS") == 0) {
+ /* Ignored later if we already sent STARTTLS. */
+ if ((discard_mask & EHLO_MASK_STARTTLS) == 0)
+ session->features |= SMTP_FEATURE_STARTTLS;
+#endif
+#ifdef USE_SASL_AUTH
+ } else if (var_smtp_sasl_enable
+ && strcasecmp(word, "AUTH") == 0) {
+ if ((discard_mask & EHLO_MASK_AUTH) == 0)
+ smtp_sasl_helo_auth(session, words);
+#endif
+ } else if (strcasecmp(word, "DSN") == 0) {
+ if ((discard_mask & EHLO_MASK_DSN) == 0)
+ session->features |= SMTP_FEATURE_DSN;
+ } else if (strcasecmp(word, "SMTPUTF8") == 0) {
+ if ((discard_mask & EHLO_MASK_SMTPUTF8) == 0)
+ session->features |= SMTP_FEATURE_SMTPUTF8;
+ }
+ n++;
+ }
+ }
+ }
+ if (msg_verbose)
+ msg_info("server features: 0x%x size %.0f",
+ session->features, (double) session->size_limit);
+
+ /*
+ * Decide if this delivery requires SMTPUTF8 server support.
+ *
+ * For now, we require that the remote SMTP server supports SMTPUTF8 when
+ * the sender requested SMTPUTF8 support.
+ *
+ * XXX EAI Refine this to: the sender requested SMTPUTF8 support AND the
+ * delivery request involves at least one UTF-8 envelope address or
+ * header value.
+ *
+ * If the sender requested SMTPUTF8 support but the delivery request
+ * involves no UTF-8 envelope address or header value, then we could
+ * still deliver such mail to a non-SMTPUTF8 server, except that we must
+ * either uxtext-encode ORCPT parameters or not send them. We cannot
+ * encode the ORCPT in xtext, because legacy SMTP requires that the
+ * unencoded address consist entirely of printable (graphic and white
+ * space) characters from the US-ASCII repertoire (RFC 3461 section 4). A
+ * correct uxtext encoder will produce a result that an xtext decoder
+ * will pass through unchanged.
+ *
+ * XXX Should we try to encode headers with RFC 2047 when delivering to a
+ * non-SMTPUTF8 server? That could make life easier for mailing lists.
+ */
+#define DELIVERY_REQUIRES_SMTPUTF8 \
+ ((request->smtputf8 & SMTPUTF8_FLAG_REQUESTED) \
+ && (request->smtputf8 & ~SMTPUTF8_FLAG_REQUESTED))
+
+ /*
+ * Require that the server supports SMTPUTF8 when delivery requires
+ * SMTPUTF8.
+ *
+ * Fix 20140706: moved this before negotiating TLS, AUTH, and so on.
+ */
+ if ((session->features & SMTP_FEATURE_SMTPUTF8) == 0
+ && DELIVERY_REQUIRES_SMTPUTF8)
+ return (smtp_mesg_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "5.6.7"),
+ "SMTPUTF8 is required, "
+ "but was not offered by host %s",
+ session->namaddr));
+
+ /*
+ * Fix 20140706: don't do silly things when the remote server announces
+ * SMTPUTF8 but not 8BITMIME support. Our primary mission is to deliver
+ * mail, not to force people into compliance.
+ */
+ if ((session->features & SMTP_FEATURE_SMTPUTF8) != 0
+ && (session->features & SMTP_FEATURE_8BITMIME) == 0) {
+ msg_info("host %s offers SMTPUTF8 support, but not 8BITMIME",
+ session->namaddr);
+ session->features |= SMTP_FEATURE_8BITMIME;
+ }
+
+ /*
+ * We use SMTP command pipelining if the server said it supported it.
+ * Since we use blocking I/O, RFC 2197 says that we should inspect the
+ * TCP window size and not send more than this amount of information.
+ * Unfortunately this information is unavailable using the sockets
+ * interface. However, we *can* get the TCP send buffer size on the local
+ * TCP/IP stack. We should be able to fill this buffer without being
+ * blocked, and then the kernel will effectively do non-blocking I/O for
+ * us by automatically writing out the contents of its send buffer while
+ * we are reading in the responses. In addition to TCP buffering we have
+ * to be aware of application-level buffering by the vstream module,
+ * which is limited to a couple kbytes.
+ *
+ * XXX No need to do this before and after STARTTLS, but it's not a big deal
+ * if we do.
+ *
+ * XXX When TLS is turned on, the SMTP-level writes will be encapsulated as
+ * TLS messages. Thus, the TCP-level payload will be larger than the
+ * SMTP-level payload. This has implications for the PIPELINING engine.
+ *
+ * To avoid deadlock, the PIPELINING engine needs to request a TCP send
+ * buffer size that can hold the unacknowledged commands plus the TLS
+ * encapsulation overhead.
+ *
+ * The PIPELINING engine keeps the unacknowledged command size <= the
+ * default VSTREAM buffer size (to avoid small-write performance issues
+ * when the VSTREAM buffer size is at its default size). With a default
+ * VSTREAM buffer size of 4096 there is no reason to increase the
+ * unacknowledged command size as the TCP MSS increases. It's safer to
+ * spread the remote SMTP server's recipient processing load over time,
+ * than dumping a very large recipient list all at once.
+ *
+ * For TLS encapsulation overhead we make a conservative guess: take the
+ * current protocol overhead of ~40 bytes, double the number for future
+ * proofing (~80 bytes), then round up the result to the nearest power of
+ * 2 (128 bytes). Plus, be prepared for worst-case compression that
+ * expands data by 1 kbyte, so that the worst-case SMTP payload per TLS
+ * message becomes 15 kbytes.
+ */
+#define PIPELINING_BUFSIZE VSTREAM_BUFSIZE
+#ifdef USE_TLS
+#define TLS_WORST_PAYLOAD 16384
+#define TLS_WORST_COMP_OVERHD 1024
+#define TLS_WORST_PROTO_OVERHD 128
+#define TLS_WORST_SMTP_PAYLOAD (TLS_WORST_PAYLOAD - TLS_WORST_COMP_OVERHD)
+#define TLS_WORST_TOTAL_OVERHD (TLS_WORST_COMP_OVERHD + TLS_WORST_PROTO_OVERHD)
+#endif
+
+ if (session->features & SMTP_FEATURE_PIPELINING) {
+ SOCKOPT_SIZE optlen;
+ int tcp_bufsize;
+ int enc_overhead = 0;
+
+ optlen = sizeof(tcp_bufsize);
+ if (getsockopt(vstream_fileno(session->stream), SOL_SOCKET,
+ SO_SNDBUF, (char *) &tcp_bufsize, &optlen) < 0)
+ msg_fatal("%s: getsockopt: %m", myname);
+#ifdef USE_TLS
+ if (state->misc_flags & SMTP_MISC_FLAG_IN_STARTTLS)
+ enc_overhead +=
+ (1 + (PIPELINING_BUFSIZE - 1)
+ / TLS_WORST_SMTP_PAYLOAD) * TLS_WORST_TOTAL_OVERHD;
+#endif
+ if (tcp_bufsize < PIPELINING_BUFSIZE + enc_overhead) {
+ tcp_bufsize = PIPELINING_BUFSIZE + enc_overhead;
+ if (setsockopt(vstream_fileno(session->stream), SOL_SOCKET,
+ SO_SNDBUF, (char *) &tcp_bufsize, optlen) < 0)
+ msg_fatal("%s: setsockopt: %m", myname);
+ }
+ if (msg_verbose)
+ msg_info("Using %s PIPELINING, TCP send buffer size is %d, "
+ "PIPELINING buffer size is %d",
+ smtp_mode ? "ESMTP" : "LMTP",
+ tcp_bufsize, PIPELINING_BUFSIZE);
+ }
+#ifdef USE_TLS
+
+ /*
+ * Skip this part if we already sent STARTTLS.
+ */
+ if ((state->misc_flags & SMTP_MISC_FLAG_IN_STARTTLS) == 0) {
+
+ /*
+ * Optionally log unused STARTTLS opportunities.
+ */
+ if ((session->features & SMTP_FEATURE_STARTTLS) &&
+ var_smtp_tls_note_starttls_offer &&
+ state->tls->level <= TLS_LEV_NONE)
+ msg_info("Host offered STARTTLS: [%s]", STR(iter->host));
+
+ /*
+ * Decide whether or not to send STARTTLS.
+ */
+ if ((session->features & SMTP_FEATURE_STARTTLS) != 0
+ && smtp_tls_ctx != 0 && state->tls->level >= TLS_LEV_MAY) {
+
+ /*
+ * Prepare for disaster.
+ */
+ smtp_stream_setup(state->session->stream, var_smtp_starttls_tmout,
+ var_smtp_rec_deadline);
+ if ((except = vstream_setjmp(state->session->stream)) != 0)
+ return (smtp_stream_except(state, except,
+ "receiving the STARTTLS response"));
+
+ /*
+ * Send STARTTLS. Recurse when the server accepts STARTTLS, after
+ * resetting the SASL and EHLO features lists.
+ *
+ * Reset the SASL mechanism list to avoid spurious warnings.
+ *
+ * Use the smtp_sasl_tls_security_options feature to allow SASL
+ * mechanisms that may not be allowed with plain-text
+ * connections.
+ */
+ smtp_chat_cmd(session, "STARTTLS");
+ if ((resp = smtp_chat_resp(session))->code / 100 == 2) {
+#ifdef USE_SASL_AUTH
+ if (session->features & SMTP_FEATURE_AUTH)
+ smtp_sasl_cleanup(session);
+#endif
+ session->features = saved_features;
+ /* XXX Mix-up of per-session and per-request flags. */
+ state->misc_flags |= SMTP_MISC_FLAG_IN_STARTTLS;
+ tls_helo_status = smtp_start_tls(state);
+ state->misc_flags &= ~SMTP_MISC_FLAG_IN_STARTTLS;
+ return (tls_helo_status);
+ }
+
+ /*
+ * Give up if we must use TLS but the server rejects STARTTLS
+ * although support for it was announced in the EHLO response.
+ */
+ session->features &= ~SMTP_FEATURE_STARTTLS;
+ if (TLS_REQUIRED(state->tls->level))
+ return (smtp_site_fail(state, STR(iter->host), resp,
+ "TLS is required, but host %s refused to start TLS: %s",
+ session->namaddr,
+ translit(resp->str, "\n", " ")));
+ /* Else try to continue in plain-text mode. */
+ }
+
+ /*
+ * Give up if we must use TLS but can't for various reasons.
+ *
+ * 200412 Be sure to provide the default clause at the bottom of this
+ * block. When TLS is required we must never, ever, end up in
+ * plain-text mode.
+ */
+ if (TLS_REQUIRED(state->tls->level)) {
+ if (!(session->features & SMTP_FEATURE_STARTTLS)) {
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.4"),
+ "TLS is required, but was not offered by host %s",
+ session->namaddr));
+ } else if (smtp_tls_ctx == 0) {
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.5"),
+ "TLS is required, but our TLS engine is unavailable"));
+ } else {
+ msg_warn("%s: TLS is required but unavailable, don't know why",
+ myname);
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.0"),
+ "TLS is required, but unavailable"));
+ }
+ }
+ }
+#endif
+#ifdef USE_SASL_AUTH
+ if (var_smtp_sasl_enable && (session->features & SMTP_FEATURE_AUTH))
+ return (smtp_sasl_helo_login(state));
+#endif
+
+ return (0);
+}
+
+#ifdef USE_TLS
+
+/* smtp_start_tls - turn on TLS and recurse into the HELO dialog */
+
+static int smtp_start_tls(SMTP_STATE *state)
+{
+ SMTP_SESSION *session = state->session;
+ SMTP_ITERATOR *iter = state->iterator;
+ TLS_CLIENT_START_PROPS start_props;
+ VSTRING *serverid;
+ SMTP_RESP fake;
+ TLS_CLIENT_INIT_PROPS init_props;
+ VSTREAM *tlsproxy;
+ VSTRING *port_buf;
+
+ /*
+ * When the TLS handshake succeeds, we can reuse a connection only if TLS
+ * remains turned on for the lifetime of that connection. This requires
+ * that the TLS library state is maintained in some proxy process, for
+ * example, in tlsproxy(8). We then store the proxy file handle in the
+ * connection cache, and reuse that file handle.
+ *
+ * Otherwise, we must turn off connection caching. We can't turn off TLS in
+ * one SMTP client process, save the open connection to a cache which is
+ * shared with all SMTP clients, migrate the connection to another SMTP
+ * client, and resume TLS there. When the TLS handshake fails, we can't
+ * reuse the SMTP connection either, because the conversation is in an
+ * unknown state.
+ */
+ if (state->tls->conn_reuse == 0)
+ DONT_CACHE_THIS_SESSION;
+
+ /*
+ * The following assumes sites that use TLS in a perverse configuration:
+ * multiple hosts per hostname, or even multiple hosts per IP address.
+ * All this without a shared TLS session cache, and they still want to
+ * use TLS session caching???
+ *
+ * The TLS session cache records the trust chain verification status of
+ * cached sessions. Different transports may have different CAfile or
+ * CApath settings, perhaps to allow authenticated connections to sites
+ * with private CA certs without trusting said private certs for other
+ * sites. So we cannot assume that a trust chain valid for one transport
+ * is valid for another. Therefore the client session id must include
+ * either the transport name or the values of CAfile and CApath. We use
+ * the transport name.
+ *
+ * XXX: We store only one session per lookup key. Ideally the the key maps
+ * 1-to-1 to a server TLS session cache. We use the IP address, port and
+ * ehlo response name to build a lookup key that works for split caches
+ * (that announce distinct names) behind a load balancer.
+ *
+ * XXX: The TLS library will salt the serverid with further details of the
+ * protocol and cipher requirements including the server ehlo response.
+ * Deferring the helo to the digested suffix results in more predictable
+ * SSL session lookup key lengths.
+ */
+ serverid = vstring_alloc(10);
+ smtp_key_prefix(serverid, "&", state->iterator, SMTP_KEY_FLAG_SERVICE
+ | SMTP_KEY_FLAG_CUR_NEXTHOP /* With port */
+ | SMTP_KEY_FLAG_HOSTNAME
+ | SMTP_KEY_FLAG_ADDR);
+
+ if (state->tls->conn_reuse) {
+ TLS_CLIENT_PARAMS tls_params;
+
+ /*
+ * Send all our wishes in one big request.
+ */
+ TLS_PROXY_CLIENT_INIT_PROPS(&init_props,
+ log_param = VAR_LMTP_SMTP(TLS_LOGLEVEL),
+ log_level = var_smtp_tls_loglevel,
+ verifydepth = var_smtp_tls_scert_vd,
+ cache_type
+ = LMTP_SMTP_SUFFIX(TLS_MGR_SCACHE),
+ chain_files = var_smtp_tls_chain_files,
+ cert_file = var_smtp_tls_cert_file,
+ key_file = var_smtp_tls_key_file,
+ dcert_file = var_smtp_tls_dcert_file,
+ dkey_file = var_smtp_tls_dkey_file,
+ eccert_file = var_smtp_tls_eccert_file,
+ eckey_file = var_smtp_tls_eckey_file,
+ CAfile = var_smtp_tls_CAfile,
+ CApath = var_smtp_tls_CApath,
+ mdalg = var_smtp_tls_fpt_dgst);
+ TLS_PROXY_CLIENT_START_PROPS(&start_props,
+ timeout = var_smtp_starttls_tmout,
+ tls_level = state->tls->level,
+ nexthop = session->tls_nexthop,
+ host = STR(iter->host),
+ namaddr = session->namaddrport,
+ sni = state->tls->sni,
+ serverid = vstring_str(serverid),
+ helo = session->helo,
+ protocols = state->tls->protocols,
+ cipher_grade = state->tls->grade,
+ cipher_exclusions
+ = vstring_str(state->tls->exclusions),
+ matchargv = state->tls->matchargv,
+ mdalg = var_smtp_tls_fpt_dgst,
+ dane = state->tls->dane);
+
+ /*
+ * The tlsproxy(8) server enforces timeouts that are larger than
+ * those specified by the tlsproxy(8) client. These timeouts are a
+ * safety net for the case that the tlsproxy(8) client fails to
+ * enforce time limits. Normally, the tlsproxy(8) client would time
+ * out and trigger a plaintext event in the tlsproxy(8) server, and
+ * cause it to tear down the session.
+ *
+ * However, the tlsproxy(8) server has no insight into the SMTP
+ * protocol, and therefore it cannot by itself support different
+ * timeouts at different SMTP protocol stages. Instead, we specify
+ * the largest timeout (end-of-data) and rely on the SMTP client to
+ * time out first, which normally results in a plaintext event in the
+ * tlsproxy(8) server. Unfortunately, we cannot permit plaintext
+ * events during the TLS handshake, so we specify a separate timeout
+ * for that stage (the end-of-data timeout would be unreasonably
+ * large anyway).
+ */
+#define PROXY_OPEN_FLAGS \
+ (TLS_PROXY_FLAG_ROLE_CLIENT | TLS_PROXY_FLAG_SEND_CONTEXT)
+
+ port_buf = vstring_alloc(100); /* minimize fragmentation */
+ vstring_sprintf(port_buf, "%d", ntohs(iter->port));
+ tlsproxy =
+ tls_proxy_open(var_tlsproxy_service, PROXY_OPEN_FLAGS,
+ session->stream, STR(iter->addr),
+ STR(port_buf), var_smtp_starttls_tmout,
+ var_smtp_data2_tmout, state->service,
+ tls_proxy_client_param_from_config(&tls_params),
+ &init_props, &start_props);
+ vstring_free(port_buf);
+
+ /*
+ * To insert tlsproxy(8) between this process and the remote SMTP
+ * server, we swap the file descriptors between the tlsproxy and
+ * session->stream VSTREAMS, so that we don't lose all the
+ * user-configurable session->stream attributes (such as longjump
+ * buffers or timeouts).
+ *
+ * TODO: the tlsproxy RPCs should return more error detail than a "NO"
+ * result. OTOH, the in-process TLS engine does not return such info
+ * either.
+ *
+ * If the tlsproxy request fails we do not fall back to the in-process
+ * TLS stack. Reason: the admin enabled connection reuse to respect
+ * receiver policy; silently violating such policy would not be
+ * useful.
+ *
+ * We also don't fall back to the in-process TLS stack under low-traffic
+ * conditions, to avoid frustrating attempts to debug a problem with
+ * using the tlsproxy(8) service.
+ */
+ if (tlsproxy == 0) {
+ session->tls_context = 0;
+ } else {
+ vstream_control(tlsproxy,
+ CA_VSTREAM_CTL_DOUBLE,
+ CA_VSTREAM_CTL_END);
+ vstream_control(session->stream,
+ CA_VSTREAM_CTL_SWAP_FD(tlsproxy),
+ CA_VSTREAM_CTL_END);
+ (void) vstream_fclose(tlsproxy); /* direct-to-server stream! */
+
+ /*
+ * There must not be any pending data in the stream buffers
+ * before we read the TLS context attributes.
+ */
+ vstream_fpurge(session->stream, VSTREAM_PURGE_BOTH);
+
+ /*
+ * After plumbing the plaintext stream, receive the TLS context
+ * object. For this we use the same VSTREAM buffer that we also
+ * use to receive subsequent SMTP commands, therefore we must be
+ * prepared for the possibility that the remote SMTP server
+ * starts talking immediately. The tlsproxy implementation sends
+ * the TLS context before remote content. The attribute protocol
+ * is robust enough that an adversary cannot insert their own TLS
+ * context attributes.
+ */
+ session->tls_context = tls_proxy_context_receive(session->stream);
+ if (session->tls_context) {
+ session->features |= SMTP_FEATURE_FROM_PROXY;
+ tls_log_summary(TLS_ROLE_CLIENT, TLS_USAGE_NEW,
+ session->tls_context);
+ }
+ }
+ } else { /* state->tls->conn_reuse */
+
+ /*
+ * As of Postfix 2.5, tls_client_start() tries hard to always
+ * complete the TLS handshake. It records the verification and match
+ * status in the resulting TLScontext. It is now up to the
+ * application to abort the TLS connection if it chooses.
+ *
+ * XXX When tls_client_start() fails then we don't know what state the
+ * SMTP connection is in, so we give up on this connection even if we
+ * are not required to use TLS.
+ *
+ * Large parameter lists are error-prone, so we emulate a language
+ * feature that C does not have natively: named parameter lists.
+ */
+ session->tls_context =
+ TLS_CLIENT_START(&start_props,
+ ctx = smtp_tls_ctx,
+ stream = session->stream,
+ fd = -1,
+ timeout = var_smtp_starttls_tmout,
+ tls_level = state->tls->level,
+ nexthop = session->tls_nexthop,
+ host = STR(iter->host),
+ namaddr = session->namaddrport,
+ sni = state->tls->sni,
+ serverid = vstring_str(serverid),
+ helo = session->helo,
+ protocols = state->tls->protocols,
+ cipher_grade = state->tls->grade,
+ cipher_exclusions
+ = vstring_str(state->tls->exclusions),
+ matchargv = state->tls->matchargv,
+ mdalg = var_smtp_tls_fpt_dgst,
+ dane = state->tls->dane);
+
+ /*
+ * At this point there must not be any pending data in the stream
+ * buffers.
+ */
+ vstream_fpurge(session->stream, VSTREAM_PURGE_BOTH);
+ } /* state->tls->conn_reuse */
+
+ vstring_free(serverid);
+
+ if (session->tls_context == 0) {
+
+ /*
+ * We must avoid further I/O, the peer is in an undefined state.
+ */
+ DONT_USE_FORBIDDEN_SESSION;
+
+ /*
+ * If TLS is optional, try delivery to the same server over a
+ * plaintext connection. Otherwise we would defer mail forever with
+ * destinations that have no alternate MX host.
+ *
+ * Don't fall back to plaintext if we were willing to use SASL-over-TLS
+ * authentication. If the server doesn't announce SASL support over
+ * plaintext connections, then we don't want delivery to fail with
+ * "relay access denied".
+ *
+ * If TLS is opportunistic, don't throttle the destination, otherwise if
+ * the mail is volume is high enough we may have difficulty ever
+ * draining even the deferred mail, as new mail provides a constant
+ * stream of negative feedback.
+ */
+ if (PLAINTEXT_FALLBACK_OK_AFTER_STARTTLS_FAILURE)
+ RETRY_AS_PLAINTEXT;
+ return (smtp_misc_fail(state, state->tls->level == TLS_LEV_MAY ?
+ SMTP_NOTHROTTLE : SMTP_THROTTLE,
+ DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.5"),
+ "Cannot start TLS: handshake failure"));
+ }
+
+ /*
+ * If we are verifying the server certificate and are not happy with the
+ * result, abort the delivery here. We have a usable TLS session with the
+ * server, so no need to disable I/O, ... we can even be polite and send
+ * "QUIT".
+ *
+ * See src/tls/tls_level.c and src/tls/tls.h. Levels above "encrypt" require
+ * matching. Levels >= "dane" require CA or DNSSEC trust.
+ *
+ * When DANE TLSA records specify an end-entity certificate, the trust and
+ * match bits always coincide, but it is fine to report the wrong
+ * end-entity certificate as untrusted rather than unmatched.
+ */
+ if (TLS_MUST_TRUST(state->tls->level))
+ if (!TLS_CERT_IS_TRUSTED(session->tls_context))
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.5"),
+ "Server certificate not trusted"));
+ if (TLS_MUST_MATCH(state->tls->level))
+ if (!TLS_CERT_IS_MATCHED(session->tls_context))
+ return (smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.5"),
+ "Server certificate not verified"));
+
+ /*
+ * At this point we have to re-negotiate the "EHLO" to reget the
+ * feature-list.
+ */
+ return (smtp_helo(state));
+}
+
+#endif
+
+/* smtp_hbc_logger - logging call-back for header/body checks */
+
+static void smtp_hbc_logger(void *context, const char *action,
+ const char *where, const char *content,
+ const char *text)
+{
+ const SMTP_STATE *state = (SMTP_STATE *) context;
+
+ if (*text) {
+ msg_info("%s: %s: %s %.60s: %s",
+ state->request->queue_id, action, where, content, text);
+ } else {
+ msg_info("%s: %s: %s %.60s",
+ state->request->queue_id, action, where, content);
+ }
+}
+
+/* smtp_text_out - output one header/body record */
+
+static void smtp_text_out(void *context, int rec_type,
+ const char *text, ssize_t len,
+ off_t unused_offset)
+{
+ SMTP_STATE *state = (SMTP_STATE *) context;
+ SMTP_SESSION *session = state->session;
+ ssize_t data_left;
+ const char *data_start;
+
+ /*
+ * Deal with an impedance mismatch between Postfix queue files (record
+ * length <= $message_line_length_limit) and SMTP (DATA record length <=
+ * $smtp_line_length_limit). The code below does a little too much work
+ * when the SMTP line length limit is disabled, but it avoids code
+ * duplication, and thus, it avoids testing and maintenance problems.
+ */
+ data_left = len;
+ data_start = text;
+ do {
+ if (state->space_left == var_smtp_line_limit
+ && data_left > 0 && *data_start == '.')
+ smtp_fputc('.', session->stream);
+ if (var_smtp_line_limit > 0 && data_left >= state->space_left) {
+ smtp_fputs(data_start, state->space_left, session->stream);
+ data_start += state->space_left;
+ data_left -= state->space_left;
+ state->space_left = var_smtp_line_limit;
+ if (data_left > 0 || rec_type == REC_TYPE_CONT) {
+ smtp_fputc(' ', session->stream);
+ state->space_left -= 1;
+ }
+ } else {
+ if (rec_type == REC_TYPE_CONT) {
+ smtp_fwrite(data_start, data_left, session->stream);
+ state->space_left -= data_left;
+ } else {
+ smtp_fputs(data_start, data_left, session->stream);
+ state->space_left = var_smtp_line_limit;
+ }
+ break;
+ }
+ } while (data_left > 0);
+}
+
+/* smtp_format_out - output one header/body record */
+
+static void PRINTFLIKE(3, 4) smtp_format_out(void *, int, const char *,...);
+
+static void smtp_format_out(void *context, int rec_type, const char *fmt,...)
+{
+ static VSTRING *vp;
+ va_list ap;
+
+ if (vp == 0)
+ vp = vstring_alloc(100);
+ va_start(ap, fmt);
+ vstring_vsprintf(vp, fmt, ap);
+ va_end(ap);
+ smtp_text_out(context, rec_type, vstring_str(vp), VSTRING_LEN(vp), 0);
+}
+
+/* smtp_header_out - output one message header */
+
+static void smtp_header_out(void *context, int unused_header_class,
+ const HEADER_OPTS *unused_info,
+ VSTRING *buf, off_t offset)
+{
+ char *start = vstring_str(buf);
+ char *line;
+ char *next_line;
+
+ /*
+ * This code destroys the header. We could try to avoid clobbering it,
+ * but we're not going to use the data any further.
+ */
+ for (line = start; line; line = next_line) {
+ next_line = split_at(line, '\n');
+ smtp_text_out(context, REC_TYPE_NORM, line, next_line ?
+ next_line - line - 1 : strlen(line), offset);
+ }
+}
+
+/* smtp_header_rewrite - rewrite message header before output */
+
+static void smtp_header_rewrite(void *context, int header_class,
+ const HEADER_OPTS *header_info,
+ VSTRING *buf, off_t offset)
+{
+ SMTP_STATE *state = (SMTP_STATE *) context;
+ int did_rewrite = 0;
+ char *line;
+ char *start;
+ char *next_line;
+ char *end_line;
+ char *result;
+
+ /*
+ * Apply optional header filtering.
+ */
+ if (smtp_header_checks) {
+ result = hbc_header_checks(context, smtp_header_checks, header_class,
+ header_info, buf, offset);
+ if (result == 0)
+ return;
+ if (result == HBC_CHECKS_STAT_ERROR) {
+ msg_warn("%s: smtp header checks lookup error",
+ state->request->queue_id);
+ vstream_longjmp(state->session->stream, SMTP_ERR_DATA);
+ }
+ if (result != STR(buf)) {
+ vstring_strcpy(buf, result);
+ myfree(result);
+ }
+ }
+
+ /*
+ * Rewrite primary header addresses that match the smtp_generic_maps. The
+ * cleanup server already enforces that all headers have proper lengths
+ * and that all addresses are in proper form, so we don't have to repeat
+ * that.
+ */
+ if (smtp_generic_maps && header_info && header_class == MIME_HDR_PRIMARY
+ && (header_info->flags & (HDR_OPT_SENDER | HDR_OPT_RECIP)) != 0) {
+ TOK822 *tree;
+ TOK822 **addr_list;
+ TOK822 **tpp;
+
+ tree = tok822_parse(vstring_str(buf)
+ + strlen(header_info->name) + 1);
+ addr_list = tok822_grep(tree, TOK822_ADDR);
+ for (tpp = addr_list; *tpp; tpp++)
+ did_rewrite |= smtp_map11_tree(tpp[0], smtp_generic_maps,
+ smtp_ext_prop_mask & EXT_PROP_GENERIC);
+ if (did_rewrite) {
+ vstring_truncate(buf, strlen(header_info->name));
+ vstring_strcat(buf, ": ");
+ tok822_externalize(buf, tree, TOK822_STR_HEAD);
+ }
+ myfree((void *) addr_list);
+ tok822_free_tree(tree);
+ }
+
+ /*
+ * Pass through unmodified headers without reconstruction.
+ */
+ if (did_rewrite == 0) {
+ smtp_header_out(context, header_class, header_info, buf, offset);
+ return;
+ }
+
+ /*
+ * A rewritten address list contains one address per line. The code below
+ * replaces newlines by spaces, to fit as many addresses on a line as
+ * possible (without rearranging the order of addresses). Prepending
+ * white space to the beginning of lines is delegated to the output
+ * routine.
+ *
+ * Code derived from cleanup_fold_header().
+ */
+ for (line = start = vstring_str(buf); line != 0; line = next_line) {
+ end_line = line + strcspn(line, "\n");
+ if (line > start) {
+ if (end_line - start < 70) { /* TAB counts as one */
+ line[-1] = ' ';
+ } else {
+ start = line;
+ }
+ }
+ next_line = *end_line ? end_line + 1 : 0;
+ }
+
+ /*
+ * Prepend a tab to continued header lines that went through the address
+ * rewriting machinery. Just like smtp_header_out(), this code destroys
+ * the header. We could try to avoid clobbering it, but we're not going
+ * to use the data any further.
+ *
+ * Code derived from cleanup_out_header().
+ */
+ for (line = start = vstring_str(buf); line != 0; line = next_line) {
+ next_line = split_at(line, '\n');
+ if (line == start || IS_SPACE_TAB(*line)) {
+ smtp_text_out(state, REC_TYPE_NORM, line, next_line ?
+ next_line - line - 1 : strlen(line), offset);
+ } else {
+ smtp_format_out(state, REC_TYPE_NORM, "\t%s", line);
+ }
+ }
+}
+
+/* smtp_body_rewrite - rewrite message body before output */
+
+static void smtp_body_rewrite(void *context, int type,
+ const char *buf, ssize_t len,
+ off_t offset)
+{
+ SMTP_STATE *state = (SMTP_STATE *) context;
+ char *result;
+
+ /*
+ * Apply optional body filtering.
+ */
+ if (smtp_body_checks) {
+ result = hbc_body_checks(context, smtp_body_checks, buf, len, offset);
+ if (result == buf) {
+ smtp_text_out(state, type, buf, len, offset);
+ } else if (result == HBC_CHECKS_STAT_ERROR) {
+ msg_warn("%s: smtp body checks lookup error",
+ state->request->queue_id);
+ vstream_longjmp(state->session->stream, SMTP_ERR_DATA);
+ } else if (result != 0) {
+ smtp_text_out(state, type, result, strlen(result), offset);
+ myfree(result);
+ }
+ }
+}
+
+/* smtp_mime_fail - MIME problem */
+
+static void smtp_mime_fail(SMTP_STATE *state, int mime_errs)
+{
+ const MIME_STATE_DETAIL *detail;
+ SMTP_RESP fake;
+
+ detail = mime_state_detail(mime_errs);
+ smtp_mesg_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, detail->dsn),
+ "%s", detail->text);
+}
+
+/* smtp_loop - exercise the SMTP protocol engine */
+
+static int smtp_loop(SMTP_STATE *state, NOCLOBBER int send_state,
+ NOCLOBBER int recv_state)
+{
+ const char *myname = "smtp_loop";
+ DELIVER_REQUEST *request = state->request;
+ SMTP_SESSION *session = state->session;
+ SMTP_ITERATOR *iter = state->iterator;
+ SMTP_RESP *resp;
+ RECIPIENT *rcpt;
+ VSTRING *next_command = vstring_alloc(100);
+ int *NOCLOBBER survivors = 0;
+ NOCLOBBER int next_state;
+ NOCLOBBER int next_rcpt;
+ NOCLOBBER int send_rcpt;
+ NOCLOBBER int recv_rcpt;
+ NOCLOBBER int nrcpt;
+ NOCLOBBER int recv_done;
+ int except;
+ int rec_type;
+ NOCLOBBER int prev_type = 0;
+ NOCLOBBER int mail_from_rejected;
+ NOCLOBBER int downgrading;
+ int mime_errs;
+ SMTP_RESP fake;
+ int fail_status;
+
+ /*
+ * Macros for readability.
+ */
+#define REWRITE_ADDRESS(dst, src) do { \
+ vstring_strcpy(dst, src); \
+ if (*(src) && smtp_generic_maps) \
+ smtp_map11_internal(dst, smtp_generic_maps, \
+ smtp_ext_prop_mask & EXT_PROP_GENERIC); \
+ } while (0)
+
+#define QUOTE_ADDRESS(dst, src) do { \
+ if (*(src) && var_smtp_quote_821_env) { \
+ quote_821_local(dst, src); \
+ } else { \
+ vstring_strcpy(dst, src); \
+ } \
+ } while (0)
+
+ /* Caution: changes to RETURN() also affect code outside the main loop. */
+
+#define RETURN(x) do { \
+ if (recv_state != SMTP_STATE_LAST) \
+ DONT_CACHE_THIS_SESSION; \
+ vstring_free(next_command); \
+ if (survivors) \
+ myfree((void *) survivors); \
+ if (session->mime_state) \
+ session->mime_state = mime_state_free(session->mime_state); \
+ return (x); \
+ } while (0)
+
+#define SENDER_IS_AHEAD \
+ (recv_state < send_state || recv_rcpt != send_rcpt)
+
+#define SENDER_IN_WAIT_STATE \
+ (send_state == SMTP_STATE_DOT || send_state == SMTP_STATE_LAST)
+
+#define SENDING_MAIL \
+ (recv_state <= SMTP_STATE_DOT)
+
+#define CANT_RSET_THIS_SESSION \
+ (session->features |= SMTP_FEATURE_RSET_REJECTED)
+
+ /*
+ * Pipelining support requires two loops: one loop for sending and one
+ * for receiving. Each loop has its own independent state. Most of the
+ * time the sender can run ahead of the receiver by as much as the TCP
+ * send buffer permits. There are only two places where the sender must
+ * wait for status information from the receiver: once after sending DATA
+ * and once after sending QUIT.
+ *
+ * The sender state advances until the TCP send buffer would overflow, or
+ * until the sender needs status information from the receiver. At that
+ * point the receiver starts processing responses. Once the receiver has
+ * caught up with the sender, the sender resumes sending commands. If the
+ * receiver detects a serious problem (MAIL FROM rejected, all RCPT TO
+ * commands rejected, DATA rejected) it forces the sender to abort the
+ * SMTP dialog with RSET and QUIT.
+ */
+ nrcpt = 0;
+ next_rcpt = send_rcpt = recv_rcpt = recv_done = 0;
+ mail_from_rejected = 0;
+
+ /*
+ * Prepare for disaster. This should not be needed because the design
+ * guarantees that no output is flushed before smtp_chat_resp() is
+ * called.
+ *
+ * 1) Every SMTP command fits entirely in a VSTREAM output buffer.
+ *
+ * 2) smtp_loop() never invokes smtp_chat_cmd() without making sure that
+ * there is sufficient space for the command in the output buffer.
+ *
+ * 3) smtp_loop() flushes the output buffer to avoid server timeouts.
+ *
+ * Changing any of these would violate the design, and would likely break
+ * SMTP pipelining.
+ *
+ * We set up the error handler anyway (only upon entry to avoid wasting
+ * resources) because 1) there is code below that expects that VSTREAM
+ * timeouts are enabled, and 2) this allows us to detect if someone broke
+ * Postfix by introducing spurious flush before read operations.
+ */
+ if (send_state < SMTP_STATE_XFORWARD_NAME_ADDR
+ || send_state > SMTP_STATE_QUIT)
+ msg_panic("%s: bad sender state %d (receiver state %d)",
+ myname, send_state, recv_state);
+ smtp_stream_setup(session->stream, *xfer_timeouts[send_state],
+ var_smtp_rec_deadline);
+ if ((except = vstream_setjmp(session->stream)) != 0) {
+ msg_warn("smtp_proto: spurious flush before read in send state %d",
+ send_state);
+ RETURN(SENDING_MAIL ? smtp_stream_except(state, except,
+ xfer_states[send_state]) : -1);
+ }
+
+ /*
+ * The main protocol loop.
+ */
+ do {
+
+ /*
+ * Build the next command.
+ */
+ switch (send_state) {
+
+ /*
+ * Sanity check.
+ */
+ default:
+ msg_panic("%s: bad sender state %d", myname, send_state);
+
+ /*
+ * Build the XFORWARD command. With properly sanitized
+ * information, the command length stays within the 512 byte
+ * command line length limit.
+ *
+ * XXX smtpd_xforward_preset() initializes some fields as "unknown"
+ * and some as null; historically, pickup(8) does not send any of
+ * these, and the queue manager presets absent fields to "not
+ * available" except for the rewrite context which is preset to
+ * local by way of migration aid. These definitions need to be
+ * centralized for maintainability.
+ */
+#ifndef CAN_FORWARD_CLIENT_NAME
+#define _ATTR_AVAIL_AND_KNOWN_(val) \
+ (DEL_REQ_ATTR_AVAIL(val) && strcasecmp((val), "unknown"))
+#define CAN_FORWARD_CLIENT_NAME _ATTR_AVAIL_AND_KNOWN_
+#define CAN_FORWARD_CLIENT_ADDR _ATTR_AVAIL_AND_KNOWN_
+#define CAN_FORWARD_CLIENT_PORT _ATTR_AVAIL_AND_KNOWN_
+#define CAN_FORWARD_PROTO_NAME _ATTR_AVAIL_AND_KNOWN_
+#define CAN_FORWARD_HELO_NAME DEL_REQ_ATTR_AVAIL
+#define CAN_FORWARD_IDENT_NAME DEL_REQ_ATTR_AVAIL
+#define CAN_FORWARD_RWR_CONTEXT DEL_REQ_ATTR_AVAIL
+#endif
+
+ case SMTP_STATE_XFORWARD_NAME_ADDR:
+ vstring_strcpy(next_command, XFORWARD_CMD);
+ if ((session->features & SMTP_FEATURE_XFORWARD_NAME)
+ && CAN_FORWARD_CLIENT_NAME(request->client_name)) {
+ vstring_strcat(next_command, " " XFORWARD_NAME "=");
+ xtext_quote_append(next_command, request->client_name, "");
+ }
+ if ((session->features & SMTP_FEATURE_XFORWARD_ADDR)
+ && CAN_FORWARD_CLIENT_ADDR(request->client_addr)) {
+ vstring_strcat(next_command, " " XFORWARD_ADDR "=");
+ xtext_quote_append(next_command, request->client_addr, "");
+ }
+ if ((session->features & SMTP_FEATURE_XFORWARD_PORT)
+ && CAN_FORWARD_CLIENT_PORT(request->client_port)) {
+ vstring_strcat(next_command, " " XFORWARD_PORT "=");
+ xtext_quote_append(next_command, request->client_port, "");
+ }
+ if (session->send_proto_helo)
+ next_state = SMTP_STATE_XFORWARD_PROTO_HELO;
+ else
+ next_state = SMTP_STATE_MAIL;
+ break;
+
+ case SMTP_STATE_XFORWARD_PROTO_HELO:
+ vstring_strcpy(next_command, XFORWARD_CMD);
+ if ((session->features & SMTP_FEATURE_XFORWARD_PROTO)
+ && CAN_FORWARD_PROTO_NAME(request->client_proto)) {
+ vstring_strcat(next_command, " " XFORWARD_PROTO "=");
+ xtext_quote_append(next_command, request->client_proto, "");
+ }
+ if ((session->features & SMTP_FEATURE_XFORWARD_HELO)
+ && CAN_FORWARD_HELO_NAME(request->client_helo)) {
+ vstring_strcat(next_command, " " XFORWARD_HELO "=");
+ xtext_quote_append(next_command, request->client_helo, "");
+ }
+ if ((session->features & SMTP_FEATURE_XFORWARD_IDENT)
+ && CAN_FORWARD_IDENT_NAME(request->log_ident)) {
+ vstring_strcat(next_command, " " XFORWARD_IDENT "=");
+ xtext_quote_append(next_command, request->log_ident, "");
+ }
+ if ((session->features & SMTP_FEATURE_XFORWARD_DOMAIN)
+ && CAN_FORWARD_RWR_CONTEXT(request->rewrite_context)) {
+ vstring_strcat(next_command, " " XFORWARD_DOMAIN "=");
+ xtext_quote_append(next_command,
+ strcmp(request->rewrite_context, MAIL_ATTR_RWR_LOCAL) ?
+ XFORWARD_DOM_REMOTE : XFORWARD_DOM_LOCAL, "");
+ }
+ next_state = SMTP_STATE_MAIL;
+ break;
+
+ /*
+ * Build the MAIL FROM command.
+ */
+ case SMTP_STATE_MAIL:
+ request->msg_stats.reuse_count = session->reuse_count;
+ GETTIMEOFDAY(&request->msg_stats.conn_setup_done);
+ REWRITE_ADDRESS(session->scratch2, request->sender);
+ QUOTE_ADDRESS(session->scratch, vstring_str(session->scratch2));
+ vstring_sprintf(next_command, "MAIL FROM:<%s>",
+ vstring_str(session->scratch));
+ /* XXX Don't announce SIZE if we're going to MIME downgrade. */
+ if (session->features & SMTP_FEATURE_SIZE /* RFC 1870 */
+ && !SMTP_MIME_DOWNGRADE(session, request))
+ vstring_sprintf_append(next_command, " SIZE=%lu",
+ request->data_size);
+ if (session->features & SMTP_FEATURE_8BITMIME) { /* RFC 1652 */
+ if (strcmp(request->encoding, MAIL_ATTR_ENC_8BIT) == 0)
+ vstring_strcat(next_command, " BODY=8BITMIME");
+ else if (strcmp(request->encoding, MAIL_ATTR_ENC_7BIT) == 0)
+ vstring_strcat(next_command, " BODY=7BIT");
+ else if (strcmp(request->encoding, MAIL_ATTR_ENC_NONE) != 0)
+ msg_warn("%s: unknown content encoding: %s",
+ request->queue_id, request->encoding);
+ }
+ if (session->features & SMTP_FEATURE_DSN) {
+ if (request->dsn_envid[0]) {
+ vstring_sprintf_append(next_command, " ENVID=");
+ xtext_quote_append(next_command, request->dsn_envid, "+=");
+ }
+ if (request->dsn_ret)
+ vstring_sprintf_append(next_command, " RET=%s",
+ dsn_ret_str(request->dsn_ret));
+ }
+
+ /*
+ * Request SMTPUTF8 when the remote SMTP server supports SMTPUTF8
+ * and the sender requested SMTPUTF8 support.
+ *
+ * If the sender requested SMTPUTF8 but the remote SMTP server does
+ * not support SMTPUTF8, then we have already determined earlier
+ * that delivering this message without SMTPUTF8 will not break
+ * the SMTPUTF8 promise that was made to the sender.
+ */
+ if ((session->features & SMTP_FEATURE_SMTPUTF8) != 0
+ && (request->smtputf8 & SMTPUTF8_FLAG_REQUESTED) != 0)
+ vstring_strcat(next_command, " SMTPUTF8");
+
+ /*
+ * We authenticate the local MTA only, but not the sender.
+ */
+#ifdef USE_SASL_AUTH
+ if (var_smtp_sasl_enable
+ && var_smtp_dummy_mail_auth
+ && (session->features & SMTP_FEATURE_AUTH))
+ vstring_strcat(next_command, " AUTH=<>");
+#endif
+
+ /*
+ * CVE-2009-3555 (TLS renegotiation). Try to detect a mail
+ * hijacking attack that prepends malicious EHLO/MAIL/RCPT/DATA
+ * commands to our TLS session.
+ *
+ * For the attack to succeed, the remote SMTP server must reply to
+ * the malicious EHLO/MAIL/RCPT/DATA commands after completing
+ * TLS (re)negotiation, so that the replies arrive in our TLS
+ * session (otherwise the Postfix SMTP client would time out
+ * waiting for an answer). With some luck we can detect this
+ * specific attack as a server MAIL reply that arrives before we
+ * send our own MAIL command.
+ *
+ * We don't apply this test to the HELO command because the result
+ * would be very timing sensitive, and we don't apply this test
+ * to RCPT and DATA replies because these may be pipelined for
+ * legitimate reasons.
+ */
+#ifdef USE_TLS
+ if (var_smtp_tls_blk_early_mail_reply
+ && (state->misc_flags & SMTP_MISC_FLAG_IN_STARTTLS) != 0
+ && (vstream_peek(session->stream) > 0
+ || peekfd(vstream_fileno(session->stream)) > 0))
+ session->features |= SMTP_FEATURE_EARLY_TLS_MAIL_REPLY;
+#endif
+
+ /*
+ * We now return to our regular broadcast.
+ */
+ next_state = SMTP_STATE_RCPT;
+ break;
+
+ /*
+ * Build one RCPT TO command before we have seen the MAIL FROM
+ * response.
+ */
+ case SMTP_STATE_RCPT:
+ rcpt = request->rcpt_list.info + send_rcpt;
+ REWRITE_ADDRESS(session->scratch2, rcpt->address);
+ QUOTE_ADDRESS(session->scratch, vstring_str(session->scratch2));
+ vstring_sprintf(next_command, "RCPT TO:<%s>",
+ vstring_str(session->scratch));
+ if (session->features & SMTP_FEATURE_DSN) {
+ /* XXX DSN xtext encode address value not type. */
+ const char *orcpt_type_addr = rcpt->dsn_orcpt;
+
+ /* Fix 20140706: don't use empty rcpt->orig_addr. */
+ if (orcpt_type_addr[0] == 0 && rcpt->orig_addr[0] != 0) {
+ quote_822_local(session->scratch, rcpt->orig_addr);
+ vstring_sprintf(session->scratch2, "%s;%s",
+ /* Fix 20140707: sender must request SMTPUTF8. */
+ (request->smtputf8 != 0
+ && !allascii(vstring_str(session->scratch))) ?
+ "utf-8" : "rfc822",
+ vstring_str(session->scratch));
+ orcpt_type_addr = vstring_str(session->scratch2);
+ }
+ if (orcpt_type_addr[0] != 0) {
+ /* Fix 20140706: don't send unquoted ORCPT. */
+ /* Fix 20140707: quoting method must match orcpt type. */
+ /* Fix 20140707: handle uxtext encoder errors. */
+ if (strncasecmp(orcpt_type_addr, "utf-8;", 6) == 0) {
+ if (uxtext_quote(session->scratch,
+ orcpt_type_addr, "+=") != 0)
+ vstring_sprintf_append(next_command, " ORCPT=%s",
+ vstring_str(session->scratch));
+ } else {
+ xtext_quote(session->scratch, orcpt_type_addr, "=");
+ vstring_sprintf_append(next_command, " ORCPT=%s",
+ vstring_str(session->scratch));
+ }
+ }
+ if (rcpt->dsn_notify)
+ vstring_sprintf_append(next_command, " NOTIFY=%s",
+ dsn_notify_str(rcpt->dsn_notify));
+ }
+ if ((next_rcpt = send_rcpt + 1) == SMTP_RCPT_LEFT(state))
+ next_state = (DEL_REQ_TRACE_ONLY(request->flags)
+ && smtp_vrfy_tgt == SMTP_STATE_RCPT) ?
+ SMTP_STATE_ABORT : SMTP_STATE_DATA;
+ break;
+
+ /*
+ * Build the DATA command before we have seen all the RCPT TO
+ * responses.
+ */
+ case SMTP_STATE_DATA:
+ vstring_strcpy(next_command, "DATA");
+ next_state = SMTP_STATE_DOT;
+ break;
+
+ /*
+ * Build the "." command after we have seen the DATA response
+ * (DATA is a protocol synchronization point).
+ *
+ * Changing the connection caching state here is safe because it
+ * affects none of the not-yet processed replies to
+ * already-generated commands.
+ */
+ case SMTP_STATE_DOT:
+ vstring_strcpy(next_command, ".");
+ if (THIS_SESSION_IS_EXPIRED)
+ DONT_CACHE_THIS_SESSION;
+ next_state = THIS_SESSION_IS_CACHED ?
+ SMTP_STATE_LAST : SMTP_STATE_QUIT;
+ break;
+
+ /*
+ * The SMTP_STATE_ABORT sender state is entered by the sender
+ * when it has verified all recipients; or it is entered by the
+ * receiver when all recipients are verified or rejected, and is
+ * then left before the bottom of the main loop.
+ *
+ * Changing the connection caching state here is safe because there
+ * are no not-yet processed replies to already-generated
+ * commands.
+ */
+ case SMTP_STATE_ABORT:
+ vstring_strcpy(next_command, "RSET");
+ if (THIS_SESSION_IS_EXPIRED)
+ DONT_CACHE_THIS_SESSION;
+ next_state = THIS_SESSION_IS_CACHED ?
+ SMTP_STATE_LAST : SMTP_STATE_QUIT;
+ break;
+
+ /*
+ * Build the RSET command. This is entered as initial state from
+ * smtp_rset() and has its own dedicated state transitions. It is
+ * used to find out the status of a cached session before
+ * attempting mail delivery.
+ */
+ case SMTP_STATE_RSET:
+ vstring_strcpy(next_command, "RSET");
+ next_state = SMTP_STATE_LAST;
+ break;
+
+ /*
+ * Build the QUIT command before we have seen the "." or RSET
+ * response. This is entered as initial state from smtp_quit(),
+ * or is reached near the end of any non-cached session.
+ *
+ * Changing the connection caching state here is safe. If this
+ * command is pipelined together with a preceding command, then
+ * connection caching was already turned off. Do not clobber the
+ * "bad connection" flag.
+ */
+ case SMTP_STATE_QUIT:
+ vstring_strcpy(next_command, "QUIT");
+ next_state = SMTP_STATE_LAST;
+ if (THIS_SESSION_IS_CACHED)
+ DONT_CACHE_THIS_SESSION;
+ break;
+
+ /*
+ * The final sender state has no action associated with it.
+ */
+ case SMTP_STATE_LAST:
+ VSTRING_RESET(next_command);
+ break;
+ }
+ VSTRING_TERMINATE(next_command);
+
+ /*
+ * Process responses until the receiver has caught up. Vstreams
+ * automatically flush buffered output when reading new data.
+ *
+ * Flush unsent output if command pipelining is off or if no I/O
+ * happened for a while. This limits the accumulation of client-side
+ * delays in pipelined sessions.
+ *
+ * The PIPELINING engine will flush the VSTREAM buffer if the sender
+ * could otherwise produce more output than fits the PIPELINING
+ * buffer. This generally works because we know exactly how much
+ * output we produced since the last time that the sender and
+ * receiver synchronized the SMTP state. However this logic is not
+ * applicable after the sender enters the DATA phase, where it does
+ * not synchronize with the receiver until the <CR><LF>.<CR><LF>.
+ * Thus, the PIPELINING engine no longer knows how much data is
+ * pending in the TCP send buffer. For this reason, if PIPELINING is
+ * enabled, we always pipeline QUIT after <CR><LF>.<CR><LF>. This is
+ * safe because once the receiver reads <CR><LF>.<CR><LF>, its TCP
+ * stack either has already received the QUIT<CR><LF>, or else it
+ * acknowledges all bytes up to and including <CR><LF>.<CR><LF>,
+ * making room in the sender's TCP stack for QUIT<CR><LF>.
+ */
+#define CHECK_PIPELINING_BUFSIZE \
+ (recv_state != SMTP_STATE_DOT || send_state != SMTP_STATE_QUIT)
+
+ if (SENDER_IN_WAIT_STATE
+ || (SENDER_IS_AHEAD
+ && ((session->features & SMTP_FEATURE_PIPELINING) == 0
+ || (CHECK_PIPELINING_BUFSIZE
+ && (VSTRING_LEN(next_command) + 2
+ + vstream_bufstat(session->stream, VSTREAM_BST_OUT_PEND)
+ > PIPELINING_BUFSIZE))
+ || time((time_t *) 0)
+ - vstream_ftime(session->stream) > 10))) {
+ while (SENDER_IS_AHEAD) {
+
+ /*
+ * Sanity check.
+ */
+ if (recv_state < SMTP_STATE_XFORWARD_NAME_ADDR
+ || recv_state > SMTP_STATE_QUIT)
+ msg_panic("%s: bad receiver state %d (sender state %d)",
+ myname, recv_state, send_state);
+
+ /*
+ * Receive the next server response. Use the proper timeout,
+ * and log the proper client state in case of trouble.
+ *
+ * XXX If we lose the connection before sending end-of-data,
+ * find out if the server sent a premature end-of-data reply.
+ * If this read attempt fails, report "lost connection while
+ * sending message body", not "lost connection while sending
+ * end-of-data".
+ *
+ * "except" becomes zero just above the protocol loop, and stays
+ * zero or triggers an early return from the loop. In just
+ * one case: loss of the connection when sending the message
+ * body, we record the exception, and keep processing in the
+ * hope of detecting a premature 5XX. We must be careful to
+ * not clobber this non-zero value once it is set. The
+ * variable need not survive longjmp() calls, since the only
+ * setjmp() which does not return early is the one sets this
+ * condition, subquent failures always return early.
+ */
+#define LOST_CONNECTION_INSIDE_DATA (except == SMTP_ERR_EOF)
+
+ smtp_stream_setup(session->stream, *xfer_timeouts[recv_state],
+ var_smtp_rec_deadline);
+ if (LOST_CONNECTION_INSIDE_DATA) {
+ if (vstream_setjmp(session->stream) != 0)
+ RETURN(smtp_stream_except(state, SMTP_ERR_EOF,
+ "sending message body"));
+ } else {
+ if ((except = vstream_setjmp(session->stream)) != 0)
+ RETURN(SENDING_MAIL ? smtp_stream_except(state, except,
+ xfer_states[recv_state]) : -1);
+ }
+ resp = smtp_chat_resp(session);
+
+ /*
+ * Process the response.
+ */
+ switch (recv_state) {
+
+ /*
+ * Process the XFORWARD response.
+ */
+ case SMTP_STATE_XFORWARD_NAME_ADDR:
+ if (resp->code / 100 != 2)
+ msg_warn("host %s said: %s (in reply to %s)",
+ session->namaddrport,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_XFORWARD_NAME_ADDR]);
+ if (session->send_proto_helo)
+ recv_state = SMTP_STATE_XFORWARD_PROTO_HELO;
+ else
+ recv_state = SMTP_STATE_MAIL;
+ break;
+
+ case SMTP_STATE_XFORWARD_PROTO_HELO:
+ if (resp->code / 100 != 2)
+ msg_warn("host %s said: %s (in reply to %s)",
+ session->namaddrport,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_XFORWARD_PROTO_HELO]);
+ recv_state = SMTP_STATE_MAIL;
+ break;
+
+ /*
+ * Process the MAIL FROM response. When the server
+ * rejects the sender, set the mail_from_rejected flag so
+ * that the receiver may apply a course correction.
+ */
+ case SMTP_STATE_MAIL:
+ if (resp->code / 100 != 2) {
+ smtp_mesg_fail(state, STR(iter->host), resp,
+ "host %s said: %s (in reply to %s)",
+ session->namaddr,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_MAIL]);
+ mail_from_rejected = 1;
+ }
+
+ /*
+ * CVE-2009-3555 (TLS renegotiation). Whatever it was
+ * that arrived before we sent our MAIL FROM command, it
+ * was not a fatal-level TLS alert message. It could be a
+ * warning-level TLS alert message, or a ChangeCipherSpec
+ * message, but such messages are not normally sent in
+ * the middle of a TLS session. We disconnect and try
+ * again later.
+ */
+#ifdef USE_TLS
+ if (var_smtp_tls_blk_early_mail_reply
+ && (session->features & SMTP_FEATURE_EARLY_TLS_MAIL_REPLY)) {
+ smtp_site_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "4.7.0"),
+ "unexpected server message");
+ msg_warn("server %s violates %s policy",
+ session->namaddr,
+ VAR_LMTP_SMTP(TLS_BLK_EARLY_MAIL_REPLY));
+ mail_from_rejected = 1;
+ }
+#endif
+
+ /*
+ * We now return to our regular broadcast.
+ */
+ recv_state = SMTP_STATE_RCPT;
+ break;
+
+ /*
+ * Process one RCPT TO response. If MAIL FROM was
+ * rejected, ignore RCPT TO responses: all recipients are
+ * dead already. When all recipients are rejected the
+ * receiver may apply a course correction.
+ *
+ * XXX 2821: Section 4.5.3.1 says that a 552 RCPT TO reply
+ * must be treated as if the server replied with 452.
+ * However, this causes "too much mail data" to be
+ * treated as a recoverable error, which is wrong. I'll
+ * stick with RFC 821.
+ */
+ case SMTP_STATE_RCPT:
+ if (!mail_from_rejected) {
+#ifdef notdef
+ if (resp->code == 552) {
+ resp->code = 452;
+ resp->dsn[0] = '4';
+ }
+#endif
+ rcpt = request->rcpt_list.info + recv_rcpt;
+ if (resp->code / 100 == 2) {
+ if (!smtp_mode) {
+ if (survivors == 0)
+ survivors = (int *)
+ mymalloc(request->rcpt_list.len
+ * sizeof(int));
+ survivors[nrcpt] = recv_rcpt;
+ }
+ ++nrcpt;
+ /* If trace-only, mark the recipient done. */
+ if (DEL_REQ_TRACE_ONLY(request->flags)
+ && smtp_vrfy_tgt == SMTP_STATE_RCPT) {
+ translit(resp->str, "\n", " ");
+ smtp_rcpt_done(state, resp, rcpt);
+ }
+ } else {
+ smtp_rcpt_fail(state, rcpt, STR(iter->host), resp,
+ "host %s said: %s (in reply to %s)",
+ session->namaddr,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_RCPT]);
+ }
+ }
+ /* If trace-only, send RSET instead of DATA. */
+ if (++recv_rcpt == SMTP_RCPT_LEFT(state))
+ recv_state = (DEL_REQ_TRACE_ONLY(request->flags)
+ && smtp_vrfy_tgt == SMTP_STATE_RCPT) ?
+ SMTP_STATE_ABORT : SMTP_STATE_DATA;
+ /* XXX Also: record if non-delivering session. */
+ break;
+
+ /*
+ * Process the DATA response. When the server rejects
+ * DATA, set nrcpt to a negative value so that the
+ * receiver can apply a course correction.
+ */
+ case SMTP_STATE_DATA:
+ recv_state = SMTP_STATE_DOT;
+ if (resp->code / 100 != 3) {
+ if (nrcpt > 0)
+ smtp_mesg_fail(state, STR(iter->host), resp,
+ "host %s said: %s (in reply to %s)",
+ session->namaddr,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_DATA]);
+ nrcpt = -1;
+ }
+
+ /*
+ * In the case of a successful address probe with target
+ * equal to DATA, the remote server is now in the DATA
+ * state, and therefore we must not make any further
+ * attempt to send or receive on this connection. This
+ * means that we cannot not reuse the general-purpose
+ * course-correction logic below which sends RSET (and
+ * perhaps QUIT). Instead we "jump" straight to the exit
+ * and force an unceremonious disconnect.
+ */
+ else if (DEL_REQ_TRACE_ONLY(request->flags)
+ && smtp_vrfy_tgt == SMTP_STATE_DATA) {
+ for (nrcpt = 0; nrcpt < recv_rcpt; nrcpt++) {
+ rcpt = request->rcpt_list.info + nrcpt;
+ if (!SMTP_RCPT_ISMARKED(rcpt)) {
+ translit(resp->str, "\n", " ");
+ SMTP_RESP_SET_DSN(resp, "2.0.0");
+ smtp_rcpt_done(state, resp, rcpt);
+ }
+ }
+ DONT_CACHE_THIS_SESSION;
+ send_state = recv_state = SMTP_STATE_LAST;
+ }
+ break;
+
+ /*
+ * Process the end of message response. Ignore the
+ * response when no recipient was accepted: all
+ * recipients are dead already, and the next receiver
+ * state is SMTP_STATE_LAST/QUIT regardless. Otherwise,
+ * if the message transfer fails, bounce all remaining
+ * recipients, else cross off the recipients that were
+ * delivered.
+ */
+ case SMTP_STATE_DOT:
+ GETTIMEOFDAY(&request->msg_stats.deliver_done);
+ if (smtp_mode) {
+ if (nrcpt > 0) {
+ if (resp->code / 100 != 2) {
+ smtp_mesg_fail(state, STR(iter->host), resp,
+ "host %s said: %s (in reply to %s)",
+ session->namaddr,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_DOT]);
+ } else {
+ for (nrcpt = 0; nrcpt < recv_rcpt; nrcpt++) {
+ rcpt = request->rcpt_list.info + nrcpt;
+ if (!SMTP_RCPT_ISMARKED(rcpt)) {
+ translit(resp->str, "\n", " ");
+ smtp_rcpt_done(state, resp, rcpt);
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * With LMTP we have one response per accepted RCPT TO
+ * command. Stay in the SMTP_STATE_DOT state until we
+ * have collected all responses.
+ */
+ else {
+ if (nrcpt > 0) {
+ rcpt = request->rcpt_list.info
+ + survivors[recv_done++];
+ if (resp->code / 100 != 2) {
+ smtp_rcpt_fail(state, rcpt, STR(iter->host), resp,
+ "host %s said: %s (in reply to %s)",
+ session->namaddr,
+ translit(resp->str, "\n", " "),
+ xfer_request[SMTP_STATE_DOT]);
+ } else {
+ translit(resp->str, "\n", " ");
+ smtp_rcpt_done(state, resp, rcpt);
+ }
+ }
+ if (msg_verbose)
+ msg_info("%s: got %d of %d end-of-data replies",
+ myname, recv_done, nrcpt);
+ if (recv_done < nrcpt)
+ break;
+ }
+
+ /*
+ * XXX Do not change the connection caching state here,
+ * even if the connection caching timer expired between
+ * generating the command and processing the reply,
+ * otherwise the sender and receiver loops get out of
+ * sync. The caller will call smtp_quit() if appropriate.
+ */
+ if (var_skip_quit_resp || THIS_SESSION_IS_CACHED
+ || LOST_CONNECTION_INSIDE_DATA)
+ recv_state = SMTP_STATE_LAST;
+ else
+ recv_state = SMTP_STATE_QUIT;
+ break;
+
+ /*
+ * Receive the RSET response.
+ *
+ * The SMTP_STATE_ABORT sender state is entered by the
+ * sender when it has verified all recipients; or it is
+ * entered by the receiver when all recipients are
+ * verified or rejected, and is then left before the
+ * bottom of the main loop.
+ *
+ * XXX Do not change the connection caching state here, even
+ * if the server rejected RSET or if the connection
+ * caching timer expired between generating the command
+ * and processing the reply, otherwise the sender and
+ * receiver loops get out of sync. The caller will call
+ * smtp_quit() if appropriate.
+ */
+ case SMTP_STATE_ABORT:
+ recv_state = (var_skip_quit_resp || THIS_SESSION_IS_CACHED ?
+ SMTP_STATE_LAST : SMTP_STATE_QUIT);
+ break;
+
+ /*
+ * This is the initial receiver state from smtp_rset().
+ * It is used to find out the status of a cached session
+ * before attempting mail delivery.
+ */
+ case SMTP_STATE_RSET:
+ if (resp->code / 100 != 2)
+ CANT_RSET_THIS_SESSION;
+ recv_state = SMTP_STATE_LAST;
+ break;
+
+ /*
+ * Receive, but otherwise ignore, the QUIT response.
+ */
+ case SMTP_STATE_QUIT:
+ recv_state = SMTP_STATE_LAST;
+ break;
+ }
+ }
+
+ /*
+ * At this point, the sender and receiver are fully synchronized.
+ */
+
+ /*
+ * We know the server response to every command that was sent.
+ * Apply a course correction if necessary: the sender wants to
+ * send RCPT TO but MAIL FROM was rejected; the sender wants to
+ * send DATA but all recipients were rejected; the sender wants
+ * to deliver the message but DATA was rejected.
+ */
+ if ((send_state == SMTP_STATE_RCPT && mail_from_rejected)
+ || (send_state == SMTP_STATE_DATA && nrcpt == 0)
+ || (send_state == SMTP_STATE_DOT && nrcpt < 0)) {
+ send_state = recv_state = SMTP_STATE_ABORT;
+ send_rcpt = recv_rcpt = 0;
+ vstring_strcpy(next_command, "RSET");
+ if (THIS_SESSION_IS_EXPIRED)
+ DONT_CACHE_THIS_SESSION;
+ next_state = THIS_SESSION_IS_CACHED ?
+ SMTP_STATE_LAST : SMTP_STATE_QUIT;
+ /* XXX Also: record if non-delivering session. */
+ next_rcpt = 0;
+ }
+ }
+
+ /*
+ * Make the next sender state the current sender state.
+ */
+ if (send_state == SMTP_STATE_LAST)
+ continue;
+
+ /*
+ * Special case if the server accepted the DATA command. If the
+ * server accepted at least one recipient send the entire message.
+ * Otherwise, just send "." as per RFC 2197.
+ *
+ * XXX If there is a hard MIME error while downgrading to 7-bit mail,
+ * disconnect ungracefully, because there is no other way to cancel a
+ * transaction in progress.
+ */
+ if (send_state == SMTP_STATE_DOT && nrcpt > 0) {
+
+ smtp_stream_setup(session->stream, var_smtp_data1_tmout,
+ var_smtp_rec_deadline);
+
+ if ((except = vstream_setjmp(session->stream)) == 0) {
+
+ if (vstream_fseek(state->src, request->data_offset, SEEK_SET) < 0)
+ msg_fatal("seek queue file: %m");
+
+ downgrading = SMTP_MIME_DOWNGRADE(session, request);
+
+ /*
+ * XXX Don't downgrade just because generic_maps is turned
+ * on.
+ */
+#define SMTP_ANY_CHECKS (smtp_header_checks || smtp_body_checks)
+
+ if (downgrading || smtp_generic_maps || SMTP_ANY_CHECKS)
+ session->mime_state = mime_state_alloc(downgrading ?
+ MIME_OPT_DOWNGRADE
+ | MIME_OPT_REPORT_NESTING :
+ SMTP_ANY_CHECKS == 0 ?
+ MIME_OPT_DISABLE_MIME :
+ 0,
+ smtp_generic_maps
+ || smtp_header_checks ?
+ smtp_header_rewrite :
+ smtp_header_out,
+ (MIME_STATE_ANY_END) 0,
+ smtp_body_checks ?
+ smtp_body_rewrite :
+ smtp_text_out,
+ (MIME_STATE_ANY_END) 0,
+ (MIME_STATE_ERR_PRINT) 0,
+ (void *) state);
+ state->space_left = var_smtp_line_limit;
+
+ while ((rec_type = rec_get(state->src, session->scratch, 0)) > 0) {
+ if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT)
+ break;
+ if (session->mime_state == 0) {
+ smtp_text_out((void *) state, rec_type,
+ vstring_str(session->scratch),
+ VSTRING_LEN(session->scratch),
+ (off_t) 0);
+ } else {
+ mime_errs =
+ mime_state_update(session->mime_state, rec_type,
+ vstring_str(session->scratch),
+ VSTRING_LEN(session->scratch));
+ if (mime_errs) {
+ smtp_mime_fail(state, mime_errs);
+ RETURN(0);
+ }
+ }
+ prev_type = rec_type;
+ }
+
+ if (session->mime_state) {
+
+ /*
+ * The cleanup server normally ends MIME content with a
+ * normal text record. The following code is needed to
+ * flush an internal buffer when someone submits 8-bit
+ * mail not ending in newline via /usr/sbin/sendmail
+ * while MIME input processing is turned off, and MIME
+ * 8bit->7bit conversion is requested upon delivery.
+ *
+ * Or some error while doing generic address mapping.
+ */
+ mime_errs =
+ mime_state_update(session->mime_state, rec_type, "", 0);
+ if (mime_errs) {
+ smtp_mime_fail(state, mime_errs);
+ RETURN(0);
+ }
+ } else if (prev_type == REC_TYPE_CONT) /* missing newline */
+ smtp_fputs("", 0, session->stream);
+ if (session->features & SMTP_FEATURE_PIX_DELAY_DOTCRLF) {
+ smtp_flush(session->stream);/* hurts performance */
+ sleep(var_smtp_pix_delay); /* not to mention this */
+ }
+ if (vstream_ferror(state->src))
+ msg_fatal("queue file read error");
+ if (rec_type != REC_TYPE_XTRA) {
+ msg_warn("%s: bad record type: %d in message content",
+ request->queue_id, rec_type);
+ fail_status = smtp_mesg_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "5.3.0"),
+ "unreadable mail queue entry");
+ /* Bailing out, abort stream with prejudice */
+ (void) vstream_fpurge(session->stream, VSTREAM_PURGE_BOTH);
+ DONT_USE_FORBIDDEN_SESSION;
+ /* If bounce_append() succeeded, status is still 0 */
+ if (state->status == 0)
+ (void) mark_corrupt(state->src);
+ /* Don't override smtp_mesg_fail() here. */
+ RETURN(fail_status);
+ }
+ } else {
+ if (!LOST_CONNECTION_INSIDE_DATA)
+ RETURN(smtp_stream_except(state, except,
+ "sending message body"));
+
+ /*
+ * We will clear the stream error flag to try and read a
+ * premature 5XX response, so it is important to flush any
+ * unwritten data. Otherwise, we will try to flush it again
+ * before reading, which may incur an unnecessary delay and
+ * will prevent the reading of any response that is not
+ * already buffered (bundled with the DATA 354 response).
+ *
+ * Not much point in sending QUIT at this point, skip right to
+ * SMTP_STATE_LAST. The read engine above will likewise avoid
+ * looking for a QUIT response.
+ */
+ (void) vstream_fpurge(session->stream, VSTREAM_PURGE_WRITE);
+ next_state = SMTP_STATE_LAST;
+ }
+ }
+
+ /*
+ * Copy the next command to the buffer and update the sender state.
+ */
+ if (except == 0) {
+ smtp_chat_cmd(session, "%s", vstring_str(next_command));
+ } else {
+ DONT_CACHE_THIS_SESSION;
+ }
+ send_state = next_state;
+ send_rcpt = next_rcpt;
+ } while (recv_state != SMTP_STATE_LAST);
+ RETURN(0);
+}
+
+/* smtp_xfer - send a batch of envelope information and the message data */
+
+int smtp_xfer(SMTP_STATE *state)
+{
+ DELIVER_REQUEST *request = state->request;
+ SMTP_SESSION *session = state->session;
+ SMTP_RESP fake;
+ int send_state;
+ int recv_state;
+ int send_name_addr;
+ int result;
+
+ /*
+ * Sanity check. Recipients should be unmarked at this point.
+ */
+ if (SMTP_RCPT_LEFT(state) <= 0)
+ msg_panic("smtp_xfer: bad recipient count: %d",
+ SMTP_RCPT_LEFT(state));
+ if (SMTP_RCPT_ISMARKED(request->rcpt_list.info))
+ msg_panic("smtp_xfer: bad recipient status: %d",
+ request->rcpt_list.info->u.status);
+
+ /*
+ * See if we should even try to send this message at all. This code sits
+ * here rather than in the EHLO processing code, because of SMTP
+ * connection caching.
+ */
+ if (session->size_limit > 0 && session->size_limit < request->data_size) {
+ smtp_mesg_fail(state, DSN_BY_LOCAL_MTA,
+ SMTP_RESP_FAKE(&fake, "5.3.4"),
+ "message size %lu exceeds size limit %.0f of server %s",
+ request->data_size, (double) session->size_limit,
+ session->namaddr);
+ /* Redundant. We abort this delivery attempt. */
+ state->misc_flags |= SMTP_MISC_FLAG_COMPLETE_SESSION;
+ return (0);
+ }
+
+ /*
+ * Use XFORWARD to forward the origin of this email message across an
+ * SMTP-based content filter. Send client attribute information only if
+ * it exists (i.e. remote submission). Local submissions have no client
+ * attributes; the mail will appear to originate from the content filter
+ * which is acceptable.
+ */
+ send_name_addr =
+ var_smtp_send_xforward
+ && (((session->features & SMTP_FEATURE_XFORWARD_NAME)
+ && CAN_FORWARD_CLIENT_NAME(request->client_name))
+ || ((session->features & SMTP_FEATURE_XFORWARD_ADDR)
+ && CAN_FORWARD_CLIENT_ADDR(request->client_addr))
+ || ((session->features & SMTP_FEATURE_XFORWARD_PORT)
+ && CAN_FORWARD_CLIENT_PORT(request->client_port)));
+ session->send_proto_helo =
+ var_smtp_send_xforward
+ && (((session->features & SMTP_FEATURE_XFORWARD_PROTO)
+ && CAN_FORWARD_PROTO_NAME(request->client_proto))
+ || ((session->features & SMTP_FEATURE_XFORWARD_HELO)
+ && CAN_FORWARD_HELO_NAME(request->client_helo))
+ || ((session->features & SMTP_FEATURE_XFORWARD_IDENT)
+ && CAN_FORWARD_IDENT_NAME(request->log_ident))
+ || ((session->features & SMTP_FEATURE_XFORWARD_DOMAIN)
+ && CAN_FORWARD_RWR_CONTEXT(request->rewrite_context)));
+ if (send_name_addr)
+ recv_state = send_state = SMTP_STATE_XFORWARD_NAME_ADDR;
+ else if (session->send_proto_helo)
+ recv_state = send_state = SMTP_STATE_XFORWARD_PROTO_HELO;
+ else
+ recv_state = send_state = SMTP_STATE_MAIL;
+
+ /*
+ * Remember this session's "normal completion", even if the server 4xx-ed
+ * some or all recipients. Connection or handshake errors with a later MX
+ * host should not cause this destination be marked as unreachable.
+ */
+ result = smtp_loop(state, send_state, recv_state);
+
+ if (result == 0
+ /* Just in case */
+ && vstream_ferror(session->stream) == 0
+ && vstream_feof(session->stream) == 0)
+ state->misc_flags |= SMTP_MISC_FLAG_COMPLETE_SESSION;
+
+ return (result);
+}
+
+/* smtp_rset - send a lone RSET command */
+
+int smtp_rset(SMTP_STATE *state)
+{
+
+ /*
+ * This works because SMTP_STATE_RSET is a dedicated sender/recipient
+ * entry state, with SMTP_STATE_LAST as next sender/recipient state.
+ */
+ return (smtp_loop(state, SMTP_STATE_RSET, SMTP_STATE_RSET));
+}
+
+/* smtp_quit - send a lone QUIT command */
+
+int smtp_quit(SMTP_STATE *state)
+{
+
+ /*
+ * This works because SMTP_STATE_QUIT is the last state with a sender
+ * action, with SMTP_STATE_LAST as the next sender/recipient state.
+ */
+ return (smtp_loop(state, SMTP_STATE_QUIT, var_skip_quit_resp ?
+ SMTP_STATE_LAST : SMTP_STATE_QUIT));
+}