diff --git a/postfix/HISTORY b/postfix/HISTORY index 1982d6e69..98fd5f0ab 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -29530,3 +29530,21 @@ Apologies for any names omitted. change did not have space between POSTLOG_HOSTNAME and XDG_RUNTIME_DIR, breaking maillog_file support and graphical debugging. File: global/mail_params.h. + +20250801 + + Feature: smtpd_reject_filter_maps can selectively replace a + reject response from the Postfix SMTP server, or from a + program that replies through the Postfix SMTP server. Files: + smtpd/smtpd.c, smtpd/smtpd_chat.c, global/mail_params.h, + proto/postconf.proto, mantools/postlink. + +20250803 + + Cleanup: when "tls_required_enable = yes" and a message + contains a "TLS-Required: no" header", the Postfix SMTP + client now also ignores the recipient-side TLSRPT policy, + in addition to the already ignored recipient-side MTA-STS + and DANE policies. This prevents TLSRPT notifications for + all SMTP deliveries that do not require TLS. File: + smtp/smtp_connect.c. diff --git a/postfix/html/postconf.5.html b/postfix/html/postconf.5.html index 9b03afe72..b61babdf2 100644 --- a/postfix/html/postconf.5.html +++ b/postfix/html/postconf.5.html @@ -17393,6 +17393,58 @@ Example: + + +
smtpd_reject_filter_maps +(default: empty)
+ +

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.

+ +
smtpd_reject_footer diff --git a/postfix/html/smtpd.8.html b/postfix/html/smtpd.8.html index 47be68d07..8465bbf25 100644 --- a/postfix/html/smtpd.8.html +++ b/postfix/html/smtpd.8.html @@ -1437,6 +1437,13 @@ SMTPD(8) SMTPD(8) Do not include SMTP client session information in the Postfix SMTP server's Received: message header. + Available in Postfix version 3.11 and later: + + smtpd_reject_filter_maps (empty) + 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 diff --git a/postfix/man/man5/postconf.5 b/postfix/man/man5/postconf.5 index 8c2039ecb..73fce25dd 100644 --- a/postfix/man/man5/postconf.5 +++ b/postfix/man/man5/postconf.5 @@ -11815,6 +11815,60 @@ Example: smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination .fi .ad +.SH smtpd_reject_filter_maps (default: empty) +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. +.PP +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. +.PP +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"). +.PP +Example: +.PP +.nf +.na +/etc/postfix/main.cf: + smtpd_reject_filter_maps = regexp:/etc/postfix/smtpd_reject_filter +.fi +.ad +.PP +.nf +.na +/etc/postfix/smtpd_reject_filter: + # Replace soft reject with hard reject. + /^451 4(\e.6\e.0 Alias expansion error)/ 550 5${1} +.fi +.ad +.PP +.nf +.na + # Silly rule for demo purposes. + /^(4.+[^.])\e.*$/ $1. See you later. +.fi +.ad +.PP +.nf +.na +/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 +.fi +.ad +.PP +This feature is available in Postfix >= 3.11. .SH smtpd_reject_footer (default: empty) Optional information that is appended after each Postfix SMTP server diff --git a/postfix/man/man8/smtpd.8 b/postfix/man/man8/smtpd.8 index a671d476b..616a4a14a 100644 --- a/postfix/man/man8/smtpd.8 +++ b/postfix/man/man8/smtpd.8 @@ -1237,6 +1237,12 @@ Available in Postfix 3.10 and later: .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. .SH "SEE ALSO" .na .nf diff --git a/postfix/mantools/postlink b/postfix/mantools/postlink index d641b8995..061e0aef1 100755 --- a/postfix/mantools/postlink +++ b/postfix/mantools/postlink @@ -770,6 +770,7 @@ while (<>) { s;\bsmtpd_use_tls\b;$&;g; s;\bsmtpd_reject_footer\b;$&;g; s;\bsmtpd_reject_footer_maps\b;$&;g; + s;\bsmtpd_reject_filter_maps\b;$&;g; s;\bsmtpd_per_record_deadline\b;$&;g; s;\bsmtpd_per_request_deadline\b;$&;g; s;\bsmtpd_min_data_rate\b;$&;g; diff --git a/postfix/proto/postconf.proto b/postfix/proto/postconf.proto index 0a4a9255e..666264fe2 100644 --- a/postfix/proto/postconf.proto +++ b/postfix/proto/postconf.proto @@ -19661,3 +19661,51 @@ and therefore it does not need to provide the information required by RFC 5321. The form does still meet RFC 5322 requirements.

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)