diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe66aa1..8e0a97f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,19 @@ Release history ^^^^^^^^^^^^^^^ .. This extra heading level keeps the ToC from becoming unmanageably long +vNext +----- + +*Unreleased changes in master* + +Fixes +~~~~~ + +* **Amazon SES:** Fix bcc, which wasn't working at all on non-template sends. + (Thanks to `@mwheels`_ for reporting the issue.) + + + v7.1 ----- @@ -1064,6 +1077,7 @@ Features .. _@Lekensteyn: https://github.com/Lekensteyn .. _@lewistaylor: https://github.com/lewistaylor .. _@mbk-ok: https://github.com/mbk-ok +.. _@mwheels: https://github.com/mwheels .. _@nuschk: https://github.com/nuschk .. _@RignonNoel: https://github.com/RignonNoel .. _@sebashwa: https://github.com/sebashwa diff --git a/anymail/backends/amazon_ses.py b/anymail/backends/amazon_ses.py index 700b2a8..b68743b 100644 --- a/anymail/backends/amazon_ses.py +++ b/anymail/backends/amazon_ses.py @@ -150,9 +150,11 @@ class AmazonSESSendRawEmailPayload(AmazonSESBasePayload): MIMEText.set_payload(part, content, charset=qp_charset) def call_send_api(self, ses_client): + # Set Destinations to make sure we pick up all recipients (including bcc). + # Any non-ASCII characters in recipient domains must be encoded with Punycode. + # (Amazon SES doesn't support non-ASCII recipient usernames.) + self.params["Destinations"] = [email.address for email in self.all_recipients] self.params["RawMessage"] = { - # Note: "Destinations" is determined from message headers if not provided - # "Destinations": [email.addr_spec for email in self.all_recipients], "Data": self.mime_message.as_bytes() } return ses_client.send_raw_email(**self.params) @@ -225,8 +227,14 @@ class AmazonSESSendRawEmailPayload(AmazonSESBasePayload): def set_spoofed_to_header(self, header_to): # django.core.mail.EmailMessage.message() has already set # self.mime_message["To"] = header_to - # and performed any necessary header sanitization - self.params["Destinations"] = [email.addr_spec for email in self.all_recipients] + # and performed any necessary header sanitization. + # + # The actual "to" is already in self.all_recipients, + # which is used as the SendRawEmail Destinations later. + # + # So, nothing to do here, except prevent the default + # "unsupported feature" error. + pass def set_metadata(self, metadata): # Amazon SES has two mechanisms for adding custom data to a message: diff --git a/tests/test_amazon_ses_backend.py b/tests/test_amazon_ses_backend.py index e8478b0..0aa976a 100644 --- a/tests/test_amazon_ses_backend.py +++ b/tests/test_amazon_ses_backend.py @@ -127,6 +127,8 @@ class AmazonSESBackendStandardEmailTests(AmazonSESBackendMockAPITestCase): self.assertIn(b"\nTo: to@example.com\n", raw_mime) self.assertIn(b"\nSubject: Subject here\n", raw_mime) self.assertIn(b"\n\nHere is the message", raw_mime) + # Destinations must include all recipients: + self.assertEqual(params['Destinations'], ['to@example.com']) # Since the SES backend generates the MIME message using Django's # EmailMessage.message().to_string(), there's not really a need @@ -134,6 +136,20 @@ class AmazonSESBackendStandardEmailTests(AmazonSESBackendMockAPITestCase): # (EmailMessage.message() is well tested in the Django codebase.) # Instead, just spot-check a few things... + def test_destinations(self): + self.message.to = ['to1@example.com', '"Recipient, second" '] + self.message.cc = ['cc1@example.com', 'Also cc '] + self.message.bcc = ['bcc1@example.com', 'BCC 2 '] + self.message.send() + params = self.get_send_params() + self.assertEqual(params['Destinations'], [ + 'to1@example.com', '"Recipient, second" ', + 'cc1@example.com', 'Also cc ', + 'bcc1@example.com', 'BCC 2 ', + ]) + # Bcc's shouldn't appear in the message itself: + self.assertNotIn(b'bcc', params['RawMessage']['Data']) + def test_non_ascii_headers(self): self.message.subject = "Thử tin nhắn" # utf-8 in subject header self.message.to = ['"Người nhận" '] # utf-8 in display name @@ -149,6 +165,11 @@ class AmazonSESBackendStandardEmailTests(AmazonSESBackendMockAPITestCase): self.assertIn(b"\nCc: cc@xn--th-e0a.example.com\n", raw_mime) # SES doesn't support non-ASCII in the username@ part (RFC 6531 "SMTPUTF8" extension) + # Destinations must include all recipients: + self.assertEqual(params['Destinations'], [ + '=?utf-8?b?TmfGsOG7nWkgbmjhuq1u?= ', + 'cc@xn--th-e0a.example.com']) + def test_attachments(self): text_content = "• Item one\n• Item two\n• Item three" # those are \u2022 bullets ("\N{BULLET}") self.message.attach(filename="Une pièce jointe.txt", # utf-8 chars in filename @@ -336,7 +357,7 @@ class AmazonSESBackendAnymailFeatureTests(AmazonSESBackendMockAPITestCase): self.message.send() params = self.get_send_params() raw_mime = params['RawMessage']['Data'] - self.assertEqual(params['Destinations'], ["envelope-to@example.com"]) + self.assertEqual(params['Destinations'], ["Envelope "]) self.assertIn(b"\nTo: Spoofed \n", raw_mime) self.assertNotIn(b"envelope-to@example.com", raw_mime) @@ -533,7 +554,6 @@ class AmazonSESBackendAnymailFeatureTests(AmazonSESBackendMockAPITestCase): self.assertNotIn('ConfigurationSetName', params) self.assertNotIn('DefaultTags', params) self.assertNotIn('DefaultTemplateData', params) - self.assertNotIn('Destinations', params) self.assertNotIn('FromArn', params) self.assertNotIn('Message', params) self.assertNotIn('ReplyToAddresses', params)