diff options
Diffstat (limited to 'src/smtp/smtp_trouble.c')
-rw-r--r-- | src/smtp/smtp_trouble.c | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/src/smtp/smtp_trouble.c b/src/smtp/smtp_trouble.c new file mode 100644 index 0000000..60880df --- /dev/null +++ b/src/smtp/smtp_trouble.c @@ -0,0 +1,476 @@ +/*++ +/* NAME +/* smtp_trouble 3 +/* SUMMARY +/* error handler policies +/* SYNOPSIS +/* #include "smtp.h" +/* +/* int smtp_sess_fail(state) +/* SMTP_STATE *state; +/* +/* int smtp_site_fail(state, mta_name, resp, format, ...) +/* SMTP_STATE *state; +/* const char *mta_name; +/* SMTP_RESP *resp; +/* const char *format; +/* +/* int smtp_mesg_fail(state, mta_name, resp, format, ...) +/* SMTP_STATE *state; +/* const char *mta_name; +/* SMTP_RESP *resp; +/* const char *format; +/* +/* void smtp_rcpt_fail(state, recipient, mta_name, resp, format, ...) +/* SMTP_STATE *state; +/* RECIPIENT *recipient; +/* const char *mta_name; +/* SMTP_RESP *resp; +/* const char *format; +/* +/* int smtp_stream_except(state, exception, description) +/* SMTP_STATE *state; +/* int exception; +/* const char *description; +/* AUXILIARY FUNCTIONS +/* int smtp_misc_fail(state, throttle, mta_name, resp, format, ...) +/* SMTP_STATE *state; +/* int throttle; +/* const char *mta_name; +/* SMTP_RESP *resp; +/* const char *format; +/* DESCRIPTION +/* This module handles all non-fatal errors that can happen while +/* attempting to deliver mail via SMTP, and implements the policy +/* of how to deal with the error. Depending on the nature of +/* the problem, delivery of a single message is deferred, delivery +/* of all messages to the same domain is deferred, or one or more +/* recipients are given up as non-deliverable and a bounce log is +/* updated. In any case, the recipient is marked as either KEEP +/* (try again with a backup host) or DROP (delete recipient from +/* delivery request). +/* +/* In addition, when an unexpected response code is seen such +/* as 3xx where only 4xx or 5xx are expected, or any error code +/* that suggests a syntax error or something similar, the +/* protocol error flag is set so that the postmaster receives +/* a transcript of the session. No notification is generated for +/* what appear to be configuration errors - very likely, they +/* would suffer the same problem and just cause more trouble. +/* +/* In case of a soft error, action depends on whether the error +/* qualifies for trying the request with other mail servers (log +/* an informational record only and try a backup server) or +/* whether this is the final server (log recipient delivery status +/* records and delete the recipient from the request). +/* +/* smtp_sess_fail() takes a pre-formatted error report after +/* failure to complete some protocol handshake. The policy is +/* as with smtp_site_fail(). +/* +/* smtp_site_fail() handles the case where the program fails to +/* complete the initial handshake: the server is not reachable, +/* is not running, does not want talk to us, or we talk to ourselves. +/* The \fIcode\fR gives an error status code; the \fIformat\fR +/* argument gives a textual description. +/* The policy is: soft error, non-final server: log an informational +/* record why the host is being skipped; soft error, final server: +/* defer delivery of all remaining recipients and mark the destination +/* as problematic; hard error: bounce all remaining recipients. +/* The session is marked as "do not cache". +/* The result is non-zero. +/* +/* smtp_mesg_fail() handles the case where the smtp server +/* does not accept the sender address or the message data, +/* or when the local MTA is unable to convert the message data. +/* The policy is: soft error, non-final server: log an informational +/* record why the host is being skipped; soft error, final server: +/* defer delivery of all remaining recipients; hard error: bounce all +/* remaining recipients. +/* The result is non-zero. +/* +/* smtp_misc_fail() provides a more detailed interface than +/* smtp_site_fail() and smtp_mesg_fail(), which are convenience +/* wrappers around smtp_misc_fail(). The throttle argument +/* is either SMTP_THROTTLE or SMTP_NOTHROTTLE; it is used only +/* in the "soft error, final server" policy, and determines +/* whether a destination will be marked as problematic. +/* +/* smtp_rcpt_fail() handles the case where a recipient is not +/* accepted by the server for reasons other than that the server +/* recipient limit is reached. +/* The policy is: soft error, non-final server: log an informational +/* record why the recipient is being skipped; soft error, final server: +/* defer delivery of this recipient; hard error: bounce this +/* recipient. +/* +/* smtp_stream_except() handles the exceptions generated by +/* the smtp_stream(3) module (i.e. timeouts and I/O errors). +/* The \fIexception\fR argument specifies the type of problem. +/* The \fIdescription\fR argument describes at what stage of +/* the SMTP dialog the problem happened. +/* The policy is: non-final server: log an informational record +/* with the reason why the host is being skipped; final server: +/* defer delivery of all remaining recipients. +/* Retry plaintext delivery after TLS post-handshake session +/* failure, provided that at least one recipient was not +/* deferred or rejected during the TLS phase, and that global +/* preconditions for plaintext fallback are met. +/* The session is marked as "do not cache". +/* The result is non-zero. +/* +/* Arguments: +/* .IP state +/* SMTP client state per delivery request. +/* .IP resp +/* Server response including reply code and text. +/* .IP recipient +/* Undeliverable recipient address information. +/* .IP format +/* Human-readable description of why mail is not deliverable. +/* DIAGNOSTICS +/* Panic: unknown exception code. +/* SEE ALSO +/* smtp_proto(3) smtp high-level protocol +/* smtp_stream(3) smtp low-level protocol +/* defer(3) basic message defer interface +/* bounce(3) basic message bounce interface +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <stdlib.h> /* 44BSD stdarg.h uses abort() */ +#include <stdarg.h> +#include <string.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <stringops.h> + +/* Global library. */ + +#include <smtp_stream.h> +#include <deliver_request.h> +#include <deliver_completed.h> +#include <bounce.h> +#include <defer.h> +#include <mail_error.h> +#include <dsn_buf.h> +#include <dsn.h> +#include <mail_params.h> + +/* Application-specific. */ + +#include "smtp.h" +#include "smtp_sasl.h" + +/* smtp_check_code - check response code */ + +static void smtp_check_code(SMTP_SESSION *session, int code) +{ + + /* + * The intention of this code is to alert the postmaster when the local + * Postfix SMTP client screws up, protocol wise. RFC 821 says that x0z + * replies "refer to syntax errors, syntactically correct commands that + * don't fit any functional category, and unimplemented or superfluous + * commands". Unfortunately, this also triggers postmaster notices when + * remote servers screw up, protocol wise. This is becoming a common + * problem now that response codes are configured manually as part of + * anti-UCE systems, by people who aren't aware of RFC details. + * + * Fix 20190621: don't cache an SMTP session after an SMTP protocol error. + * The protocol may be in a bad state. Disable caching here so that the + * protocol engine will send QUIT. + */ + if (code < 400 || code > 599 + || code == 555 /* RFC 1869, section 6.1. */ + || (code >= 500 && code < 510)) { + session->error_mask |= MAIL_ERROR_PROTOCOL; + DONT_CACHE_THIS_SESSION; + } +} + +/* smtp_bulk_fail - skip, defer or bounce recipients, maybe throttle queue */ + +static int smtp_bulk_fail(SMTP_STATE *state, int throttle_queue) +{ + DELIVER_REQUEST *request = state->request; + SMTP_SESSION *session = state->session; + DSN_BUF *why = state->why; + RECIPIENT *rcpt; + int status; + int aggregate_status; + int soft_error = (STR(why->status)[0] == '4'); + int soft_bounce_error = (STR(why->status)[0] == '5' && var_soft_bounce); + int nrcpt; + + /* + * Don't defer the recipients just yet when this error qualifies them for + * delivery to a backup server. Just log something informative to show + * why we're skipping this host. + */ + if ((soft_error || soft_bounce_error) + && (state->misc_flags & SMTP_MISC_FLAG_FINAL_SERVER) == 0) { + msg_info("%s: %s", request->queue_id, STR(why->reason)); + for (nrcpt = 0; nrcpt < SMTP_RCPT_LEFT(state); nrcpt++) { + rcpt = request->rcpt_list.info + nrcpt; + if (SMTP_RCPT_ISMARKED(rcpt)) + continue; + SMTP_RCPT_KEEP(state, rcpt); + } + } + + /* + * Defer or bounce all the remaining recipients, and delete them from the + * delivery request. If a bounce fails, defer instead and do not qualify + * the recipient for delivery to a backup server. + */ + else { + + /* + * If we are still in the connection set-up phase, update the set-up + * completion time here, otherwise the time spent in set-up latency + * will be attributed as message transfer latency. + * + * All remaining recipients have failed at this point, so we update the + * delivery completion time stamp so that multiple recipient status + * records show the same delay values. + */ + if (request->msg_stats.conn_setup_done.tv_sec == 0) { + GETTIMEOFDAY(&request->msg_stats.conn_setup_done); + request->msg_stats.deliver_done = + request->msg_stats.conn_setup_done; + } else + GETTIMEOFDAY(&request->msg_stats.deliver_done); + + (void) DSN_FROM_DSN_BUF(why); + aggregate_status = 0; + for (nrcpt = 0; nrcpt < SMTP_RCPT_LEFT(state); nrcpt++) { + rcpt = request->rcpt_list.info + nrcpt; + if (SMTP_RCPT_ISMARKED(rcpt)) + continue; + status = (soft_error ? defer_append : bounce_append) + (DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id, + &request->msg_stats, rcpt, + session ? session->namaddrport : "none", &why->dsn); + if (status == 0) + deliver_completed(state->src, rcpt->offset); + SMTP_RCPT_DROP(state, rcpt); + aggregate_status |= status; + } + state->status |= aggregate_status; + if ((state->misc_flags & SMTP_MISC_FLAG_COMPLETE_SESSION) == 0 + && throttle_queue && aggregate_status + && request->hop_status == 0) + request->hop_status = DSN_COPY(&why->dsn); + } + + /* + * Don't cache this session. We can't talk to this server. + */ + if (throttle_queue && session) + DONT_CACHE_THROTTLED_SESSION; + + return (-1); +} + +/* smtp_sess_fail - skip site, defer or bounce all recipients */ + +int smtp_sess_fail(SMTP_STATE *state) +{ + + /* + * We can't avoid copying copying lots of strings into VSTRING buffers, + * because this error information is collected by a routine that + * terminates BEFORE the error is reported. + */ + return (smtp_bulk_fail(state, SMTP_THROTTLE)); +} + +/* vsmtp_fill_dsn - fill in temporary DSN structure */ + +static void vsmtp_fill_dsn(SMTP_STATE *state, const char *mta_name, + const char *status, const char *reply, + const char *format, va_list ap) +{ + DSN_BUF *why = state->why; + + /* + * We could avoid copying lots of strings into VSTRING buffers, because + * this error information is given to us by a routine that terminates + * AFTER the error is reported. However, this results in ugly kludges + * when informal text needs to be formatted. So we maintain consistency + * with other error reporting in the SMTP client even if we waste a few + * cycles. + * + * Fix 20190621: don't cache an SMTP session after an SMTP protocol error. + * The protocol may be in a bad state. Disable caching here so that the + * protocol engine will send QUIT. + */ + VSTRING_RESET(why->reason); + if (mta_name && status && status[0] != '4' && status[0] != '5') { + SMTP_SESSION *session = state->session; + + session->error_mask |= MAIL_ERROR_PROTOCOL; + DONT_CACHE_THIS_SESSION; + vstring_strcpy(why->reason, "Protocol error: "); + status = "5.5.0"; + } + vstring_vsprintf_append(why->reason, format, ap); + dsb_formal(why, status, DSB_DEF_ACTION, + mta_name ? DSB_MTYPE_DNS : DSB_MTYPE_NONE, mta_name, + reply ? DSB_DTYPE_SMTP : DSB_DTYPE_NONE, reply); +} + +/* smtp_misc_fail - maybe throttle queue; skip/defer/bounce all recipients */ + +int smtp_misc_fail(SMTP_STATE *state, int throttle, const char *mta_name, + SMTP_RESP *resp, const char *format,...) +{ + va_list ap; + + /* + * Initialize. + */ + va_start(ap, format); + vsmtp_fill_dsn(state, mta_name, resp->dsn, resp->str, format, ap); + va_end(ap); + + if (state->session && mta_name) + smtp_check_code(state->session, resp->code); + + /* + * Skip, defer or bounce recipients, and throttle this queue. + */ + return (smtp_bulk_fail(state, throttle)); +} + +/* smtp_rcpt_fail - skip, defer, or bounce recipient */ + +void smtp_rcpt_fail(SMTP_STATE *state, RECIPIENT *rcpt, const char *mta_name, + SMTP_RESP *resp, const char *format,...) +{ + DELIVER_REQUEST *request = state->request; + SMTP_SESSION *session = state->session; + DSN_BUF *why = state->why; + int status; + int soft_error; + int soft_bounce_error; + va_list ap; + + /* + * Sanity check. + */ + if (SMTP_RCPT_ISMARKED(rcpt)) + msg_panic("smtp_rcpt_fail: recipient <%s> is marked", rcpt->address); + + /* + * Initialize. + */ + va_start(ap, format); + vsmtp_fill_dsn(state, mta_name, resp->dsn, resp->str, format, ap); + va_end(ap); + soft_error = STR(why->status)[0] == '4'; + soft_bounce_error = (STR(why->status)[0] == '5' && var_soft_bounce); + + if (state->session && mta_name) + smtp_check_code(state->session, resp->code); + + /* + * Don't defer this recipient record just yet when this error qualifies + * for trying other mail servers. Just log something informative to show + * why we're skipping this recipient now. + */ + if ((soft_error || soft_bounce_error) + && (state->misc_flags & SMTP_MISC_FLAG_FINAL_SERVER) == 0) { + msg_info("%s: %s", request->queue_id, STR(why->reason)); + SMTP_RCPT_KEEP(state, rcpt); + } + + /* + * Defer or bounce this recipient, and delete from the delivery request. + * If the bounce fails, defer instead and do not qualify the recipient + * for delivery to a backup server. + * + * Note: we may still make an SMTP connection to deliver other recipients + * that did qualify for delivery to a backup server. + */ + else { + (void) DSN_FROM_DSN_BUF(state->why); + status = (soft_error ? defer_append : bounce_append) + (DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id, + &request->msg_stats, rcpt, + session ? session->namaddrport : "none", &why->dsn); + if (status == 0) + deliver_completed(state->src, rcpt->offset); + SMTP_RCPT_DROP(state, rcpt); + state->status |= status; + } +} + +/* smtp_stream_except - defer domain after I/O problem */ + +int smtp_stream_except(SMTP_STATE *state, int code, const char *description) +{ + SMTP_SESSION *session = state->session; + DSN_BUF *why = state->why; + + /* + * Sanity check. + */ + if (session == 0) + msg_panic("smtp_stream_except: no session"); + + /* + * Initialize. + */ + switch (code) { + default: + msg_panic("smtp_stream_except: unknown exception %d", code); + case SMTP_ERR_EOF: + dsb_simple(why, "4.4.2", "lost connection with %s while %s", + session->namaddr, description); +#ifdef USE_TLS + if (PLAINTEXT_FALLBACK_OK_AFTER_TLS_SESSION_FAILURE) + RETRY_AS_PLAINTEXT; +#endif + break; + case SMTP_ERR_TIME: + dsb_simple(why, "4.4.2", "conversation with %s timed out while %s", + session->namaddr, description); +#ifdef USE_TLS + if (PLAINTEXT_FALLBACK_OK_AFTER_TLS_SESSION_FAILURE) + RETRY_AS_PLAINTEXT; +#endif + break; + case SMTP_ERR_DATA: + session->error_mask |= MAIL_ERROR_DATA; + dsb_simple(why, "4.3.0", "local data error while talking to %s", + session->namaddr); + } + + /* + * The smtp_bulk_fail() call below will not throttle the destination when + * falling back to plaintext, because RETRY_AS_PLAINTEXT clears the + * FINAL_SERVER flag. + */ + return (smtp_bulk_fail(state, SMTP_THROTTLE)); +} |