mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
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:
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user