Mandrill
Anymail integrates with the Mandrill transactional email service from MailChimp, using their /messages/send HTTP API.
Note
Limited Mandrill Testing
Anymail is developed to the public Mandrill documentation, but unlike other supported ESPs, we are unable to regularly test against the live Mandrill APIs. (MailChimp doesn’t offer ongoing testing access for open source packages like Anymail. We do have a limited use trial account, but we try to save that for debugging specific issues reported by Anymail users.)
If you are using only Mandrill, and unlikely to ever need a different ESP, you might prefer using MailChimp’s official mailchimp-transactional-python package instead of Anymail.
Settings
EMAIL_BACKEND
To use Anymail’s Mandrill backend, set:
EMAIL_BACKEND = "anymail.backends.mandrill.EmailBackend"
in your settings.py.
MANDRILL_API_KEY
Required. Your Mandrill API key:
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.
MANDRILL_WEBHOOK_KEY
Required if using Anymail’s webhooks. The “webhook authentication key” issued by Mandrill. See Authenticating webhook requests in the Mandrill docs.
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 AnymailWebhookValidationFailure
errors
from your webhooks, set this to the exact webhook URL you entered
in Mandrill’s settings.
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.)
esp_extra support
To use Mandrill features not directly supported by Anymail, you can
set a message’s 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:
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 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']
).
Similarly, Anymail allows Mandrill’s 'template_content'
in esp_extra
(top level) either as a pythonic dict
(similar to Anymail’s
merge_global_data
) or
as Mandrill’s more complex list of name/content dicts.
Limitations and quirks
- Non-ASCII attachment filenames will be garbled
Mandrill’s /messages/send API does not properly handle non-ASCII characters in attachment filenames. As a result, some email clients will display those characters incorrectly. The only workaround is to limit attachment filenames to ASCII when sending through Mandrill. (Verified and reported to MailChimp support 4/2022; see Anymail discussion #257 for more details.)
- Cc and bcc depend on “preserve_recipients”
Mandrill’s handing of
cc
andbcc
addresses depends on whether itspreserve_recipients
option is enabled for the message.When preserve recipients is True, a single message is sent to all recipients. The To and Cc headers list all
to
andcc
addresses, and the message is blind copied to allbcc
addresses. (This is usually how people expectcc
andbcc
to work.)When preserve recipients if False, Mandrill sends multiple copies of the message, one per recipient. Each message has only that recipient’s address in the To header (even for
cc
andbcc
addresses), so recipients do not see each others’ email addresses.
The default for
preserve_recipients
depends on Mandrill’s account level setting “Expose the list of recipients when sending to multiple addresses” (checked sets preserve recipients to True). However, Anymail overrides this setting toFalse
for any messages that use batch sending features.For individual non-batch messages, you can override your account default using Anymail’s esp_extra:
message.esp_extra = {"message": {"preserve_recipients": True}}
. You can also use Anymail’s Global send defaults setting to override it for all non-batch messages.- No merge headers support
Mandrill’s API does not provide a way to support Anymail’s
merge_headers
.- Envelope sender uses only domain
Anymail’s
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.)
Batch sending/merge and ESP templates
Mandrill offers both ESP stored templates and batch sending with per-recipient merge data.
You can use a Mandrill stored template by setting a message’s
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 merge_data
and merge_global_data
message attributes.
# 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=["[email protected]", "Bob <[email protected]>"] ) # (you'd probably also set a similar html body with merge fields) message.merge_data = { '[email protected]': {'name': "Alice", 'order_no': "12345"}, '[email protected]': {'name': "Bob", 'order_no': "54321"}, } message.merge_global_data = { 'ship_date': "May 15", }
When you supply per-recipient 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.
Status tracking and inbound webhooks
If you are using Anymail’s normalized status tracking and/or inbound handling, setting up Anymail’s webhook URL requires deploying your Django project twice:
First, follow the instructions to configure Anymail’s webhooks. 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:
https://random:random@yoursite.example.com/anymail/mandrill/
random:random is an
ANYMAIL_WEBHOOK_SECRET
shared secretyoursite.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.)
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
MANDRILL_WEBHOOK_KEY
. (You may also need to setMANDRILL_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 AnymailWebhookValidationFailure
with a different URL than you
provided Mandrill, you may need to examine your Django SECURE_PROXY_SSL_HEADER
,
USE_X_FORWARDED_HOST
, and/or USE_X_FORWARDED_PORT
settings. If all
else fails, you can set Anymail’s MANDRILL_WEBHOOK_URL
to the same public webhook URL you gave Mandrill.
Mandrill will report these Anymail 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 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.)
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.
Changes to settings
MANDRILL_API_KEY
Will still work, but consider moving it into the
ANYMAIL
settings dict, or changing it toANYMAIL_MANDRILL_API_KEY
.MANDRILL_SETTINGS
Use
ANYMAIL_SEND_DEFAULTS
and/orANYMAIL_MANDRILL_SEND_DEFAULTS
(see Global send defaults).There is one slight behavioral difference between
ANYMAIL_SEND_DEFAULTS
and Djrill’sMANDRILL_SETTINGS
: in Djrill, settingtags
ormerge_vars
on a message would completely override any global settings defaults. In Anymail, those message attributes are merged with the values fromANYMAIL_SEND_DEFAULTS
.MANDRILL_SUBACCOUNT
Set esp_extra globally in
ANYMAIL_SEND_DEFAULTS
:ANYMAIL = { ... "MANDRILL_SEND_DEFAULTS": { "esp_extra": { "message": { "subaccount": "<your subaccount>" } } } }
MANDRILL_IGNORE_RECIPIENT_STATUS
Renamed to
ANYMAIL_IGNORE_RECIPIENT_STATUS
(or justIGNORE_RECIPIENT_STATUS
in theANYMAIL
settings dict).DJRILL_WEBHOOK_SECRET
andDJRILL_WEBHOOK_SECRET_NAME
Replaced with HTTP basic auth. See Securing webhooks.
DJRILL_WEBHOOK_SIGNATURE_KEY
Use
ANYMAIL_MANDRILL_WEBHOOK_KEY
instead.DJRILL_WEBHOOK_URL
Often no longer required: Anymail can normally use Django’s
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
SECURE_PROXY_SSL_HEADER
,USE_X_FORWARDED_HOST
, and/orUSE_X_FORWARDED_PORT
settings to work with your proxy server. If that’s not possible, you can setANYMAIL_MANDRILL_WEBHOOK_URL
to explicitly declare the webhook url.
Changes to EmailMessage attributes
message.send_at
If you are using an aware datetime for
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
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:mandrill_response = message.anymail_status.esp_response.json()
message.template_name
Anymail renames this to
template_id
.message.merge_vars
andmessage.global_merge_vars
Anymail renames these to
merge_data
andmerge_global_data
, respectively.message.use_template_from
andmessage.use_template_subject
With Anymail, set
message.from_email = None
ormessage.subject = None
to use the values from the stored template.message.return_path_domain
With Anymail, set
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 @.
- 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 esp_extra dict as described above.
Changed in version 10.0: These Djrill-specific attributes are no longer supported, and will be silently ignored. (Earlier versions raised a
DeprecationWarning
but still worked.)You can also use the following git grep expression to find potential problems:
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 Content-ID header to decide whether to treat an image as inline. Anymail looks for 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 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 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 Tracking sent mail status
and Receiving mail.
The equivalent of Djrill’s data
parameter is available
to your signal receiver as
event.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 AnymailTrackingEvent
and
AnymailInboundEvent
instead for easy portability
to other ESPs.