mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Mailgun, SparkPost: support multiple from_email addresses
[RFC-5322 allows](https://tools.ietf.org/html/rfc5322#section-3.6.2) multiple addresses in the From header. Django's SMTP backend supports this, as a single comma-separated string (*not* a list of strings like the recipient params): from_email='one@example.com, two@example.com' to=['one@example.com', 'two@example.com'] Both Mailgun and SparkPost support multiple From addresses (and Postmark accepts them, though truncates to the first one on their end). For compatibility with Django -- and because Anymail attempts to support all ESP features -- Anymail now allows multiple From addresses, too, for ESPs that support it. Note: as a practical matter, deliverability with multiple From addresses is pretty bad. (Google outright rejects them.) This change also reworks Anymail's internal ParsedEmail object, and approach to parsing addresses, for better consistency with Django's SMTP backend and improved error messaging. In particular, Django (and now Anymail) allows multiple email addresses in a single recipient string: to=['one@example.com', 'two@example.com, three@example.com'] len(to) == 2 # but there will be three recipients Fixes #60
This commit is contained in:
@@ -7,66 +7,135 @@ from django.test import SimpleTestCase, RequestFactory, override_settings
|
||||
from django.utils.translation import ugettext_lazy, string_concat
|
||||
|
||||
from anymail.exceptions import AnymailInvalidAddress
|
||||
from anymail.utils import (ParsedEmail,
|
||||
from anymail.utils import (parse_address_list, ParsedEmail,
|
||||
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
|
||||
update_deep,
|
||||
get_request_uri, get_request_basic_auth)
|
||||
|
||||
|
||||
class ParsedEmailTests(SimpleTestCase):
|
||||
"""Test utils.ParsedEmail"""
|
||||
|
||||
# Anymail (and Djrill) have always used EmailMessage.encoding, which defaults to None.
|
||||
# (Django substitutes settings.DEFAULT_ENCODING='utf-8' when converting to a mime message,
|
||||
# but Anymail has never used that code.)
|
||||
ADDRESS_ENCODING = None
|
||||
class ParseAddressListTests(SimpleTestCase):
|
||||
"""Test utils.parse_address_list"""
|
||||
|
||||
def test_simple_email(self):
|
||||
parsed = ParsedEmail("test@example.com", self.ADDRESS_ENCODING)
|
||||
parsed_list = parse_address_list(["test@example.com"])
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
parsed = parsed_list[0]
|
||||
self.assertIsInstance(parsed, ParsedEmail)
|
||||
self.assertEqual(parsed.email, "test@example.com")
|
||||
self.assertEqual(parsed.name, "")
|
||||
self.assertEqual(parsed.address, "test@example.com")
|
||||
self.assertEqual(parsed.localpart, "test")
|
||||
self.assertEqual(parsed.domain, "example.com")
|
||||
|
||||
def test_display_name(self):
|
||||
parsed = ParsedEmail('"Display Name, Inc." <test@example.com>', self.ADDRESS_ENCODING)
|
||||
parsed_list = parse_address_list(['"Display Name, Inc." <test@example.com>'])
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
parsed = parsed_list[0]
|
||||
self.assertEqual(parsed.email, "test@example.com")
|
||||
self.assertEqual(parsed.name, "Display Name, Inc.")
|
||||
self.assertEqual(parsed.address, '"Display Name, Inc." <test@example.com>')
|
||||
self.assertEqual(parsed.localpart, "test")
|
||||
self.assertEqual(parsed.domain, "example.com")
|
||||
|
||||
def test_obsolete_display_name(self):
|
||||
# you can get away without the quotes if there are no commas or parens
|
||||
# (but it's not recommended)
|
||||
parsed = ParsedEmail('Display Name <test@example.com>', self.ADDRESS_ENCODING)
|
||||
parsed_list = parse_address_list(['Display Name <test@example.com>'])
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
parsed = parsed_list[0]
|
||||
self.assertEqual(parsed.email, "test@example.com")
|
||||
self.assertEqual(parsed.name, "Display Name")
|
||||
self.assertEqual(parsed.address, 'Display Name <test@example.com>')
|
||||
|
||||
def test_unicode_display_name(self):
|
||||
parsed = ParsedEmail(u'"Unicode \N{HEAVY BLACK HEART}" <test@example.com>', self.ADDRESS_ENCODING)
|
||||
parsed_list = parse_address_list([u'"Unicode \N{HEAVY BLACK HEART}" <test@example.com>'])
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
parsed = parsed_list[0]
|
||||
self.assertEqual(parsed.email, "test@example.com")
|
||||
self.assertEqual(parsed.name, u"Unicode \N{HEAVY BLACK HEART}")
|
||||
# display-name automatically shifts to quoted-printable/base64 for non-ascii chars:
|
||||
# formatted display-name automatically shifts to quoted-printable/base64 for non-ascii chars:
|
||||
self.assertEqual(parsed.address, '=?utf-8?b?VW5pY29kZSDinaQ=?= <test@example.com>')
|
||||
|
||||
def test_invalid_display_name(self):
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
with self.assertRaisesMessage(AnymailInvalidAddress, "Invalid email address 'webmaster'"):
|
||||
parse_address_list(['webmaster'])
|
||||
|
||||
with self.assertRaisesMessage(AnymailInvalidAddress, "Maybe missing quotes around a display-name?"):
|
||||
# this parses as multiple email addresses, because of the comma:
|
||||
ParsedEmail('Display Name, Inc. <test@example.com>', self.ADDRESS_ENCODING)
|
||||
parse_address_list(['Display Name, Inc. <test@example.com>'])
|
||||
|
||||
def test_idn(self):
|
||||
parsed_list = parse_address_list([u"idn@\N{ENVELOPE}.example.com"])
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
parsed = parsed_list[0]
|
||||
self.assertEqual(parsed.email, u"idn@\N{ENVELOPE}.example.com")
|
||||
self.assertEqual(parsed.address, "idn@xn--4bi.example.com") # punycode-encoded domain
|
||||
self.assertEqual(parsed.localpart, "idn")
|
||||
self.assertEqual(parsed.domain, u"\N{ENVELOPE}.example.com")
|
||||
|
||||
def test_none_address(self):
|
||||
# used for, e.g., telling Mandrill to use template default from_email
|
||||
parsed = ParsedEmail(None, self.ADDRESS_ENCODING)
|
||||
self.assertEqual(parsed.email, None)
|
||||
self.assertEqual(parsed.name, None)
|
||||
self.assertEqual(parsed.address, None)
|
||||
self.assertEqual(parse_address_list([None]), [])
|
||||
self.assertEqual(parse_address_list(None), [])
|
||||
|
||||
def test_empty_address(self):
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
ParsedEmail('', self.ADDRESS_ENCODING)
|
||||
parse_address_list([''])
|
||||
|
||||
def test_whitespace_only_address(self):
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
ParsedEmail(' ', self.ADDRESS_ENCODING)
|
||||
parse_address_list([' '])
|
||||
|
||||
def test_invalid_address(self):
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
parse_address_list(['localonly'])
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
parse_address_list(['localonly@'])
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
parse_address_list(['@domainonly'])
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
parse_address_list(['<localonly@>'])
|
||||
with self.assertRaises(AnymailInvalidAddress):
|
||||
parse_address_list(['<@domainonly>'])
|
||||
|
||||
def test_email_list(self):
|
||||
parsed_list = parse_address_list(["first@example.com", "second@example.com"])
|
||||
self.assertEqual(len(parsed_list), 2)
|
||||
self.assertEqual(parsed_list[0].email, "first@example.com")
|
||||
self.assertEqual(parsed_list[1].email, "second@example.com")
|
||||
|
||||
def test_multiple_emails(self):
|
||||
# Django's EmailMessage allows multiple, comma-separated emails
|
||||
# in a single recipient string. (It passes them along to the backend intact.)
|
||||
# (Depending on this behavior is not recommended.)
|
||||
parsed_list = parse_address_list(["first@example.com, second@example.com"])
|
||||
self.assertEqual(len(parsed_list), 2)
|
||||
self.assertEqual(parsed_list[0].email, "first@example.com")
|
||||
self.assertEqual(parsed_list[1].email, "second@example.com")
|
||||
|
||||
def test_invalid_in_list(self):
|
||||
# Make sure it's not just concatenating list items...
|
||||
# the bare "Display Name" below should *not* get merged with
|
||||
# the email in the second item
|
||||
with self.assertRaisesMessage(AnymailInvalidAddress, "Display Name"):
|
||||
parse_address_list(['"Display Name"', '<valid@example.com>'])
|
||||
|
||||
def test_single_string(self):
|
||||
# bare strings are used by the from_email parsing in BasePayload
|
||||
parsed_list = parse_address_list("one@example.com")
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
self.assertEqual(parsed_list[0].email, "one@example.com")
|
||||
|
||||
def test_lazy_strings(self):
|
||||
parsed_list = parse_address_list([ugettext_lazy('"Example, Inc." <one@example.com>')])
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
self.assertEqual(parsed_list[0].name, "Example, Inc.")
|
||||
self.assertEqual(parsed_list[0].email, "one@example.com")
|
||||
|
||||
parsed_list = parse_address_list(ugettext_lazy("one@example.com"))
|
||||
self.assertEqual(len(parsed_list), 1)
|
||||
self.assertEqual(parsed_list[0].name, "")
|
||||
self.assertEqual(parsed_list[0].email, "one@example.com")
|
||||
|
||||
|
||||
class LazyCoercionTests(SimpleTestCase):
|
||||
|
||||
Reference in New Issue
Block a user