feat: DKIM-sign bounce messages (mainly "user does not exist")

This is based on Jagoda's https://github.com/chatmail/relay/pull/874
but comes with a simpler and more robust test.

TODO: requires https://github.com/chatmail/filtermail/pull/149
This commit is contained in:
holger krekel
2026-05-11 23:21:28 +02:00
parent 4ebde2825d
commit 9eb043e229
3 changed files with 49 additions and 2 deletions

View File

@@ -51,12 +51,16 @@ smtps inet n - y - 5000 smtpd
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
#628 inet n - y - - qmqpd
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
{% if not config.ipv4_relay %} -o cleanup_service_name=signlocal
{% endif %}cleanup unix n - y - 0 cleanup
qmgr unix n - n 300 1 qmgr
#qmgr unix n - n 300 1 oqmgr
tlsmgr unix - - y 1000? 1 tlsmgr
rewrite unix - - y - - trivial-rewrite
bounce unix - - y - 0 bounce
{% if not config.ipv4_relay %} -o internal_mail_filter_classes=bounce
-o cleanup_service_name=signlocal
{% endif %}
defer unix - - y - 0 bounce
trace unix - - y - 0 bounce
verify unix - - y - 1 verify
@@ -102,6 +106,15 @@ filter unix - n n - - lmtp
authclean unix n - - - 0 cleanup
-o header_checks=regexp:/etc/postfix/submission_header_cleanup
{% if not config.ipv4_relay %}
# DKIM-sign locally generated mail (bounces, DSNs).
# These bypass smtpd, so they need explicit milter configuration.
signlocal unix n - - - 0 cleanup
-o syslog_name=postfix/signlocal
-o milter_macro_daemon_name=ORIGINATING
-o non_smtpd_milters=unix:opendkim/opendkim.sock
{% endif %}
lmtp-filtermail unix - - y - 10000 lmtp
-o syslog_name=postfix/lmtp-filtermail
-o lmtp_header_checks=

View File

@@ -194,6 +194,34 @@ def test_reject_missing_dkim(cmsetup, maildata, from_addr):
s.sendmail(from_addr=from_addr, to_addrs=recipient.addr, msg=msg)
def test_bounces_are_dkim_signed(cmsetup, cmsetup2, maildata, maildomain):
# we send a message to non-existant user and expect a bounce message
# which will only get through if the bounce message was DKIM-signed
if is_valid_ipv4(maildomain):
pytest.skip("DKIM is not configured on IPv4-only relays")
sender = cmsetup2.gen_users(1)[0]
nonexistent = f"nosuchuser_test42@{cmsetup.maildomain}"
msg = maildata(
"encrypted.eml",
from_addr=sender.addr,
to_addr=nonexistent,
).as_string()
sender.smtp.sendmail(sender.addr, [nonexistent], msg)
def bounce_in_inbox():
messages = sender.imap.fetch_all_messages()
for m in messages:
if "mail delivery" in m.lower() or "undelivered" in m.lower():
return m
raise ValueError("bounce not yet in inbox")
bounce = try_n_times(30, bounce_in_inbox)
assert "nosuchuser_test42" in bounce
def try_n_times(n, f):
for _ in range(n - 1):
try:

View File

@@ -19,6 +19,7 @@ def format_mail_domain(raw_domain: str) -> str:
DomainValidator().validate_domain_re(raw_domain)
return raw_domain
conftestdir = Path(__file__).parent
@@ -466,6 +467,11 @@ def cmsetup(maildomain, gencreds, ssl_context):
return CMSetup(maildomain, gencreds, ssl_context)
@pytest.fixture
def cmsetup2(maildomain2, gencreds, ssl_context):
return CMSetup(maildomain2, gencreds, ssl_context)
class CMSetup:
def __init__(self, maildomain, gencreds, ssl_context):
self.maildomain = maildomain
@@ -476,7 +482,7 @@ class CMSetup:
print(f"Creating {num} online users")
users = []
for i in range(num):
addr, password = self.gencreds()
addr, password = self.gencreds(format_mail_domain(self.maildomain))
user = CMUser(self.maildomain, addr, password, self.ssl_context)
assert user.smtp
users.append(user)