Mandrill

Anymail integrates with the Mandrill 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 mandrill 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. More info in Mandrill’s KB.

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']).

Similary, 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.

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=["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 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 webhooks

If you are using Anymail’s normalized status tracking, setting up Anymail’s webhook URL requires deploying your Django project twice:

  1. First, follow the instructions to configure Anymail’s webhooks. You must deploy before adding the webhook URL to Mandrill, because it will attempt to verify the URL against your production server.

    Follow Mandrill’s instructions to add Anymail’s webhook URL in their settings:

    https://random:random@yoursite.example.com/anymail/mandrill/tracking/

    Be sure to check the boxes in the Mandrill settings for the event types you want to receive. The same Anymail tracking URL can handle all Mandrill “message” and “change” 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 MANDRILL_WEBHOOK_KEY. (You may also need to set 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 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_types: sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed. 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 to ANYMAIL_MANDRILL_API_KEY.
MANDRILL_SETTINGS

Use ANYMAIL_SEND_DEFAULTS and/or ANYMAIL_MANDRILL_SEND_DEFAULTS (see Global send defaults).

There is one slight behavioral difference between ANYMAIL_SEND_DEFAULTS and Djrill’s MANDRILL_SETTINGS: in Djrill, setting tags or merge_vars on a message would completely override any global settings defaults. In Anymail, those message attributes are merged with the values from ANYMAIL_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 just IGNORE_RECIPIENT_STATUS in the ANYMAIL settings dict).
DJRILL_WEBHOOK_SECRET and DJRILL_WEBHOOK_SECRET_NAME
Replaced with HTTP basic auth. See Securing webhooks.
DJRILL_WEBHOOK_SIGNATURE_KEY
Use ANYMAIL_MANDRILL_WEBHOOK_KEY instead.
DJRILL_WEBHOOK_URL
Use ANYMAIL_MANDRILL_WEBHOOK_URL, or eliminate if your Django server is not behind a proxy that changes hostnames.

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 and message.global_merge_vars
Anymail renames these to merge_data and 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 esp_extra dict as described above.

Although the Djrill style attributes are still supported (for now), Anymail will issue a 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:

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. (It does not currently handle inbound message webhooks.) Anymail parses and normalizes the event data passed to the signal receiver: see Tracking sent mail status.

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 instead.