From 1c43ccaf44d8818636fb8ecad5b5c60ca605dcfe Mon Sep 17 00:00:00 2001
From: Wietse Z Venema An optional filter that can replace a reject response from the
+Postfix SMTP server itself, or from a program that replies through
+the Postfix SMTP server. The filter is applied before the optional
+reject footers are appended. Typically, the filter will be a regexp:
+or pcre: table, where the left-hand side specifies a pattern, and
+the right-hand side specifies replacement text. The input is a server response that starts with a 4XX or 5XX
+reply code (see RFC 5321), usually followed by an enhanced status
+code (see RFC 3463) and text. The filter returns replacement text
+or indicates that there was no match. This feature cannot be used
+to change a reject reply into a non-reject one or vice versa. LIMITATION: smtpd_reject_filter_maps will not replace text that
+was already logged before the Postfix SMTP server replies to the
+remote SMTP client. To help with logfile analysis, the Postfix SMTP
+server logs both the unmodified reply (logged below as "reject
+filter in") and the replacement reply (logged below as "reject
+filter out").
+
+ Example: This feature is available in Postfix ≥ 3.11.
+/etc/postfix/main.cf:
+ smtpd_reject_filter_maps = regexp:/etc/postfix/smtpd_reject_filter
+
+
+
+/etc/postfix/smtpd_reject_filter:
+ # Replace soft reject with hard reject.
+ /^451 4(\.6\.0 Alias expansion error)/ 550 5${1}
+
+
+
+ # Silly rule for demo purposes.
+ /^(4.+[^.])\.*$/ $1. See you later.
+
+
+
+/var/log/maillog:
+ NOQUEUE: reject filter in: 451 4.6.0 Alias expansion error
+ NOQUEUE: reject filter out: 550 5.6.0 Alias expansion error
+
+
+
This feature is available in Postfix ≥ 3.10.
+ +%PARAM smtpd_reject_filter_maps + +An optional filter that can replace a reject response from the +Postfix SMTP server itself, or from a program that replies through +the Postfix SMTP server. The filter is applied before the optional +reject footers are appended. Typically, the filter will be a regexp: +or pcre: table, where the left-hand side specifies a pattern, and +the right-hand side specifies replacement text.
+ +The input is a server response that starts with a 4XX or 5XX +reply code (see RFC 5321), usually followed by an enhanced status +code (see RFC 3463) and text. The filter returns replacement text +or indicates that there was no match. This feature cannot be used +to change a reject reply into a non-reject one or vice versa.
+ +LIMITATION: smtpd_reject_filter_maps will not replace text that +was already logged before the Postfix SMTP server replies to the +remote SMTP client. To help with logfile analysis, the Postfix SMTP +server logs both the unmodified reply (logged below as "reject +filter in") and the replacement reply (logged below as "reject +filter out"). + +
Example:
+ ++/etc/postfix/main.cf: + smtpd_reject_filter_maps = regexp:/etc/postfix/smtpd_reject_filter ++ +
+/etc/postfix/smtpd_reject_filter: + # Replace soft reject with hard reject. + /^451 4(\.6\.0 Alias expansion error)/ 550 5${1} ++ +
+ # Silly rule for demo purposes. + /^(4.+[^.])\.*$/ $1. See you later. ++ +
+/var/log/maillog: + NOQUEUE: reject filter in: 451 4.6.0 Alias expansion error + NOQUEUE: reject filter out: 550 5.6.0 Alias expansion error ++ +
This feature is available in Postfix ≥ 3.11.
diff --git a/postfix/proto/stop.double-history b/postfix/proto/stop.double-history index 0bf01b7ed..4df865938 100644 --- a/postfix/proto/stop.double-history +++ b/postfix/proto/stop.double-history @@ -194,3 +194,4 @@ proto proto COMPATIBILITY_README html src global config_known_tcp_ports c postmulti postmulti c virtual virtual c request Reported by John Doe File tlsproxy tlsproxy c + smtpd smtpd c smtpd smtpd_chat c global mail_params h diff --git a/postfix/proto/stop.spell-cc b/postfix/proto/stop.spell-cc index 9a5925cde..ff0517210 100644 --- a/postfix/proto/stop.spell-cc +++ b/postfix/proto/stop.spell-cc @@ -1867,3 +1867,4 @@ ossl deduplicates intmax lflag +REPLYCODE diff --git a/postfix/src/global/mail_params.h b/postfix/src/global/mail_params.h index 275116ff2..551020a86 100644 --- a/postfix/src/global/mail_params.h +++ b/postfix/src/global/mail_params.h @@ -2568,7 +2568,8 @@ extern int var_local_rcpt_code; " $" VAR_SMTP_BODY_CHKS \ " $" VAR_SMTP_HEAD_CHKS \ " $" VAR_SMTP_MIME_CHKS \ - " $" VAR_SMTP_NEST_CHKS + " $" VAR_SMTP_NEST_CHKS \ + " $" VAR_SMTPD_REJECT_FILTER_MAPS extern char *var_proxy_read_maps; #define VAR_PROXY_WRITE_MAPS "proxy_write_maps" @@ -4534,6 +4535,13 @@ extern int var_sockmap_max_reply; #define DEF_SMTPD_HIDE_CLIENT_SESSION "no" extern int var_smtpd_hide_client_session; + /* + * SMTP server reject response filter. + */ +#define VAR_SMTPD_REJECT_FILTER_MAPS "smtpd_reject_filter_maps" +#define DEF_SMTPD_REJECT_FILTER_MAPS "" +extern char *var_smtpd_reject_filter_maps; + /* LICENSE /* .ad /* .fi diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 6be519523..c03965bdd 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 "20250801" +#define MAIL_RELEASE_DATE "20250803" #define MAIL_VERSION_NUMBER "3.11" #ifdef SNAPSHOT diff --git a/postfix/src/smtp/smtp_connect.c b/postfix/src/smtp/smtp_connect.c index e4b60791a..8c26e9f64 100644 --- a/postfix/src/smtp/smtp_connect.c +++ b/postfix/src/smtp/smtp_connect.c @@ -507,24 +507,6 @@ static int smtp_get_effective_tls_level(DSN_BUF *why, SMTP_STATE *state) SMTP_ITERATOR *iter = state->iterator; SMTP_TLS_POLICY *tls = state->tls; - /* - * If the message contains a "TLS-Required: no" header, update the - * iterator to limit the policy at TLS_LEV_MAY. - * - * We must do this early to avoid possible failure if TLSA record lookups - * fail, or if TLSA records are found, but can't be activated because the - * security level has been reset to "may". - * - * Note that the REQUIRETLS verb in ESMTP overrides the "TLS-Required: no" - * header. - */ -#ifdef USE_TLS - if (var_tls_required_enable - && (state->request->sendopts & SOPT_REQUIRETLS_HEADER)) { - iter->tlsreqno = 1; - } -#endif - /* * Determine the TLS level for this destination. */ @@ -970,15 +952,40 @@ static void smtp_connect_inet(SMTP_STATE *state, const char *nexthop, SMTP_ITER_INIT(iter, dest, NO_HOST, NO_ADDR, port, state); + /* + * If a "TLS-Required: no" header is in effect, update the iterator + * to override TLS policy selection and to limit the security level + * to "may". Do not reset the security level after policy selection, + * as that would result in errors. For example, when TLSA records are + * looked up for security level "dane", and then the security level + * is reset to "may", the activation of those TLSA records will fail. + * + * Note that the REQUIRETLS verb in ESMTP overrides the "TLS-Required: + * no" header. + */ +#ifdef USE_TLS + if (var_tls_required_enable + && (state->request->sendopts & SOPT_REQUIRETLS_HEADER)) { + iter->tlsreqno = 1; + } +#endif + /* * TODO(wietse) If the domain publishes a TLSRPT policy, they expect * that clients use SMTP over TLS. Should we upgrade a TLS security * level of "may" to "encrypt"? This would disable falling back to * plaintext, and could break interoperability with receivers that * crank up security up to 11. + * + * As of change 20250803, with "TLS-Required: no", the SMTP client also + * ignores the recipient-side policy mechanism TLSRPT, in addition to + * the already ignored DANE and MTA-STS mechanisms. This prevents + * TLSRPT notifications for all SMTP deliveries that do not require + * TLS. */ #ifdef USE_TLSRPT if (smtp_mode && var_smtp_tlsrpt_enable + && iter->tlsreqno == 0 && tls_level_lookup(var_smtp_tls_level) > TLS_LEV_NONE && !valid_hostaddr(domain, DONT_GRIPE)) smtp_tlsrpt_create_wrapper(state, domain); diff --git a/postfix/src/smtpd/smtpd.c b/postfix/src/smtpd/smtpd.c index 9c11b18ca..3d3bc79ac 100644 --- a/postfix/src/smtpd/smtpd.c +++ b/postfix/src/smtpd/smtpd.c @@ -1179,6 +1179,12 @@ /* .IP "\fBsmtpd_hide_client_session (no)\fR" /* Do not include SMTP client session information in the Postfix /* SMTP server's Received: message header. +/* .PP +/* Available in Postfix version 3.11 and later: +/* .IP "\fBsmtpd_reject_filter_maps (empty)\fR" +/* An optional filter that can replace a reject response from the +/* Postfix SMTP server itself, or from a program that replies through +/* the Postfix SMTP server. /* SEE ALSO /* anvil(8), connection/rate limiting /* cleanup(8), message canonicalization @@ -1478,6 +1484,7 @@ bool var_smtpd_tls_auth_only; char *var_smtpd_cmd_filter; char *var_smtpd_rej_footer; char *var_smtpd_rej_ftr_maps; +char *var_smtpd_reject_filter_maps; char *var_smtpd_acl_perm_log; char *var_smtpd_dns_re_filter; @@ -6635,9 +6642,9 @@ static void pre_jail_init(char *unused_name, char **unused_argv) var_smtpd_dns_re_filter); /* - * Reject footer. + * Reject filter and footer. */ - if (*var_smtpd_rej_ftr_maps) + if (*var_smtpd_rej_ftr_maps || *var_smtpd_reject_filter_maps) smtpd_chat_pre_jail_init(); } @@ -6911,6 +6918,7 @@ int main(int argc, char **argv) VAR_SMTPD_POLICY_CONTEXT, DEF_SMTPD_POLICY_CONTEXT, &var_smtpd_policy_context, 0, 0, VAR_SMTPD_DNS_RE_FILTER, DEF_SMTPD_DNS_RE_FILTER, &var_smtpd_dns_re_filter, 0, 0, VAR_SMTPD_REJ_FTR_MAPS, DEF_SMTPD_REJ_FTR_MAPS, &var_smtpd_rej_ftr_maps, 0, 0, + VAR_SMTPD_REJECT_FILTER_MAPS, DEF_SMTPD_REJECT_FILTER_MAPS, &var_smtpd_reject_filter_maps, 0, 0, VAR_HFROM_FORMAT, DEF_HFROM_FORMAT, &var_hfrom_format, 1, 0, VAR_SMTPD_FORBID_BARE_LF_EXCL, DEF_SMTPD_FORBID_BARE_LF_EXCL, &var_smtpd_forbid_bare_lf_excl, 0, 0, VAR_SMTPD_FORBID_BARE_LF, DEF_SMTPD_FORBID_BARE_LF, &var_smtpd_forbid_bare_lf, 1, 0, diff --git a/postfix/src/smtpd/smtpd_chat.c b/postfix/src/smtpd/smtpd_chat.c index 278e5362a..ab4ad83c8 100644 --- a/postfix/src/smtpd/smtpd_chat.c +++ b/postfix/src/smtpd/smtpd_chat.c @@ -112,8 +112,9 @@ #include "smtpd_chat.h" /* - * Reject footer. + * Reject filter and footer maps. */ +static MAPS *smtpd_reject_filter_maps; static MAPS *smtpd_rej_ftr_maps; #define STR vstring_str @@ -128,6 +129,14 @@ void smtpd_chat_pre_jail_init(void) if (init_count++ != 0) msg_panic("smtpd_chat_pre_jail_init: multiple calls"); + /* + * SMTP server reject filter. + */ + if (*var_smtpd_reject_filter_maps) + smtpd_reject_filter_maps = maps_create(VAR_SMTPD_REJECT_FILTER_MAPS, + var_smtpd_reject_filter_maps, + DICT_FLAG_LOCK); + /* * SMTP server reject footer. */ @@ -206,6 +215,7 @@ void vsmtpd_chat_reply(SMTPD_STATE *state, const char *format, va_list ap) char *cp; char *next; char *end; + const char *alt_reply; const char *footer; /* @@ -215,8 +225,30 @@ void vsmtpd_chat_reply(SMTPD_STATE *state, const char *format, va_list ap) if (state->error_count >= var_smtpd_soft_erlim) sleep(delay = var_smtpd_err_sleep); + /* + * Postfix generates single-line reject responses, but Milters may + * generate multi-line rejects with the SMFIR_REPLYCODE request. + */ vstring_vsprintf(state->buffer, format, ap); + cp = STR(state->buffer); + if ((*cp == '4' || *cp == '5') + && smtpd_reject_filter_maps != 0 + && (alt_reply = maps_find(smtpd_reject_filter_maps, cp, 0)) != 0) { + const char *queue_id = state->queue_id ? state->queue_id : "NOQUEUE"; + /* XXX Enforce this for each line of a multi-line reply. */ + if ((alt_reply[0] != '4' && alt_reply[0] != '5') + || !ISDIGIT(alt_reply[1]) || !ISDIGIT(alt_reply[2]) + || (alt_reply[3] != ' ' && alt_reply[3] != '-') + || (ISDIGIT(alt_reply[4]) && (alt_reply[4] != alt_reply[0]))) { + msg_warn("%s: ignoring invalid reject filter result: %s", + queue_id, alt_reply); + } else { + msg_info("%s: reply filter in: %s", queue_id, cp); + msg_info("%s: reply filter out: %s", queue_id, alt_reply); + vstring_strcpy(state->buffer, alt_reply); + } + } if ((*(cp = STR(state->buffer)) == '4' || *cp == '5') && ((smtpd_rej_ftr_maps != 0 && (footer = maps_find(smtpd_rej_ftr_maps, cp, 0)) != 0)