Update (almost) all the docs

This commit is contained in:
medmunds
2016-03-09 18:37:11 -08:00
parent 8f0f2d3d83
commit 20c6350140
28 changed files with 1741 additions and 1192 deletions

View 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

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

View 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
View 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
View 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
View 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