mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Amazon SES: Work around SES bug that corrupts non-ASCII message bodies.
If you are using an SES ConfigurationSet with open or click tracking enabled, SES replaces non-ASCII characters with question marks as it rewrites the message to add tracking, if the bodies are sent with `Content-Transfer-Encoding: 8bit` (which is Django's default for utf8 body parts). Force potentially problematic parts to use CTE: quoted-printable as a workaround. Fixes #115.
This commit is contained in:
@@ -60,6 +60,11 @@ Fixes
|
|||||||
ending in ".com" could cause Gmail to block messages sent with inline attachments.
|
ending in ".com" could cause Gmail to block messages sent with inline attachments.
|
||||||
(Mailgun, Mailjet, Mandrill and SparkPost have APIs affected by this.
|
(Mailgun, Mailjet, Mandrill and SparkPost have APIs affected by this.
|
||||||
See `#112`_ for more details.)
|
See `#112`_ for more details.)
|
||||||
|
* **Amazon SES:** Work around an
|
||||||
|
`Amazon SES bug <https://forums.aws.amazon.com/thread.jspa?threadID=287048>`__
|
||||||
|
that can corrupt non-ASCII message bodies if you are using SES's open or click
|
||||||
|
tracking. (See `#115`_ for more details. Thanks to `@varche1`_ for isolating
|
||||||
|
the specific conditions that trigger the bug.)
|
||||||
|
|
||||||
Other
|
Other
|
||||||
~~~~~
|
~~~~~
|
||||||
@@ -774,6 +779,7 @@ Features
|
|||||||
.. _#110: https://github.com/anymail/issues/110
|
.. _#110: https://github.com/anymail/issues/110
|
||||||
.. _#111: https://github.com/anymail/issues/111
|
.. _#111: https://github.com/anymail/issues/111
|
||||||
.. _#112: https://github.com/anymail/issues/112
|
.. _#112: https://github.com/anymail/issues/112
|
||||||
|
.. _#115: https://github.com/anymail/issues/115
|
||||||
|
|
||||||
.. _@calvin: https://github.com/calvin
|
.. _@calvin: https://github.com/calvin
|
||||||
.. _@joshkersey: https://github.com/joshkersey
|
.. _@joshkersey: https://github.com/joshkersey
|
||||||
@@ -781,4 +787,5 @@ Features
|
|||||||
.. _@lewistaylor: https://github.com/lewistaylor
|
.. _@lewistaylor: https://github.com/lewistaylor
|
||||||
.. _@RignonNoel: https://github.com/RignonNoel
|
.. _@RignonNoel: https://github.com/RignonNoel
|
||||||
.. _@sebbacon: https://github.com/sebbacon
|
.. _@sebbacon: https://github.com/sebbacon
|
||||||
|
.. _@varche1: https://github.com/varche1
|
||||||
.. _@yourcelf: https://github.com/yourcelf
|
.. _@yourcelf: https://github.com/yourcelf
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
from email.charset import Charset, QP
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
from django.core.mail import BadHeaderError
|
from django.core.mail import BadHeaderError
|
||||||
|
|
||||||
@@ -130,6 +132,23 @@ class AmazonSESSendRawEmailPayload(AmazonSESBasePayload):
|
|||||||
# (message.message() will have already checked subject for BadHeaderError)
|
# (message.message() will have already checked subject for BadHeaderError)
|
||||||
self.mime_message.replace_header("Subject", HeaderBugWorkaround(self.message.subject))
|
self.mime_message.replace_header("Subject", HeaderBugWorkaround(self.message.subject))
|
||||||
|
|
||||||
|
# Work around an Amazon SES bug where, if all of:
|
||||||
|
# - the message body (text or html) contains non-ASCII characters
|
||||||
|
# - the body is sent with `Content-Transfer-Encoding: 8bit`
|
||||||
|
# (which is Django email's default for most non-ASCII bodies)
|
||||||
|
# - you are using an SES ConfigurationSet with open or click tracking enabled
|
||||||
|
# then SES replaces the non-ASCII characters with question marks as it rewrites
|
||||||
|
# the message to add tracking. Forcing `CTE: quoted-printable` avoids the problem.
|
||||||
|
# (https://forums.aws.amazon.com/thread.jspa?threadID=287048)
|
||||||
|
for part in self.mime_message.walk():
|
||||||
|
if part.get_content_maintype() == "text" and part["Content-Transfer-Encoding"] == "8bit":
|
||||||
|
content = part.get_payload()
|
||||||
|
del part["Content-Transfer-Encoding"]
|
||||||
|
qp_charset = Charset(part.get_content_charset("us-ascii"))
|
||||||
|
qp_charset.body_encoding = QP
|
||||||
|
# (can't use part.set_payload, because SafeMIMEText can undo this workaround)
|
||||||
|
MIMEText.set_payload(part, content, charset=qp_charset)
|
||||||
|
|
||||||
def call_send_api(self, ses_client):
|
def call_send_api(self, ses_client):
|
||||||
self.params["RawMessage"] = {
|
self.params["RawMessage"] = {
|
||||||
# Note: "Destinations" is determined from message headers if not provided
|
# Note: "Destinations" is determined from message headers if not provided
|
||||||
|
|||||||
@@ -244,6 +244,26 @@ class AmazonSESBackendStandardEmailTests(AmazonSESBackendMockAPITestCase):
|
|||||||
sent_message = self.get_sent_message()
|
sent_message = self.get_sent_message()
|
||||||
self.assertEqual(sent_message["Subject"], self.message.subject)
|
self.assertEqual(sent_message["Subject"], self.message.subject)
|
||||||
|
|
||||||
|
def test_body_avoids_cte_8bit(self):
|
||||||
|
"""Anymail works around an Amazon SES bug that can corrupt non-ASCII bodies."""
|
||||||
|
# (see detailed comments in the backend code)
|
||||||
|
self.message.body = "Это text body"
|
||||||
|
self.message.attach_alternative("<p>Это html body</p>", "text/html")
|
||||||
|
self.message.send()
|
||||||
|
sent_message = self.get_sent_message()
|
||||||
|
|
||||||
|
# Make sure none of the text parts use `Content-Transfer-Encoding: 8bit`.
|
||||||
|
# (Technically, either quoted-printable or base64 would be OK, but base64 text parts
|
||||||
|
# have a reputation for triggering spam filters, so just require quoted-printable.)
|
||||||
|
text_part_encodings = [
|
||||||
|
(part.get_content_type(), part["Content-Transfer-Encoding"])
|
||||||
|
for part in sent_message.walk()
|
||||||
|
if part.get_content_maintype() == "text"]
|
||||||
|
self.assertEqual(text_part_encodings, [
|
||||||
|
("text/plain", "quoted-printable"),
|
||||||
|
("text/html", "quoted-printable"),
|
||||||
|
])
|
||||||
|
|
||||||
def test_api_failure(self):
|
def test_api_failure(self):
|
||||||
error_response = {
|
error_response = {
|
||||||
'Error': {
|
'Error': {
|
||||||
|
|||||||
Reference in New Issue
Block a user