From: Wietse Venema Date: Sun, 5 Aug 2018 05:00:00 +0000 (-0500) Subject: postfix-3.4-20180805-nonprod X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=120208e2d9b967d602d4133e95e6544e1cfde1d6;p=thirdparty%2Fpostfix.git postfix-3.4-20180805-nonprod --- diff --git a/postfix/HISTORY b/postfix/HISTORY index 25a231d51..07f0442af 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -23630,3 +23630,19 @@ Apologies for any names omitted. VSTRING during vstream_fflush(); added a simple 'allow' filter for vstream_control() requests; added a unit test. File: util/vstream.c. + +20180805 + + Cleanup: vbuf_get() now sets the EOF flag, so that reading + from a VSTRING stream works as expected. File: util/vbuf.c. + + SMTP server support for CHUNKING (BDAT) per RFC 3030. The + SMTP server is the only program that knows the difference + between mail received with BDAT or DATA. Both use the same + smtpd_data_restrictions and smtpd_end_of_data_restrictions, + both send one Milter DATA event per mail transaction, and + both send one DATA command ending in . to + an smtpd_proxy_filter. Files: global/ehlo_mask.h, + global/smtp_stream.c, global/smtp_stream.c, global/smtp_stream.h, + postscreen/postscreen_smtpd.c, smtpd/smtpd.c, smtpd/smtpd.h, + smtpd/smtpd_chat.c, smtpd/smtpd_chat.h, smtpd/smtpd_state.c. diff --git a/postfix/RELEASE_NOTES b/postfix/RELEASE_NOTES index 3c429a651..91f8f30dd 100644 --- a/postfix/RELEASE_NOTES +++ b/postfix/RELEASE_NOTES @@ -25,6 +25,19 @@ more recent Eclipse Public License 2.0. Recipients can choose to take the software under the license of their choice. Those who are more comfortable with the IPL can continue with that license. +Major changes with snapshot 20180805-nonprod +============================================= + +Preliminary support for RFC 3030 CHUNKING (BDAT) without BINARYMIME. +The Postfix SMTP server is the only program that knows the difference +between mail that was received with BDAT or DATA. + +In both cases, the Postfix SMTP server will use smtpd_data_restrictions +and smtpd_end_of_data_restrictions, it will send one Milter DATA event +per mail transaction, and it will send one DATA command ending in +. to an smtpd_proxy_filter. There is no difference +in the Postfix queue file content. + Incompatble change with snapshot 20180701 ========================================= diff --git a/postfix/html/smtpd.8.html b/postfix/html/smtpd.8.html index 141e8336f..e2800798f 100644 --- a/postfix/html/smtpd.8.html +++ b/postfix/html/smtpd.8.html @@ -50,6 +50,7 @@ SMTPD(8) SMTPD(8) RFC 2554 (AUTH command) RFC 2821 (SMTP protocol) RFC 2920 (SMTP pipelining) + RFC 3030 (CHUNKING without BINARYMIME) RFC 3207 (STARTTLS command) RFC 3461 (SMTP DSN extension) RFC 3463 (Enhanced status codes) diff --git a/postfix/makedefs b/postfix/makedefs index 945a6360b..704bb60e6 100644 --- a/postfix/makedefs +++ b/postfix/makedefs @@ -878,7 +878,7 @@ CCARGS="$CCARGS -DSNAPSHOT" # Non-production: needs thorough testing, or major changes are still # needed before the code stabilizes. -#CCARGS="$CCARGS -DNONPROD" +CCARGS="$CCARGS -DNONPROD" # Workaround: prepend Postfix include files before other include files. CCARGS="-I. -I../../include $CCARGS" diff --git a/postfix/man/man8/smtpd.8 b/postfix/man/man8/smtpd.8 index 10db32d00..60caccb95 100644 --- a/postfix/man/man8/smtpd.8 +++ b/postfix/man/man8/smtpd.8 @@ -56,6 +56,7 @@ RFC 2034 (SMTP enhanced status codes) RFC 2554 (AUTH command) RFC 2821 (SMTP protocol) RFC 2920 (SMTP pipelining) +RFC 3030 (CHUNKING without BINARYMIME) RFC 3207 (STARTTLS command) RFC 3461 (SMTP DSN extension) RFC 3463 (Enhanced status codes) diff --git a/postfix/src/global/ehlo_mask.c b/postfix/src/global/ehlo_mask.c index 1671beb3e..7ebcb9c2c 100644 --- a/postfix/src/global/ehlo_mask.c +++ b/postfix/src/global/ehlo_mask.c @@ -19,6 +19,7 @@ /* #define EHLO_MASK_ENHANCEDSTATUSCODES (1<<10) /* #define EHLO_MASK_DSN (1<<11) /* #define EHLO_MASK_SMTPUTF8 (1<<12) +/* #define EHLO_MASK_CHUNKING (1<<13) /* #define EHLO_MASK_SILENT (1<<15) /* /* int ehlo_mask(keyword_list) @@ -77,6 +78,7 @@ static const NAME_MASK ehlo_mask_table[] = { "ENHANCEDSTATUSCODES", EHLO_MASK_ENHANCEDSTATUSCODES, "DSN", EHLO_MASK_DSN, "EHLO_MASK_SMTPUTF8", EHLO_MASK_SMTPUTF8, + "CHUNKING", EHLO_MASK_CHUNKING, "SILENT-DISCARD", EHLO_MASK_SILENT, /* XXX In-band signaling */ 0, }; diff --git a/postfix/src/global/ehlo_mask.h b/postfix/src/global/ehlo_mask.h index 3ef2a2d87..ed0f7dc02 100644 --- a/postfix/src/global/ehlo_mask.h +++ b/postfix/src/global/ehlo_mask.h @@ -28,6 +28,7 @@ #define EHLO_MASK_ENHANCEDSTATUSCODES (1<<10) #define EHLO_MASK_DSN (1<<11) #define EHLO_MASK_SMTPUTF8 (1<<12) +#define EHLO_MASK_CHUNKING (1<<13) #define EHLO_MASK_SILENT (1<<15) extern int ehlo_mask(const char *); diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 89664e231..a65430223 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20180708" +#define MAIL_RELEASE_DATE "20180805" #define MAIL_VERSION_NUMBER "3.4" #ifdef SNAPSHOT diff --git a/postfix/src/global/smtp_stream.c b/postfix/src/global/smtp_stream.c index b9f1c506f..217be6169 100644 --- a/postfix/src/global/smtp_stream.c +++ b/postfix/src/global/smtp_stream.c @@ -37,6 +37,11 @@ /* ssize_t len; /* VSTREAM *stream; /* +/* void smtp_fread(vp, len, stream) +/* VSTRING *vp; +/* ssize_t len; +/* VSTREAM *stream; +/* /* void smtp_fputc(ch, stream) /* int ch; /* VSTREAM *stream; @@ -45,6 +50,12 @@ /* VSTREAM *stream; /* char *format; /* va_list ap; +/* AUXILIARY API +/* int smtp_get_noexcept(vp, stream, maxlen, flags) +/* VSTRING *vp; +/* VSTREAM *stream; +/* ssize_t maxlen; +/* int flags; /* LEGACY API /* void smtp_timeout_setup(stream, timeout) /* VSTREAM *stream; @@ -95,11 +106,18 @@ /* Long strings are not broken. No CR LF is appended. The stream /* is not flushed. /* +/* smtp_fread() appends the specified number of bytes from the +/* stream to the buffer. The result is not null-terminated. +/* /* smtp_fputc() writes one character to the named stream. /* The stream is not flushed. /* /* smtp_vprintf() is the machine underneath smtp_printf(). /* +/* smtp_get_noexcept() implements the subset of smtp_get() +/* without timeouts and without making long jumps. Instead, +/* query the stream status with vstream_feof() etc. +/* /* smtp_timeout_setup() is a backwards-compatibility interface /* for programs that don't require per-record deadline support. /* DIAGNOSTICS @@ -303,6 +321,29 @@ int smtp_fgetc(VSTREAM *stream) /* smtp_get - read one line from SMTP peer */ int smtp_get(VSTRING *vp, VSTREAM *stream, ssize_t bound, int flags) +{ + int last_char; + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + last_char = smtp_get_noexcept(vp, stream, bound, flags); + + /* + * EOF is bad, whether or not it happens in the middle of a record. Don't + * allow data that was truncated because of EOF. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_get"); + if (vstream_feof(stream) || vstream_ferror(stream)) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_get"); + return (last_char); +} + +/* smtp_get_noexcept - read one line from SMTP peer, without exceptions */ + +int smtp_get_noexcept(VSTRING *vp, VSTREAM *stream, ssize_t bound, int flags) { int last_char; int next_char; @@ -316,7 +357,6 @@ int smtp_get(VSTRING *vp, VSTREAM *stream, ssize_t bound, int flags) * XXX 2821: Section 4.1.1.4 says that an SMTP server must not recognize * bare LF as record terminator. */ - smtp_timeout_reset(stream); last_char = (bound == 0 ? vstring_get(vp, stream) : vstring_get_bound(vp, stream, bound)); @@ -367,14 +407,6 @@ int smtp_get(VSTRING *vp, VSTREAM *stream, ssize_t bound, int flags) && next_char != '\n') /* void */ ; - /* - * EOF is bad, whether or not it happens in the middle of a record. Don't - * allow data that was truncated because of EOF. - */ - if (vstream_ftimeout(stream)) - smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_get"); - if (vstream_feof(stream) || vstream_ferror(stream)) - smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_get"); return (last_char); } @@ -427,6 +459,33 @@ void smtp_fwrite(const char *cp, ssize_t todo, VSTREAM *stream) smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fwrite"); } +/* smtp_fread - read one buffer from SMTP peer */ + +void smtp_fread(VSTRING *vp, ssize_t todo, VSTREAM *stream) +{ + int err; + + if (todo <= 0) + msg_panic("smtp_fread: zero or negative todo %ld", (long) todo); + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + VSTRING_SPACE(vp, todo); + err = (vstream_fread(stream, vstring_end(vp), todo) != todo); + if (err == 0) + VSTRING_AT_OFFSET(vp, VSTRING_LEN(vp) + todo); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_fread"); + if (err != 0) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fread"); +} + /* smtp_fputc - write to SMTP peer */ void smtp_fputc(int ch, VSTREAM *stream) diff --git a/postfix/src/global/smtp_stream.h b/postfix/src/global/smtp_stream.h index bdb143675..066eb5452 100644 --- a/postfix/src/global/smtp_stream.h +++ b/postfix/src/global/smtp_stream.h @@ -38,8 +38,10 @@ extern void PRINTFLIKE(2, 3) smtp_printf(VSTREAM *, const char *,...); extern void smtp_flush(VSTREAM *); extern int smtp_fgetc(VSTREAM *); extern int smtp_get(VSTRING *, VSTREAM *, ssize_t, int); +extern int smtp_get_noexcept(VSTRING *, VSTREAM *, ssize_t, int); extern void smtp_fputs(const char *, ssize_t len, VSTREAM *); extern void smtp_fwrite(const char *, ssize_t len, VSTREAM *); +extern void smtp_fread(VSTRING *, ssize_t len, VSTREAM *); extern void smtp_fputc(int, VSTREAM *); extern void smtp_vprintf(VSTREAM *, const char *, va_list); diff --git a/postfix/src/postscreen/postscreen_smtpd.c b/postfix/src/postscreen/postscreen_smtpd.c index 814eedc1c..152748f38 100644 --- a/postfix/src/postscreen/postscreen_smtpd.c +++ b/postfix/src/postscreen/postscreen_smtpd.c @@ -345,6 +345,8 @@ static void psc_smtpd_format_ehlo_reply(VSTRING *buf, int discard_mask /* Fix 20140708: announce SMTPUTF8. */ if (var_smtputf8_enable && (discard_mask & EHLO_MASK_SMTPUTF8) == 0) PSC_EHLO_APPEND(saved_len, psc_temp, "250-SMTPUTF8\r\n"); + if ((discard_mask & EHLO_MASK_CHUNKING) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-CHUNKING\r\n"); STR(psc_temp)[saved_len + 3] = ' '; } diff --git a/postfix/src/smtpd/smtpd.c b/postfix/src/smtpd/smtpd.c index 5006e7449..5d7fac0ff 100644 --- a/postfix/src/smtpd/smtpd.c +++ b/postfix/src/smtpd/smtpd.c @@ -46,6 +46,7 @@ /* RFC 2554 (AUTH command) /* RFC 2821 (SMTP protocol) /* RFC 2920 (SMTP pipelining) +/* RFC 3030 (CHUNKING without BINARYMIME) /* RFC 3207 (STARTTLS command) /* RFC 3461 (SMTP DSN extension) /* RFC 3463 (Enhanced status codes) @@ -1906,6 +1907,8 @@ static int ehlo_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv) EHLO_APPEND(state, "DSN"); if (var_smtputf8_enable && (discard_mask & EHLO_MASK_SMTPUTF8) == 0) EHLO_APPEND(state, "SMTPUTF8"); + if ((discard_mask & EHLO_MASK_CHUNKING) == 0) + EHLO_APPEND(state, "CHUNKING"); /* * Send the reply. @@ -2734,6 +2737,18 @@ static void mail_reset(SMTPD_STATE *state) state->milter_argv = 0; state->milter_argc = 0; } + + /* + * BDAT. + */ + state->bdat_state = SMTPD_BDAT_NONE; + if (state->bdat_get_stream) { + (void) vstream_fclose(state->bdat_get_stream); + state->bdat_get_stream = 0; + } + if (state->bdat_get_buffer) + VSTRING_RESET(state->bdat_get_buffer); + state->bdat_last_chunk_size = 0; } /* rcpt_cmd - process RCPT TO command */ @@ -2766,6 +2781,11 @@ static int rcpt_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv) smtpd_chat_reply(state, "503 5.5.1 Error: need MAIL command"); return (-1); } + if (SMTPD_IN_BDAT_TRANSACTION(state)) { + state->error_mask |= MAIL_ERROR_PROTOCOL; + smtpd_chat_reply(state, "503 5.5.1 Error: RCPT in BDAT transaction"); + return (-1); + } if (argc < 3 || strcasecmp(argv[1].strval, "to:") != 0) { state->error_mask |= MAIL_ERROR_PROTOCOL; @@ -3115,45 +3135,37 @@ static void comment_sanitize(VSTRING *comment_string) VSTRING_TERMINATE(comment_string); } +static void common_pre_message(SMTPD_STATE *state, + int (*out_record) (VSTREAM *, int, const char *, ssize_t), + int (*out_fprintf) (VSTREAM *, int, const char *,...), + VSTREAM *out_stream, int out_error); +static void receive_data_message(SMTPD_STATE *state, + int (*out_record) (VSTREAM *, int, const char *, ssize_t), + int (*out_fprintf) (VSTREAM *, int, const char *,...), + VSTREAM *out_stream, int out_error); +static int common_post_message_handling(SMTPD_STATE *state); + /* data_cmd - process DATA command */ static int data_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) { SMTPD_PROXY *proxy; const char *err; - char *start; - int len; - int curr_rec_type; - int prev_rec_type; - int first = 1; - VSTRING *why = 0; - int saved_err; int (*out_record) (VSTREAM *, int, const char *, ssize_t); int (*out_fprintf) (VSTREAM *, int, const char *,...); VSTREAM *out_stream; int out_error; - char **cpp; - const CLEANUP_STAT_DETAIL *detail; - const char *rfc3848_sess; - const char *rfc3848_auth; - const char *with_protocol = (state->flags & SMTPD_FLAG_SMTPUTF8) ? - "UTF8SMTP" : state->protocol; - -#ifdef USE_TLS - VSTRING *peer_CN; - VSTRING *issuer_CN; - -#endif -#ifdef USE_SASL_AUTH - VSTRING *username; - -#endif /* * Sanity checks. With ESMTP command pipelining the client can send DATA * before all recipients are rejected, so don't report that as a protocol * error. */ + if (SMTPD_IN_BDAT_TRANSACTION(state)) { + state->error_mask |= MAIL_ERROR_PROTOCOL; + smtpd_chat_reply(state, "503 5.5.1 Error: DATA in BDAT transaction"); + return (-1); + } if (state->rcpt_count == 0) { if (!SMTPD_IN_MAIL_TRANSACTION(state)) { state->error_mask |= MAIL_ERROR_PROTOCOL; @@ -3202,6 +3214,37 @@ static int data_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) out_fprintf = rec_fprintf; out_error = CLEANUP_STAT_WRITE; } + common_pre_message(state, out_record, out_fprintf, out_stream, out_error); + smtpd_chat_reply(state, "354 End data with ."); + state->where = SMTPD_AFTER_DATA; + receive_data_message(state, out_record, out_fprintf, out_stream, out_error); + return common_post_message_handling(state); +} + +/* common_pre_message - finish envelope and open message segment */ + +static void common_pre_message(SMTPD_STATE *state, + int (*out_record) (VSTREAM *, int, const char *, ssize_t), + int (*out_fprintf) (VSTREAM *, int, const char *,...), + VSTREAM *out_stream, + int out_error) +{ + SMTPD_PROXY *proxy = state->proxy; + char **cpp; + const char *rfc3848_sess; + const char *rfc3848_auth; + const char *with_protocol = (state->flags & SMTPD_FLAG_SMTPUTF8) ? + "UTF8SMTP" : state->protocol; + +#ifdef USE_TLS + VSTRING *peer_CN; + VSTRING *issuer_CN; + +#endif +#ifdef USE_SASL_AUTH + VSTRING *username; + +#endif /* * Flush out a first batch of access table actions that are delegated to @@ -3321,8 +3364,22 @@ static int data_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) "\t(envelope-from %s)", STR(state->buffer)); #endif } - smtpd_chat_reply(state, "354 End data with ."); - state->where = SMTPD_AFTER_DATA; +} + +/* receive_data_message - finish envelope and open message segment */ + +static void receive_data_message(SMTPD_STATE *state, + int (*out_record) (VSTREAM *, int, const char *, ssize_t), + int (*out_fprintf) (VSTREAM *, int, const char *,...), + VSTREAM *out_stream, + int out_error) +{ + SMTPD_PROXY *proxy = state->proxy; + char *start; + int len; + int curr_rec_type; + int prev_rec_type; + int first = 1; /* * Copy the message content. If the cleanup process has a problem, keep @@ -3370,6 +3427,18 @@ static int data_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) } } state->where = SMTPD_AFTER_DOT; +} + +/* common_post_message_handling - commit message or report error */ + +static int common_post_message_handling(SMTPD_STATE *state) +{ + SMTPD_PROXY *proxy = state->proxy; + const char *err; + VSTRING *why = 0; + int saved_err; + const CLEANUP_STAT_DETAIL *detail; + if (state->err == CLEANUP_STAT_OK && SMTPD_STAND_ALONE(state) == 0 && (err = smtpd_check_eod(state)) != 0) { @@ -3564,6 +3633,263 @@ static int data_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) return (saved_err); } +/* skip_bdat - skip content and respond to BDAT error */ + +static int skip_bdat(SMTPD_STATE *state, off_t skip_size, + const char *format,...) +{ + va_list ap; + off_t done; + off_t len; + + /* + * Read and discard content from the remote SMTP client. TODO: drop the + * connection in case of overload. + */ + for (done = 0; done < skip_size; done += len) { + VSTRING_RESET(state->buffer); + if ((len = skip_size - done) > VSTREAM_BUFSIZE) + len = VSTREAM_BUFSIZE; + smtp_fread(state->buffer, len, state->client); + } + + /* + * Send the response to the remote SMTP client. + */ + va_start(ap, format); + vsmtpd_chat_reply(state, format, ap); + va_end(ap); + + /* + * Drop subsequent BDAT payloads until BDAT LAST or RSET. + */ + state->bdat_state = SMTPD_BDAT_ERROR; + return (-1); +} + +/* bdat_cmd - process BDAT command */ + +static int bdat_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv) +{ + SMTPD_PROXY *proxy; + const char *err; + off_t chunk_size; + off_t done; + off_t read_len; + char *start; + int len; + int curr_rec_type; + int (*out_record) (VSTREAM *, int, const char *, ssize_t); + int (*out_fprintf) (VSTREAM *, int, const char *,...); + VSTREAM *out_stream; + int out_error; + + /* + * Hang up if the BDAT command is malformed. The next input would be raw + * message content and that would trigger lots of command errors. + */ + if (argc < 2 || argc > 3 || !alldig(argv[1].strval) + || (chunk_size = off_cvt_string(argv[1].strval)) < 0 + || (argc == 3 && strcasecmp(argv[2].strval, "LAST") != 0)) { + state->error_mask |= MAIL_ERROR_PROTOCOL; + msg_warn("%s: malformed BDAT command syntax from %s: %.100s", + state->queue_id ? state->queue_id : "NOQUEUE", + state->namaddr, printable(vstring_str(state->buffer), '?')); + smtpd_chat_reply(state, "421 5.5.4 Syntax: BDAT count [LAST]"); + return (-1); + } + + /* + * BDAT commands may be pipelined within a MAIL transaction. After a + * previous BDAT command failed, accept BDAT requests and skip BDAT + * payloads to maintain synchronization with the remote SMTP client. + */ + if (state->bdat_state == SMTPD_BDAT_ERROR) + return skip_bdat(state, chunk_size, + "551 5.0.0 Discarded %ld bytes after earlier error", + (long) chunk_size); + + /* + * Special handling for the first BDAT command in a MAIL transaction, + * treating it as a kind of "DATA" command in policy evaluation. + */ + if (!SMTPD_IN_BDAT_TRANSACTION(state)) { + + /* + * With ESMTP command pipelining a client may send BDAT before all + * recipients are rejected. That is not necessarily a good idea. but + * we should not treat such a BDAT command as a protocol error. + */ + if (state->rcpt_count == 0) { + if (!SMTPD_IN_MAIL_TRANSACTION(state)) { + state->error_mask |= MAIL_ERROR_PROTOCOL; + return skip_bdat(state, chunk_size, + "503 5.5.1 Error: need RCPT command"); + } else { + return skip_bdat(state, chunk_size, + "554 5.5.1 Error: no valid recipients"); + } + } + if (SMTPD_STAND_ALONE(state) == 0 + && (err = smtpd_check_data(state)) != 0) { + return skip_bdat(state, chunk_size, "%s", err); + } + if (state->milters != 0 + && (state->saved_flags & MILTER_SKIP_FLAGS) == 0 + && (err = milter_data_event(state->milters)) != 0 + && (err = check_milter_reply(state, err)) != 0) { + return skip_bdat(state, chunk_size, "%s", err); + } + proxy = state->proxy; + if (proxy != 0 && proxy->cmd(state, SMTPD_PROX_WANT_MORE, + SMTPD_CMD_DATA) != 0) { + return skip_bdat(state, chunk_size, "%s", STR(proxy->reply)); + } + } + /* Block too large chunks. */ + if (state->act_size > var_message_limit - chunk_size) { + state->error_mask |= MAIL_ERROR_POLICY; + msg_warn("%s: BDAT request from %s exceeds message size limit", + state->queue_id ? state->queue_id : "NOQUEUE", + state->namaddr); + return skip_bdat(state, chunk_size, + "552 5.3.4 Chunk exceeds message size limit"); + } + /* Block trickle attacks with too small successive chunks. */ + if (SMTPD_IN_BDAT_TRANSACTION(state) && argc != 3 + && state->bdat_last_chunk_size + chunk_size < 20) { + msg_warn("%s: multiple small BDAT requests from %s", + state->queue_id ? state->queue_id : "NOQUEUE", + state->namaddr); + return skip_bdat(state, chunk_size, + "551 5.7.1 Too many small BDAT requests"); + } + state->bdat_last_chunk_size = chunk_size; + + /* + * One level of indirection to choose between normal or proxied + * operation. We want to avoid massive code duplication within tons of + * if-else clauses. TODO: store this in its own data structure, or in + * SMTPD_STATE. + */ + proxy = state->proxy; + if (proxy) { + out_stream = proxy->stream; + out_record = proxy->rec_put; + out_fprintf = proxy->rec_fprintf; + out_error = CLEANUP_STAT_PROXY; + } else { + out_stream = state->cleanup; + out_record = rec_put; + out_fprintf = rec_fprintf; + out_error = CLEANUP_STAT_WRITE; + } + if (!SMTPD_IN_BDAT_TRANSACTION(state)) { + common_pre_message(state, out_record, out_fprintf, + out_stream, out_error); + if (state->bdat_get_buffer == 0) + state->bdat_get_buffer = vstring_alloc(VSTREAM_BUFSIZE); + else + VSTRING_RESET(state->bdat_get_buffer); + state->bdat_prev_rec_type = 0; + } + if (state->bdat_get_stream != 0) + msg_panic("bdat_cmd: bdat_get_stream not null"); + + state->bdat_state = (argc == 3 ? SMTPD_BDAT_LAST : SMTPD_BDAT_OK); + + /* + * Copy the message content. If the cleanup process has a problem, keep + * reading until the remote stops sending, then complain. Produce typed + * records from the SMTP stream so we can handle data that spans buffers. + */ +#define SWAP(type, a, b) do { \ + type _c_; _c_ = (a); (a) = (b); (b) = _c_; \ + } while (0) + + /* Read the chunk one fragment at a time. */ + for (done = 0; done < chunk_size; done += read_len) { + if ((read_len = chunk_size - done) > VSTREAM_BUFSIZE) + read_len = VSTREAM_BUFSIZE; + /* Kludge: append the chunk fragment to the last partial record. */ + if (VSTRING_LEN(state->bdat_get_buffer) > 0) + SWAP(VSTRING *, state->bdat_get_buffer, state->buffer); + else + VSTRING_RESET(state->buffer); + /* Caution: this makes a long jump in case of EOF or timeout. */ + smtp_fread(state->buffer, read_len, state->client); + state->bdat_get_stream = vstream_memopen(state->buffer, O_RDONLY); + for ( /* */ ; /* */ ; state->bdat_prev_rec_type = curr_rec_type) { + if (smtp_get_noexcept(state->bdat_get_buffer, + state->bdat_get_stream, + var_line_limit, + SMTP_GET_FLAG_NONE) == '\n') { + /* Stopped at end-of-line. */ + curr_rec_type = REC_TYPE_NORM; + } else if (!vstream_feof(state->bdat_get_stream)) { + /* Stopped at var_line_limit. */ + curr_rec_type = REC_TYPE_CONT; + } else if (state->bdat_state == SMTPD_BDAT_LAST + && read_len == chunk_size - done) { + /* Final chunk does not end in end-of-line. Fake it. */ + curr_rec_type = REC_TYPE_NORM; + } else { + /* Stopped at end of buffer, maybe in a partial record. */ + break; + } + start = vstring_str(state->bdat_get_buffer); + len = VSTRING_LEN(state->bdat_get_buffer); + if (state->err == CLEANUP_STAT_OK) { + if (var_message_limit > 0 + && var_message_limit - state->act_size < len + 2) { + state->err = CLEANUP_STAT_SIZE; + msg_warn("%s: queue file size limit exceeded", + state->queue_id ? state->queue_id : "NOQUEUE"); + } else { + state->act_size += len + 2; + if (*start == '.' && proxy != 0 + && state->bdat_prev_rec_type != REC_TYPE_CONT) + /* out_fprintf would break text containing nulls. */ + vstring_prepend(state->bdat_get_buffer, ".", 1); + if (out_record(out_stream, curr_rec_type, + vstring_str(state->bdat_get_buffer), + VSTRING_LEN(state->bdat_get_buffer)) < 0) + state->err = out_error; + } + } + } + vstream_fclose(state->bdat_get_stream); + state->bdat_get_stream = 0; + } + + /* + * Special handling for BDAT LAST (successful or unsuccessful). + */ + if (state->bdat_state == SMTPD_BDAT_LAST) { + return common_post_message_handling(state); + } + + /* + * Unsuccessful non-final BDAT command. common_post_message_handling() + * resets all MAIL transaction state including BDAT state. To avoid + * useless error messages due to pipelined BDAT commands, set the + * SMTPD_BDAT_ERROR state to accept BDAT commands and skip BDAT payloads. + */ + else if (state->err != CLEANUP_STAT_OK) { + (void) common_post_message_handling(state); + state->bdat_state = SMTPD_BDAT_ERROR; + return (-1); + } + + /* + * Successful non-final BDAT command. + */ + else { + smtpd_chat_reply(state, "250 2.0.0 Ok: %ld bytes", (long) chunk_size); + return (0); + } +} + /* rset_cmd - process RSET */ static int rset_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv) @@ -4836,6 +5162,13 @@ typedef struct SMTPD_CMD { int total_count; } SMTPD_CMD; + /* + * Per RFC 2920: "In particular, the commands RSET, MAIL FROM, SEND FROM, + * SOML FROM, SAML FROM, and RCPT TO can all appear anywhere in a pipelined + * command group. The EHLO, DATA, VRFY, EXPN, TURN, QUIT, and NOOP commands + * can only appear as the last command in a group". RFC 3030 allows BDAT + * commands to be pipelined as well. + */ #define SMTPD_CMD_FLAG_LIMIT (1<<0) /* limit usage */ #define SMTPD_CMD_FLAG_PRE_TLS (1<<1) /* allow before STARTTLS */ #define SMTPD_CMD_FLAG_LAST (1<<2) /* last in PIPELINING command group */ @@ -4858,6 +5191,7 @@ static SMTPD_CMD smtpd_cmd_table[] = { {SMTPD_CMD_MAIL, mail_cmd,}, {SMTPD_CMD_RCPT, rcpt_cmd,}, {SMTPD_CMD_DATA, data_cmd, SMTPD_CMD_FLAG_LAST,}, + {SMTPD_CMD_BDAT, bdat_cmd,}, {SMTPD_CMD_RSET, rset_cmd, SMTPD_CMD_FLAG_LIMIT,}, {SMTPD_CMD_NOOP, noop_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,}, {SMTPD_CMD_VRFY, vrfy_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_LAST,}, diff --git a/postfix/src/smtpd/smtpd.h b/postfix/src/smtpd/smtpd.h index 4facfb080..e12a5123e 100644 --- a/postfix/src/smtpd/smtpd.h +++ b/postfix/src/smtpd/smtpd.h @@ -189,6 +189,19 @@ typedef struct { */ VSTRING *ehlo_buf; ARGV *ehlo_argv; + + /* + * BDAT transaction state. + */ +#define SMTPD_BDAT_NONE 0 /* not in BDAT transaction */ +#define SMTPD_BDAT_OK 1 /* in BDAT, accepting chunks */ +#define SMTPD_BDAT_LAST 2 /* in BDAT, last chunk */ +#define SMTPD_BDAT_ERROR 3 /* in BDAT, dropping chunks */ + int bdat_state; /* see above */ + off_t bdat_last_chunk_size; /* trickle defense */ + VSTREAM *bdat_get_stream; /* memory stream from BDAT chunk */ + VSTRING *bdat_get_buffer; /* read from memory stream */ + int bdat_prev_rec_type; } SMTPD_STATE; #define SMTPD_FLAG_HANGUP (1<<0) /* 421/521 disconnect */ @@ -198,7 +211,7 @@ typedef struct { /* Security: don't reset SMTPD_FLAG_AUTH_USED. */ #define SMTPD_MASK_MAIL_KEEP \ - ~(SMTPD_FLAG_SMTPUTF8) /* Fix 20140706 */ + ~(SMTPD_FLAG_SMTPUTF8) /* Fix 20140706 */ #define SMTPD_STATE_XFORWARD_INIT (1<<0) /* xforward preset done */ #define SMTPD_STATE_XFORWARD_NAME (1<<1) /* client name received */ @@ -236,6 +249,7 @@ extern void smtpd_state_reset(SMTPD_STATE *); #define SMTPD_CMD_MAIL "MAIL" #define SMTPD_CMD_RCPT "RCPT" #define SMTPD_CMD_DATA "DATA" +#define SMTPD_CMD_BDAT "BDAT" #define SMTPD_CMD_EOD SMTPD_AFTER_DOT /* XXX Was: END-OF-DATA */ #define SMTPD_CMD_RSET "RSET" #define SMTPD_CMD_NOOP "NOOP" @@ -321,6 +335,12 @@ extern void smtpd_state_reset(SMTPD_STATE *); */ #define SMTPD_IN_MAIL_TRANSACTION(state) ((state)->sender != 0) + /* + * Are we in a BDAT transaction? + */ +#define SMTPD_IN_BDAT_TRANSACTION(state) \ + ((state)->bdat_state != SMTPD_BDAT_NONE) + /* * SMTPD peer information lookup. */ diff --git a/postfix/src/smtpd/smtpd_chat.c b/postfix/src/smtpd/smtpd_chat.c index 8ba46b01c..14bd94f57 100644 --- a/postfix/src/smtpd/smtpd_chat.c +++ b/postfix/src/smtpd/smtpd_chat.c @@ -148,6 +148,16 @@ void smtpd_chat_query(SMTPD_STATE *state) void smtpd_chat_reply(SMTPD_STATE *state, const char *format,...) { va_list ap; + + va_start(ap, format); + vsmtpd_chat_reply(state, format, ap); + va_end(ap); +} + +/* vsmtpd_chat_reply - format, send and record an SMTP response */ + +void vsmtpd_chat_reply(SMTPD_STATE *state, const char *format, va_list ap) +{ int delay = 0; char *cp; char *next; @@ -160,9 +170,7 @@ void smtpd_chat_reply(SMTPD_STATE *state, const char *format,...) if (state->error_count >= var_smtpd_soft_erlim) sleep(delay = var_smtpd_err_sleep); - va_start(ap, format); vstring_vsprintf(state->buffer, format, ap); - va_end(ap); if (*var_smtpd_rej_footer && (*(cp = STR(state->buffer)) == '4' || *cp == '5')) diff --git a/postfix/src/smtpd/smtpd_chat.h b/postfix/src/smtpd/smtpd_chat.h index 6ac49ac0a..5e9982ad4 100644 --- a/postfix/src/smtpd/smtpd_chat.h +++ b/postfix/src/smtpd/smtpd_chat.h @@ -15,6 +15,7 @@ extern void smtpd_chat_reset(SMTPD_STATE *); extern void smtpd_chat_query(SMTPD_STATE *); extern void PRINTFLIKE(2, 3) smtpd_chat_reply(SMTPD_STATE *, const char *,...); +extern void vsmtpd_chat_reply(SMTPD_STATE *, const char *, va_list); extern void smtpd_chat_notify(SMTPD_STATE *); /* LICENSE diff --git a/postfix/src/smtpd/smtpd_state.c b/postfix/src/smtpd/smtpd_state.c index 87e62fa44..7e04cf23d 100644 --- a/postfix/src/smtpd/smtpd_state.c +++ b/postfix/src/smtpd/smtpd_state.c @@ -150,7 +150,6 @@ void smtpd_state_init(SMTPD_STATE *state, VSTREAM *stream, state->tls_context = 0; #endif - /* * Minimal initialization to support external authentication (e.g., * XCLIENT) without having to enable SASL in main.cf. @@ -183,6 +182,14 @@ void smtpd_state_init(SMTPD_STATE *state, VSTREAM *stream, state->ehlo_argv = 0; state->ehlo_buf = 0; + + /* + * BDAT. + */ + state->bdat_state = SMTPD_BDAT_NONE; + state->bdat_get_stream = 0; + state->bdat_get_buffer = 0; + state->bdat_last_chunk_size = 0; } /* smtpd_state_reset - cleanup after disconnect */ @@ -231,4 +238,12 @@ void smtpd_state_reset(SMTPD_STATE *state) if (state->tlsproxy) /* still open after longjmp */ vstream_fclose(state->tlsproxy); #endif + + /* + * BDAT. + */ + if (state->bdat_get_stream) + (void) vstream_fclose(state->bdat_get_stream); + if (state->bdat_get_buffer) + vstring_free(state->bdat_get_buffer); } diff --git a/postfix/src/util/vbuf.c b/postfix/src/util/vbuf.c index 6f4ef4557..ecc6f197b 100644 --- a/postfix/src/util/vbuf.c +++ b/postfix/src/util/vbuf.c @@ -169,7 +169,8 @@ int vbuf_unget(VBUF *bp, int ch) int vbuf_get(VBUF *bp) { - return (bp->get_ready(bp) ? VBUF_EOF : VBUF_GET(bp)); + return (bp->get_ready(bp) ? + ((bp->flags |= VBUF_FLAG_EOF), VBUF_EOF) : VBUF_GET(bp)); } /* vbuf_put - handle write buffer full condition */ diff --git a/postfix/src/util/vstream.c b/postfix/src/util/vstream.c index b85d51159..d4191ab43 100644 --- a/postfix/src/util/vstream.c +++ b/postfix/src/util/vstream.c @@ -446,6 +446,8 @@ /* The deadline feature is activated. /* .IP VSTREAM_FLAG_DOUBLE /* The double-buffering feature is activated. +/* .IP VSTREAM_FLAG_MEMORY +/* The stream is connected to a VSTRING buffer. /* DIAGNOSTICS /* Panics: interface violations. Fatal errors: out of memory. /* SEE ALSO