diff --git a/cmdeploy/src/cmdeploy/postfix/master.cf.j2 b/cmdeploy/src/cmdeploy/postfix/master.cf.j2 index bf108fec..83f3a142 100644 --- a/cmdeploy/src/cmdeploy/postfix/master.cf.j2 +++ b/cmdeploy/src/cmdeploy/postfix/master.cf.j2 @@ -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= diff --git a/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py b/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py index 300b19fb..9a32ef7a 100644 --- a/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py +++ b/cmdeploy/src/cmdeploy/tests/online/test_1_basic.py @@ -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: diff --git a/cmdeploy/src/cmdeploy/tests/plugin.py b/cmdeploy/src/cmdeploy/tests/plugin.py index 87fa5576..c61b4426 100644 --- a/cmdeploy/src/cmdeploy/tests/plugin.py +++ b/cmdeploy/src/cmdeploy/tests/plugin.py @@ -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)