Skip to content

Commit

Permalink
Resend: Remove obsolete display name workaround
Browse files Browse the repository at this point in the history
  • Loading branch information
medmunds committed Feb 21, 2025
1 parent e548f6c commit 6acdf36
Show file tree
Hide file tree
Showing 3 changed files with 7 additions and 138 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ Fixes
Other
~~~~~

* **Resend:** Remove Anymail's workaround for an earlier Resend API bug with
punctuation in address display names. Resend has fixed the bug.

* **SendGrid:** Remove Anymail's workaround for an earlier SendGrid API bug with
punctuation in address display names. SendGrid has fixed the bug.

Expand Down
75 changes: 4 additions & 71 deletions anymail/backends/resend.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import mimetypes
from email.charset import QP, Charset
from email.header import decode_header, make_header
from email.headerregistry import Address

from ..exceptions import AnymailRequestsAPIError
from ..message import AnymailRecipientStatus
Expand All @@ -12,11 +9,6 @@
)
from .base_requests import AnymailRequestsBackend, RequestsPayload

# Used to force RFC-2047 encoded word
# in address formatting workaround
QP_CHARSET = Charset("utf-8")
QP_CHARSET.header_encoding = QP


class EmailBackend(AnymailRequestsBackend):
"""
Expand All @@ -39,18 +31,6 @@ def __init__(self, **kwargs):
)
if not api_url.endswith("/"):
api_url += "/"

# Undocumented setting to control workarounds for Resend display-name issues
# (see below). If/when Resend improves their API, you can disable Anymail's
# workarounds by adding `"RESEND_WORKAROUND_DISPLAY_NAME_BUGS": False`
# to your `ANYMAIL` settings.
self.workaround_display_name_bugs = get_anymail_setting(
"workaround_display_name_bugs",
esp_name=esp_name,
kwargs=kwargs,
default=True,
)

super().__init__(api_url, **kwargs)

def build_message_payload(self, message, defaults):
Expand Down Expand Up @@ -118,7 +98,7 @@ def serialize_data(self):
payload = []
for to_email, to in zip(to_emails, self.to_recipients):
data = self.data.copy()
data["to"] = [to_email] # formatted for Resend (w/ workarounds)
data["to"] = [to_email]
if to.addr_spec in self.merge_metadata:
# Merge global metadata with any per-recipient metadata.
recipient_metadata = self.metadata.copy()
Expand Down Expand Up @@ -149,59 +129,14 @@ def serialize_data(self):
def init_payload(self):
self.data = {} # becomes json

def _resend_email_address(self, address):
"""
Return EmailAddress address formatted for use with Resend.
Works around a Resend bug that rejects properly formatted RFC 5322
addresses that have the display-name enclosed in double quotes (e.g.,
any display-name containing a comma), by substituting an RFC 2047
encoded word.
This works for all Resend address fields _except_ `from` (see below).
"""
formatted = address.address
if self.backend.workaround_display_name_bugs:
if formatted.startswith('"'):
# Workaround: force RFC-2047 encoded word
formatted = str(
Address(
display_name=QP_CHARSET.header_encode(address.display_name),
addr_spec=address.addr_spec,
)
)
return formatted

def set_from_email(self, email):
# Can't use the address header workaround above for the `from` field:
# self.data["from"] = self._resend_email_address(email)
# When `from` uses RFC-2047 encoding, Resend returns a "security_error"
# status 451, "The email payload contain invalid characters".
formatted = email.address
if self.backend.workaround_display_name_bugs:
if formatted.startswith("=?"):
# Workaround: use an *unencoded* (Unicode str) display-name.
# This allows use of non-ASCII characters (which Resend rejects when
# encoded with RFC 2047). Some punctuation will still result in unusual
# behavior or cause an "invalid `from` field" 422 error, but there's
# nothing we can do about that.
formatted = str(
# email.headerregistry.Address str format uses unencoded Unicode
Address(
# Convert RFC 2047 display name back to Unicode str
display_name=str(
make_header(decode_header(email.display_name))
),
addr_spec=email.addr_spec,
)
)
self.data["from"] = formatted
self.data["from"] = email.address

def set_recipients(self, recipient_type, emails):
assert recipient_type in ["to", "cc", "bcc"]
if emails:
field = recipient_type
self.data[field] = [self._resend_email_address(email) for email in emails]
self.data[field] = [email.address for email in emails]
self.recipients += emails
if recipient_type == "to":
self.to_recipients = emails
Expand All @@ -211,9 +146,7 @@ def set_subject(self, subject):

def set_reply_to(self, emails):
if emails:
self.data["reply_to"] = [
self._resend_email_address(email) for email in emails
]
self.data["reply_to"] = [email.address for email in emails]

def set_extra_headers(self, headers):
# Resend requires header values to be strings (not integers) as of 2023-10-20.
Expand Down
67 changes: 0 additions & 67 deletions tests/test_resend_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from decimal import Decimal
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.utils import formataddr

from django.core import mail
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -100,72 +99,6 @@ def test_name_addr(self):
data["bcc"], ["Blind Copy <bcc1@example.com>", "bcc2@example.com"]
)

def test_display_name_workarounds(self):
# Resend's API has a bug that rejects a display-name in double quotes
# (per RFC 5322 section 3.4). Attempting to omit the quotes works, unless
# the display-name also contains a comma. Try to avoid the whole problem
# by using RFC 2047 encoded words for addresses Resend will parse incorrectly.
msg = mail.EmailMessage(
"Subject",
"Message",
formataddr(("Félix Företag, Inc.", "from@example.com")),
[
'"To, comma" <to1@example.com>',
"non–ascii <to2@example.com>",
"=?utf-8?q?pre_encoded?= <to3@example.com>",
],
reply_to=['"Reply, comma" <reply1@example.com>'],
)
msg.send()
data = self.get_api_call_json()
self.assertEqual(
data["from"],
# for `from` field only, avoid RFC 2047 and retain non-ASCII characters:
'"Félix Företag, Inc." <from@example.com>',
)
self.assertEqual(
data["to"],
[
"=?utf-8?q?To=2C_comma?= <to1@example.com>",
"=?utf-8?b?bm9u4oCTYXNjaWk=?= <to2@example.com>",
"=?utf-8?q?pre_encoded?= <to3@example.com>",
],
)
self.assertEqual(
data["reply_to"], ["=?utf-8?q?Reply=2C_comma?= <reply1@example.com>"]
)

@override_settings(ANYMAIL_RESEND_WORKAROUND_DISPLAY_NAME_BUGS=False)
def test_undocumented_workaround_setting(self):
# Same test as above, but workarounds disabled
msg = mail.EmailMessage(
"Subject",
"Message",
'"Félix Företag" <from@example.com>',
[
'"To, comma" <to1@example.com>',
"non–ascii <to2@example.com>",
"=?utf-8?q?pre_encoded?= <to3@example.com>",
],
reply_to=['"Reply, comma" <reply1@example.com>'],
)
msg.send()
data = self.get_api_call_json()
self.assertEqual(
data["from"],
# (Django uses base64 encoded word unless QP is shorter)
"=?utf-8?b?RsOpbGl4IEbDtnJldGFn?= <from@example.com>",
)
self.assertEqual(
data["to"],
[
'"To, comma" <to1@example.com>',
"=?utf-8?b?bm9u4oCTYXNjaWk=?= <to2@example.com>",
"=?utf-8?q?pre_encoded?= <to3@example.com>",
],
)
self.assertEqual(data["reply_to"], ['"Reply, comma" <reply1@example.com>'])

def test_email_message(self):
email = mail.EmailMessage(
"Subject",
Expand Down

0 comments on commit 6acdf36

Please sign in to comment.