diff options
Diffstat (limited to 'src/smtp/smtp_proto.c')
-rw-r--r-- | src/smtp/smtp_proto.c | 2515 |
1 files changed, 2515 insertions, 0 deletions
diff --git a/src/smtp/smtp_proto.c b/src/smtp/smtp_proto.c new file mode 100644 index 0000000..097d518 --- /dev/null +++ b/src/smtp/smtp_proto.c @@ -0,0 +1,2515 @@ +/*++ +/* 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_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_req_deadline, 0); + 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_req_deadline, 0); + 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_req_deadline, 0); + 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 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. + * + * NOTE: We use "IS_MATCHED" to satisfy policy, but "IS_SECURED" to log + * effective security. Thus "half-dane" is never "Verified" only + * "Trusted", but matching is enforced here. + * + * NOTE: When none of the TLSA records were usable, "dane" and "half-dane" + * fall back to "encrypt", updating the tls_context level accordingly, so + * we must check that here, and not state->tls->level. + */ + if (TLS_MUST_MATCH(session->tls_context->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 %.200s: %s", + state->request->queue_id, action, where, content, text); + } else { + msg_info("%s: %s: %s %.200s", + 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 (ENFORCING_SIZE_LIMIT(var_smtp_line_limit) + && 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; + + /* + * XXX This can insert a line break into the middle of a + * multi-byte character (not necessarily UTF-8). Note that + * multibyte characters can span queue file records, for + * example if line_length_limit == smtp_line_length_limit. + */ + if (state->logged_line_length_limit == 0) { + msg_info("%s: breaking line > %d bytes with <CR><LF>SPACE", + state->request->queue_id, var_smtp_line_limit); + state->logged_line_length_limit = 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_out_raw_or_mime - output buffer, raw output or MIME-aware */ + +static int smtp_out_raw_or_mime(SMTP_STATE *state, int rec_type, VSTRING *buf) +{ + SMTP_SESSION *session = state->session; + int mime_errs; + + if (session->mime_state == 0) { + smtp_text_out((void *) state, rec_type, vstring_str(buf), + VSTRING_LEN(buf), (off_t) 0); + } else { + mime_errs = + mime_state_update(session->mime_state, rec_type, + vstring_str(buf), VSTRING_LEN(buf)); + if (mime_errs) { + smtp_mime_fail(state, mime_errs); + return (-1); + } + } + return (0); +} + +/* smtp_out_add_header - format address header, uses session->scratch* */ + +static int smtp_out_add_header(SMTP_STATE *state, const char *label, + const char *lt, const char *addr, + const char *gt) +{ + SMTP_SESSION *session = state->session; + + smtp_rewrite_generic_internal(session->scratch2, addr); + vstring_sprintf(session->scratch, "%s: %s", label, lt); + smtp_quote_822_address_flags(session->scratch, + vstring_str(session->scratch2), + QUOTE_FLAG_DEFAULT | QUOTE_FLAG_APPEND); + vstring_strcat(session->scratch, gt); + return (smtp_out_raw_or_mime(state, REC_TYPE_NORM, session->scratch)); +} + +/* smtp_out_add_headers - output additional headers, uses session->scratch* */ + +static int smtp_out_add_headers(SMTP_STATE *state) +{ + /* Prepend headers in the same order as mail_copy.c. */ + if (smtp_cli_attr.flags & SMTP_CLI_FLAG_RETURN_PATH) + if (smtp_out_add_header(state, "Return-Path", "<", + state->request->sender, ">") < 0) + return (-1); + if (smtp_cli_attr.flags & SMTP_CLI_FLAG_ORIG_RCPT) + if (smtp_out_add_header(state, "X-Original-To", "", + state->request->rcpt_list.info->orig_addr, "") < 0) + return (-1); + if (smtp_cli_attr.flags & SMTP_CLI_FLAG_DELIVERED_TO) + if (smtp_out_add_header(state, "Delivered-To", "", + state->request->rcpt_list.info->address, "") < 0) + return (-1); + return (0); +} + +/* 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; + + /* 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_req_deadline, 0); + 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); + smtp_rewrite_generic_internal(session->scratch2, request->sender); + smtp_quote_821_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; + smtp_rewrite_generic_internal(session->scratch2, rcpt->address); + smtp_quote_821_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, subsequent failures always return early. + */ +#define LOST_CONNECTION_INSIDE_DATA (except == SMTP_ERR_EOF) + + smtp_stream_setup(session->stream, *xfer_timeouts[recv_state], + var_smtp_req_deadline, 0); + 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_req_deadline, var_smtp_min_data_rate); + + 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; + + if ((smtp_cli_attr.flags & SMTP_CLI_MASK_ADD_HEADERS) != 0 + && smtp_out_add_headers(state) < 0) + RETURN(0); + + while ((rec_type = rec_get(state->src, session->scratch, 0)) > 0) { + if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT) + break; + if (smtp_out_raw_or_mime(state, rec_type, + session->scratch) < 0) + 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)); +} |