Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Features
* **Brevo (Sendinblue):** Add support for inbound email. (See
`docs <https://anymail.dev/en/latest/esps/sendinblue/#sendinblue-inbound>`_.)

* **SendGrid:** Support for multiple ``reply_to`` addresses.
(Thanks to `@gdvalderrama`_ for pointing out the new API.)

Deprecations
~~~~~~~~~~~~
Expand Down Expand Up @@ -1546,6 +1548,7 @@ Features
.. _@ewingrj: https://github.com/ewingrj
.. _@fdemmer: https://github.com/fdemmer
.. _@Flexonze: https://github.com/Flexonze
.. _@gdvalderrama: https://github.com/gdvalderrama
.. _@Honza-m: https://github.com/Honza-m
.. _@janneThoft: https://github.com/janneThoft
.. _@jc-ee: https://github.com/jc-ee
Expand Down
7 changes: 2 additions & 5 deletions anymail/backends/sendgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,8 @@ def set_subject(self, subject):
self.data["subject"] = subject

def set_reply_to(self, emails):
# SendGrid only supports a single address in the reply_to API param.
if len(emails) > 1:
self.unsupported_feature("multiple reply_to addresses")
if len(emails) > 0:
self.data["reply_to"] = self.email_object(emails[0])
if emails:
self.data["reply_to_list"] = [self.email_object(email) for email in emails]

def set_extra_headers(self, headers):
# SendGrid requires header values to be strings -- not integers.
Expand Down
8 changes: 0 additions & 8 deletions docs/esps/sendgrid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,6 @@ Limitations and quirks
webhook :attr:`message_id` will fall back to "smtp-id" when "anymail_id"
isn't present.)

**Single Reply-To**
SendGrid's v3 API only supports a single Reply-To address.

If your message has multiple reply addresses, you'll get an
:exc:`~anymail.exceptions.AnymailUnsupportedFeature` error---or
if you've enabled :setting:`ANYMAIL_IGNORE_UNSUPPORTED_FEATURES`,
Anymail will use only the first one.

**Invalid Addresses**
SendGrid will accept *and send* just about anything as
a message's :attr:`from_email`. (And email protocols are
Expand Down
34 changes: 11 additions & 23 deletions tests/test_sendgrid_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def test_email_message(self):
self.assertEqual(
data["content"], [{"type": "text/plain", "value": "Body goes here"}]
)
self.assertEqual(data["reply_to"], {"email": "another@example.com"})
self.assertEqual(data["reply_to_list"], [{"email": "another@example.com"}])
self.assertEqual(
data["headers"],
{
Expand Down Expand Up @@ -243,7 +243,8 @@ def test_extra_headers(self):
# Reply-To must be moved to separate param
self.assertNotIn("Reply-To", data["headers"])
self.assertEqual(
data["reply_to"], {"name": "Do Not Reply", "email": "noreply@example.com"}
data["reply_to_list"],
[{"name": "Do Not Reply", "email": "noreply@example.com"}],
)

def test_extra_headers_serialization_error(self):
Expand All @@ -252,35 +253,20 @@ def test_extra_headers_serialization_error(self):
self.message.send()

def test_reply_to(self):
self.message.reply_to = ['"Reply recipient" <reply@example.com']
self.message.send()
data = self.get_api_call_json()
self.assertEqual(
data["reply_to"], {"name": "Reply recipient", "email": "reply@example.com"}
)

def test_multiple_reply_to(self):
# SendGrid v3 prohibits Reply-To in custom headers,
# and only allows a single reply address
self.message.reply_to = [
'"Reply recipient" <reply@example.com',
"reply2@example.com",
]
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()

@override_settings(ANYMAIL_IGNORE_UNSUPPORTED_FEATURES=True)
def test_multiple_reply_to_ignore_unsupported(self):
# Should use first Reply-To if ignoring unsupported features
self.message.reply_to = [
'"Reply recipient" <reply@example.com',
"reply2@example.com",
]
self.message.send()
data = self.get_api_call_json()
self.assertEqual(
data["reply_to"], {"name": "Reply recipient", "email": "reply@example.com"}
data["reply_to_list"],
[
{"name": "Reply recipient", "email": "reply@example.com"},
{"email": "reply2@example.com"},
],
)
self.assertNotIn("reply_to", data) # not allowed with reply_to_list

def test_attachments(self):
text_content = "* Item one\n* Item two\n* Item three"
Expand Down Expand Up @@ -1050,6 +1036,8 @@ def test_default_omits_options(self):
self.assertNotIn("headers", data)
self.assertNotIn("ip_pool_name", data)
self.assertNotIn("mail_settings", data)
self.assertNotIn("reply_to", data)
self.assertNotIn("reply_to_list", data)
self.assertNotIn("sections", data)
self.assertNotIn("send_at", data)
self.assertNotIn("template_id", data)
Expand Down
3 changes: 1 addition & 2 deletions tests/test_sendgrid_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ def test_all_options(self):
to=["to1@sink.sendgrid.net", '"Recipient 2, OK?" <to2@sink.sendgrid.net>'],
cc=["cc1@sink.sendgrid.net", "Copy 2 <cc2@sink.sendgrid.net>"],
bcc=["bcc1@sink.sendgrid.net", "Blind Copy 2 <bcc2@sink.sendgrid.net>"],
# v3 only supports single reply-to:
reply_to=['"Reply, with comma" <reply@example.com>'],
reply_to=['"Reply, with comma" <reply@example.com>', "reply2@example.com"],
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
Expand Down