summaryrefslogtreecommitdiffstats
path: root/src/smtp/smtp_chat.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/smtp/smtp_chat.c503
1 files changed, 503 insertions, 0 deletions
diff --git a/src/smtp/smtp_chat.c b/src/smtp/smtp_chat.c
new file mode 100644
index 0000000..81c63e4
--- /dev/null
+++ b/src/smtp/smtp_chat.c
@@ -0,0 +1,503 @@
+/*++
+/* 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>
+#include <hfrom_format.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;
+ }
+ if (smtp_hfrom_format == HFROM_FORMAT_CODE_STD) {
+ post_mail_fprintf(notice, "From: Mail Delivery System <%s>",
+ mail_addr_mail_daemon());
+ post_mail_fprintf(notice, "To: Postmaster <%s>", var_error_rcpt);
+ } else {
+ 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);
+}