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:
medmunds
2017-04-19 12:43:33 -07:00
parent 3c2c0b3a9d
commit 6b6793016e
13 changed files with 302 additions and 95 deletions

View File

@@ -10,8 +10,9 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase
from django.test.utils import override_settings
from anymail.exceptions import (AnymailAPIError, AnymailSerializationError,
AnymailUnsupportedFeature, AnymailRecipientsRefused)
from anymail.exceptions import (
AnymailAPIError, AnymailSerializationError,
AnymailUnsupportedFeature, AnymailRecipientsRefused, AnymailInvalidAddress)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
@@ -271,6 +272,20 @@ class PostmarkBackendStandardEmailTests(PostmarkBackendMockAPITestCase):
data = self.get_api_call_json()
self.assertNotIn('To', data)
def test_multiple_from_emails(self):
"""Postmark accepts multiple addresses in from_email (though only uses the first)"""
self.message.from_email = 'first@example.com, "From, also" <second@example.com>'
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['From'],
'first@example.com, "From, also" <second@example.com>')
# Make sure the far-more-likely scenario of a single from_email
# with an unquoted display-name issues a reasonable error:
self.message.from_email = 'Unquoted, display-name <from@example.com>'
with self.assertRaises(AnymailInvalidAddress):
self.message.send()
def test_api_failure(self):
self.set_mock_response(status_code=500)
with self.assertRaisesMessage(AnymailAPIError, "Postmark API response 500"):