Anymail additions

Anymail normalizes several common ESP features, like adding metadata or tags to a message. It also normalizes the response from the ESP’s send API.

There are three ways you can use Anymail’s ESP features with your Django email:

  • Just use Anymail’s added attributes directly on any Django EmailMessage object (or any subclass).
  • Create your email message using the AnymailMessage class, which exposes extra attributes for the ESP features.
  • Use the AnymailMessageMixin to add the Anymail extras to some other EmailMessage-derived class (your own or from another Django package).

The first approach is usually the simplest. The other two can be helpful if you are working with Python development tools that offer type checking or other static code analysis.

ESP send options (AnymailMessage)

class anymail.message.AnymailMessage

A subclass of Django’s EmailMultiAlternatives that exposes additional ESP functionality.

The constructor accepts any of the attributes below, or you can set them directly on the message at any time before sending:

from anymail.message import AnymailMessage

message = AnymailMessage(
    subject="Welcome",
    body="Welcome to our site",
    to=["New User <user1@example.com>"],
    tags=["Onboarding"],  # Anymail extra in constructor
)
# Anymail extra attributes:
message.metadata = {"onboarding_experiment": "variation 1"}
message.track_clicks = True

message.send()
status = message.anymail_status  # available after sending
status.message_id  # e.g., '<12345.67890@example.com>'
status.recipients["user1@example.com"].status  # e.g., 'queued'

Attributes you can add to messages

Note

Anymail looks for these attributes on any EmailMessage you send. (You don’t have to use AnymailMessage.)

metadata

Set this to a dict of metadata values the ESP should store with the message, for later search and retrieval.

message.metadata = {"customer": customer.id,
                    "order": order.reference_number}

ESPs have differing restrictions on metadata content. For portability, it’s best to stick to alphanumeric keys, and values that are numbers or strings.

You should format any non-string data into a string before setting it as metadata. See Formatting merge data.

tags

Set this to a list of str tags to apply to the message (usually for segmenting ESP reporting).

message.tags = ["Order Confirmation", "Test Variant A"]

ESPs have differing restrictions on tags. For portability, it’s best to stick with strings that start with an alphanumeric character. (Also, Postmark only allows a single tag per message.)

Caution

Some ESPs put metadata and tags in email headers, which are included with the email when it is delivered. Anything you put in them could be exposed to the recipients, so don’t include sensitive data.

track_opens

Set this to True or False to override your ESP account default setting for tracking when users open a message.

message.track_opens = True
track_clicks

Set this to True or False to override your ESP account default setting for tracking when users click on a link in a message.

message.track_clicks = False
send_at

Set this to a datetime, date to have the ESP wait until the specified time to send the message. (You can also use a float or int, which will be treated as a POSIX timestamp as in time.time().)

from datetime import datetime, timedelta
from django.utils.timezone import utc

message.send_at = datetime.now(utc) + timedelta(hours=1)

To avoid confusion, it’s best to provide either an aware datetime (one that has its tzinfo set), or an int or float seconds-since-the-epoch timestamp.

If you set send_at to a date or a naive datetime (without a timezone), Anymail will interpret it in Django’s current timezone. (Careful: datetime.now() returns a naive datetime, unless you call it with a timezone like in the example above.)

The sent message will be held for delivery by your ESP – not locally by Anymail.

esp_extra

Set this to a dict of additional, ESP-specific settings for the message.

Using this attribute is inherently non-portable between ESPs, and is intended as an “escape hatch” for accessing functionality that Anymail doesn’t (or doesn’t yet) support.

See the notes for each specific ESP for information on its esp_extra handling.

Status response from the ESP

anymail_status

Normalized response from the ESP API’s send call. Anymail adds this to each EmailMessage as it is sent.

The value is an AnymailStatus. See ESP send status for details.

Convenience methods

(These methods are only available on AnymailMessage or AnymailMessageMixin objects. Unlike the attributes above, they can’t be used on an arbitrary EmailMessage.)

attach_inline_image_file(path, subtype=None, idstring="img", domain=None)

Attach an inline (embedded) image to the message and return its Content-ID.

