mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Update (almost) all the docs
This commit is contained in:
449
docs/sending/anymail_additions.rst
Normal file
449
docs/sending/anymail_additions.rst
Normal file
@@ -0,0 +1,449 @@
|
||||
.. _anymail-send-features:
|
||||
|
||||
.. module:: anymail.message
|
||||
|
||||
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
|
||||
:class:`~django.core.mail.EmailMessage` object (or any subclass).
|
||||
|
||||
* Create your email message using the :class:`AnymailMessage` class,
|
||||
which exposes extra attributes for the ESP features.
|
||||
|
||||
* Use the :class:`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:: AnymailMessage
|
||||
|
||||
A subclass of Django's :class:`~django.core.mail.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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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'
|
||||
|
||||
|
||||
.. rubric:: Attributes you can add to messages
|
||||
|
||||
.. attribute:: metadata
|
||||
|
||||
Set this to a `dict` of metadata values the ESP should store
|
||||
with the message, for later search and retrieval.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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 :ref:`formatting-merge-data`.
|
||||
|
||||
|
||||
.. attribute:: tags
|
||||
|
||||
Set this to a `list` of `str` tags to apply to the message (usually
|
||||
for segmenting ESP reporting).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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 :attr:`metadata` and :attr:`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.
|
||||
|
||||
|
||||
.. attribute:: track_opens
|
||||
|
||||
Set this to `True` or `False` to override your ESP account default
|
||||
setting for tracking when users open a message.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message.track_opens = True
|
||||
|
||||
|
||||
.. attribute:: 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.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message.track_clicks = False
|
||||
|
||||
|
||||
.. attribute:: send_at
|
||||
|
||||
Set this to a `~datetime.datetime`, `~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 :func:`time.time`.)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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.datetime` (one that has its tzinfo set), or an
|
||||
`int` or `float` seconds-since-the-epoch timestamp.
|
||||
|
||||
If you set :attr:`!send_at` to a `~datetime.date` or a *naive*
|
||||
`~datetime.datetime` (without a timezone), Anymail will interpret it in
|
||||
Django's :ref:`current timezone <django:default-current-time-zone>`.
|
||||
(Careful: :meth:`datetime.now() <datetime.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.
|
||||
|
||||
|
||||
.. attribute:: 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 :ref:`specific ESP <supported-esps>` for information
|
||||
on its :attr:`!esp_extra` handling.
|
||||
|
||||
|
||||
.. rubric:: Status response from the ESP
|
||||
|
||||
.. attribute:: anymail_status
|
||||
|
||||
Normalized response from the ESP API's send call. Anymail adds this
|
||||
to each :class:`~django.core.mail.EmailMessage` as it is sent.
|
||||
|
||||
The value is an :class:`AnymailStatus`.
|
||||
See :ref:`esp-send-status` for details.
|
||||
|
||||
|
||||
.. rubric:: Convenience methods
|
||||
|
||||
(These methods are only available on :class:`AnymailMessage` or
|
||||
:class:`AnymailMessageMixin` objects. Unlike the attributes above,
|
||||
they can't be used on an arbitrary :class:`~django.core.mail.EmailMessage`.)
|
||||
|
||||
.. method:: attach_inline_image(content, subtype=None, idstring="img", domain=None)
|
||||
|
||||
Attach an inline (embedded) image to the message and return its :mailheader:`Content-ID`.
|
||||
|
||||
This calls :func:`attach_inline_image` on the message. See :ref:`inline-images`
|
||||
for details and an example.
|
||||
|
||||
|
||||
.. _esp-send-status:
|
||||
|
||||
ESP send status
|
||||
---------------
|
||||
|
||||
.. class:: AnymailStatus
|
||||
|
||||
When you send a message through an Anymail backend, Anymail adds
|
||||
an :attr:`~AnymailMessage.anymail_status` attribute to the
|
||||
:class:`~django.core.mail.EmailMessage`, with a normalized version
|
||||
of the ESP's response.
|
||||
|
||||
:attr:`~AnymailMessage.anymail_status` will be an object with these attributes:
|
||||
|
||||
.. attribute:: 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` :mailheader:`Message-ID` as the id:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, :attr:`!message_id` will be a
|
||||
`set` of all the message IDs across all recipients:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message.anymail_status.message_id
|
||||
# set(['16fd2706-8baf-433b-82eb-8c7fada847da',
|
||||
# '886313e1-3b8a-5372-9b90-0c9aee199e5d'])
|
||||
|
||||
|
||||
.. attribute:: status
|
||||
|
||||
A `set` of send statuses, across all recipients (to, cc, bcc) of the
|
||||
message, or `None` if the send call failed.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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 :ref:`event-tracking` features to be notified
|
||||
of delivery status events.
|
||||
|
||||
|
||||
.. attribute:: 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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message = EmailMultiAlternatives(
|
||||
to=["you@example.com", "Me <me@example.com>"],
|
||||
subject="Re: The apocalypse")
|
||||
message.send()
|
||||
|
||||
message.anymail_status.recipient["you@example.com"].status
|
||||
# 'sent'
|
||||
message.anymail_status.recipient["me@example.com"].status
|
||||
# 'queued'
|
||||
message.anymail_status.recipient["me@example.com"].message_id
|
||||
# '886313e1-3b8a-5372-9b90-0c9aee199e5d'
|
||||
|
||||
Will be an empty dict if the send call failed.
|
||||
|
||||
|
||||
.. attribute:: esp_response
|
||||
|
||||
The raw response from the ESP API call. The exact type varies by
|
||||
backend. Accessing this is inherently non-portable.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# This will work with a requests-based backend:
|
||||
message.anymail_status.esp_response.json()
|
||||
|
||||
|
||||
.. _inline-images:
|
||||
|
||||
Inline images
|
||||
-------------
|
||||
|
||||
Anymail includes a convenience function to simplify attaching inline images to email.
|
||||
|
||||
.. function:: attach_inline_image(message, content, subtype=None, idstring="img", domain=None)
|
||||
|
||||
Attach an inline (embedded) image to the message and return its :mailheader:`Content-ID`.
|
||||
|
||||
In your HTML message body, prefix the returned id with `cid:` to make an
|
||||
`<img>` src attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from anymail.message import attach_inline_image
|
||||
|
||||
# read image content -- be sure to open the file in binary mode:
|
||||
with f = open("path/to/picture.jpg", "rb"):
|
||||
raw_image_data = f.read()
|
||||
|
||||
message = EmailMultiAlternatives( ... )
|
||||
cid = attach_inline_image(message, raw_image_data)
|
||||
html = '... <img alt="Picture" src="cid:%s"> ...' % cid
|
||||
message.attach_alternative(html, "text/html")
|
||||
|
||||
message.send()
|
||||
|
||||
|
||||
`message` must be an :class:`~django.core.mail.EmailMessage` (or subclass) object.
|
||||
|
||||
`content` must be the binary image data (e.g., read from a file).
|
||||
|
||||
`subtype` is an optional MIME :mimetype:`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
|
||||
:func:`~email.utils.make_msgid` to generate the :mailheader:`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.)
|
||||
|
||||
This function works with *any* Django :class:`~django.core.mail.EmailMessage` --
|
||||
it's not specific to Anymail email backends. You can use it with messages sent
|
||||
through Django's SMTP backend or any other that properly supports MIME attachments.
|
||||
|
||||
(This function is also available as the
|
||||
:meth:`~anymail.message.AnymailMessage.attach_inline_image` method
|
||||
on Anymail's :class:`~anymail.message.AnymailMessage` and
|
||||
:class:`~anymail.message.AnymailMessageMixin` classes.)
|
||||
|
||||
|
||||
.. _send-defaults:
|
||||
|
||||
Global send defaults
|
||||
--------------------
|
||||
|
||||
.. setting:: ANYMAIL_SEND_DEFAULTS
|
||||
|
||||
In your :file:`settings.py`, you can set :setting:`!ANYMAIL_SEND_DEFAULTS`
|
||||
to a `dict` of default options that will apply to all messages sent through Anymail:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ANYMAIL = {
|
||||
...
|
||||
"SEND_DEFAULTS": {
|
||||
"metadata": {"district": "North", "source": "unknown"},
|
||||
"tags": ["myapp", "version3"],
|
||||
"track_clicks": True,
|
||||
"track_opens": True,
|
||||
},
|
||||
}
|
||||
|
||||
At send time, the attributes on each :class:`~django.core.mail.EmailMessage`
|
||||
get merged with the global send defaults. For example, with the
|
||||
settings above:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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 :class:`!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`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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:: AnymailMessageMixin
|
||||
|
||||
Mixin class that adds Anymail's ESP extra attributes and convenience methods
|
||||
to other :class:`~django.core.mail.EmailMessage` subclasses.
|
||||
|
||||
For example, with the `django-mail-templated`_ package's custom EmailMessage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
|
||||
|
||||
.. _django-mail-templated: https://pypi.python.org/pypi/django-mail-templated
|
||||
162
docs/sending/django_email.rst
Normal file
162
docs/sending/django_email.rst
Normal file
@@ -0,0 +1,162 @@
|
||||
.. currentmodule:: anymail
|
||||
|
||||
.. _sending-django-email:
|
||||
|
||||
Django email support
|
||||
====================
|
||||
|
||||
Anymail builds on Django's core email functionality. If you are already sending
|
||||
email using Django's default SMTP :class:`~django.core.mail.backends.smtp.EmailBackend`,
|
||||
switching to Anymail will be easy. Anymail is designed to "just work" with Django.
|
||||
|
||||
If you're not familiar with Django's email functions, please take a look at
|
||||
":mod:`sending email <django.core.mail>`" in the Django docs first.
|
||||
|
||||
Anymail supports most of the functionality of Django's :class:`~django.core.mail.EmailMessage`
|
||||
and :class:`~django.core.mail.EmailMultiAlternatives` classes.
|
||||
|
||||
Anymail handles **all** outgoing email sent through Django's
|
||||
:mod:`django.core.mail` package, including :func:`~django.core.mail.send_mail`,
|
||||
:func:`~django.core.mail.send_mass_mail`, the :class:`~django.core.mail.EmailMessage` class,
|
||||
and even :func:`~django.core.mail.mail_admins`.
|
||||
If you'd like to selectively send only some messages through Anymail,
|
||||
or you'd like to use different ESPs for particular messages,
|
||||
there are ways to use :ref:`multiple email backends <multiple-backends>`.
|
||||
|
||||
|
||||
.. _sending-html:
|
||||
|
||||
HTML email
|
||||
----------
|
||||
|
||||
To send an HTML message, you can simply use Django's :func:`~django.core.mail.send_mail`
|
||||
function with the ``html_message`` parameter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.mail import send_mail
|
||||
|
||||
send_mail("Subject", "text body", "from@example.com",
|
||||
["to@example.com"], html_message="<html>html body</html>")
|
||||
|
||||
However, many Django email capabilities -- and additional Anymail features --
|
||||
are only available when working with an :class:`~django.core.mail.EmailMultiAlternatives`
|
||||
object. Use its :meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative`
|
||||
method to send HTML:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
|
||||
msg = EmailMultiAlternatives("Subject", "text body",
|
||||
"from@example.com", ["to@example.com"])
|
||||
msg.attach_alternative("<html>html body</html>", "text/html")
|
||||
# you can set any other options on msg here, then...
|
||||
msg.send()
|
||||
|
||||
It's good practice to send equivalent content in your plain-text body
|
||||
and the html version.
|
||||
|
||||
|
||||
.. _sending-attachments:
|
||||
|
||||
Attachments
|
||||
-----------
|
||||
|
||||
Anymail will send a message's attachments to your ESP. You can add attachments
|
||||
with the :meth:`~django.core.mail.EmailMessage.attach` or
|
||||
:meth:`~django.core.mail.EmailMessage.attach_file` methods
|
||||
of Django's :class:`~django.core.mail.EmailMessage`.
|
||||
|
||||
Note that some ESPs impose limits on the size and type of attachments they
|
||||
will send.
|
||||
|
||||
.. rubric:: Inline images
|
||||
|
||||
If your message has any image attachments with :mailheader:`Content-ID` headers,
|
||||
Anymail will tell your ESP to treat them as inline images rather than ordinary
|
||||
attached files.
|
||||
|
||||
You can construct an inline image attachment yourself with Python's
|
||||
:class:`python:email.mime.image.MIMEImage`, or you can use the convenience
|
||||
function :func:`~message.attach_inline_image` included with
|
||||
Anymail. See :ref:`inline-images` in the "Anymail additions" section.
|
||||
|
||||
|
||||
.. _message-headers:
|
||||
|
||||
Additional headers
|
||||
------------------
|
||||
|
||||
Anymail passes additional headers to your ESP. (Some ESPs may limit
|
||||
which headers they'll allow.)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
msg = EmailMessage( ...
|
||||
headers={
|
||||
"List-Unsubscribe": unsubscribe_url,
|
||||
"X-Example-Header": "myapp",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
.. _unsupported-features:
|
||||
|
||||
Unsupported features
|
||||
--------------------
|
||||
|
||||
Some email capabilities aren't supported by all ESPs. When you try to send a
|
||||
message using features Anymail can't communicate to the current ESP, you'll get an
|
||||
:exc:`~exceptions.AnymailUnsupportedFeature` error, and the message won't be sent.
|
||||
|
||||
For example, very few ESPs support alternative message parts added with
|
||||
:meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative`
|
||||
(other than a single :mimetype:`text/html` part that becomes the HTML body).
|
||||
If you try to send a message with other alternative parts, Anymail will
|
||||
raise :exc:`~exceptions.AnymailUnsupportedFeature`.
|
||||
|
||||
.. setting:: ANYMAIL_UNSUPPORTED_FEATURE_ERRORS
|
||||
|
||||
If you'd like to silently ignore :exc:`~exceptions.AnymailUnsupportedFeature`
|
||||
errors and send the messages anyway, set :setting:`!ANYMAIL_UNSUPPORTED_FEATURE_ERRORS`
|
||||
to `False` in your settings.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ANYMAIL = {
|
||||
...
|
||||
"UNSUPPORTED_FEATURE_ERRORS": False,
|
||||
}
|
||||
|
||||
|
||||
.. _recipients-refused:
|
||||
|
||||
Refused recipients
|
||||
------------------
|
||||
|
||||
If *all* recipients (to, cc, bcc) of a message are invalid or rejected by
|
||||
your ESP *at send time,* the send call will raise an
|
||||
:exc:`~exceptions.AnymailRecipientsRefused` error.
|
||||
|
||||
You can examine the message's :attr:`~message.AnymailMessage.anymail_status`
|
||||
attribute to determine the cause of the error. (See :ref:`esp-send-status`.)
|
||||
|
||||
If a single message is sent to multiple recipients, and *any* recipient is valid
|
||||
(or the message is queued by your ESP because of rate limiting or
|
||||
:attr:`~message.AnymailMessage.send_at`), then this exception will not be raised.
|
||||
You can still examine the message's :attr:`~message.AnymailMessage.anymail_status`
|
||||
property after the send to determine the status of each recipient.
|
||||
|
||||
You can disable this exception by setting :setting:`ANYMAIL_IGNORE_RECIPIENT_STATUS`
|
||||
to `True` in your settings.py, which will cause Anymail to treat any non-API-error response
|
||||
from your ESP as a successful send.
|
||||
|
||||
.. note::
|
||||
|
||||
Many ESPs don't check recipient status during the send API call. For example,
|
||||
Mailgun always queues sent messages, so you'll never catch
|
||||
:exc:`AnymailRecipientsRefused` with the Mailgun backend.
|
||||
|
||||
For those ESPs, use Anymail's :ref:`delivery event tracking <event-tracking>`
|
||||
if you need to be notified of sends to blacklisted or invalid emails.
|
||||
50
docs/sending/exceptions.rst
Normal file
50
docs/sending/exceptions.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
.. _anymail-exceptions:
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
.. module:: anymail.exceptions
|
||||
|
||||
.. exception:: AnymailUnsupportedFeature
|
||||
|
||||
If the email tries to use features that aren't supported by the ESP, the send
|
||||
call will raise an :exc:`!AnymailUnsupportedFeature` error (a subclass
|
||||
of :exc:`ValueError`), and the message won't be sent.
|
||||
|
||||
You can disable this exception (ignoring the unsupported features and
|
||||
sending the message anyway, without them) by setting
|
||||
:setting:`ANYMAIL_UNSUPPORTED_FEATURE_ERRORS` to ``False``.
|
||||
|
||||
|
||||
.. exception:: AnymailRecipientsRefused
|
||||
|
||||
Raised when *all* recipients (to, cc, bcc) of a message are invalid or rejected by
|
||||
your ESP *at send time.* See :ref:`recipients-refused`.
|
||||
|
||||
You can disable this exception by setting :setting:`ANYMAIL_IGNORE_RECIPIENT_STATUS`
|
||||
to `True` in your settings.py, which will cause Anymail to treat any
|
||||
non-:exc:`AnymailAPIError` response from your ESP as a successful send.
|
||||
|
||||
|
||||
.. exception:: AnymailAPIError
|
||||
|
||||
If the ESP's API fails or returns an error response, the send call will
|
||||
raise an :exc:`!AnymailAPIError`.
|
||||
|
||||
The exception's :attr:`status_code` and :attr:`response` attributes may
|
||||
help explain what went wrong. (Tip: you may also be able to check the API log in
|
||||
your ESP's dashboard. See :ref:`troubleshooting`.)
|
||||
|
||||
|
||||
.. exception:: AnymailSerializationError
|
||||
|
||||
The send call will raise a :exc:`!AnymailSerializationError`
|
||||
if there are message attributes Anymail doesn't know how to represent
|
||||
to your ESP.
|
||||
|
||||
The most common cause of this error is including values other than
|
||||
strings and numbers in your :attr:`merge_data` or :attr:`metadata`.
|
||||
(E.g., you need to format `Decimal` and `date` data to
|
||||
strings before setting them into :attr:`merge_data`.)
|
||||
|
||||
See :ref:`formatting-merge-data` for more information.
|
||||
13
docs/sending/index.rst
Normal file
13
docs/sending/index.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
.. _sending-email:
|
||||
|
||||
Sending email
|
||||
-------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
django_email
|
||||
anymail_additions
|
||||
templates
|
||||
tracking
|
||||
exceptions
|
||||
146
docs/sending/templates.rst
Normal file
146
docs/sending/templates.rst
Normal file
@@ -0,0 +1,146 @@
|
||||
.. _merge-vars:
|
||||
|
||||
Mail merge and ESP templates
|
||||
============================
|
||||
|
||||
Anymail has some features to simplify using your ESP's email
|
||||
templates and merge-variable features in a portable way.
|
||||
|
||||
However, ESP templating languages are generally proprietary,
|
||||
which makes them inherently non-portable. Although Anymail
|
||||
can normalize the Django code you write to supply merge
|
||||
variables to your ESP, it can't help you avoid needing
|
||||
to rewrite your email templates if you switch ESPs.
|
||||
|
||||
:ref:`Using Django templates <django-templates>` can be a
|
||||
better, portable and maintainable option.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Normalized merge variables and template identification
|
||||
are coming to Anymail soon.
|
||||
|
||||
|
||||
.. currentmodule:: anymail.message
|
||||
|
||||
.. _esp-templates:
|
||||
|
||||
ESP templates
|
||||
-------------
|
||||
|
||||
.. To use a *Mandrill* (MailChimp) template stored in your Mandrill account,
|
||||
.. set a :attr:`template_name` and (optionally) :attr:`template_content`
|
||||
.. on your :class:`~django.core.mail.EmailMessage` object::
|
||||
..
|
||||
.. from django.core.mail import EmailMessage
|
||||
..
|
||||
.. msg = EmailMessage(subject="Shipped!", from_email="store@example.com",
|
||||
.. to=["customer@example.com", "accounting@example.com"])
|
||||
.. msg.template_name = "SHIPPING_NOTICE" # A Mandrill template name
|
||||
.. msg.template_content = { # Content blocks to fill in
|
||||
.. 'TRACKING_BLOCK': "<a href='.../*|TRACKINGNO|*'>track it</a>"
|
||||
.. }
|
||||
.. msg.global_merge_vars = { # Merge tags in your template
|
||||
.. 'ORDERNO': "12345", 'TRACKINGNO': "1Z987"
|
||||
.. }
|
||||
.. msg.merge_vars = { # Per-recipient merge tags
|
||||
.. 'accounting@example.com': {'NAME': "Pat"},
|
||||
.. 'customer@example.com': {'NAME': "Kim"}
|
||||
.. }
|
||||
.. msg.send()
|
||||
..
|
||||
.. If :attr:`template_name` is set, Djrill will use Mandrill's
|
||||
.. `messages/send-template API <https://mandrillapp.com/api/docs/messages.html#method=send-template>`_,
|
||||
.. and will ignore any `body` text set on the `EmailMessage`.
|
||||
..
|
||||
.. All of Djrill's other :ref:`Mandrill-specific options <anymail-send-features>`
|
||||
.. can be used with templates.
|
||||
|
||||
|
||||
.. attribute:: AnymailMessage.template_name
|
||||
|
||||
|
||||
.. attribute:: AnymailMessage.global_merge_vars
|
||||
|
||||
.. ``dict``: merge variables to use for all recipients (most useful with :ref:`mandrill-templates`). ::
|
||||
..
|
||||
.. message.global_merge_vars = {'company': "ACME", 'offer': "10% off"}
|
||||
..
|
||||
.. Merge data must be strings or other JSON-serializable types.
|
||||
.. (See :ref:`formatting-merge-data` for details.)
|
||||
|
||||
.. attribute:: AnymailMessage.merge_vars
|
||||
|
||||
.. ``dict``: per-recipient merge variables (most useful with :ref:`mandrill-templates`). The keys
|
||||
.. in the dict are the recipient email addresses, and the values are dicts of merge vars for
|
||||
.. each recipient::
|
||||
..
|
||||
.. message.merge_vars = {
|
||||
.. 'wiley@example.com': {'offer': "15% off anvils"},
|
||||
.. 'rr@example.com': {'offer': "instant tunnel paint"}
|
||||
.. }
|
||||
..
|
||||
.. Merge data must be strings or other JSON-serializable types.
|
||||
.. (See :ref:`formatting-merge-data` for details.)
|
||||
|
||||
|
||||
.. _formatting-merge-data:
|
||||
|
||||
Formatting merge data
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you're using a `date`, `datetime`, `Decimal`, or anything other
|
||||
than strings and integers,
|
||||
you'll need to format them into strings for use as merge data::
|
||||
|
||||
product = Product.objects.get(123) # A Django model
|
||||
total_cost = Decimal('19.99')
|
||||
ship_date = date(2015, 11, 18)
|
||||
|
||||
# Won't work -- you'll get "not JSON serializable" exceptions:
|
||||
msg.global_merge_vars = {
|
||||
'PRODUCT': product,
|
||||
'TOTAL_COST': total_cost,
|
||||
'SHIP_DATE': ship_date
|
||||
}
|
||||
|
||||
# Do something this instead:
|
||||
msg.global_merge_vars = {
|
||||
'PRODUCT': product.name, # assuming name is a CharField
|
||||
'TOTAL_COST': "%.2f" % total_cost,
|
||||
'SHIP_DATE': ship_date.strftime('%B %d, %Y') # US-style "March 15, 2015"
|
||||
}
|
||||
|
||||
These are just examples. You'll need to determine the best way to format
|
||||
your merge data as strings.
|
||||
|
||||
Although floats are allowed in merge vars, you'll generally want to format them
|
||||
into strings yourself to avoid surprises with floating-point precision.
|
||||
|
||||
Anymail will raise :exc:`~anymail.exceptions.AnymailSerializationError` if you attempt
|
||||
to send a message with non-json-serializable data.
|
||||
|
||||
|
||||
.. How To Use Default Mandrill Subject and From fields
|
||||
.. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
..
|
||||
.. To use default Mandrill "subject" or "from" field from your template definition
|
||||
.. (overriding your EmailMessage and Django defaults), set the following attrs:
|
||||
.. :attr:`use_template_subject` and/or :attr:`use_template_from` on
|
||||
.. your :class:`~django.core.mail.EmailMessage` object::
|
||||
..
|
||||
.. msg.use_template_subject = True
|
||||
.. msg.use_template_from = True
|
||||
.. msg.send()
|
||||
..
|
||||
.. .. attribute:: use_template_subject
|
||||
..
|
||||
.. If `True`, Djrill will omit the subject, and Mandrill will
|
||||
.. use the default subject from the template.
|
||||
..
|
||||
.. .. attribute:: use_template_from
|
||||
..
|
||||
.. If `True`, Djrill will omit the "from" field, and Mandrill will
|
||||
.. use the default "from" from the template.
|
||||
|
||||
178
docs/sending/tracking.rst
Normal file
178
docs/sending/tracking.rst
Normal file
@@ -0,0 +1,178 @@
|
||||
.. _event-tracking:
|
||||
|
||||
Tracking sent mail status
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
|
||||
Normalized event-tracking webhooks and signals are coming
|
||||
to Anymail soon.
|
||||
|
||||
|
||||
.. `Mandrill webhooks`_ are used for notification about outbound messages
|
||||
.. (bounces, clicks, etc.), and also for delivering inbound email
|
||||
.. processed through Mandrill.
|
||||
..
|
||||
.. Djrill includes optional support for Mandrill's webhook notifications.
|
||||
.. If enabled, it will send a Django signal for each event in a webhook.
|
||||
.. Your code can connect to this signal for further processing.
|
||||
|
||||
.. _Mandrill webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks
|
||||
|
||||
.. _webhooks-config:
|
||||
|
||||
Configuring tracking webhooks
|
||||
-----------------------------
|
||||
|
||||
.. warning:: Webhook Security
|
||||
|
||||
Webhooks are ordinary urls---they're wide open to the internet.
|
||||
You must take steps to secure webhooks, or anyone could submit
|
||||
random (or malicious) data to your app simply by invoking your
|
||||
webhook URL. For security:
|
||||
|
||||
* Your webhook should only be accessible over SSL (https).
|
||||
(This is beyond the scope of Anymail.)
|
||||
|
||||
* Your webhook must include a random, secret key, known only to your
|
||||
app and your ESP. Anymail will verify calls to your webhook, and will
|
||||
reject calls without the correct key.
|
||||
|
||||
.. * You can, optionally include the two settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY`
|
||||
.. and :setting:`DJRILL_WEBHOOK_URL` to enforce `webhook signature`_ checking
|
||||
|
||||
.. _webhook signature: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests
|
||||
|
||||
|
||||
.. To enable Djrill webhook processing you need to create and set a webhook
|
||||
.. secret in your project settings, include the Djrill url routing, and
|
||||
.. then add the webhook in the Mandrill control panel.
|
||||
..
|
||||
.. 1. In your project's :file:`settings.py`, add a :setting:`DJRILL_WEBHOOK_SECRET`:
|
||||
..
|
||||
.. .. code-block:: python
|
||||
..
|
||||
.. DJRILL_WEBHOOK_SECRET = "<create your own random secret>"
|
||||
..
|
||||
.. substituting a secret you've generated just for Mandrill webhooks.
|
||||
.. (Do *not* use your Mandrill API key or Django SECRET_KEY for this!)
|
||||
..
|
||||
.. An easy way to generate a random secret is to run the command below in a shell:
|
||||
..
|
||||
.. .. code-block:: console
|
||||
..
|
||||
.. $ python -c "from django.utils import crypto; print crypto.get_random_string(16)"
|
||||
..
|
||||
..
|
||||
.. 2. In your base :file:`urls.py`, add routing for the Djrill urls:
|
||||
..
|
||||
.. .. code-block:: python
|
||||
..
|
||||
.. urlpatterns = patterns('',
|
||||
.. ...
|
||||
.. url(r'^djrill/', include(djrill.urls)),
|
||||
.. )
|
||||
..
|
||||
..
|
||||
.. 3. Now you need to tell Mandrill about your webhook:
|
||||
..
|
||||
.. * For receiving events on sent messages (e.g., bounces or clickthroughs),
|
||||
.. you'll do this in Mandrill's `webhooks control panel`_.
|
||||
.. * For setting up inbound email through Mandrill, you'll add your webhook
|
||||
.. to Mandrill's `inbound settings`_ under "Routes" for your domain.
|
||||
.. * And if you want both, you'll need to add the webhook in both places.
|
||||
..
|
||||
.. In all cases, the "Post to URL" is
|
||||
.. :samp:`{https://yoursite.example.com}/djrill/webhook/?secret={your-secret}`
|
||||
.. substituting your app's own domain, and changing *your-secret* to the secret
|
||||
.. you created in step 1.
|
||||
..
|
||||
.. (For sent-message webhooks, don't forget to tick the "Trigger on Events"
|
||||
.. checkboxes for the events you want to receive.)
|
||||
..
|
||||
..
|
||||
.. Once you've completed these steps and your Django app is live on your site,
|
||||
.. you can use the Mandrill "Test" commands to verify your webhook configuration.
|
||||
.. Then see the next section for setting up Django signal handlers to process
|
||||
.. the webhooks.
|
||||
..
|
||||
.. Incidentally, you have some control over the webhook url.
|
||||
.. If you'd like to change the "djrill" prefix, that comes from
|
||||
.. the url config in step 2. And if you'd like to change
|
||||
.. the *name* of the "secret" query string parameter, you can set
|
||||
.. :setting:`DJRILL_WEBHOOK_SECRET_NAME` in your :file:`settings.py`.
|
||||
..
|
||||
.. For extra security, Mandrill provides a signature in the request header
|
||||
.. X-Mandrill-Signature. If you want to verify this signature, you need to provide
|
||||
.. the settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` with the webhook-specific
|
||||
.. signature key that can be found in the Mandrill admin panel and
|
||||
.. :setting:`DJRILL_WEBHOOK_URL` where you should enter the exact URL, including
|
||||
.. that you entered in Mandrill when creating the webhook.
|
||||
|
||||
.. _webhooks control panel: https://mandrillapp.com/settings/webhooks
|
||||
.. _inbound settings: https://mandrillapp.com/inbound
|
||||
|
||||
|
||||
.. _webhook-usage:
|
||||
|
||||
Tracking event signals
|
||||
----------------------
|
||||
|
||||
.. Once you've enabled webhooks, Djrill will send a ``djrill.signals.webhook_event``
|
||||
.. custom `Django signal`_ for each Mandrill event it receives.
|
||||
.. You can connect your own receiver function to this signal for further processing.
|
||||
..
|
||||
.. Be sure to read Django's `listening to signals`_ docs for information on defining
|
||||
.. and connecting signal receivers.
|
||||
..
|
||||
.. Examples:
|
||||
..
|
||||
.. .. code-block:: python
|
||||
..
|
||||
.. from djrill.signals import webhook_event
|
||||
.. from django.dispatch import receiver
|
||||
..
|
||||
.. @receiver(webhook_event)
|
||||
.. def handle_bounce(sender, event_type, data, **kwargs):
|
||||
.. if event_type == 'hard_bounce' or event_type == 'soft_bounce':
|
||||
.. print "Message to %s bounced: %s" % (
|
||||
.. data['msg']['email'],
|
||||
.. data['msg']['bounce_description']
|
||||
.. )
|
||||
..
|
||||
.. @receiver(webhook_event)
|
||||
.. def handle_inbound(sender, event_type, data, **kwargs):
|
||||
.. if event_type == 'inbound':
|
||||
.. print "Inbound message from %s: %s" % (
|
||||
.. data['msg']['from_email'],
|
||||
.. data['msg']['subject']
|
||||
.. )
|
||||
..
|
||||
.. @receiver(webhook_event)
|
||||
.. def handle_whitelist_sync(sender, event_type, data, **kwargs):
|
||||
.. if event_type == 'whitelist_add' or event_type == 'whitelist_remove':
|
||||
.. print "Rejection whitelist update: %s email %s (%s)" % (
|
||||
.. data['action'],
|
||||
.. data['reject']['email'],
|
||||
.. data['reject']['reason']
|
||||
.. )
|
||||
..
|
||||
..
|
||||
.. Note that your webhook_event signal handlers will be called for all Mandrill
|
||||
.. webhook callbacks, so you should always check the `event_type` param as shown
|
||||
.. in the examples above to ensure you're processing the expected events.
|
||||
..
|
||||
.. Mandrill batches up multiple events into a single webhook call.
|
||||
.. Djrill will invoke your signal handler once for each event in the batch.
|
||||
..
|
||||
.. The available fields in the `data` param are described in Mandrill's documentation:
|
||||
.. `sent-message webhooks`_, `inbound webhooks`_, and `whitelist/blacklist sync webooks`_.
|
||||
|
||||
.. _Django signal: https://docs.djangoproject.com/en/stable/topics/signals/
|
||||
.. _inbound webhooks:
|
||||
http://help.mandrill.com/entries/22092308-What-is-the-format-of-inbound-email-webhooks-
|
||||
.. _listening to signals:
|
||||
https://docs.djangoproject.com/en/stable/topics/signals/#listening-to-signals
|
||||
.. _sent-message webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks
|
||||
.. _whitelist/blacklist sync webooks:
|
||||
https://mandrill.zendesk.com/hc/en-us/articles/205583297-Sync-Event-Webhook-format
|
||||
Reference in New Issue
Block a user