mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
This fixes a low severity security issue affecting Anymail v0.2--v1.3. Django error reporting includes the value of your Anymail WEBHOOK_AUTHORIZATION setting. In a properly-configured deployment, this should not be cause for concern. But if you have somehow exposed your Django error reports (e.g., by mis-deploying with DEBUG=True or by sending error reports through insecure channels), anyone who gains access to those reports could discover your webhook shared secret. An attacker could use this to post fabricated or malicious Anymail tracking/inbound events to your app, if you are using those Anymail features. The fix renames Anymail's webhook shared secret setting so that Django's error reporting mechanism will [sanitize][0] it. If you are using Anymail's event tracking and/or inbound webhooks, you should upgrade to this release and change "WEBHOOK_AUTHORIZATION" to "WEBHOOK_SECRET" in the ANYMAIL section of your settings.py. You may also want to [rotate the shared secret][1] value, particularly if you have ever exposed your Django error reports to untrusted individuals. If you are only using Anymail's EmailBackends for sending email and have not set up Anymail's webhooks, this issue does not affect you. The old WEBHOOK_AUTHORIZATION setting is still allowed in this release, but will issue a system-check warning when running most Django management commands. It will be removed completely in a near-future release, as a breaking change. Thanks to Charlie DeTar (@yourcelf) for responsibly reporting this security issue through private channels. [0]: https://docs.djangoproject.com/en/stable/ref/settings/#debug [1]: https://anymail.readthedocs.io/en/1.4/tips/securing_webhooks/#use-a-shared-authorization-secret
431 lines
16 KiB
ReStructuredText
431 lines
16 KiB
ReStructuredText
.. _mandrill-backend:
|
|
|
|
Mandrill
|
|
========
|
|
|
|
Anymail integrates with the `Mandrill <http://mandrill.com/>`__
|
|
transactional email service from MailChimp.
|
|
|
|
.. note:: **Limited Support for Mandrill**
|
|
|
|
Anymail is developed to the public Mandrill documentation, but unlike
|
|
other supported ESPs, we are unable to test or debug against the live
|
|
Mandrill APIs. (MailChimp discourages use of Mandrill by "developers,"
|
|
and doesn't offer testing access for packages like Anymail.)
|
|
|
|
As a result, Anymail bugs with Mandrill will generally be discovered
|
|
by Anymail's users, in production; Anymail's maintainers often won't
|
|
be able to answer Mandrill-specific questions; and fixes and improvements
|
|
for Mandrill will tend to lag other ESPs.
|
|
|
|
If you are integrating only Mandrill, and not considering one of Anymail's
|
|
other ESPs, you might prefer using MailChimp's official
|
|
:pypi:`mandrill` python package instead of Anymail.
|
|
|
|
|
|
Settings
|
|
--------
|
|
|
|
.. rubric:: EMAIL_BACKEND
|
|
|
|
To use Anymail's Mandrill backend, set:
|
|
|
|
.. code-block:: python
|
|
|
|
EMAIL_BACKEND = "anymail.backends.mandrill.EmailBackend"
|
|
|
|
in your settings.py.
|
|
|
|
|
|
.. setting:: ANYMAIL_MANDRILL_API_KEY
|
|
|
|
.. rubric:: MANDRILL_API_KEY
|
|
|
|
Required. Your Mandrill API key:
|
|
|
|
.. code-block:: python
|
|
|
|
ANYMAIL = {
|
|
...
|
|
"MANDRILL_API_KEY": "<your API key>",
|
|
}
|
|
|
|
Anymail will also look for ``MANDRILL_API_KEY`` at the
|
|
root of the settings file if neither ``ANYMAIL["MANDRILL_API_KEY"]``
|
|
nor ``ANYMAIL_MANDRILL_API_KEY`` is set.
|
|
|
|
|
|
.. setting:: ANYMAIL_MANDRILL_WEBHOOK_KEY
|
|
|
|
.. rubric:: MANDRILL_WEBHOOK_KEY
|
|
|
|
Required if using Anymail's webhooks. The "webhook authentication key"
|
|
issued by Mandrill.
|
|
`More info <https://mandrill.zendesk.com/hc/en-us/articles/205583257>`_
|
|
in Mandrill's KB.
|
|
|
|
|
|
.. setting:: ANYMAIL_MANDRILL_WEBHOOK_URL
|
|
|
|
.. rubric:: MANDRILL_WEBHOOK_URL
|
|
|
|
Required only if using Anymail's webhooks *and* the hostname your
|
|
Django server sees is different from the public webhook URL
|
|
you provided Mandrill. (E.g., if you have a proxy in front
|
|
of your Django server that forwards
|
|
"https\://yoursite.example.com" to "http\://localhost:8000/").
|
|
|
|
If you are seeing :exc:`AnymailWebhookValidationFailure` errors
|
|
from your webhooks, set this to the exact webhook URL you entered
|
|
in Mandrill's settings.
|
|
|
|
|
|
.. setting:: ANYMAIL_MANDRILL_API_URL
|
|
|
|
.. rubric:: MANDRILL_API_URL
|
|
|
|
The base url for calling the Mandrill API. The default is
|
|
``MANDRILL_API_URL = "https://mandrillapp.com/api/1.0"``,
|
|
which is the secure, production version of Mandrill's 1.0 API.
|
|
|
|
(It's unlikely you would need to change this.)
|
|
|
|
|
|
.. _mandrill-esp-extra:
|
|
|
|
esp_extra support
|
|
-----------------
|
|
|
|
To use Mandrill features not directly supported by Anymail, you can
|
|
set a message's :attr:`~anymail.message.AnymailMessage.esp_extra` to
|
|
a `dict` of parameters to merge into Mandrill's `messages/send API`_ call.
|
|
Note that a few parameters go at the top level, but Mandrill expects
|
|
most options within a `'message'` sub-dict---be sure to check their
|
|
API docs:
|
|
|
|
.. code-block:: python
|
|
|
|
message.esp_extra = {
|
|
# Mandrill expects 'ip_pool' at top level...
|
|
'ip_pool': 'Bulk Pool',
|
|
# ... but 'subaccount' must be within a 'message' dict:
|
|
'message': {
|
|
'subaccount': 'Marketing Dept.'
|
|
}
|
|
}
|
|
|
|
Anymail has special handling that lets you specify Mandrill's
|
|
`'recipient_metadata'` as a simple, pythonic `dict` (similar in form
|
|
to Anymail's :attr:`~anymail.message.AnymailMessage.merge_data`),
|
|
rather than Mandrill's more complex list of rcpt/values dicts.
|
|
You can use whichever style you prefer (but either way,
|
|
recipient_metadata must be in `esp_extra['message']`).
|
|
|
|
Similary, Anymail allows Mandrill's `'template_content'` in esp_extra
|
|
(top level) either as a pythonic `dict` (similar to Anymail's
|
|
:attr:`~anymail.message.AnymailMessage.merge_global_data`) or
|
|
as Mandrill's more complex list of name/content dicts.
|
|
|
|
.. _messages/send API:
|
|
https://mandrillapp.com/api/docs/messages.JSON.html#method=send
|
|
|
|
.. _mandrill-templates:
|
|
|
|
Batch sending/merge and ESP templates
|
|
-------------------------------------
|
|
|
|
Mandrill offers both :ref:`ESP stored templates <esp-stored-templates>`
|
|
and :ref:`batch sending <batch-send>` with per-recipient merge data.
|
|
|
|
You can use a Mandrill stored template by setting a message's
|
|
:attr:`~anymail.message.AnymailMessage.template_id` to the
|
|
template's name. Alternatively, you can refer to merge fields
|
|
directly in an EmailMessage's subject and body---the message itself
|
|
is used as an on-the-fly template.
|
|
|
|
In either case, supply the merge data values with Anymail's
|
|
normalized :attr:`~anymail.message.AnymailMessage.merge_data`
|
|
and :attr:`~anymail.message.AnymailMessage.merge_global_data`
|
|
message attributes.
|
|
|
|
.. code-block:: python
|
|
|
|
# This example defines the template inline, using Mandrill's
|
|
# default MailChimp merge *|field|* syntax.
|
|
# You could use a stored template, instead, with:
|
|
# message.template_id = "template name"
|
|
message = EmailMessage(
|
|
...
|
|
subject="Your order *|order_no|* has shipped",
|
|
body="""Hi *|name|*,
|
|
We shipped your order *|order_no|*
|
|
on *|ship_date|*.""",
|
|
to=["alice@example.com", "Bob <bob@example.com>"]
|
|
)
|
|
# (you'd probably also set a similar html body with merge fields)
|
|
message.merge_data = {
|
|
'alice@example.com': {'name': "Alice", 'order_no': "12345"},
|
|
'bob@example.com': {'name': "Bob", 'order_no': "54321"},
|
|
}
|
|
message.merge_global_data = {
|
|
'ship_date': "May 15",
|
|
}
|
|
|
|
When you supply per-recipient :attr:`~anymail.message.AnymailMessage.merge_data`,
|
|
Anymail automatically forces Mandrill's `preserve_recipients` option to false,
|
|
so that each person in the message's "to" list sees only their own email address.
|
|
|
|
To use the subject or from address defined with a Mandrill template, set the message's
|
|
`subject` or `from_email` attribute to `None`.
|
|
|
|
See the `Mandrill's template docs`_ for more information.
|
|
|
|
.. _Mandrill's template docs:
|
|
https://mandrill.zendesk.com/hc/en-us/articles/205582507-Getting-Started-with-Templates
|
|
|
|
|
|
.. _mandrill-webhooks:
|
|
.. _mandrill-inbound:
|
|
|
|
Status tracking and inbound webhooks
|
|
------------------------------------
|
|
|
|
If you are using Anymail's normalized :ref:`status tracking <event-tracking>`
|
|
and/or :ref:`inbound <inbound>` handling, setting up Anymail's webhook URL
|
|
requires deploying your Django project twice:
|
|
|
|
1. First, follow the instructions to
|
|
:ref:`configure Anymail's webhooks <webhooks-configuration>`. You *must deploy*
|
|
before adding the webhook URL to Mandrill, because Mandrill will attempt
|
|
to verify the URL against your production server.
|
|
|
|
Once you've deployed, then set Anymail's webhook URL in Mandrill, following their
|
|
instructions for `tracking event webhooks`_ (be sure to check the boxes for the
|
|
events you want to receive) and/or `inbound route webhooks`_.
|
|
In either case, the webhook url is:
|
|
|
|
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/`
|
|
|
|
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
|
|
* *yoursite.example.com* is your Django site
|
|
* (Note: Unlike Anymail's other supported ESPs, the Mandrill webhook uses this
|
|
single url for both tracking and inbound events.)
|
|
|
|
2. Mandrill will provide you a "webhook authentication key" once it verifies the URL
|
|
is working. Add this to your Django project's Anymail settings under
|
|
:setting:`MANDRILL_WEBHOOK_KEY <ANYMAIL_MANDRILL_WEBHOOK_KEY>`.
|
|
(You may also need to set :setting:`MANDRILL_WEBHOOK_URL <ANYMAIL_MANDRILL_WEBHOOK_URL>`
|
|
depending on your server config.) Then deploy your project again.
|
|
|
|
Mandrill implements webhook signing on the entire event payload, and Anymail verifies this
|
|
signature. Until the correct webhook key is set, Anymail will raise
|
|
an exception for any webhook calls from Mandrill (other than the initial validation request).
|
|
|
|
Mandrill's webhook signature also covers the exact posting URL. Anymail can usually
|
|
figure out the correct (public) URL where Mandrill called your webhook. But if you're
|
|
getting an :exc:`AnymailWebhookValidationFailure` with a different URL than you
|
|
provided Mandrill, you may need to examine your Django :setting:`SECURE_PROXY_SSL_HEADER`,
|
|
:setting:`USE_X_FORWARDED_HOST`, and/or :setting:`USE_X_FORWARDED_PORT` settings. If all
|
|
else fails, you can set Anymail's :setting:`MANDRILL_WEBHOOK_URL <ANYMAIL_MANDRILL_WEBHOOK_URL>`
|
|
to the same public webhook URL you gave Mandrill.
|
|
|
|
Mandrill will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
|
|
sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed, inbound. Mandrill does
|
|
not support delivered events. Mandrill "whitelist" and "blacklist" change events will show up
|
|
as Anymail's unknown event_type.
|
|
|
|
The event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event` field will be
|
|
a `dict` of Mandrill event fields, for a single event. (Although Mandrill calls
|
|
webhooks with batches of events, Anymail will invoke your signal receiver separately
|
|
for each event in the batch.)
|
|
|
|
.. _tracking event webhooks:
|
|
https://mandrill.zendesk.com/hc/en-us/articles/205583217-Introduction-to-Webhooks
|
|
.. _inbound route webhooks:
|
|
https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview
|
|
|
|
|
|
.. versionchanged:: 1.3
|
|
Earlier Anymail releases used :samp:`.../anymail/mandrill/{tracking}/` as the tracking
|
|
webhook url. With the addition of inbound handling, Anymail has dropped "tracking"
|
|
from the recommended url for new installations. But the older url is still
|
|
supported. Existing installations can continue to use it---and can even install it
|
|
on a Mandrill *inbound* route to avoid issuing a new webhook key.
|
|
|
|
|
|
.. _migrating-from-djrill:
|
|
|
|
Migrating from Djrill
|
|
---------------------
|
|
|
|
Anymail has its origins as a fork of the `Djrill`_
|
|
package, which supported only Mandrill. If you are migrating
|
|
from Djrill to Anymail -- e.g., because you are thinking
|
|
of switching ESPs -- you'll need to make a few changes
|
|
to your code.
|
|
|
|
.. _Djrill: https://github.com/brack3t/Djrill
|
|
|
|
Changes to settings
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
``MANDRILL_API_KEY``
|
|
Will still work, but consider moving it into the :setting:`ANYMAIL`
|
|
settings dict, or changing it to :setting:`ANYMAIL_MANDRILL_API_KEY`.
|
|
|
|
``MANDRILL_SETTINGS``
|
|
Use :setting:`ANYMAIL_SEND_DEFAULTS` and/or :setting:`ANYMAIL_MANDRILL_SEND_DEFAULTS`
|
|
(see :ref:`send-defaults`).
|
|
|
|
There is one slight behavioral difference between :setting:`ANYMAIL_SEND_DEFAULTS`
|
|
and Djrill's ``MANDRILL_SETTINGS``: in Djrill, setting :attr:`tags` or
|
|
:attr:`merge_vars` on a message would completely override any global
|
|
settings defaults. In Anymail, those message attributes are merged with
|
|
the values from :setting:`ANYMAIL_SEND_DEFAULTS`.
|
|
|
|
``MANDRILL_SUBACCOUNT``
|
|
Set :ref:`esp_extra <mandrill-esp-extra>`
|
|
globally in :setting:`ANYMAIL_SEND_DEFAULTS`:
|
|
|
|
.. code-block:: python
|
|
|
|
ANYMAIL = {
|
|
...
|
|
"MANDRILL_SEND_DEFAULTS": {
|
|
"esp_extra": {
|
|
"message": {
|
|
"subaccount": "<your subaccount>"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
``MANDRILL_IGNORE_RECIPIENT_STATUS``
|
|
Renamed to :setting:`ANYMAIL_IGNORE_RECIPIENT_STATUS`
|
|
(or just `IGNORE_RECIPIENT_STATUS` in the :setting:`ANYMAIL`
|
|
settings dict).
|
|
|
|
``DJRILL_WEBHOOK_SECRET`` and ``DJRILL_WEBHOOK_SECRET_NAME``
|
|
Replaced with HTTP basic auth. See :ref:`securing-webhooks`.
|
|
|
|
``DJRILL_WEBHOOK_SIGNATURE_KEY``
|
|
Use :setting:`ANYMAIL_MANDRILL_WEBHOOK_KEY` instead.
|
|
|
|
``DJRILL_WEBHOOK_URL``
|
|
Often no longer required: Anymail can normally use Django's
|
|
:meth:`HttpRequest.build_absolute_uri <django.http.HttpRequest.build_absolute_uri>`
|
|
to figure out the complete webhook url that Mandrill called.
|
|
|
|
If you are experiencing webhook authorization errors, the best solution is to adjust
|
|
your Django :setting:`SECURE_PROXY_SSL_HEADER`, :setting:`USE_X_FORWARDED_HOST`, and/or
|
|
:setting:`USE_X_FORWARDED_PORT` settings to work with your proxy server.
|
|
If that's not possible, you can set :setting:`ANYMAIL_MANDRILL_WEBHOOK_URL` to explicitly
|
|
declare the webhook url.
|
|
|
|
|
|
Changes to EmailMessage attributes
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
``message.send_at``
|
|
If you are using an aware datetime for
|
|
:attr:`~anymail.message.AnymailMessage.send_at`,
|
|
it will keep working unchanged with Anymail.
|
|
|
|
If you are using a date (without a time), or a naive datetime,
|
|
be aware that these now default to Django's current_timezone,
|
|
rather than UTC as in Djrill.
|
|
|
|
(As with Djrill, it's best to use an aware datetime
|
|
that says exactly when you want the message sent.)
|
|
|
|
|
|
``message.mandrill_response``
|
|
Anymail normalizes ESP responses, so you don't have to be familiar
|
|
with the format of Mandrill's JSON.
|
|
See :attr:`~anymail.message.AnymailMessage.anymail_status`.
|
|
|
|
The *raw* ESP response is attached to a sent message as
|
|
``anymail_status.esp_response``, so the direct replacement
|
|
for message.mandrill_response is:
|
|
|
|
.. code-block:: python
|
|
|
|
mandrill_response = message.anymail_status.esp_response.json()
|
|
|
|
``message.template_name``
|
|
Anymail renames this to :attr:`~anymail.message.AnymailMessage.template_id`.
|
|
|
|
``message.merge_vars`` and ``message.global_merge_vars``
|
|
Anymail renames these to :attr:`~anymail.message.AnymailMessage.merge_data`
|
|
and :attr:`~anymail.message.AnymailMessage.merge_global_data`, respectively.
|
|
|
|
``message.use_template_from`` and ``message.use_template_subject``
|
|
With Anymail, set ``message.from_email = None`` or ``message.subject = None``
|
|
to use the values from the stored template.
|
|
|
|
**Other Mandrill-specific attributes**
|
|
Djrill allowed nearly all Mandrill API parameters to be set
|
|
as attributes directly on an EmailMessage. With Anymail, you
|
|
should instead set these in the message's
|
|
:ref:`esp_extra <mandrill-esp-extra>` dict as described above.
|
|
|
|
Although the Djrill style attributes are still supported (for now),
|
|
Anymail will issue a :exc:`DeprecationWarning` if you try to use them.
|
|
These warnings are visible during tests (with Django's default test
|
|
runner), and will explain how to update your code.
|
|
|
|
You can also use the following git grep expression to find potential
|
|
problems:
|
|
|
|
.. code-block:: console
|
|
|
|
git grep -w \
|
|
-e 'async' -e 'auto_html' -e 'auto_text' -e 'from_name' -e 'global_merge_vars' \
|
|
-e 'google_analytics_campaign' -e 'google_analytics_domains' -e 'important' \
|
|
-e 'inline_css' -e 'ip_pool' -e 'merge_language' -e 'merge_vars' \
|
|
-e 'preserve_recipients' -e 'recipient_metadata' -e 'return_path_domain' \
|
|
-e 'signing_domain' -e 'subaccount' -e 'template_content' -e 'template_name' \
|
|
-e 'tracking_domain' -e 'url_strip_qs' -e 'use_template_from' -e 'use_template_subject' \
|
|
-e 'view_content_link'
|
|
|
|
|
|
**Inline images**
|
|
Djrill (incorrectly) used the presence of a :mailheader:`Content-ID`
|
|
header to decide whether to treat an image as inline. Anymail
|
|
looks for :mailheader:`Content-Disposition: inline`.
|
|
|
|
If you were constructing MIMEImage inline image attachments
|
|
for your Djrill messages, in addition to setting the Content-ID,
|
|
you should also add::
|
|
|
|
image.add_header('Content-Disposition', 'inline')
|
|
|
|
Or better yet, use Anymail's new :ref:`inline-images`
|
|
helper functions to attach your inline images.
|
|
|
|
|
|
Changes to webhooks
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
Anymail uses HTTP basic auth as a shared secret for validating webhook
|
|
calls, rather than Djrill's "secret" query parameter. See
|
|
:ref:`securing-webhooks`. (A slight advantage of basic auth over query
|
|
parameters is that most logging and analytics systems are aware of the
|
|
need to keep auth secret.)
|
|
|
|
Anymail replaces `djrill.signals.webhook_event` with
|
|
`anymail.signals.tracking` for delivery tracking events,
|
|
and `anymail.signals.inbound` for inbound events.
|
|
Anymail parses and normalizes
|
|
the event data passed to the signal receiver: see :ref:`event-tracking`
|
|
and :ref:`inbound`.
|
|
|
|
The equivalent of Djrill's ``data`` parameter is available
|
|
to your signal receiver as
|
|
:attr:`event.esp_event <anymail.signals.AnymailTrackingEvent.esp_event>`,
|
|
and for most events, the equivalent of Djrill's ``event_type`` parameter
|
|
is `event.esp_event['event']`. But consider working with Anymail's
|
|
normalized :class:`~anymail.signals.AnymailTrackingEvent` and
|
|
:class:`~anymail.signals.AnymailInboundEvent` instead for easy portability
|
|
to other ESPs.
|