This calls attach_inline_image_file() on the message. See Inline images for details and an example.

attach_inline_image(content, filename=None, subtype=None, idstring="img", domain=None)

Attach an inline (embedded) image to the message and return its Content-ID.

This calls attach_inline_image() on the message. See Inline images for details and an example.

ESP send status

class anymail.message.AnymailStatus

When you send a message through an Anymail backend, Anymail adds an anymail_status attribute to the EmailMessage, with a normalized version of the ESP’s response.

Anymail backends create this attribute as they process each message. Before that, anymail_status won’t be present on an ordinary Django EmailMessage or EmailMultiAlternatives—you’ll get an AttributeError if you try to access it.

This might cause problems in your test cases, because Django substitutes its own locmem EmailBackend during testing (so anymail_status never gets attached to the EmailMessage). If you run into this, you can: change your code to guard against a missing anymail_status attribute; switch from using EmailMessage to AnymailMessage (or the AnymailMessageMixin) to ensure the anymail_status attribute is always there; or substitute Anymail’s test backend in any affected test cases.

After sending through an Anymail backend, anymail_status will be an object with these attributes:

message_id

The message id assigned by the ESP, or None if the send call failed.

The exact format varies by ESP. Some use a UUID or similar; some use an RFC 2822 Message-ID as the id:

message.anymail_status.message_id
# '<20160306015544.116301.25145@example.org>'

Some ESPs assign a unique message ID for each recipient (to, cc, bcc) of a single message. In that case, message_id will be a set of all the message IDs across all recipients:

message.anymail_status.message_id
# set(['16fd2706-8baf-433b-82eb-8c7fada847da',
#      '886313e1-3b8a-5372-9b90-0c9aee199e5d'])
status

A set of send statuses, across all recipients (to, cc, bcc) of the message, or None if the send call failed.

message1.anymail_status.status
# set(['queued'])  # all recipients were queued
message2.anymail_status.status
# set(['rejected', 'sent'])  # at least one recipient was sent,
                             # and at least one rejected

# This is an easy way to check there weren't any problems:
if message3.anymail_status.status.issubset({'queued', 'sent'}):
    print("ok!")

Anymail normalizes ESP sent status to one of these values:

  • 'sent' the ESP has sent the message (though it may or may not end up delivered)
  • 'queued' the ESP has accepted the message and will try to send it asynchronously
  • 'invalid' the ESP considers the sender or recipient email invalid
  • 'rejected' the recipient is on an ESP blacklist (unsubscribe, previous bounces, etc.)
  • 'failed' the attempt to send failed for some other reason
  • 'unknown' anything else

Not all ESPs check recipient emails during the send API call – some simply queue the message, and report problems later. In that case, you can use Anymail’s Tracking sent mail status features to be notified of delivery status events.

recipients

A dict of per-recipient message ID and status values.

The dict is keyed by each recipient’s base email address (ignoring any display name). Each value in the dict is an object with status and message_id properties:

message = EmailMultiAlternatives(
    to=["you@example.com", "Me <me@example.com>"],
    subject="Re: The apocalypse")
message.send()

message.anymail_status.recipients["you@example.com"].status
# 'sent'
message.anymail_status.recipients["me@example.com"].status
# 'queued'
message.anymail_status.recipients["me@example.com"].message_id
# '886313e1-3b8a-5372-9b90-0c9aee199e5d'

Will be an empty dict if the send call failed.

esp_response

The raw response from the ESP API call. The exact type varies by backend. Accessing this is inherently non-portable.

# This will work with a requests-based backend:
message.anymail_status.esp_response.json()

Inline images

Anymail includes convenience functions to simplify attaching inline images to email.

These functions work with any Django EmailMessage – they’re not specific to Anymail email backends. You can use them with messages sent through Django’s SMTP backend or any other that properly supports MIME attachments.

(Both functions are also available as convenience methods on Anymail’s AnymailMessage and AnymailMessageMixin classes.)

anymail.message.attach_inline_image_file(message, path, subtype=None, idstring="img", domain=None)

Attach an inline (embedded) image to the message and return its Content-ID.

