diff --git a/postfix/HISTORY b/postfix/HISTORY index 1bb663eaa..94555dc73 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -28881,40 +28881,55 @@ Apologies for any names omitted. 20250119 - Feature: REQUIRETLS verb in SMTP. According to RFC 8689 this - not only requires TLS, but also requires server certificate - matching. TODO: parameters, defaults, files. + Feature: support for the REQUIRETLS verb in SMTP. According + to RFC 8689, this requires TLS server certificate matching. + Files: cleanup/cleanup_api.c, global/cleanup_strflags.c, + global/post_mail.c, global/post_mail.c, global/ehlo_mask.[hc], + global/ehlo_mask_test.c, local/forward.c, smtpd/smtpd.c, + smtp/smtp_connect.c, smtp/smtp_proto.c. + +20250120 + + Added a configuration parameter "tls_required_enable (default: + yes) to control support for the "TLS-Required: no" message + header. Files: global/mail_params.[hc], cleanup/cleanup_message.c. + + Added a configuration parameter "requiretls_enable" (default: + yes). Files: cleanup/cleanup_api.c, global/cleanup_strflags.c, + global/post_mail.c, global/post_mail.c, global/ehlo_mask.[hc], + global/ehlo_mask_test.c, local/forward.c, smtpd/smtpd.c, + smtp/smtp_connect.c, smtp/smtp_proto.c. + + REQUIRETLS support: After a certificate check fails, or a + remote SMTP server does not announce REQUIRETLS support, + the Postfix SMTP client will override the RFC 8689 5.x.x. + status and treat it as a soft error, until there are no + more alternate MX servers to try. Files: smtp/smtp.h, + smtp/smtp_proto.c, smtp/smtp_trouble.c. + + Completed: when a message received with REQUIRETLS is + returned in a delivery status notification, return the + message headers only, and do not request delivery with + REQUIRETLS. Files: bounce/bounce_notify_service.c, + bounce/bounce_one_service.c, bounce/bounce_trace_service.c, + bounce/bounce_verp_service.c, bounce/bounce_warn_service.c. TODO: - If the TLS policy, or remote server, does not support - REQUIRETLS, try alternate MX hosts before returning the - message as undeliverable. Code: smtp_connect.c, - smtp_proto.c (ehlo response and post-TLS handshake check). + The RFC says that REQUIRETLS applies to LMTP. Dovecot supports + TLS, but how common is it for Postfix to verify a Dovecot + server certificate? Should we add a 'cheat' setting that does + not enforce REQUIRETLS? If a message contains "TLS-Required: no", should a bounce message also contain this header? - If a message with REQUIRETLS is bounced, and we return a - headers-only bounce (NOTIFY-HDRS) does the bounce still - need REQUIRETLS? - If the Postfix SMTP server accepted REQUIRETLS, should that stay in effect if, before the message is forwarded, the configuration is changed to "requiretls_enable = no"? Same for "postsuper -r". - Same question for "TLS-Required: no". + Ditto for "tls_required_enable = no" and "TLS-Required: no". - Problem. After a certificate check fails, we want the SMTP - client to try other MX servers. This requires a 4.x.x status - code. But then, the Postfix SMTP client will defer the - message. - - How does the delivery_status_filter solve that? - - With "smtp_tls_security_level = secure" and "smtp_mx_session_limit - = 2" the Postfix SMTP client will make three connections. - - Simplify cleanup_envelope_test. Write the initial SIZE record + Simplify the cleanup_envelope_test. Write the initial SIZE record to /dev/null, don't call cleanup_final(), and verify the value of state->sendopts. diff --git a/postfix/proto/stop.double-history b/postfix/proto/stop.double-history index ef83bc3c8..cb73a10e3 100644 --- a/postfix/proto/stop.double-history +++ b/postfix/proto/stop.double-history @@ -159,3 +159,5 @@ proto proto socketmap_table qmgr qmgr_deliver c qmgr qmgr_message c qmqpd qmqpd c smtp smtp_proto c smtpd smtpd c verify verify c operations Files cleanup cleanup h cleanup cleanup_message c + global ehlo_mask_test c local forward c smtpd smtpd c + more alternate MX servers to try Files smtp smtp h diff --git a/postfix/src/bounce/bounce_notify_service.c b/postfix/src/bounce/bounce_notify_service.c index b8068f176..4f1a7b101 100644 --- a/postfix/src/bounce/bounce_notify_service.c +++ b/postfix/src/bounce/bounce_notify_service.c @@ -98,6 +98,15 @@ int bounce_notify_service(int flags, char *service, char *queue_name, char *postmaster; int count; + /* + * If the original sender requested REQUIRETLS, return headers only, and + * do not enforce REQUIRETLS for the delivery status notification. + */ + if ((sendopts & SOPT_REQUIRETLS_ESMTP) != 0) { + dsn_ret = DSN_RET_HDRS; + sendopts &= ~SOPT_REQUIRETLS_ESMTP; + } + /* * Initialize. Open queue file, bounce log, etc. * @@ -196,7 +205,8 @@ int bounce_notify_service(int flags, char *service, char *queue_name, && bounce_header_dsn(bounce, bounce_info) == 0 && bounce_diagnostic_dsn(bounce, bounce_info, DSN_NOTIFY_OVERRIDE) > 0) { - bounce_original(bounce, bounce_info, DSN_RET_FULL); + bounce_original(bounce, bounce_info, dsn_ret ? + dsn_ret : DSN_RET_FULL); bounce_status = post_mail_fclose(bounce); if (bounce_status == 0) msg_info("%s: postmaster non-delivery notification: %s", diff --git a/postfix/src/bounce/bounce_notify_util.c b/postfix/src/bounce/bounce_notify_util.c index 3a077f651..50fc9af39 100644 --- a/postfix/src/bounce/bounce_notify_util.c +++ b/postfix/src/bounce/bounce_notify_util.c @@ -533,6 +533,14 @@ int bounce_header(VSTREAM *bounce, BOUNCE_INFO *bounce_info, post_mail_fprintf(bounce, "In-Reply-To: %s", STR(bounce_info->orig_msgid)); } + /* + * Trade confidentiality against availability. + */ +#if 0 + if (var_tls_required_enable) + post_mail_fprintf(bounce, "TLS-Required: no"); +#endif + /* * Auto-Submitted header, as per RFC 3834. */ diff --git a/postfix/src/bounce/bounce_notify_verp.c b/postfix/src/bounce/bounce_notify_verp.c index 84e20b8e2..3fa2c2c08 100644 --- a/postfix/src/bounce/bounce_notify_verp.c +++ b/postfix/src/bounce/bounce_notify_verp.c @@ -111,6 +111,15 @@ int bounce_notify_verp(int flags, char *service, char *queue_name, if (strcasecmp_utf8(recipient, mail_addr_double_bounce()) == 0) msg_panic("%s: attempt to bounce a double bounce", myname); + /* + * If the original sender requested REQUIRETLS, return headers only, and + * do not enforce REQUIRETLS for the delivery status notification. + */ + if ((sendopts & SOPT_REQUIRETLS_ESMTP) != 0) { + dsn_ret = DSN_RET_HDRS; + sendopts &= ~SOPT_REQUIRETLS_ESMTP; + } + /* * Initialize. Open queue file, bounce log, etc. */ diff --git a/postfix/src/bounce/bounce_one_service.c b/postfix/src/bounce/bounce_one_service.c index ab6cac829..53442198b 100644 --- a/postfix/src/bounce/bounce_one_service.c +++ b/postfix/src/bounce/bounce_one_service.c @@ -96,6 +96,15 @@ int bounce_one_service(int flags, char *queue_name, char *queue_id, var_notify_classes); VSTRING *new_id = vstring_alloc(10); + /* + * If the original sender requested REQUIRETLS, return headers only, and + * do not enforce REQUIRETLS for the delivery status notification. + */ + if ((sendopts & SOPT_REQUIRETLS_ESMTP) != 0) { + dsn_ret = DSN_RET_HDRS; + sendopts &= ~SOPT_REQUIRETLS_ESMTP; + } + /* * Initialize. Open queue file, bounce log, etc. */ @@ -162,7 +171,8 @@ int bounce_one_service(int flags, char *queue_name, char *queue_id, && bounce_recipient_log(bounce, bounce_info) == 0 && bounce_header_dsn(bounce, bounce_info) == 0 && bounce_recipient_dsn(bounce, bounce_info) == 0) - bounce_original(bounce, bounce_info, DSN_RET_FULL); + bounce_original(bounce, bounce_info, dsn_ret ? + dsn_ret : DSN_RET_FULL); bounce_status = post_mail_fclose(bounce); if (bounce_status == 0) msg_info("%s: postmaster non-delivery notification: %s", diff --git a/postfix/src/bounce/bounce_trace_service.c b/postfix/src/bounce/bounce_trace_service.c index cb29bbb26..451f584f4 100644 --- a/postfix/src/bounce/bounce_trace_service.c +++ b/postfix/src/bounce/bounce_trace_service.c @@ -95,6 +95,15 @@ int bounce_trace_service(int flags, char *service, char *queue_name, int count; const char *sender; + /* + * If the original sender requested REQUIRETLS, do not enforce REQUIRETLS + * for the delivery status notification. The trace service always returns + * headers only. + */ + if ((sendopts & SOPT_REQUIRETLS_ESMTP) != 0) { + sendopts &= ~SOPT_REQUIRETLS_ESMTP; + } + /* * For consistency with fail/delay notifications, send notification for a * non-bounce message as a single-bounce message, send notification for a diff --git a/postfix/src/bounce/bounce_warn_service.c b/postfix/src/bounce/bounce_warn_service.c index f9fc7641b..62c263859 100644 --- a/postfix/src/bounce/bounce_warn_service.c +++ b/postfix/src/bounce/bounce_warn_service.c @@ -98,6 +98,15 @@ int bounce_warn_service(int unused_flags, char *service, char *queue_name, char *postmaster; int count; + /* + * If the original sender requested REQUIRETLS, return headers only, and + * do not enforce REQUIRETLS for the delivery status notification. + */ + if ((sendopts & SOPT_REQUIRETLS_ESMTP) != 0) { + dsn_ret = DSN_RET_HDRS; + sendopts &= ~SOPT_REQUIRETLS_ESMTP; + } + /* * Initialize. Open queue file, bounce log, etc. * @@ -185,7 +194,8 @@ int bounce_warn_service(int unused_flags, char *service, char *queue_name, && bounce_header_dsn(bounce, bounce_info) == 0 && bounce_diagnostic_dsn(bounce, bounce_info, DSN_NOTIFY_OVERRIDE) > 0) { - bounce_original(bounce, bounce_info, DSN_RET_FULL); + bounce_original(bounce, bounce_info, dsn_ret ? + dsn_ret : DSN_RET_FULL); bounce_status = post_mail_fclose(bounce); if (bounce_status == 0) msg_info("%s: postmaster delay notification: %s", diff --git a/postfix/src/cleanup/cleanup_message.c b/postfix/src/cleanup/cleanup_message.c index b9a7e9360..cdff5bf05 100644 --- a/postfix/src/cleanup/cleanup_message.c +++ b/postfix/src/cleanup/cleanup_message.c @@ -653,7 +653,7 @@ static void cleanup_header_callback(void *context, int header_class, if (state->hop_count == 1) argv_add(state->auto_hdrs, vstring_str(header_buf), ARGV_END); } - if (hdr_opts->type == HDR_TLS_REQUIRED) { + if (hdr_opts->type == HDR_TLS_REQUIRED && var_tls_required_enable) { char *cp = vstring_str(header_buf) + strlen(hdr_opts->name) + 1; while (ISSPACE(*cp)) diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 53350b533..d7bc61477 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 "20250119" +#define MAIL_RELEASE_DATE "20250120" #define MAIL_VERSION_NUMBER "3.10" #ifdef SNAPSHOT diff --git a/postfix/src/smtp/smtp.h b/postfix/src/smtp/smtp.h index 696865fac..9dc8c70cf 100644 --- a/postfix/src/smtp/smtp.h +++ b/postfix/src/smtp/smtp.h @@ -638,8 +638,9 @@ extern void smtp_rcpt_done(SMTP_STATE *, SMTP_RESP *, RECIPIENT *); /* * smtp_trouble.c */ -#define SMTP_THROTTLE 1 -#define SMTP_NOTHROTTLE 0 +#define SMTP_MISC_FAIL_NONE 0 +#define SMTP_MISC_FAIL_THROTTLE (1<<0) +#define SMTP_MISC_FAIL_SOFT_NON_FINAL (1<<1) extern int smtp_sess_fail(SMTP_STATE *); extern int PRINTFLIKE(5, 6) smtp_misc_fail(SMTP_STATE *, int, const char *, SMTP_RESP *, const char *,...); @@ -649,9 +650,9 @@ extern void PRINTFLIKE(5, 6) smtp_rcpt_fail(SMTP_STATE *, RECIPIENT *, extern int smtp_stream_except(SMTP_STATE *, int, const char *); #define smtp_site_fail(state, mta, resp, ...) \ - smtp_misc_fail((state), SMTP_THROTTLE, (mta), (resp), __VA_ARGS__) + smtp_misc_fail((state), SMTP_MISC_FAIL_THROTTLE, (mta), (resp), __VA_ARGS__) #define smtp_mesg_fail(state, mta, resp, ...) \ - smtp_misc_fail((state), SMTP_NOTHROTTLE, (mta), (resp), __VA_ARGS__) + smtp_misc_fail((state), SMTP_MISC_FAIL_NONE, (mta), (resp), __VA_ARGS__) /* * smtp_unalias.c diff --git a/postfix/src/smtp/smtp_proto.c b/postfix/src/smtp/smtp_proto.c index e134c5790..919b09df8 100644 --- a/postfix/src/smtp/smtp_proto.c +++ b/postfix/src/smtp/smtp_proto.c @@ -685,18 +685,20 @@ int smtp_helo(SMTP_STATE *state) /* * Require that the server announces REQUIRETLS when the sender required - * REQUIRETLS. TODO(wietse) try alternative MX hosts before returning the - * message as undeliverable. + * REQUIRETLS. Return the message as undeliverable only when there are no + * more alternative MX hosts. */ #ifdef USE_TLS if ((state->misc_flags & SMTP_MISC_FLAG_IN_STARTTLS) != 0 && (session->features & SMTP_FEATURE_REQUIRETLS) == 0 && (request->sendopts & SOPT_REQUIRETLS_ESMTP) != 0) - return (smtp_mesg_fail(state, DSN_BY_LOCAL_MTA, + return (smtp_misc_fail(state, SMTP_MISC_FAIL_SOFT_NON_FINAL, + DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "5.7.30"), - "REQUIRETLS is required, " - "but was not offered by host %s", - session->namaddr)); + "Sender requires authenticated TLS, but no " + "mail server was found with REQUIRETLS " + "support. The last attempted server " + "was %s", session->namaddr)); #endif /* @@ -1163,7 +1165,7 @@ static int smtp_start_tls(SMTP_STATE *state) if (PLAINTEXT_FALLBACK_OK_AFTER_STARTTLS_FAILURE) RETRY_AS_PLAINTEXT; return (smtp_misc_fail(state, state->tls->level == TLS_LEV_MAY ? - SMTP_NOTHROTTLE : SMTP_THROTTLE, + SMTP_MISC_FAIL_NONE : SMTP_MISC_FAIL_THROTTLE, DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "4.7.5"), "Cannot start TLS: handshake failure")); @@ -1211,15 +1213,18 @@ static int smtp_start_tls(SMTP_STATE *state) /* * Require a server certificate match when the sender requested - * REQUIRETLS. TODO(wietse) try alternative MX hosts before - * returning the message as undeliverable. + * REQUIRETLS. Return the message as undeliverable only when + * there are no more alternative MX hosts. */ if ((state->request->sendopts & SOPT_REQUIRETLS_ESMTP)) { - return (smtp_site_fail(state, DSN_BY_LOCAL_MTA, + return (smtp_misc_fail(state, SMTP_MISC_FAIL_SOFT_NON_FINAL, + DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "5.7.10"), "Sender requires a TLS server " - "certificate match, but the remote " - "server certificate did not match")); + "certificate match, but no matching " + "mail server was found. The last " + "attempted server was %s", + session->namaddr)); } return (smtp_site_fail(state, DSN_BY_LOCAL_MTA, SMTP_RESP_FAKE(&fake, "4.7.5"), diff --git a/postfix/src/smtp/smtp_trouble.c b/postfix/src/smtp/smtp_trouble.c index 60880df6a..b5834c6fa 100644 --- a/postfix/src/smtp/smtp_trouble.c +++ b/postfix/src/smtp/smtp_trouble.c @@ -33,9 +33,9 @@ /* int exception; /* const char *description; /* AUXILIARY FUNCTIONS -/* int smtp_misc_fail(state, throttle, mta_name, resp, format, ...) +/* int smtp_misc_fail(state, flags, mta_name, resp, format, ...) /* SMTP_STATE *state; -/* int throttle; +/* int flags; /* const char *mta_name; /* SMTP_RESP *resp; /* const char *format; @@ -91,8 +91,11 @@ /* /* smtp_misc_fail() provides a more detailed interface than /* smtp_site_fail() and smtp_mesg_fail(), which are convenience -/* wrappers around smtp_misc_fail(). The throttle argument -/* is either SMTP_THROTTLE or SMTP_NOTHROTTLE; it is used only +/* wrappers around smtp_misc_fail(). The flags argument is either +/* SMTP_MISC_FAIL_NONE or the bitwise OR of SMTP_MISC_FAIL_THROTTLE +/* (throttle the destination) and/or SMTP_MISC_FAIL_SOFT_NON_FINAL +/* (if the server was not the last one to try, treat a hard error +/* as a soft error); SMTP_MISC_FAIL_THROTTLE is used only /* in the "soft error, final server" policy, and determines /* whether a destination will be marked as problematic. /* @@ -210,7 +213,7 @@ static void smtp_check_code(SMTP_SESSION *session, int code) /* smtp_bulk_fail - skip, defer or bounce recipients, maybe throttle queue */ -static int smtp_bulk_fail(SMTP_STATE *state, int throttle_queue) +static int smtp_bulk_fail(SMTP_STATE *state, int flags) { DELIVER_REQUEST *request = state->request; SMTP_SESSION *session = state->session; @@ -220,8 +223,21 @@ static int smtp_bulk_fail(SMTP_STATE *state, int throttle_queue) int aggregate_status; int soft_error = (STR(why->status)[0] == '4'); int soft_bounce_error = (STR(why->status)[0] == '5' && var_soft_bounce); + int throttle_queue = (flags & SMTP_MISC_FAIL_THROTTLE); int nrcpt; + /* + * Sanity check. + */ + if ((flags & SMTP_MISC_FAIL_SOFT_NON_FINAL) != 0) { + if (soft_error) { + msg_warn("smtp_bulk_fail: ignoring SMTP_MISC_FAIL_SOFT_NON_FINAL " + "for a soft error"); + } else { + soft_error = (state->misc_flags & SMTP_MISC_FLAG_FINAL_SERVER) == 0; + } + } + /* * Don't defer the recipients just yet when this error qualifies them for * delivery to a backup server. Just log something informative to show @@ -302,7 +318,7 @@ int smtp_sess_fail(SMTP_STATE *state) * because this error information is collected by a routine that * terminates BEFORE the error is reported. */ - return (smtp_bulk_fail(state, SMTP_THROTTLE)); + return (smtp_bulk_fail(state, SMTP_MISC_FAIL_THROTTLE)); } /* vsmtp_fill_dsn - fill in temporary DSN structure */ @@ -342,7 +358,7 @@ static void vsmtp_fill_dsn(SMTP_STATE *state, const char *mta_name, /* smtp_misc_fail - maybe throttle queue; skip/defer/bounce all recipients */ -int smtp_misc_fail(SMTP_STATE *state, int throttle, const char *mta_name, +int smtp_misc_fail(SMTP_STATE *state, int flags, const char *mta_name, SMTP_RESP *resp, const char *format,...) { va_list ap; @@ -360,7 +376,7 @@ int smtp_misc_fail(SMTP_STATE *state, int throttle, const char *mta_name, /* * Skip, defer or bounce recipients, and throttle this queue. */ - return (smtp_bulk_fail(state, throttle)); + return (smtp_bulk_fail(state, flags)); } /* smtp_rcpt_fail - skip, defer, or bounce recipient */ @@ -472,5 +488,5 @@ int smtp_stream_except(SMTP_STATE *state, int code, const char *description) * falling back to plaintext, because RETRY_AS_PLAINTEXT clears the * FINAL_SERVER flag. */ - return (smtp_bulk_fail(state, SMTP_THROTTLE)); + return (smtp_bulk_fail(state, SMTP_MISC_FAIL_THROTTLE)); } diff --git a/postfix/src/smtpd/smtpd.c b/postfix/src/smtpd/smtpd.c index 168307064..a19955aa1 100644 --- a/postfix/src/smtpd/smtpd.c +++ b/postfix/src/smtpd/smtpd.c @@ -2687,6 +2687,7 @@ static int mail_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv) /* Already processed early. */ ; #ifdef USE_TLS } else if (var_requiretls_enable + && state->tls_context != 0 && (state->ehlo_discard_mask & EHLO_MASK_REQUIRETLS) == 0 && strcasecmp(arg, "REQUIRETLS") == 0) { /* RFC 8689 */ state->flags |= SMTPD_FLAG_REQUIRETLS;