From 5987e8377a7ec93687203f7a0d12fdb7ed54ef06 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Wed, 11 Dec 2024 11:54:54 -0800 Subject: [PATCH] Postmark inbound: Fix ValueError with long, non-ASCII name Receiving a Postmark inbound message with a long From or recipient Name field could result in "ValueError: Header values may not contain linefeed or carriage return characters" if the name included non-ASCII characters. Anymail's EmailAddress calls Django's sanitize_address(), which can introduce folding whitespace, causing an error in the AnymailInboundMessage constructor. Only Postmark inbound is affected, as no other webhooks construct an EmailAddress. (Longer-term, Anymail should stop using the undocumented Django sanitize_address.) --- CHANGELOG.rst | 3 +++ anymail/utils.py | 6 +++++- tests/test_postmark_inbound.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ffd931a0..43b65e52 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -53,6 +53,9 @@ Fixes "queued" in the ``anymail_status`` (rather than throwing an error or reporting "sent"). (Thanks to `@jmduke`_ for reporting the issue.) +* **Postmark:** Fix an error in inbound handling with long email address display + names that include non-ASCII characters. + v12.0 ----- diff --git a/anymail/utils.py b/anymail/utils.py index da804f8a..404a4533 100644 --- a/anymail/utils.py +++ b/anymail/utils.py @@ -1,5 +1,6 @@ import base64 import mimetypes +import re from base64 import b64encode from collections.abc import Mapping, MutableMapping from copy import copy, deepcopy @@ -342,7 +343,10 @@ def formataddr(self, encoding=None): default None uses ascii if possible, else 'utf-8' (quoted-printable utf-8/base64) """ - return sanitize_address((self.display_name, self.addr_spec), encoding) + sanitized = sanitize_address((self.display_name, self.addr_spec), encoding) + # sanitize_address() can introduce FWS with a long, non-ASCII display name. + # Must unfold it: + return re.sub(r"(\r|\n|\r\n)[ \t]", "", sanitized) def __str__(self): return self.address diff --git a/tests/test_postmark_inbound.py b/tests/test_postmark_inbound.py index 10e98fdf..707579a3 100644 --- a/tests/test_postmark_inbound.py +++ b/tests/test_postmark_inbound.py @@ -392,3 +392,32 @@ def test_check_payload(self): ) # Don't care about the actual test message contents here, # just want to make sure it parses and signals inbound without error. + + def test_spammy_address(self): + # This long FromFull.Name caused an error in AnymailInboundMessage construction, + # due to folding introduced in EmailAddress.formataddr() + from_name = ( + "* * * 💲 Snag Your Free Gift! Click Here: " + "https://spam.example.com/uploads/phish.php?123456 💲 * * *" + ) + raw_event = { + "MessageStream": "inbound", + "FromFull": { + "Email": "noreply@spam.example.com", + "Name": from_name, + }, + } + response = self.client.post( + "/anymail/postmark/inbound/", + content_type="application/json", + data=json.dumps(raw_event), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.inbound_handler, + sender=PostmarkInboundWebhookView, + event=ANY, + esp_name="Postmark", + ) + message = kwargs["event"].message + self.assertEqual(message.from_email.display_name, from_name)