mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-21 20:31:06 -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,8 +9,9 @@ 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,
|
||||
force_non_lazy, force_non_lazy_list, force_non_lazy_dict, is_lazy)
|
||||
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)
|
||||
|
||||
|
||||
class AnymailBaseBackend(BaseEmailBackend):
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user