Skip to content

Commit 567fbb5

Browse files
committed
Amazon SES: Fix header encoding problem
A combination of long display name and commas (or other special characters) could result in invalid address headers. See details in #369. Fix by removing unnecessary email.policy override, which was causing new header folding code to run with headers built using Compat32 legacy header encoding. The two don't mix. Fixes #369
1 parent c4b2e08 commit 567fbb5

File tree

3 files changed

+40
-9
lines changed

3 files changed

+40
-9
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ Features
6565
Fixes
6666
~~~~~
6767

68+
* **Amazon SES:** Fix a bug that could result in sending a broken address header
69+
if it had a long display name containing both non-ASCII characters and commas.
70+
(Thanks to `@andresmrm`_ for isolating and reporting the issue.)
71+
6872
* **SendGrid:** In the tracking webhook, correctly report "bounced address"
6973
(recipients dropped due to earlier bounces) as reject reason ``"bounced"``.
7074
(Thanks to `@vitaliyf`_.)
@@ -1639,6 +1643,7 @@ Features
16391643

16401644
.. _@ailionx: https://github.com/ailionx
16411645
.. _@alee: https://github.com/alee
1646+
.. _@andresmrm: https://github.com/andresmrm
16421647
.. _@anstosa: https://github.com/anstosa
16431648
.. _@Arondit: https://github.com/Arondit
16441649
.. _@b0d0nne11: https://github.com/b0d0nne11

anymail/backends/amazon_ses.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,10 @@ def generate_raw_message(self):
165165
Serialize self.mime_message as an RFC-5322/-2045 MIME message,
166166
encoded as 7bit-clean, us-ascii byte data.
167167
"""
168-
# Amazon SES does not support `Content-Transfer-Encoding: 8bit`. And using 8bit
169-
# with SES open or click tracking results in mis-encoded characters. To avoid
170-
# this, convert any 8bit parts to 7bit quoted printable or base64. (We own
171-
# self.mime_message, so destructively modifying it should be OK.)
172-
# (You might think cte_type="7bit" in the email.policy below would cover this,
173-
# but it seems that cte_type is only examined as the MIME parts are constructed,
174-
# not when an email.generator serializes them.)
168+
# Amazon SES discourages `Content-Transfer-Encoding: 8bit`. And using
169+
# 8bit with SES open or click tracking results in mis-encoded characters.
170+
# To avoid this, convert any 8bit parts to 7bit quoted printable or base64.
171+
# (We own self.mime_message, so destructively modifying it should be OK.)
175172
for part in self.mime_message.walk():
176173
if part["Content-Transfer-Encoding"] == "8bit":
177174
del part["Content-Transfer-Encoding"]
@@ -181,7 +178,8 @@ def generate_raw_message(self):
181178
else:
182179
email.encoders.encode_base64(part)
183180

184-
self.mime_message.policy = email.policy.default.clone(cte_type="7bit")
181+
# (All message and part headers should already be 7bit clean,
182+
# so there's no need to try to override email.policy here.)
185183
return self.mime_message.as_bytes()
186184

187185
def parse_recipient_status(self, response):

tests/test_amazon_ses_backend.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,17 @@ def test_commas_in_subject(self):
318318
sent_message = self.get_sent_message()
319319
self.assertEqual(sent_message["Subject"], self.message.subject)
320320

321+
def test_broken_address_header(self):
322+
# https://github.com/anymail/django-anymail/issues/369
323+
self.message.to = ['"Người nhận a very very long, name" <to@example.com>']
324+
self.message.cc = [
325+
'"A véry long name with non-ASCII char and, comma" <cc@example.com>'
326+
]
327+
self.message.send()
328+
sent_message = self.get_sent_message()
329+
self.assertEqual(sent_message["To"], self.message.to[0])
330+
self.assertEqual(sent_message["Cc"], self.message.cc[0])
331+
321332
def test_no_cte_8bit(self):
322333
"""Anymail works around an Amazon SES bug that can corrupt non-ASCII bodies."""
323334
# (see detailed comments in the backend code)
@@ -333,12 +344,14 @@ def test_no_cte_8bit(self):
333344
self.message.attach(att)
334345

335346
self.message.send()
336-
sent_message = self.get_sent_message()
347+
raw_mime = self.get_send_params()["Content"]["Raw"]["Data"]
348+
self.assertTrue(raw_mime.isascii()) # 7-bit clean
337349

338350
# Make sure none of the resulting parts use `Content-Transfer-Encoding: 8bit`.
339351
# (Technically, either quoted-printable or base64 would be OK, but base64 text
340352
# parts have a reputation for triggering spam filters, so just require
341353
# quoted-printable for them.)
354+
sent_message = self.get_sent_message()
342355
part_encodings = [
343356
(part.get_content_type(), part["Content-Transfer-Encoding"])
344357
for part in sent_message.walk()
@@ -355,6 +368,21 @@ def test_no_cte_8bit(self):
355368
],
356369
)
357370

371+
def test_no_cte_8bit_root(self):
372+
# Same test as above, but with a non-multipart message using 8bit at root
373+
self.message.body = "Это text body"
374+
self.message.send()
375+
376+
raw_mime = self.get_send_params()["Content"]["Raw"]["Data"]
377+
self.assertTrue(raw_mime.isascii()) # 7-bit clean
378+
379+
sent_message = self.get_sent_message()
380+
part_encodings = [
381+
(part.get_content_type(), part["Content-Transfer-Encoding"])
382+
for part in sent_message.walk()
383+
]
384+
self.assertEqual(part_encodings, [("text/plain", "quoted-printable")])
385+
358386
def test_api_failure(self):
359387
error_response = {
360388
"Error": {

0 commit comments

Comments
 (0)