diff --git a/chatmaild/src/chatmaild/filtermail.py b/chatmaild/src/chatmaild/filtermail.py index 79fe1b66..0d6638ba 100644 --- a/chatmaild/src/chatmaild/filtermail.py +++ b/chatmaild/src/chatmaild/filtermail.py @@ -34,6 +34,41 @@ def check_encrypted(message): return True +def check_mdn(message, envelope): + if len(envelope.rcpt_tos) != 1: + return False + + for name in ["auto-submitted", "chat-version"]: + if not message.get(name): + return False + + if message.get_content_type() != "multipart/report": + return False + + body = message.get_body() + if body.get_content_type() != "text/plain": + return False + + if list(body.iter_attachments()) or list(body.iter_parts()): + return False + + # even with all mime-structural checks an attacker + # could try to abuse the subject or body to contain links or other + # annoyance -- we only check for http links for now + # and reasonable sizes + + subject = message.get("subject") + if "http" in subject or len(subject) > 50: + return False # actually could serve as a flag for malicious attempt + + text = body.get_payload() + # how long the read-receipt can become? + if len(text) > 500 or "http" in text: + return False + + return True + + class SMTPController(Controller): def factory(self): return SMTP(self.handler, **self.SMTP_kwargs) @@ -82,6 +117,9 @@ def check_DATA(envelope): if envelope.mail_from.lower() != from_addr.lower(): return f"500 Invalid FROM <{from_addr!r}> for <{envelope.mail_from!r}>" + if not mail_encrypted and check_mdn(message, envelope): + return + envelope_from_domain = from_addr.split("@").pop() for recipient in envelope.rcpt_tos: if envelope.mail_from == recipient: diff --git a/tests/chatmaild/test_filtermail.py b/tests/chatmaild/test_filtermail.py index 13b0c92f..df71fd8a 100644 --- a/tests/chatmaild/test_filtermail.py +++ b/tests/chatmaild/test_filtermail.py @@ -1,4 +1,4 @@ -from chatmaild.filtermail import check_encrypted, check_DATA, SendRateLimiter +from chatmaild.filtermail import check_encrypted, check_DATA, SendRateLimiter, check_mdn import pytest @@ -41,8 +41,33 @@ def test_filtermail_encryption_detection(maildata): assert not check_encrypted(msg) -def test_filtermail_mdn_is_not_encrypted(maildata): - assert not check_encrypted(maildata("mdn.eml")) +def test_filtermail_is_mdn(maildata, gencreds): + from_addr = gencreds()[0] + to_addr = gencreds()[0] + ".other" + msg = maildata("mdn.eml", from_addr, to_addr) + + class env: + mail_from = from_addr + rcpt_tos = [to_addr] + content = msg.as_bytes() + + assert check_mdn(msg, env) + print(msg.as_string()) + assert not check_DATA(env) + + +def test_filtermail_to_multiple_recipients_no_mdn(maildata, gencreds): + from_addr = gencreds()[0] + to_addr = gencreds()[0] + ".other" + thirdaddr = gencreds()[0] + msg = maildata("mdn.eml", from_addr, to_addr) + + class env: + mail_from = from_addr + rcpt_tos = [to_addr, thirdaddr] + content = msg.as_bytes() + + assert not check_mdn(msg, env) def test_send_rate_limiter(): diff --git a/tests/conftest.py b/tests/conftest.py index 37a789ee..b97536d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -290,7 +290,7 @@ class Remote: def maildata(request, gencreds): datadir = conftestdir.joinpath("mail-data") - def maildata(name, parsed=True, from_addr=None, to_addr=None): + def maildata(name, from_addr=None, to_addr=None): if from_addr is None: from_addr = gencreds()[0] if to_addr is None: diff --git a/tests/mail-data/mdn.eml b/tests/mail-data/mdn.eml index 5d9558d4..67ea71f0 100644 --- a/tests/mail-data/mdn.eml +++ b/tests/mail-data/mdn.eml @@ -1,6 +1,6 @@ Subject: Message opened -From: -To: +From: <{from_addr}> +To: <{to_addr}> Date: Sun, 15 Oct 2023 16:43:25 +0000 Message-ID: Auto-Submitted: auto-replied