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)
Availability of each of these features varies by ESP, and there may be additional limitations even when an ESP does support a particular feature. Be sure to check Anymail’s docs for your specific ESP. If you try to use a feature your ESP does not offer, Anymail will raise an unsupported feature error.
- 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 <[email protected]>"], 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., '<[email protected]>' status.recipients["[email protected]"].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 useAnymailMessage
.)- envelope_sender
Set this to a
str
email address that should be used as the message’s envelope sender. If supported by your ESP, this will become the Return-Path in the recipient’s mailbox.(Envelope sender is also known as bounce address, MAIL FROM, envelope from, unixfrom, SMTP FROM command, return path, and several other terms. Confused? Here’s some good info on how envelope sender relates to return path.)
ESP support for envelope sender varies widely. Be sure to check Anymail’s docs for your specific ESP before attempting to use it. And note that those ESPs who do support it will often use only the domain portion of the envelope sender address, overriding the part before the @ with their own encoded bounce mailbox.
[The
envelope_sender
attribute is unique to Anymail. If you also use Django’s SMTP EmailBackend, you can portably control envelope sender by instead settingmessage.extra_headers["From"]
to the desired email header From, andmessage.from_email
to the envelope sender. Anymail also allows this approach, for compatibility with the SMTP EmailBackend. See the notes in Django’s bug tracker.]
- merge_headers
Added in version 11.0.
On a message with multiple recipients, if your ESP supports it, you can set this to a
dict
of per-recipient extra email headers. Each key in the dict is a recipient email (address portion only), and its value is a dict of header fields and values for that recipient:message.to = ["[email protected]", "R. Runner <[email protected]>"] message.extra_headers = { # Headers for all recipients "List-Unsubscribe-Post": "List-Unsubscribe=One-Click", } message.merge_headers = { # Per-recipient headers "[email protected]": { "List-Unsubscribe": "<https://example.com/unsubscribe/12345>", }, "[email protected]": { "List-Unsubscribe": "<https://example.com/unsubscribe/98765>", }, }
When
merge_headers
is set, Anymail will use the ESP’s batch sending option, so that eachto
recipient gets an individual message (and doesn’t see the other emails on theto
list).Many ESPs restrict which headers are allowed. Be sure to check Anymail’s ESP-specific docs for your ESP. (Also, special handling for From, To and Reply-To headers does not apply to
merge_headers
.)If
merge_headers
defines a particular header for only some recipients, the default for other recipients comes from the message’s extra_headers. If not defined there, behavior varies by ESP: some will include the header field only for recipients where you have provided it; other ESPs will send an empty header field to the other recipients.
- metadata
If your ESP supports tracking arbitrary metadata, you can set this to a
dict
of metadata values the ESP should store with the message, for later search and retrieval. This can be useful with Anymail’s status tracking webhooks.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.
Depending on the ESP, this metadata could be exposed to the recipients in the message headers, so don’t include sensitive data.
- merge_metadata
On a message with multiple recipients, if your ESP supports it, you can set this to a
dict
of per-recipient metadata values the ESP should store with the message, for later search and retrieval. Each key in the dict is a recipient email (address portion only), and its value is a dict of metadata for that recipient:message.to = ["[email protected]", "Mr. Runner <[email protected]>"] message.merge_metadata = { "[email protected]": {"customer": 123, "order": "acme-zxyw"}, "[email protected]": {"customer": 45678, "order": "acme-wblt"}, }
When
merge_metadata
is set, Anymail will use the ESP’s batch sending option, so that eachto
recipient gets an individual message (and doesn’t see the other emails on theto
list).All of the notes on
metadata
keys and value formatting also apply tomerge_metadata
. If there are conflicting keys, themerge_metadata
values will take precedence overmetadata
for that recipient.Depending on the ESP, this metadata could be exposed to the recipients in the message headers, so don’t include sensitive data.
- tags
If your ESP supports it, you can set this to a
list
ofstr
tags to apply to the message. This can be useful for segmenting your ESP’s reports, and is also often used with Anymail’s status tracking webhooks.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, a few ESPs allow only a single tag per message.)
Caution
Some ESPs put
metadata
(and a recipient’smerge_metadata
) andtags
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
If your ESP supports open tracking, you can set this to
True
orFalse
to override your ESP’s default for this particular message. (Most ESPs let you configure open tracking defaults at the account or sending domain level.)For example, if you have configured your ESP to not insert open tracking pixels by default, this will attempt to enable that for this one message:
message.track_opens = True
- track_clicks
If your ESP supports click tracking, you can set this to
True
orFalse
to override your ESP’s default for this particular message. (Most ESPs let you configure click tracking defaults at the account or sending domain level.)For example, if you have configured your ESP to normally rewrite links to add click tracking, this will attempt to disable that for this one message:
message.track_clicks = False
- send_at
If your ESP supports scheduled transactional sending, you can set this to a
datetime
to have the ESP delay sending the message until the specified time. (You can also use afloat
orint
, which will be treated as a POSIX timestamp as intime.time()
.)from datetime import datetime, timedelta, timezone message.send_at = datetime.now(timezone.utc) + timedelta(hours=1)
To avoid confusion, it’s best to provide either an aware
datetime
(one that has its tzinfo set), or anint
orfloat
seconds-since-the-epoch timestamp.If you set
send_at
to adate
or a naivedatetime
(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
Although Anymail normalizes common ESP features, many ESPs offer additional functionality that doesn’t map neatly to Anymail’s standard options. You can use
esp_extra
as an “escape hatch” to access ESP functionality that Anymail doesn’t (or doesn’t yet) support.Set it to a
dict
of additional, ESP-specific settings for the message. See the notes for each specific ESP for information on itsesp_extra
handling.Using this attribute is inherently non-portable between ESPs, so it’s best to avoid it unless absolutely necessary. If you ever want to switch ESPs, you will need to update or remove all uses of
esp_extra
to avoid unexpected behavior.
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 below for details.
Convenience methods
(These methods are only available on
AnymailMessage
orAnymailMessageMixin
objects. Unlike the attributes above, they can’t be used on an arbitraryEmailMessage
.)- 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 theEmailMessage
, 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 theAnymailMessageMixin
) 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 # '<[email protected]>'
Some ESPs assign a unique message ID for each recipient (to, cc, bcc) of a single message. In that case,
message_id
will be aset
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, orNone
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 suppression list (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
andmessage_id
properties:message = EmailMultiAlternatives( to=["[email protected]", "Me <[email protected]>"], subject="Re: The apocalypse") message.send() message.anymail_status.recipients["[email protected]"].status # 'sent' message.anymail_status.recipients["[email protected]"].status # 'queued' message.anymail_status.recipients["[email protected]"].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, # for an ESP whose send API provides a JSON response: 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 anEmailMessage
(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
anddomain
are optional, and are passed to Python’smake_msgid()
to generate the Content-ID. Generally the defaults should be fine.Changed in version 4.0: If you don’t supply a
domain
, Anymail will use the simple string “inline” rather thanmake_msgid()
’s default local hostname. This avoids a problem with ESPs that confuse Content-ID and attachment filename: if your local server’s hostname ends in “.com”, Gmail could block messages with inline attachments generated by earlier Anymail versions and sent through these ESPs.
- 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 anEmailMessage
(or subclass) object.content
must be the binary image datafilename
is an optionalstr
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
anddomain
are as described inattach_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