diff --git a/cmdeploy/src/cmdeploy/postfix/main.cf.j2 b/cmdeploy/src/cmdeploy/postfix/main.cf.j2 index 57ef64e0..a10b0e2c 100644 --- a/cmdeploy/src/cmdeploy/postfix/main.cf.j2 +++ b/cmdeploy/src/cmdeploy/postfix/main.cf.j2 @@ -103,3 +103,11 @@ smtpd_peername_lookup = no default_transport = lmtp-filtermail:inet:[127.0.0.1]:{{ config.filtermail_lmtp_port_transport }} lmtp-filtermail_initial_destination_concurrency=10000 lmtp-filtermail_destination_concurrency_limit=10000 + +{% if not config.ipv4_relay %} +# DKIM-sign locally generated mail (bounces, DSNs). +# These bypass smtpd, so they need explicit milter configuration. +non_smtpd_milters = unix:opendkim/opendkim.sock +internal_mail_filter_classes = bounce +milter_macro_daemon_name = ORIGINATING +{% endif %} 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)