Fix: flag extra_headers["To"] as unsupported

Django's SMTP EmailBackend allows spoofing the To header by setting
`message.extra_headers["To"]`` different from `message.to`.

No current Anymail ESP supports this. Treat extra_headers["To"] as
an unsupported ESP feature, to flag attempts to use it.

Also document Anymail's special header handling that replicates
Django's SMTP EmailBackend behavior.
This commit is contained in:
medmunds
2018-02-27 13:43:58 -08:00
parent 07fbeac6bd
commit 06c7077e37
3 changed files with 61 additions and 4 deletions

View File

@@ -300,12 +300,22 @@ class BasePayload(object):
# but message.from_email should be used as the envelope_sender. See: # but message.from_email should be used as the envelope_sender. See:
# - https://code.djangoproject.com/ticket/9214 # - 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/message.py#L269
# - https://github.com/django/django/blob/1.8/django/core/mail/backends/smtp.py#L118-L123 # - https://github.com/django/django/blob/1.8/django/core/mail/backends/smtp.py#L118
header_from = parse_address_list(headers.pop('From', None)) header_from = parse_address_list(headers.pop('From'))
envelope_sender = parse_single_address(self.message.from_email) # must be single address envelope_sender = parse_single_address(self.message.from_email) # must be single address
self.set_from_email_list(header_from) self.set_from_email_list(header_from)
self.set_envelope_sender(envelope_sender) self.set_envelope_sender(envelope_sender)
if 'To' in headers:
# If message.extra_headers['To'] is supplied, message.to is used only as the envelope
# recipients (SMTP.sendmail to_addrs), and the header To is spoofed. See:
# - https://github.com/django/django/blob/1.8/django/core/mail/message.py#L270
# - https://github.com/django/django/blob/1.8/django/core/mail/backends/smtp.py#L119-L120
# No current ESP supports this, so this code is mainly here to flag
# the SMTP backend's behavior as an unsupported feature in Anymail:
header_to = headers.pop('To')
self.set_spoofed_to_header(header_to)
if headers: if headers:
self.set_extra_headers(headers) self.set_extra_headers(headers)
@@ -443,6 +453,11 @@ class BasePayload(object):
raise NotImplementedError("%s.%s must implement add_attachment or set_attachments" % raise NotImplementedError("%s.%s must implement add_attachment or set_attachments" %
(self.__class__.__module__, self.__class__.__name__)) (self.__class__.__module__, self.__class__.__name__))
def set_spoofed_to_header(self, header_to):
# In the unlikely case an ESP supports *completely replacing* the To message header
# without altering the actual envelope recipients, the backend can implement this.
self.unsupported_feature("spoofing `To` header")
# Anymail-specific payload construction # Anymail-specific payload construction
def set_envelope_sender(self, email): def set_envelope_sender(self, email):
self.unsupported_feature("envelope_sender") self.unsupported_feature("envelope_sender")

View File

@@ -78,7 +78,7 @@ headers, Anymail will tell your ESP to treat them as inline rather than ordinary
attached files. If you want to reference an attachment from an `<img>` in your attached files. If you want to reference an attachment from an `<img>` in your
HTML source, the attachment also needs a :mailheader:`Content-ID` header. HTML source, the attachment also needs a :mailheader:`Content-ID` header.
Anymail's comes with :func:`~message.attach_inline_image` and Anymail comes with :func:`~message.attach_inline_image` and
:func:`~message.attach_inline_image_file` convenience functions that :func:`~message.attach_inline_image_file` convenience functions that
do the right thing. See :ref:`inline-images` in the "Anymail additions" section. do the right thing. See :ref:`inline-images` in the "Anymail additions" section.
@@ -95,10 +95,11 @@ Additional headers
------------------ ------------------
Anymail passes additional headers to your ESP. (Some ESPs may limit Anymail passes additional headers to your ESP. (Some ESPs may limit
which headers they'll allow.) which headers they'll allow.) EmailMessage expects a `dict` of headers:
.. code-block:: python .. code-block:: python
# Use `headers` when creating an EmailMessage
msg = EmailMessage( ... msg = EmailMessage( ...
headers={ headers={
"List-Unsubscribe": unsubscribe_url, "List-Unsubscribe": unsubscribe_url,
@@ -106,6 +107,39 @@ which headers they'll allow.)
} }
) )
# Or use the `extra_headers` attribute later
msg.extra_headers["In-Reply-To"] = inbound_msg["Message-ID"]
Anymail treats header names as case-*insensitive* (because that's how email handles them).
If you supply multiple headers that differ only in case, only one of them will make it
into the resulting email.
Django's default :class:`SMTP EmailBackend <django.core.mail.backends.smtp.EmailBackend>`
has special handling for certain headers. Anymail replicates its behavior for compatibility:
.. Django doesn't doc EmailMessage :attr:`to`, :attr:`from_email`, etc. So just link to
the :class:`EmailMessage` docs to refer to them.
* If you supply a "Reply-To" header, it will *override* the message's
:class:`reply_to <django.core.mail.EmailMessage>` attribute.
* If you supply a "From" header, it will override the message's
:class:`from_email <django.core.mail.EmailMessage>` and become the :mailheader:`From` field the
recipient sees. In addition, the original :class:`from_email <django.core.mail.EmailMessage>` value
will be used as the message's :attr:`~anymail.message.AnymailMessage.envelope_sender`, which becomes
the :mailheader:`Return-Path` at the recipient end. (Only if your ESP supports altering envelope
sender, otherwise you'll get an :ref:`unsupported feature <unsupported-features>` error.)
* If you supply a "To" header, you'll get an :ref:`unsupported feature <unsupported-features>` error.
With Django's SMTP EmailBackend, this can be used to show the recipient a :mailheader:`To` address
that's different from the actual envelope recipients in the message's
:class:`to <django.core.mail.EmailMessage>` list. Spoofing the :mailheader:`To` header like this
is popular with spammers, and none of Anymail's supported ESPs allow it.
.. versionchanged:: 2.0
Improved header-handling compatibility with Django's SMTP EmailBackend.
.. _unsupported-features: .. _unsupported-features:

View File

@@ -371,3 +371,11 @@ class SpecialHeaderTests(TestBackendTestCase):
self.assertEqual(params['from'].address, "Header From <header@example.com>") self.assertEqual(params['from'].address, "Header From <header@example.com>")
self.assertEqual(params['envelope_sender'], "envelope@bounces.example.com") self.assertEqual(params['envelope_sender'], "envelope@bounces.example.com")
self.assertNotIn("From", params.get('extra_headers', {})) # From was removed from extra-headers self.assertNotIn("From", params.get('extra_headers', {})) # From was removed from extra-headers
def test_spoofed_to_header(self):
"""Django treats message.to as envelope-recipient if message.extra_headers['To'] is set"""
# No current ESP supports this (and it's unlikely they would)
self.message.to = ["actual-recipient@example.com"]
self.message.extra_headers = {"To": "Apparent Recipient <but-not-really@example.com>"}
with self.assertRaisesMessage(AnymailUnsupportedFeature, "spoofing `To` header"):
self.message.send()