Skip to content

Commit

Permalink
Postmark inbound: Fix ValueError with long, non-ASCII name
Browse files Browse the repository at this point in the history
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.)
  • Loading branch information
medmunds committed Dec 11, 2024
1 parent 77b9701 commit 5987e83
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
6 changes: 5 additions & 1 deletion anymail/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions tests/test_postmark_inbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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)

0 comments on commit 5987e83

Please sign in to comment.