diff --git a/CHANGELOG.md b/CHANGELOG.md index d7af3644..187230fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - Update iroh-relay to 0.35.0 ([#650](https://github.com/chatmail/relay/pull/650)) +- filtermail: accept mails from Protonmail + ([#616](https://github.com/chatmail/relay/pull/655)) + - Ignore all RCPT TO: parameters ([#651](https://github.com/chatmail/relay/pull/651)) diff --git a/chatmaild/src/chatmaild/filtermail.py b/chatmaild/src/chatmaild/filtermail.py index 474d7a88..dd140d69 100644 --- a/chatmaild/src/chatmaild/filtermail.py +++ b/chatmaild/src/chatmaild/filtermail.py @@ -83,8 +83,14 @@ def check_openpgp_payload(payload: bytes): return False -def check_armored_payload(payload: str): - prefix = "-----BEGIN PGP MESSAGE-----\r\n\r\n" +def check_armored_payload(payload: str, outgoing: bool): + """Check the armored PGP message for invalid content. + + :param payload: the armored PGP message + :param outgoing: whether the message is outgoing or incoming + :return: whether the message is a valid PGP message + """ + prefix = "-----BEGIN PGP MESSAGE-----\r\n" if not payload.startswith(prefix): return False payload = payload.removeprefix(prefix) @@ -96,6 +102,17 @@ def check_armored_payload(payload: str): return False payload = payload.removesuffix(suffix) + # Disallow comments in outgoing messages + version_comment = "Version: " + if payload.startswith(version_comment): + version_line = payload.splitlines()[0] + payload = payload.removeprefix(version_line) + if outgoing: + return False + + while payload.startswith("\r\n"): + payload = payload.removeprefix("\r\n") + # Remove CRC24. payload = payload.rpartition("=")[0] @@ -131,7 +148,7 @@ def is_securejoin(message): return True -def check_encrypted(message): +def check_encrypted(message, outgoing=True): """Check that the message is an OpenPGP-encrypted message. MIME structure of the message must correspond to . @@ -158,7 +175,7 @@ def check_encrypted(message): if part.get_content_type() != "application/octet-stream": return False - if not check_armored_payload(part.get_payload()): + if not check_armored_payload(part.get_payload(), outgoing=outgoing): return False else: return False @@ -241,7 +258,7 @@ class OutgoingBeforeQueueHandler: logging.info(f"Processing DATA message from {envelope.mail_from}") message = BytesParser(policy=policy.default).parsebytes(envelope.content) - mail_encrypted = check_encrypted(message) + mail_encrypted = check_encrypted(message, outgoing=True) _, from_addr = parseaddr(message.get("from").strip()) @@ -301,7 +318,7 @@ class IncomingBeforeQueueHandler: logging.info(f"Processing DATA message from {envelope.mail_from}") message = BytesParser(policy=policy.default).parsebytes(envelope.content) - mail_encrypted = check_encrypted(message) + mail_encrypted = check_encrypted(message, outgoing=False) if mail_encrypted or is_securejoin(message): print("Incoming: Filtering encrypted mail.", file=sys.stderr) diff --git a/chatmaild/src/chatmaild/tests/test_filtermail.py b/chatmaild/src/chatmaild/tests/test_filtermail.py index d11f34e9..e39f4a09 100644 --- a/chatmaild/src/chatmaild/tests/test_filtermail.py +++ b/chatmaild/src/chatmaild/tests/test_filtermail.py @@ -241,8 +241,9 @@ def test_cleartext_passthrough_senders(gencreds, handler, maildata): def test_check_armored_payload(): - payload = """-----BEGIN PGP MESSAGE-----\r -\r + prefix = "-----BEGIN PGP MESSAGE-----\r\n" + comment = "Version: ProtonMail\r\n" + payload = """\r wU4DSqFx0d1yqAoSAQdAYkX/ZN/Az4B0k7X47zKyWrXxlDEdS3WOy0Yf2+GJTFgg\r Zk5ql0mLG8Ze+ZifCS0XMO4otlemSyJ0K1ZPdFMGzUDBTgNqzkFabxXoXRIBB0AM\r 755wlX41X6Ay3KhnwBq7yEqSykVH6F3x11iHPKraLCAGZoaS8bKKNy/zg5slda1X\r @@ -278,16 +279,25 @@ UN4fiB0KR9JyG2ayUdNJVkXZSZLnHyRgiaadlpUo16LVvw==\r \r """ - assert check_armored_payload(payload) == True + commented_payload = prefix + comment + payload + assert check_armored_payload(commented_payload, outgoing=False) == True + assert check_armored_payload(commented_payload, outgoing=True) == False + + payload = prefix + payload + assert check_armored_payload(payload, outgoing=False) == True + assert check_armored_payload(payload, outgoing=True) == True payload = payload.removesuffix("\r\n") - assert check_armored_payload(payload) == True + assert check_armored_payload(payload, outgoing=False) == True + assert check_armored_payload(payload, outgoing=True) == True payload = payload.removesuffix("\r\n") - assert check_armored_payload(payload) == True + assert check_armored_payload(payload, outgoing=False) == True + assert check_armored_payload(payload, outgoing=True) == True payload = payload.removesuffix("\r\n") - assert check_armored_payload(payload) == True + assert check_armored_payload(payload, outgoing=False) == True + assert check_armored_payload(payload, outgoing=True) == True payload = """-----BEGIN PGP MESSAGE-----\r \r @@ -295,7 +305,8 @@ HELLOWORLD -----END PGP MESSAGE-----\r \r """ - assert check_armored_payload(payload) == False + assert check_armored_payload(payload, outgoing=False) == False + assert check_armored_payload(payload, outgoing=True) == False payload = """-----BEGIN PGP MESSAGE-----\r \r @@ -303,7 +314,8 @@ HELLOWORLD -----END PGP MESSAGE-----\r \r """ - assert check_armored_payload(payload) == False + assert check_armored_payload(payload, outgoing=False) == False + assert check_armored_payload(payload, outgoing=True) == False # Test payload using partial body length # as generated by GopenPGP. @@ -345,4 +357,5 @@ myLbG7cJB787QjplEyVe2P/JBO6xYvbkJLf9Q+HaviTO25rugRSrYsoKMDfO8VlQ\r =6iHb\r -----END PGP MESSAGE-----\r """ - assert check_armored_payload(payload) == True + assert check_armored_payload(payload, outgoing=False) == True + assert check_armored_payload(payload, outgoing=True) == True