Skip to content

Commit 26a13fb

Browse files
committed
feat: DKIM-sign bounce messages (mainly "user does not exist")
This was originally based on Jagoda's #874 but then the postfix config was simplified, and it comes with a simpler and more robust test.
1 parent d054fbb commit 26a13fb

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

cmdeploy/src/cmdeploy/postfix/main.cf.j2

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,11 @@ smtpd_peername_lookup = no
103103
default_transport = lmtp-filtermail:inet:[127.0.0.1]:{{ config.filtermail_lmtp_port_transport }}
104104
lmtp-filtermail_initial_destination_concurrency=10000
105105
lmtp-filtermail_destination_concurrency_limit=10000
106+
107+
{% if not config.ipv4_relay %}
108+
# DKIM-sign locally generated mail (bounces, DSNs).
109+
# These bypass smtpd, so they need explicit milter configuration.
110+
non_smtpd_milters = unix:opendkim/opendkim.sock
111+
internal_mail_filter_classes = bounce
112+
milter_macro_daemon_name = ORIGINATING
113+
{% endif %}

cmdeploy/src/cmdeploy/tests/online/test_1_basic.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,34 @@ def test_reject_missing_dkim(cmsetup, maildata, from_addr):
194194
s.sendmail(from_addr=from_addr, to_addrs=recipient.addr, msg=msg)
195195

196196

197+
def test_bounces_are_dkim_signed(cmsetup, cmsetup2, maildata, maildomain):
198+
# we send a message to non-existant user and expect a bounce message
199+
# which will only get through if the bounce message was DKIM-signed
200+
201+
if is_valid_ipv4(maildomain):
202+
pytest.skip("DKIM is not configured on IPv4-only relays")
203+
204+
sender = cmsetup2.gen_users(1)[0]
205+
nonexistent = f"nosuchuser_test42@{cmsetup.maildomain}"
206+
207+
msg = maildata(
208+
"encrypted.eml",
209+
from_addr=sender.addr,
210+
to_addr=nonexistent,
211+
).as_string()
212+
sender.smtp.sendmail(sender.addr, [nonexistent], msg)
213+
214+
def bounce_in_inbox():
215+
messages = sender.imap.fetch_all_messages()
216+
for m in messages:
217+
if "mail delivery" in m.lower() or "undelivered" in m.lower():
218+
return m
219+
raise ValueError("bounce not yet in inbox")
220+
221+
bounce = try_n_times(30, bounce_in_inbox)
222+
assert "nosuchuser_test42" in bounce
223+
224+
197225
def try_n_times(n, f):
198226
for _ in range(n - 1):
199227
try:

cmdeploy/src/cmdeploy/tests/plugin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def format_mail_domain(raw_domain: str) -> str:
1919
DomainValidator().validate_domain_re(raw_domain)
2020
return raw_domain
2121

22+
2223
conftestdir = Path(__file__).parent
2324

2425

@@ -466,6 +467,11 @@ def cmsetup(maildomain, gencreds, ssl_context):
466467
return CMSetup(maildomain, gencreds, ssl_context)
467468

468469

470+
@pytest.fixture
471+
def cmsetup2(maildomain2, gencreds, ssl_context):
472+
return CMSetup(maildomain2, gencreds, ssl_context)
473+
474+
469475
class CMSetup:
470476
def __init__(self, maildomain, gencreds, ssl_context):
471477
self.maildomain = maildomain
@@ -476,7 +482,7 @@ def gen_users(self, num):
476482
print(f"Creating {num} online users")
477483
users = []
478484
for i in range(num):
479-
addr, password = self.gencreds()
485+
addr, password = self.gencreds(format_mail_domain(self.maildomain))
480486
user = CMUser(self.maildomain, addr, password, self.ssl_context)
481487
assert user.smtp
482488
users.append(user)

0 commit comments

Comments
 (0)