diff options
Diffstat (limited to '')
-rw-r--r-- | src/smtp/smtp_chat.c | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/src/smtp/smtp_chat.c b/src/smtp/smtp_chat.c new file mode 100644 index 0000000..bff5986 --- /dev/null +++ b/src/smtp/smtp_chat.c @@ -0,0 +1,496 @@ +/*++ +/* NAME +/* smtp_chat 3 +/* SUMMARY +/* SMTP client request/response support +/* SYNOPSIS +/* #include "smtp.h" +/* +/* typedef struct { +/* .in +4 +/* int code; /* SMTP code, not sanitized */ +/* char *dsn; /* enhanced status, sanitized */ +/* char *str; /* unmodified SMTP reply */ +/* VSTRING *dsn_buf; +/* VSTRING *str_buf; +/* .in -4 +/* } SMTP_RESP; +/* +/* void smtp_chat_cmd(session, format, ...) +/* SMTP_SESSION *session; +/* const char *format; +/* +/* DICT *smtp_chat_resp_filter; +/* +/* SMTP_RESP *smtp_chat_resp(session) +/* SMTP_SESSION *session; +/* +/* void smtp_chat_notify(session) +/* SMTP_SESSION *session; +/* +/* void smtp_chat_init(session) +/* SMTP_SESSION *session; +/* +/* void smtp_chat_reset(session) +/* SMTP_SESSION *session; +/* DESCRIPTION +/* This module implements SMTP client support for request/reply +/* conversations, and maintains a limited SMTP transaction log. +/* +/* smtp_chat_cmd() formats a command and sends it to an SMTP server. +/* Optionally, the command is logged. +/* +/* smtp_chat_resp() reads one SMTP server response. It extracts +/* the SMTP reply code and enhanced status code from the text, +/* and concatenates multi-line responses to one string, using +/* a newline as separator. Optionally, the server response +/* is logged. +/* .IP \(bu +/* Postfix never sanitizes the extracted SMTP reply code except +/* to ensure that it is a three-digit code. A malformed reply +/* results in a null extracted SMTP reply code value. +/* .IP \(bu +/* Postfix always sanitizes the extracted enhanced status code. +/* When the server's SMTP status code is 2xx, 4xx or 5xx, +/* Postfix requires that the first digit of the server's +/* enhanced status code matches the first digit of the server's +/* SMTP status code. In case of a mis-match, or when the +/* server specified no status code, the extracted enhanced +/* status code is set to 2.0.0, 4.0.0 or 5.0.0 instead. With +/* SMTP reply codes other than 2xx, 4xx or 5xx, the extracted +/* enhanced status code is set to a default value of 5.5.0 +/* (protocol error) for reasons outlined under the next bullet. +/* .IP \(bu +/* Since the SMTP reply code may violate the protocol even +/* when it is correctly formatted, Postfix uses the sanitized +/* extracted enhanced status code to decide whether an error +/* condition is permanent or transient. This means that the +/* caller may have to update the enhanced status code when it +/* discovers that a server reply violates the SMTP protocol, +/* even though it was correctly formatted. This happens when +/* the client and server get out of step due to a broken proxy +/* agent. +/* .PP +/* smtp_chat_resp_filter specifies an optional filter to +/* transform one server reply line before it is parsed. The +/* filter is invoked once for each line of a multi-line reply. +/* +/* smtp_chat_notify() sends a copy of the SMTP transaction log +/* to the postmaster for review. The postmaster notice is sent only +/* when delivery is possible immediately. It is an error to call +/* smtp_chat_notify() when no SMTP transaction log exists. +/* +/* smtp_chat_init() initializes the per-session transaction log. +/* This must be done at the beginning of a new SMTP session. +/* +/* smtp_chat_reset() resets the transaction log. This is +/* typically done at the beginning or end of an SMTP session, +/* or within a session to discard non-error information. +/* DIAGNOSTICS +/* Fatal errors: memory allocation problem, server response exceeds +/* configurable limit. +/* All other exceptions are handled by long jumps (see smtp_stream(3)). +/* SEE ALSO +/* smtp_stream(3) SMTP session I/O support +/* msg(3) generic logging 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 <ctype.h> +#include <stdlib.h> +#include <setjmp.h> +#include <string.h> +#include <limits.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <vstream.h> +#include <argv.h> +#include <stringops.h> +#include <line_wrap.h> +#include <mymalloc.h> + +/* Global library. */ + +#include <recipient_list.h> +#include <deliver_request.h> +#include <smtp_stream.h> +#include <mail_params.h> +#include <mail_addr.h> +#include <post_mail.h> +#include <mail_error.h> +#include <dsn_util.h> + +/* Application-specific. */ + +#include "smtp.h" + + /* + * Server reply transformations. + */ +DICT *smtp_chat_resp_filter; + +/* smtp_chat_init - initialize SMTP transaction log */ + +void smtp_chat_init(SMTP_SESSION *session) +{ + session->history = 0; +} + +/* smtp_chat_reset - reset SMTP transaction log */ + +void smtp_chat_reset(SMTP_SESSION *session) +{ + if (session->history) { + argv_free(session->history); + session->history = 0; + } +} + +/* smtp_chat_append - append record to SMTP transaction log */ + +static void smtp_chat_append(SMTP_SESSION *session, const char *direction, + const char *data) +{ + char *line; + + if (session->history == 0) + session->history = argv_alloc(10); + line = concatenate(direction, data, (char *) 0); + argv_add(session->history, line, (char *) 0); + myfree(line); +} + +/* smtp_chat_cmd - send an SMTP command */ + +void smtp_chat_cmd(SMTP_SESSION *session, const char *fmt,...) +{ + va_list ap; + + /* + * Format the command, and update the transaction log. + */ + va_start(ap, fmt); + vstring_vsprintf(session->buffer, fmt, ap); + va_end(ap); + smtp_chat_append(session, "Out: ", STR(session->buffer)); + + /* + * Optionally log the command first, so we can see in the log what the + * program is trying to do. + */ + if (msg_verbose) + msg_info("> %s: %s", session->namaddrport, STR(session->buffer)); + + /* + * Send the command to the SMTP server. + */ + smtp_fputs(STR(session->buffer), LEN(session->buffer), session->stream); + + /* + * Force flushing of output does not belong here. It is done in the + * smtp_loop() main protocol loop when reading the server response, and + * in smtp_helo() when reading the EHLO response after sending the EHLO + * command. + * + * If we do forced flush here, then we must longjmp() on error, and a + * matching "prepare for disaster" error handler must be set up before + * every smtp_chat_cmd() call. + */ +#if 0 + + /* + * Flush unsent data to avoid timeouts after slow DNS lookups. + */ + if (time((time_t *) 0) - vstream_ftime(session->stream) > 10) + vstream_fflush(session->stream); + + /* + * Abort immediately if the connection is broken. + */ + if (vstream_ftimeout(session->stream)) + vstream_longjmp(session->stream, SMTP_ERR_TIME); + if (vstream_ferror(session->stream)) + vstream_longjmp(session->stream, SMTP_ERR_EOF); +#endif +} + +/* smtp_chat_resp - read and process SMTP server response */ + +SMTP_RESP *smtp_chat_resp(SMTP_SESSION *session) +{ + static SMTP_RESP rdata; + char *cp; + int last_char; + int three_digs = 0; + size_t len; + const char *new_reply; + int chat_append_flag; + int chat_append_skipped = 0; + + /* + * Initialize the response data buffer. + */ + if (rdata.str_buf == 0) { + rdata.dsn_buf = vstring_alloc(10); + rdata.str_buf = vstring_alloc(100); + } + + /* + * Censor out non-printable characters in server responses. Concatenate + * multi-line server responses. Separate the status code from the text. + * Leave further parsing up to the application. + * + * We can't parse or store input that exceeds var_line_limit, so we just + * skip over it to simplify the remainder of the code below. + */ + VSTRING_RESET(rdata.str_buf); + for (;;) { + last_char = smtp_get(session->buffer, session->stream, var_line_limit, + SMTP_GET_FLAG_SKIP); + /* XXX Update the per-line time limit. */ + printable(STR(session->buffer), '?'); + if (last_char != '\n') + msg_warn("%s: response longer than %d: %.30s...", + session->namaddrport, var_line_limit, STR(session->buffer)); + if (msg_verbose) + msg_info("< %s: %.100s", session->namaddrport, STR(session->buffer)); + + /* + * Defend against a denial of service attack by limiting the amount + * of multi-line text that we are willing to store. + */ + chat_append_flag = (LEN(rdata.str_buf) < var_line_limit); + if (chat_append_flag) + smtp_chat_append(session, "In: ", STR(session->buffer)); + else { + if (chat_append_skipped == 0) + msg_warn("%s: multi-line response longer than %d %.30s...", + session->namaddrport, var_line_limit, STR(rdata.str_buf)); + if (chat_append_skipped < INT_MAX) + chat_append_skipped++; + } + + /* + * Server reply substitution, for fault-injection testing, or for + * working around broken systems. Use with care. + */ + if (smtp_chat_resp_filter != 0) { + new_reply = dict_get(smtp_chat_resp_filter, STR(session->buffer)); + if (new_reply != 0) { + msg_info("%s: replacing server reply \"%s\" with \"%s\"", + session->namaddrport, STR(session->buffer), new_reply); + vstring_strcpy(session->buffer, new_reply); + if (chat_append_flag) { + smtp_chat_append(session, "Replaced-by: ", ""); + smtp_chat_append(session, " ", new_reply); + } + } else if (smtp_chat_resp_filter->error != 0) { + msg_warn("%s: table %s:%s lookup error for %s", + session->state->request->queue_id, + smtp_chat_resp_filter->type, + smtp_chat_resp_filter->name, + printable(STR(session->buffer), '?')); + vstream_longjmp(session->stream, SMTP_ERR_DATA); + } + } + if (chat_append_flag) { + if (LEN(rdata.str_buf)) + VSTRING_ADDCH(rdata.str_buf, '\n'); + vstring_strcat(rdata.str_buf, STR(session->buffer)); + } + + /* + * Parse into code and text. Do not ignore garbage (see below). + */ + for (cp = STR(session->buffer); *cp && ISDIGIT(*cp); cp++) + /* void */ ; + if ((three_digs = (cp - STR(session->buffer) == 3)) != 0) { + if (*cp == '-') + continue; + if (*cp == ' ' || *cp == 0) + break; + } + + /* + * XXX Do not simply ignore garbage in the server reply when ESMTP + * command pipelining is turned on. For example, after sending + * ".<CR><LF>QUIT<CR><LF>" and receiving garbage followed by a + * legitimate 2XX reply, Postfix recognizes the server's QUIT reply + * as the END-OF-DATA reply after garbage, causing mail to be lost. + * + * Without the ability to store per-domain status information in queue + * files, automatic workarounds are problematic: + * + * - Automatically deferring delivery creates a "repeated delivery" + * problem when garbage arrives after the DATA stage. Without the + * workaround, Postfix delivers only once. + * + * - Automatically deferring delivery creates a "no delivery" problem + * when the garbage arrives before the DATA stage. Without the + * workaround, mail might still get through. + * + * - Automatically turning off pipelining for delayed mail affects + * deliveries to correctly implemented servers, and may also affect + * delivery of large mailing lists. + * + * So we leave the decision with the administrator, but we don't force + * them to take action, like we would with automatic deferral. If + * loss of mail is not acceptable then they can turn off pipelining + * for specific sites, or they can turn off pipelining globally when + * they find that there are just too many broken sites. + * + * 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. + */ + session->error_mask |= MAIL_ERROR_PROTOCOL; + DONT_CACHE_THIS_SESSION; + if (session->features & SMTP_FEATURE_PIPELINING) { + msg_warn("%s: non-%s response from %s: %.100s", + session->state->request->queue_id, + smtp_mode ? "ESMTP" : "LMTP", + session->namaddrport, STR(session->buffer)); + if (var_helpful_warnings) + msg_warn("to prevent loss of mail, turn off command pipelining " + "for %s with the %s parameter", + STR(session->iterator->addr), + VAR_LMTP_SMTP(EHLO_DIS_MAPS)); + } + } + + /* + * Extract RFC 821 reply code and RFC 2034 detail. Use a default detail + * code if none was given. + * + * Ignore out-of-protocol enhanced status codes: codes that accompany 3XX + * replies, or codes whose initial digit is out of sync with the reply + * code. + * + * XXX Potential stability problem. In order to save memory, the queue + * manager stores DSNs in a compact manner: + * + * - empty strings are represented by null pointers, + * + * - the status and reason are required to be non-empty. + * + * Other Postfix daemons inherit this behavior, because they use the same + * DSN support code. This means that everything that receives DSNs must + * cope with null pointers for the optional DSN attributes, and that + * everything that provides DSN information must provide a non-empty + * status and reason, otherwise the DSN support code wil panic(). + * + * Thus, when the remote server sends a malformed reply (or 3XX out of + * context) we should not panic() in DSN_COPY() just because we don't + * have a status. Robustness suggests that we supply a status here, and + * that we leave it up to the down-stream code to override the + * server-supplied status in case of an error we can't detect here, such + * as an out-of-order server reply. + */ + VSTRING_TERMINATE(rdata.str_buf); + vstring_strcpy(rdata.dsn_buf, "5.5.0"); /* SAFETY! protocol error */ + if (three_digs != 0) { + rdata.code = atoi(STR(session->buffer)); + if (strchr("245", STR(session->buffer)[0]) != 0) { + for (cp = STR(session->buffer) + 4; *cp == ' '; cp++) + /* void */ ; + if ((len = dsn_valid(cp)) > 0 && *cp == *STR(session->buffer)) { + vstring_strncpy(rdata.dsn_buf, cp, len); + } else { + vstring_strcpy(rdata.dsn_buf, "0.0.0"); + STR(rdata.dsn_buf)[0] = STR(session->buffer)[0]; + } + } + } else { + rdata.code = 0; + } + rdata.dsn = STR(rdata.dsn_buf); + rdata.str = STR(rdata.str_buf); + return (&rdata); +} + +/* print_line - line_wrap callback */ + +static void print_line(const char *str, int len, int indent, void *context) +{ + VSTREAM *notice = (VSTREAM *) context; + + post_mail_fprintf(notice, " %*s%.*s", indent, "", len, str); +} + +/* smtp_chat_notify - notify postmaster */ + +void smtp_chat_notify(SMTP_SESSION *session) +{ + const char *myname = "smtp_chat_notify"; + VSTREAM *notice; + char **cpp; + + /* + * Sanity checks. + */ + if (session->history == 0) + msg_panic("%s: no conversation history", myname); + if (msg_verbose) + msg_info("%s: notify postmaster", myname); + + /* + * Construct a message for the postmaster, explaining what this is all + * about. This is junk mail: don't send it when the mail posting service + * is unavailable, and use the double bounce sender address, to prevent + * mail bounce wars. Always prepend one space to message content that we + * generate from untrusted data. + */ +#define NULL_TRACE_FLAGS 0 +#define NO_QUEUE_ID ((VSTRING *) 0) +#define LENGTH 78 +#define INDENT 4 + + notice = post_mail_fopen_nowait(mail_addr_double_bounce(), + var_error_rcpt, + MAIL_SRC_MASK_NOTIFY, NULL_TRACE_FLAGS, + SMTPUTF8_FLAG_NONE, NO_QUEUE_ID); + if (notice == 0) { + msg_warn("postmaster notify: %m"); + return; + } + post_mail_fprintf(notice, "From: %s (Mail Delivery System)", + mail_addr_mail_daemon()); + post_mail_fprintf(notice, "To: %s (Postmaster)", var_error_rcpt); + post_mail_fprintf(notice, "Subject: %s %s client: errors from %s", + var_mail_name, smtp_mode ? "SMTP" : "LMTP", + session->namaddrport); + post_mail_fputs(notice, ""); + post_mail_fprintf(notice, "Unexpected response from %s.", + session->namaddrport); + post_mail_fputs(notice, ""); + post_mail_fputs(notice, "Transcript of session follows."); + post_mail_fputs(notice, ""); + argv_terminate(session->history); + for (cpp = session->history->argv; *cpp; cpp++) + line_wrap(printable(*cpp, '?'), LENGTH, INDENT, print_line, + (void *) notice); + post_mail_fputs(notice, ""); + post_mail_fprintf(notice, "For other details, see the local mail logfile"); + (void) post_mail_fclose(notice); +} |