Add ESP templates, batch send and merge

* message.template_id to use ESP stored templates
* message.merge_data and merge_global_data
  to supply per-recipient/global merge variables
  (with or without an ESP stored template)
* When using per-recipient merge_data, tell ESP to use
  batch send: individual message per "to" address.
  (Mailgun does this automatically; SendGrid requires
  using a different "to" field; Mandrill requires
  `preserve_recipients=False`; Postmark doesn't
  support *this type* of batch sending with merge data.)
* Allow message.from_email=None (must be set after
  init) and message.subject=None to suppress those
  fields in API calls (for ESPs that allow "From" and
  "Subject" in their template definitions).

Mailgun:
* Emulate merge_global_data by copying to
  recipient-variables for each recipient.

SendGrid:
* Add delimiters to merge field names via
  esp_extra['merge_field_format'] or
  ANYMAIL_SENDGRID_MERGE_FIELD_FORMAT setting.

Mandrill:
* Remove Djrill versions of these features;
  update migration notes.

Closes #5.
This commit is contained in:
medmunds
2016-05-03 18:25:37 -07:00
parent 271eb5c926
commit 75730e8219
20 changed files with 882 additions and 245 deletions

View File

@@ -1,7 +1,7 @@
.. _anymail-send-features:
.. module:: anymail.message
.. _anymail-send-features:
Anymail additions
=================

View File

