mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Feature: Add envelope_sender
New EmailMessage attribute `envelope_sender` controls ESP's sender, sending domain, or return path where supported: * Mailgun: overrides SENDER_DOMAIN on individual message (domain portion only) * Mailjet: becomes `Sender` API param * Mandrill: becomes `return_path_domain` API param (domain portion only) * SparkPost: becomes `return_path` API param * Other ESPs: not believed to be supported Also support undocumented Django SMTP backend behavior, where envelope sender is given by `message.from_email` when `message.extra_headers["From"]` is set. Fixes #91.
This commit is contained in:
@@ -9,7 +9,8 @@ from requests.structures import CaseInsensitiveDict
|
||||
from ..exceptions import AnymailCancelSend, AnymailError, AnymailUnsupportedFeature, AnymailRecipientsRefused
|
||||
from ..message import AnymailStatus
|
||||
from ..signals import pre_send, post_send
|
||||
from ..utils import (Attachment, UNSET, combine, last, get_anymail_setting, parse_address_list,
|
||||
from ..utils import (
|
||||
Attachment, UNSET, combine, last, get_anymail_setting, parse_address_list, parse_single_address,
|
||||
force_non_lazy, force_non_lazy_list, force_non_lazy_dict, is_lazy)
|
||||
|
||||
|
||||
@@ -230,6 +231,7 @@ class BasePayload(object):
|
||||
)
|
||||
anymail_message_attrs = (
|
||||
# Anymail expando-props
|
||||
('envelope_sender', last, parse_single_address),
|
||||
('metadata', combine, force_non_lazy_dict),
|
||||
('send_at', last, 'aware_datetime'),
|
||||
('tags', combine, force_non_lazy_list),
|
||||
@@ -293,6 +295,17 @@ class BasePayload(object):
|
||||
# This matches the behavior of Django's EmailMessage.message().
|
||||
self.set_reply_to(parse_address_list([reply_to]))
|
||||
|
||||
if 'From' in headers:
|
||||
# If message.extra_headers['From'] is supplied, it should override message.from_email,
|
||||
# but message.from_email should be used as the envelope_sender. See:
|
||||
# - https://code.djangoproject.com/ticket/9214
|
||||
# - https://github.com/django/django/blob/1.8/django/core/mail/message.py#L269
|
||||
# - https://github.com/django/django/blob/1.8/django/core/mail/backends/smtp.py#L118-L123
|
||||
header_from = parse_address_list(headers.pop('From', None))
|
||||
envelope_sender = parse_single_address(self.message.from_email) # must be single address
|
||||
self.set_from_email_list(header_from)
|
||||
self.set_envelope_sender(envelope_sender)
|
||||
|
||||
if headers:
|
||||
self.set_extra_headers(headers)
|
||||
|
||||
@@ -431,6 +444,9 @@ class BasePayload(object):
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
# Anymail-specific payload construction
|
||||
def set_envelope_sender(self, email):
|
||||
self.unsupported_feature("envelope_sender")
|
||||
|
||||
def set_metadata(self, metadata):
|
||||
self.unsupported_feature("metadata")
|
||||
|
||||
|
||||
@@ -162,6 +162,10 @@ class MailgunPayload(RequestsPayload):
|
||||
(field, (name, attachment.content, attachment.mimetype))
|
||||
)
|
||||
|
||||
def set_envelope_sender(self, email):
|
||||
# Only the domain is used
|
||||
self.sender_domain = email.domain
|
||||
|
||||
def set_metadata(self, metadata):
|
||||
for key, value in metadata.items():
|
||||
self.data["v:%s" % key] = value
|
||||
|
||||
@@ -221,6 +221,9 @@ class MailjetPayload(RequestsPayload):
|
||||
"content": attachment.b64content
|
||||
})
|
||||
|
||||
def set_envelope_sender(self, email):
|
||||
self.data["Sender"] = email.addr_spec # ??? v3 docs unclear
|
||||
|
||||
def set_metadata(self, metadata):
|
||||
# Mailjet expects a single string payload
|
||||
self.data["Mj-EventPayLoad"] = self.serialize_json(metadata)
|
||||
|
||||
@@ -139,6 +139,10 @@ class MandrillPayload(RequestsPayload):
|
||||
"content": attachment.b64content
|
||||
})
|
||||
|
||||
def set_envelope_sender(self, email):
|
||||
# Only the domain is used
|
||||
self.data["message"]["return_path_domain"] = email.domain
|
||||
|
||||
def set_metadata(self, metadata):
|
||||
self.data["message"]["metadata"] = metadata
|
||||
|
||||
@@ -268,6 +272,10 @@ class MandrillPayload(RequestsPayload):
|
||||
self.deprecation_warning('message.merge_vars', 'message.merge_data')
|
||||
self.set_merge_data(merge_vars)
|
||||
|
||||
def set_return_path_domain(self, domain):
|
||||
self.deprecation_warning('message.return_path_domain', 'message.envelope_sender')
|
||||
self.esp_extra.setdefault('message', {})['return_path_domain'] = domain
|
||||
|
||||
def set_template_name(self, template_name):
|
||||
self.deprecation_warning('message.template_name', 'message.template_id')
|
||||
self.set_template_id(template_name)
|
||||
|
||||
@@ -172,6 +172,9 @@ class SparkPostPayload(BasePayload):
|
||||
'data': attachment.b64content})
|
||||
|
||||
# Anymail-specific payload construction
|
||||
def set_envelope_sender(self, email):
|
||||
self.params['return_path'] = email.addr_spec
|
||||
|
||||
def set_metadata(self, metadata):
|
||||
self.params['metadata'] = metadata
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ class TestPayload(BasePayload):
|
||||
def set_from_email(self, email):
|
||||
self.params['from'] = email
|
||||
|
||||
def set_envelope_sender(self, email):
|
||||
self.params['envelope_sender'] = email.addr_spec
|
||||
|
||||
def set_to(self, emails):
|
||||
self.params['to'] = emails
|
||||
self.recipient_emails += [email.addr_spec for email in emails]
|
||||
|
||||
@@ -20,6 +20,7 @@ class AnymailMessageMixin(object):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.esp_extra = kwargs.pop('esp_extra', UNSET)
|
||||
self.envelope_sender = kwargs.pop('envelope_sender', UNSET)
|
||||
self.metadata = kwargs.pop('metadata', UNSET)
|
||||
self.send_at = kwargs.pop('send_at', UNSET)
|
||||
self.tags = kwargs.pop('tags', UNSET)
|
||||
|
||||
@@ -13,7 +13,6 @@ from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_T
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.timezone import utc, get_fixed_timezone
|
||||
# noinspection PyUnresolvedReferences
|
||||
from six.moves.urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from .exceptions import AnymailConfigurationError, AnymailInvalidAddress
|
||||
@@ -161,6 +160,21 @@ def parse_address_list(address_list):
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_single_address(address):
|
||||
"""Parses a single EmailAddress from str address, or raises AnymailInvalidAddress
|
||||
|
||||
:param str address: the fully-formatted email str to parse
|
||||
:return :class:`EmailAddress`: if address contains a single email
|
||||
:raises :exc:`AnymailInvalidAddress`: if address contains no or multiple emails
|
||||
"""
|
||||
parsed = parse_address_list([address])
|
||||
count = len(parsed)
|
||||
if count > 1:
|
||||
raise AnymailInvalidAddress("Only one email address is allowed; found %d in %r" % (count, address))
|
||||
else:
|
||||
return parsed[0]
|
||||
|
||||
|
||||
class EmailAddress(object):
|
||||
"""A sanitized, complete email address with easy access
|
||||
to display-name, addr-spec (email), etc.
|
||||
|
||||
@@ -28,11 +28,12 @@ The table below summarizes the Anymail features supported for each ESP.
|
||||
|
||||
.. currentmodule:: anymail.message
|
||||
|
||||
============================================ ========== ========== ========== ========== ========== ============ ===========
|
||||
============================================ =========== ========== =========== ========== ========== ============ ===========
|
||||
Email Service Provider |Mailgun| |Mailjet| |Mandrill| |Postmark| |SendGrid| |SendinBlue| |SparkPost|
|
||||
============================================ ========== ========== ========== ========== ========== ============ ===========
|
||||
============================================ =========== ========== =========== ========== ========== ============ ===========
|
||||
.. rubric:: :ref:`Anymail send options <anymail-send-options>`
|
||||
-----------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.envelope_sender` Domain only Yes Domain only No No No Yes
|
||||
:attr:`~AnymailMessage.metadata` Yes Yes Yes No Yes Yes Yes
|
||||
:attr:`~AnymailMessage.send_at` Yes No Yes No Yes No Yes
|
||||
:attr:`~AnymailMessage.tags` Yes Max 1 tag Yes Max 1 tag Yes Max 1 tag Max 1 tag
|
||||
@@ -40,20 +41,20 @@ Email Service Provider |Mailgun| |Mailjet| |Mandrill|
|
||||
:attr:`~AnymailMessage.track_opens` Yes Yes Yes Yes Yes No Yes
|
||||
|
||||
.. rubric:: :ref:`templates-and-merge`
|
||||
-----------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.template_id` No Yes Yes Yes Yes Yes Yes
|
||||
:attr:`~AnymailMessage.merge_data` Yes Yes Yes No Yes No Yes
|
||||
:attr:`~AnymailMessage.merge_global_data` (emulated) Yes Yes Yes Yes Yes Yes
|
||||
|
||||
.. rubric:: :ref:`Status <esp-send-status>` and :ref:`event tracking <event-tracking>`
|
||||
-----------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.anymail_status` Yes Yes Yes Yes Yes Yes Yes
|
||||
|AnymailTrackingEvent| from webhooks Yes Yes Yes Yes Yes No Yes
|
||||
|
||||
.. rubric:: :ref:`Inbound handling <inbound>`
|
||||
-----------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------
|
||||
|AnymailInboundEvent| from webhooks Yes Yes Yes Yes Yes No Yes
|
||||
============================================ ========== ========== ========== ========== ========== ============ ===========
|
||||
============================================ =========== ========== =========== ========== ========== ============ ===========
|
||||
|
||||
|
||||
Trying to choose an ESP? Please **don't** start with this table. It's far more
|
||||
|
||||
@@ -94,15 +94,22 @@ Mailgun account is configured for "*mail1*.example.com", you should provide
|
||||
|
||||
|
||||
If you need to override the sender domain for an individual message,
|
||||
include `sender_domain` in Anymail's :attr:`~anymail.message.AnymailMessage.esp_extra`
|
||||
for that message:
|
||||
use Anymail's :attr:`~anymail.message.AnymailMessage.envelope_sender`
|
||||
(only the domain is used; anything before the @ is ignored):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message = EmailMessage(from_email="marketing@example.com", ...)
|
||||
message.esp_extra = {"sender_domain": "mail2.example.com"}
|
||||
message.envelope_sender = "anything@mail2.example.com" # the "anything@" is ignored
|
||||
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
Earlier Anymail versions looked for a special `sender_domain` key in the message's
|
||||
:attr:`~anymail.message.AnymailMessage.esp_extra` to override Mailgun's sender domain.
|
||||
This is still supported, but may be deprecated in a future release. Using
|
||||
:attr:`~anymail.message.AnymailMessage.envelope_sender` as shown above is now preferred.
|
||||
|
||||
.. _Mailgun sender domain:
|
||||
https://help.mailgun.com/hc/en-us/articles/202256730-How-do-I-pick-a-domain-name-for-my-Mailgun-account-
|
||||
|
||||
@@ -139,6 +146,12 @@ Limitations and quirks
|
||||
if you need to access that metadata from an opened, clicked, or unsubscribed
|
||||
:ref:`tracking event <event-tracking>` handler.
|
||||
|
||||
**Envelope sender uses only domain**
|
||||
Anymail's :attr:`~anymail.message.AnymailMessage.envelope_sender` is used to
|
||||
select your Mailgun :ref:`sender domain <mailgun-sender-domain>`. For
|
||||
obvious reasons, only the domain portion applies. You can use anything before
|
||||
the @, and it will be ignored.
|
||||
|
||||
|
||||
.. _mailgun-templates:
|
||||
|
||||
|
||||
@@ -126,6 +126,11 @@ Limitations and quirks
|
||||
**No delayed sending**
|
||||
Mailjet does not support :attr:`~anymail.message.AnymailMessage.send_at`.
|
||||
|
||||
**Envelope sender may require approval**
|
||||
Anymail passes :attr:`~anymail.message.AnymailMessage.envelope_sender` to
|
||||
Mailjet, but this may result in an API error if you have not received
|
||||
special approval from Mailjet support to use custom senders.
|
||||
|
||||
**Commas in recipient names**
|
||||
Mailjet's v3 API does not properly handle commas in recipient display-names
|
||||
*if* your message also uses the ``cc`` or ``bcc`` fields.
|
||||
|
||||
@@ -129,6 +129,18 @@ as Mandrill's more complex list of name/content dicts.
|
||||
.. _messages/send API:
|
||||
https://mandrillapp.com/api/docs/messages.JSON.html#method=send
|
||||
|
||||
|
||||
.. _mandrill-quirks:
|
||||
|
||||
Limitations and quirks
|
||||
----------------------
|
||||
|
||||
**Envelope sender uses only domain**
|
||||
Anymail's :attr:`~anymail.message.AnymailMessage.envelope_sender` is used to
|
||||
populate Mandrill's `'return_path_domain'`---but only the domain portion.
|
||||
(Mandrill always generates its own encoded mailbox for the envelope sender.)
|
||||
|
||||
|
||||
.. _mandrill-templates:
|
||||
|
||||
Batch sending/merge and ESP templates
|
||||
@@ -363,6 +375,13 @@ Changes to EmailMessage attributes
|
||||
With Anymail, set ``message.from_email = None`` or ``message.subject = None``
|
||||
to use the values from the stored template.
|
||||
|
||||
``message.return_path_domain``
|
||||
With Anymail, set :attr:`~anymail.message.AnymailMessage.envelope_sender`
|
||||
instead. You'll need to pass a valid email address (not just a domain),
|
||||
but Anymail will use only the domain, and will ignore anything before the @.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
**Other Mandrill-specific attributes**
|
||||
Djrill allowed nearly all Mandrill API parameters to be set
|
||||
as attributes directly on an EmailMessage. With Anymail, you
|
||||
|
||||
@@ -120,6 +120,11 @@ see :ref:`unsupported-features`.
|
||||
.. _several link-tracking options:
|
||||
http://developer.postmarkapp.com/developer-link-tracking.html
|
||||
|
||||
**No envelope sender overrides**
|
||||
Postmark does not support overriding :attr:`~anymail.message.AnymailMessage.envelope_sender`
|
||||
on individual messages. (You can configure custom return paths for each sending domain in
|
||||
the Postmark control panel.)
|
||||
|
||||
|
||||
.. _postmark-templates:
|
||||
|
||||
|
||||
@@ -195,6 +195,10 @@ Limitations and quirks
|
||||
|
||||
(Tested March, 2016)
|
||||
|
||||
**No envelope sender overrides**
|
||||
SendGrid does not support overriding :attr:`~anymail.message.AnymailMessage.envelope_sender`
|
||||
on individual messages.
|
||||
|
||||
|
||||
.. _sendgrid-templates:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.. _sendinblue-backend:
|
||||
|
||||
SendinBlue
|
||||
========
|
||||
==========
|
||||
|
||||
Anymail integrates with the `SendinBlue`_ email service, using their `Web API v3`_.
|
||||
|
||||
@@ -88,3 +88,7 @@ Limitations and quirks
|
||||
If you use a template you will suffer some limitations:
|
||||
you can't change the subject or/and the body, and all email's
|
||||
display-names will be hidden.
|
||||
|
||||
**No envelope sender overrides**
|
||||
SendinBlue does not support overriding :attr:`~anymail.message.AnymailMessage.envelope_sender`
|
||||
on individual messages.
|
||||
|
||||
@@ -127,6 +127,13 @@ Limitations and quirks
|
||||
(SparkPost's "recipient tags" are not available for tagging *messages*.
|
||||
They're associated with individual *addresses* in stored recipient lists.)
|
||||
|
||||
**Envelope sender may use domain only**
|
||||
Anymail's :attr:`~anymail.message.AnymailMessage.envelope_sender` is used to
|
||||
populate SparkPost's `'return_path'` parameter. Anymail supplies the full
|
||||
email address, but depending on your SparkPost configuration, SparkPost may
|
||||
use only the domain portion and substitute its own encoded mailbox before
|
||||
the @.
|
||||
|
||||
|
||||
.. _sparkpost-templates:
|
||||
|
||||
|
||||
@@ -68,6 +68,35 @@ ESP send options (AnymailMessage)
|
||||
:class:`~django.core.mail.EmailMessage` you send.
|
||||
(You don't have to use :class:`AnymailMessage`.)
|
||||
|
||||
.. attribute:: envelope_sender
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Set this to a `str` email address that should be used as the message's
|
||||
envelope sender. If supported by your ESP, this will become the Return-Path
|
||||
in the recipient's mailbox.
|
||||
|
||||
(Envelope sender is also known as bounce address, MAIL FROM, envelope from,
|
||||
unixfrom, SMTP FROM command, return path, and `several other terms`_. Confused?
|
||||
Here's some good info on `how envelope sender relates to return path`_.)
|
||||
|
||||
ESP support for envelope sender varies widely. Be sure to check Anymail's
|
||||
docs for your :ref:`specific ESP <supported-esps>` before attempting to use it.
|
||||
And note that those ESPs who do support it will often use only the domain
|
||||
portion of the envelope sender address, overriding the part before the @ with
|
||||
their own encoded bounce mailbox.
|
||||
|
||||
[The :attr:`!envelope_sender` attribute is unique to Anymail. If you also use Django's
|
||||
SMTP EmailBackend, you can portably control envelope sender by *instead* setting
|
||||
``message.extra_headers["From"]`` to the desired email *header* :mailheader:`From`,
|
||||
and ``message.from_email`` to the *envelope sender*. Anymail also allows this approach,
|
||||
for compatibility with the SMTP EmailBackend. See the notes `in Django's bug tracker`_.]
|
||||
|
||||
.. _several other terms: https://en.wikipedia.org/wiki/Bounce_address
|
||||
.. _in Django's bug tracker: https://code.djangoproject.com/ticket/9214
|
||||
.. _how envelope sender relates to return path:
|
||||
https://www.postmastery.com/blog/about-the-return-path-header/
|
||||
|
||||
.. attribute:: metadata
|
||||
|
||||
Set this to a `dict` of metadata values the ESP should store
|
||||
|
||||
@@ -351,3 +351,23 @@ class SpecialHeaderTests(TestBackendTestCase):
|
||||
params = self.get_send_params()
|
||||
self.assertEqual(flatten_emails(params['reply_to']), ["header@example.com"])
|
||||
self.assertEqual(params['extra_headers'], {"X-Extra": "extra"}) # Reply-To no longer there
|
||||
|
||||
def test_envelope_sender(self):
|
||||
"""Django treats message.from_email as envelope-sender if messsage.extra_headers['From'] is set"""
|
||||
# Using Anymail's envelope_sender extension
|
||||
self.message.from_email = "Header From <header@example.com>"
|
||||
self.message.envelope_sender = "Envelope From <envelope@bounces.example.com>" # Anymail extension
|
||||
self.message.send()
|
||||
params = self.get_send_params()
|
||||
self.assertEqual(params['from'].address, "Header From <header@example.com>")
|
||||
self.assertEqual(params['envelope_sender'], "envelope@bounces.example.com")
|
||||
|
||||
# Using Django's undocumented message.extra_headers['From'] extension
|
||||
# (see https://code.djangoproject.com/ticket/9214)
|
||||
self.message.from_email = "Envelope From <envelope@bounces.example.com>"
|
||||
self.message.extra_headers = {"From": "Header From <header@example.com>"}
|
||||
self.message.send()
|
||||
params = self.get_send_params()
|
||||
self.assertEqual(params['from'].address, "Header From <header@example.com>")
|
||||
self.assertEqual(params['envelope_sender'], "envelope@bounces.example.com")
|
||||
self.assertNotIn("From", params.get('extra_headers', {})) # From was removed from extra-headers
|
||||
|
||||
@@ -417,13 +417,19 @@ class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase):
|
||||
})
|
||||
|
||||
def test_sender_domain(self):
|
||||
"""Mailgun send domain can come from from_email or esp_extra"""
|
||||
"""Mailgun send domain can come from from_email, envelope_sender, or esp_extra"""
|
||||
# You could also use MAILGUN_SENDER_DOMAIN in your ANYMAIL settings, as in the next test.
|
||||
# (The mailgun_integration_tests also do that.)
|
||||
self.message.from_email = "Test From <from@from-email.example.com>"
|
||||
self.message.send()
|
||||
self.assert_esp_called('/from-email.example.com/messages') # API url includes the sender-domain
|
||||
|
||||
self.message.from_email = "Test From <from@from-email.example.com>"
|
||||
self.message.envelope_sender = "anything@bounces.example.com" # only the domain part is used
|
||||
self.message.send()
|
||||
self.assert_esp_called('/bounces.example.com/messages') # overrides from_email
|
||||
|
||||
self.message.from_email = "Test From <from@from-email.example.com>"
|
||||
self.message.esp_extra = {'sender_domain': 'esp-extra.example.com'}
|
||||
self.message.send()
|
||||
self.assert_esp_called('/esp-extra.example.com/messages') # overrides from_email
|
||||
|
||||
@@ -357,6 +357,12 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
|
||||
class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
self.message.envelope_sender = "bounce-handler@bounces.example.com"
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['Sender'], "bounce-handler@bounces.example.com")
|
||||
|
||||
def test_metadata(self):
|
||||
# Mailjet expects the payload to be a single string
|
||||
# https://dev.mailjet.com/guides/#tagging-email-messages
|
||||
|
||||
@@ -270,6 +270,12 @@ class MandrillBackendStandardEmailTests(MandrillBackendMockAPITestCase):
|
||||
class MandrillBackendAnymailFeatureTests(MandrillBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
self.message.envelope_sender = "anything@bounces.example.com"
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['message']['return_path_domain'], "bounces.example.com")
|
||||
|
||||
def test_metadata(self):
|
||||
self.message.metadata = {'user_id': "12345", 'items': 6}
|
||||
self.message.send()
|
||||
|
||||
@@ -321,6 +321,13 @@ class PostmarkBackendStandardEmailTests(PostmarkBackendMockAPITestCase):
|
||||
class PostmarkBackendAnymailFeatureTests(PostmarkBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
# Postmark doesn't allow overriding envelope sender on individual messages.
|
||||
# You can configure a custom return-path domain for each server in their control panel.
|
||||
self.message.envelope_sender = "anything@bounces.example.com"
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'envelope_sender'):
|
||||
self.message.send()
|
||||
|
||||
def test_metadata(self):
|
||||
self.message.metadata = {'user_id': "12345", 'items': 6}
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'metadata'):
|
||||
|
||||
@@ -335,6 +335,12 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
|
||||
class SendGridBackendAnymailFeatureTests(SendGridBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
# SendGrid does not have a way to change envelope sender.
|
||||
self.message.envelope_sender = "anything@bounces.example.com"
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'envelope_sender'):
|
||||
self.message.send()
|
||||
|
||||
def test_metadata(self):
|
||||
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6, 'long': longtype(123)}
|
||||
self.message.send()
|
||||
|
||||
@@ -343,6 +343,12 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
|
||||
class SendGridBackendAnymailFeatureTests(SendGridBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
# SendGrid does not have a way to change envelope sender.
|
||||
self.message.envelope_sender = "anything@bounces.example.com"
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'envelope_sender'):
|
||||
self.message.send()
|
||||
|
||||
def test_metadata(self):
|
||||
# Note: SendGrid doesn't handle complex types in metadata
|
||||
self.message.metadata = {'user_id': "12345", 'items': 6}
|
||||
|
||||
@@ -270,6 +270,12 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
|
||||
class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
# SendinBlue does not have a way to change envelope sender.
|
||||
self.message.envelope_sender = "anything@bounces.example.com"
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'envelope_sender'):
|
||||
self.message.send()
|
||||
|
||||
def test_metadata(self):
|
||||
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6, 'long': longtype(123)}
|
||||
self.message.send()
|
||||
|
||||
@@ -327,6 +327,12 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
|
||||
class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
|
||||
"""Test backend support for Anymail added features"""
|
||||
|
||||
def test_envelope_sender(self):
|
||||
self.message.envelope_sender = "bounce-handler@bounces.example.com"
|
||||
self.message.send()
|
||||
params = self.get_send_params()
|
||||
self.assertEqual(params['return_path'], "bounce-handler@bounces.example.com")
|
||||
|
||||
def test_metadata(self):
|
||||
self.message.metadata = {'user_id': "12345", 'items': 'spark, post'}
|
||||
self.message.send()
|
||||
|
||||
@@ -20,7 +20,7 @@ except ImportError:
|
||||
|
||||
from anymail.exceptions import AnymailInvalidAddress
|
||||
from anymail.utils import (
|
||||
parse_address_list, EmailAddress,
|
||||
parse_address_list, parse_single_address, EmailAddress,
|
||||
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
|
||||
update_deep,
|
||||
get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst)
|
||||
@@ -150,6 +150,16 @@ class ParseAddressListTests(SimpleTestCase):
|
||||
self.assertEqual(parsed_list[0].display_name, "")
|
||||
self.assertEqual(parsed_list[0].addr_spec, "one@example.com")
|
||||
|
||||
def test_parse_one(self):
|
||||
parsed = parse_single_address("one@example.com")
|
||||
self.assertEqual(parsed.address, "one@example.com")
|
||||
|
||||
with self.assertRaisesMessage(AnymailInvalidAddress, "Only one email address is allowed; found 2"):
|
||||
parse_single_address("one@example.com, two@example.com")
|
||||
|
||||
with self.assertRaisesMessage(AnymailInvalidAddress, "Invalid email address"):
|
||||
parse_single_address(" ")
|
||||
|
||||
|
||||
class LazyCoercionTests(SimpleTestCase):
|
||||
"""Test utils.is_lazy and force_non_lazy*"""
|
||||
|
||||
Reference in New Issue
Block a user