mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
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:
@@ -1,7 +1,7 @@
|
||||
.. _anymail-send-features:
|
||||
|
||||
.. module:: anymail.message
|
||||
|
||||
.. _anymail-send-features:
|
||||
|
||||
Anymail additions
|
||||
=================
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user