In your HTML message body, prefix the returned id with cid: to make an <img> src attribute:

from django.core.mail import EmailMultiAlternatives
from anymail.message import attach_inline_image_file

message = EmailMultiAlternatives( ... )
cid = attach_inline_image_file(message, 'path/to/picture.jpg')
html = '... <img alt="Picture" src="cid:%s"> ...' % cid
message.attach_alternative(html, 'text/html')

message.send()

message must be an EmailMessage (or subclass) object.

path must be the pathname to an image file. (Its basename will also be used as the attachment’s filename, which may be visible in some email clients.)

subtype is an optional MIME image subtype, e.g., "png" or "jpg". By default, this is determined automatically from the content.

idstring and domain are optional, and are passed to Python’s make_msgid() to generate the Content-ID. Generally the defaults should be fine. (But be aware the default domain can leak your server’s local hostname in the resulting email.)

anymail.message.attach_inline_image(message, content, filename=None, subtype=None, idstring="img", domain=None)

This is a version of attach_inline_image_file() that accepts raw image data, rather than reading it from a file.

message must be an EmailMessage (or subclass) object.

content must be the binary image data

filename is an optional str that will be used as as the attachment’s filename – e.g., "picture.jpg". This may be visible in email clients that choose to display the image as an attachment as well as making it available for inline use (this is up to the email client). It should be a base filename, without any path info.

subtype, idstring and domain are as described in attach_inline_image_file()

Global send defaults

In your settings.py, you can set ANYMAIL_SEND_DEFAULTS to a dict of default options that will apply to all messages sent through Anymail:

ANYMAIL = {
    ...
    "SEND_DEFAULTS": {
        "metadata": {"district": "North", "source": "unknown"},
        "tags": ["myapp", "version3"],
        "track_clicks": True,
        "track_opens": True,
    },
}

At send time, the attributes on each EmailMessage get merged with the global send defaults. For example, with the settings above:

message = AnymailMessage(...)
message.tags = ["welcome"]
message.metadata = {"source": "Ads", "user_id": 12345}
message.track_clicks = False

message.send()
# will send with:
#   tags: ["myapp", "version3", "welcome"] (merged with defaults)
#   metadata: {"district": "North", "source": "Ads", "user_id": 12345} (merged)
#   track_clicks: False (message overrides defaults)
#   track_opens: True (from the defaults)

To prevent a message from using a particular global default, set that attribute to None. (E.g., message.tags = None will send the message with no tags, ignoring the global default.)

Anymail’s send defaults actually work for all django.core.mail.EmailMessage attributes. So you could set "bcc": ["always-copy@example.com"] to add a bcc to every message. (You could even attach a file to every message – though your recipients would probably find that annoying!)

You can also set ESP-specific global defaults. If there are conflicts, the ESP-specific value will override the main SEND_DEFAULTS:

ANYMAIL = {
    ...
    "SEND_DEFAULTS": {
        "tags": ["myapp", "version3"],
    },
    "POSTMARK_SEND_DEFAULTS": {
        # Postmark only supports a single tag
        "tags": ["version3"],  # overrides SEND_DEFAULTS['tags'] (not merged!)
    },
    "MAILGUN_SEND_DEFAULTS": {
        "esp_extra": {"o:dkim": "no"},  # Disable Mailgun DKIM signatures
    },
}

AnymailMessageMixin

class anymail.message.AnymailMessageMixin

Mixin class that adds Anymail’s ESP extra attributes and convenience methods to other EmailMessage subclasses.

For example, with the django-mail-templated package’s custom EmailMessage:

from anymail.message import AnymailMessageMixin
from mail_templated import EmailMessage

class TemplatedAnymailMessage(AnymailMessageMixin, EmailMessage):
    """
    An EmailMessage that supports both Mail-Templated
    and Anymail features
    """
    pass

msg = TemplatedAnymailMessage(
    template_name="order_confirmation.tpl",  # Mail-Templated arg
    track_opens=True,  # Anymail arg
    ...
)
msg.context = {"order_num": "12345"}  # Mail-Templated attribute
msg.tags = ["templated"]  # Anymail attribute