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.
</pre>
+</DD>
+
+<DT><b><a name="smtpd_reject_filter_maps">smtpd_reject_filter_maps</a>
+(default: empty)</b></DT><DD>
+
+<p> 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 <a href="regexp_table.5.html">regexp</a>:
+or <a href="pcre_table.5.html">pcre</a>: table, where the left-hand side specifies a pattern, and
+the right-hand side specifies replacement text. </p>
+
+<p> The input is a server response that starts with a 4XX or 5XX
+reply code (see <a href="https://tools.ietf.org/html/rfc5321">RFC 5321</a>), usually followed by an enhanced status
+code (see <a href="https://tools.ietf.org/html/rfc3463">RFC 3463</a>) 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. </p>
+
+<p> LIMITATION: <a href="postconf.5.html#smtpd_reject_filter_maps">smtpd_reject_filter_maps</a> 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").
+
+<p> Example: </p>
+
+<pre>
+/etc/postfix/<a href="postconf.5.html">main.cf</a>:
+ <a href="postconf.5.html#smtpd_reject_filter_maps">smtpd_reject_filter_maps</a> = <a href="regexp_table.5.html">regexp</a>:/etc/postfix/smtpd_reject_filter
+</pre>
+
+<pre>
+/etc/postfix/smtpd_reject_filter:
+ # Replace soft reject with hard reject.
+ /^451 4(\.6\.0 Alias expansion error)/ 550 5${1}
+</pre>
+
+<pre>
+ # Silly rule for demo purposes.
+ /^(4.+[^.])\.*$/ $1. See you later.
+</pre>
+
+<pre>
+/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
+</pre>
+
+<p> This feature is available in Postfix ≥ 3.11. </p>
+
+
</DD>
<DT><b><a name="smtpd_reject_footer">smtpd_reject_footer</a>
Do not include SMTP client session information in the Postfix
SMTP server's Received: message header.
+ Available in Postfix version 3.11 and later:
+
+ <b><a href="postconf.5.html#smtpd_reject_filter_maps">smtpd_reject_filter_maps</a> (empty)</b>
+ 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.
+
<b><a name="see_also">SEE ALSO</a></b>
<a href="anvil.8.html">anvil(8)</a>, connection/rate limiting
<a href="cleanup.8.html">cleanup(8)</a>, message canonicalization
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
.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
s;\bsmtpd_use_tls\b;<a href="postconf.5.html#smtpd_use_tls">$&</a>;g;
s;\bsmtpd_reject_footer\b;<a href="postconf.5.html#smtpd_reject_footer">$&</a>;g;
s;\bsmtpd_reject_footer_maps\b;<a href="postconf.5.html#smtpd_reject_footer_maps">$&</a>;g;
+ s;\bsmtpd_reject_filter_maps\b;<a href="postconf.5.html#smtpd_reject_filter_maps">$&</a>;g;
s;\bsmtpd_per_record_deadline\b;<a href="postconf.5.html#smtpd_per_record_deadline">$&</a>;g;
s;\bsmtpd_per_request_deadline\b;<a href="postconf.5.html#smtpd_per_request_deadline">$&</a>;g;
s;\bsmtpd_min_data_rate\b;<a href="postconf.5.html#smtpd_min_data_rate">$&</a>;g;
RFC 5321. The form does still meet RFC 5322 requirements. </p>
<p> This feature is available in Postfix ≥ 3.10. </p>
+
+%PARAM smtpd_reject_filter_maps
+
+<p> 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. </p>
+
+<p> 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. </p>
+
+<p> 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").
+
+<p> Example: </p>
+
+<pre>
+/etc/postfix/main.cf:
+ smtpd_reject_filter_maps = regexp:/etc/postfix/smtpd_reject_filter
+</pre>
+
+<pre>
+/etc/postfix/smtpd_reject_filter:
+ # Replace soft reject with hard reject.
+ /^451 4(\.6\.0 Alias expansion error)/ 550 5${1}
+</pre>
+
+<pre>
+ # Silly rule for demo purposes.
+ /^(4.+[^.])\.*$/ $1. See you later.
+</pre>
+
+<pre>
+/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
+</pre>
+
+<p> This feature is available in Postfix ≥ 3.11. </p>
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
deduplicates
intmax
lflag
+REPLYCODE
" $" 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"
#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
* 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
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.
*/
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);
/* .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
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;
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();
}
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,
#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
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.
*/
char *cp;
char *next;
char *end;
+ const char *alt_reply;
const char *footer;
/*
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)