@@ -1,120 +1,216 @@
.. _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.
.. currentmodule:: anymail.message
.. _esp-templates:
.. _templates-and-merge:
ESP templates
-------------
Batch sending/merge and ESP templates
=====================================
.. warning::
If your ESP offers templates and batch-sending/merge capabilities,
Anymail can simplify using them in a portable way. Anymail doesn't
translate template syntax between ESPs, but it does normalize using
templates and providing merge data for batch sends.
These normalized ESP-template attributes aren't implemented yet.
but are planned for a future Anymail update. If you are using
your ESP's transactional templates,
`your input <https://github.com/anymail/django-anymail/issues/5>`_
would be appreciated.
Here's an example using both an ESP stored template and merge data:
.. code-block:: python
from django.core.mail import EmailMessage
message = EmailMessage(
subject=None, # use the subject in our stored template
from_email="marketing@example.com",
to=["Wile E. <wile@example.com>", "rr@example.com"])
message.template_id = "after_sale_followup_offer" # use this ESP stored template
message.merge_data = { # per-recipient data to merge into the template
'wile@example.com': {'NAME': "Wile E.",
'OFFER': "15% off anvils"},
'rr@example.com': {'NAME': "Mr. Runner"},
}
message.merge_global_data = { # merge data for all recipients
'PARTNER': "Acme, Inc.",
'OFFER': "5% off any Acme product", # a default if OFFER missing for recipient
}
message.send()
The message's :attr:`~AnymailMessage.template_id` identifies a template stored
at your ESP which provides the message body and subject. (Assuming the
ESP supports those features.)
The message's :attr:`~AnymailMessage.merge_data` supplies the per-recipient
data to substitute for merge fields in your template. Setting this attribute
also lets Anymail know it should use the ESP's :ref:`batch sending <batch-send>`
feature to deliver separate, individually-customized messages
to each address on the "to" list. (Again, assuming your ESP
supports that.)
.. note::
Templates and batch sending capabilities can vary widely
between ESPs, as can the syntax for merge fields. Be sure
to read the notes for :ref:`your specific ESP <supported-esps>`,
and test carefully with a small recipient list before
launching a gigantic batch send.
Although related and often used together, :ref:`esp-stored-templates`
and :ref:`merge data <merge-data>` are actually independent features.
For example, some ESPs will let you use merge field syntax
directly in your :class:`~django.core.mail.EmailMessage`
body, so you can do customized batch sending without needing
to define a stored template at the ESP.
.. 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.
.. _esp-stored-templates:
ESP stored templates
--------------------
Many ESPs support transactional email templates that are stored and
managed within your ESP account. To use an ESP stored template
with Anymail, set :attr:`~AnymailMessage.template_id`
on an :class:`~django.core.mail.EmailMessage`.
.. attribute:: AnymailMessage.template_id
(not yet implemented)
The identifier of the ESP stored template you want to use.
For most ESPs, this is a `str` name or unique id.
(See the notes for your :ref:`specific ESP <supported-esps>`.)
.. attribute:: AnymailMessage.global_merge_vars
.. code-block:: python
(not yet implemented)
message.template_id = "after_sale_followup_offer"
.. ``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.)
With most ESPs, using a stored template will ignore any
body (plain-text or HTML) from the :class:`~django.core.mail.EmailMessage`
object.
.. attribute:: AnymailMessage.merge_vars
A few ESPs also allow you to define the message's subject as part of the template,
but any subject you set on the :class:`~django.core.mail.EmailMessage`
will override the template subject. To use the subject stored with the ESP template,
set the message's `subject` to `None`:
(not yet implemented)
.. code-block:: python
.. ``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.)
message.subject = None # use subject from template (if supported)
Similarly, some ESPs can also specify the "from" address in the template
definition. Set `message.from_email = None` to use the template's "from."
(You must set this attribute *after* constructing an
:class:`~django.core.mail.EmailMessage` object; passing
`from_email=None` to the constructor will use Django's
:setting:`DEFAULT_FROM_EMAIL` setting, overriding your template value.)
.. _batch-send:
.. _merge-data:
Batch sending with merge data
-----------------------------
Several ESPs support "batch transactional sending," where a single API call can send messages
to multiple recipients. The message is customized for each email on the "to" list
by merging per-recipient data into the body and other message fields.
To use batch sending with Anymail (for ESPs that support it):
* Use "merge fields" (sometimes called "substitution variables" or similar)
in your message. This could be in an :ref:`ESP stored template <esp-stored-templates>`
referenced by :attr:`~AnymailMessage.template_id`,
or with some ESPs you can use merge fields directly in your
:class:`~django.core.mail.EmailMessage` (meaning the message itself
is treated as an on-the-fly template).
* Set the message's :attr:`~AnymailMessage.merge_data` attribute to define merge field
substitutions for each recipient, and optionally set :attr:`~AnymailMessage.merge_global_data`
to defaults or values to use for all recipients.
* Specify all of the recipients for the batch in the message's `to` list.
.. caution::
It's critical to set the :attr:`~AnymailMessage.merge_data` attribute:
this is how Anymail recognizes the message as a batch send.
When you provide merge_data, Anymail will tell the ESP to send an individual customized
message to each "to" address. Without it, you may get a single message to everyone,
exposing all of the email addresses to all recipients.
(If you don't have any per-recipient customizations, but still want individual messages,
just set merge_data to an empty dict.)
The exact syntax for merge fields varies by ESP. It might be something like
`*|NAME|*` or `-name-` or `<%name%>`. (Check the notes for
:ref:`your ESP <supported-esps>`, and remember you'll need to change
the template if you later switch ESPs.)
.. attribute:: AnymailMessage.merge_data
A `dict` of *per-recipient* template substitution/merge data. Each key in the
dict is a recipient email address, and its value is a `dict` of merge field
names and values to use for that recipient:
.. code-block:: python
message.merge_data = {
'wile@example.com': {'NAME': "Wile E.",
'OFFER': "15% off anvils"},
'rr@example.com': {'NAME': "Mr. Runner",
'OFFER': "instant tunnel paint"},
}
When `merge_data` is set, Anymail will use the ESP's batch sending option,
so that each `to` recipient gets an individual message (and doesn't see the
other emails on the `to` list).
.. attribute:: AnymailMessage.merge_global_data
A `dict` of template substitution/merge data to use for *all* recipients.
Keys are merge field names in your message template:
.. code-block:: python
message.merge_global_data = {
'PARTNER': "Acme, Inc.",
'OFFER': "5% off any Acme product", # a default OFFER
}
Merge data values must be strings. (Some ESPs also allow other
JSON-serializable types like lists or dicts.)
See :ref:`formatting-merge-data` for more information.
Like all :ref:`anymail-send-features`, you can use these extended template and
merge attributes with any :class:`~django.core.mail.EmailMessage` or subclass object.
(It doesn't have to be an :class:`AnymailMessage`.)
Tip: you can add :attr:`~!AnymailMessage.merge_global_data` to your
global Anymail :ref:`send defaults <send-defaults>` to supply merge data
available to all batch sends (e.g, site name, contact info). The global
defaults will be merged with any per-message :attr:`~!AnymailMessage.merge_global_data`.
.. _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::
than strings and integers, you'll need to format them into strings
for use as merge data:
.. code-block:: python
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 = {
# Won't work -- you'll get "not JSON serializable" errors at send time:
message.merge_global_data = {
'PRODUCT': product,
'TOTAL_COST': total_cost,
'SHIP_DATE': ship_date
}
# Do something this instead:
msg.global_merge_vars = {
message.merge_global_data = {
'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"
@@ -123,32 +219,38 @@ you'll need to format them into strings for use as merge data::
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
Although floats are usually allowed in merge data, 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.
to send a message with merge data (or metadata) that can't be sent to your ESP.
.. 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.
ESP templates vs. Django templates
----------------------------------
ESP templating languages are generally proprietary,
which makes them inherently non-portable.
Anymail only exposes the stored template capabilities that your ESP
already offers, and then simplifies providing merge data in a portable way.
It won't translate between different ESP template syntaxes, and it
can't do a batch send if your ESP doesn't support it.
There are two common cases where ESP template
and merge features are particularly useful with Anymail:
* When the people who develop and maintain your transactional
email templates are different from the people who maintain
your Django page templates. (For example, you use a single
ESP for both marketing and transactional email, and your
marketing team manages all the ESP email templates.)
* When you want to use your ESP's batch-sending capabilities
for performance reasons, where a single API call can
trigger individualized messages to hundreds or thousands of recipients.
(For example, sending a daily batch of shipping notifications.)
If neither of these cases apply, you may find that
:ref:`using Django templates <django-templates>` can be a more
portable and maintainable approach for building transactional email.