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:
@@ -43,11 +43,10 @@ built-in `django.core.mail` package. It includes:
|
||||
* Simplified inline images for HTML email
|
||||
* Normalized sent-message status and tracking notification, by connecting
|
||||
your ESP's webhooks to Django signals
|
||||
* "Batch transactional" sends using your ESP's merge and template features
|
||||
|
||||
Support is planned for:
|
||||
Support is also planned for:
|
||||
|
||||
* "Bulk-transactional" sends using your ESP's template facility,
|
||||
with portable declaration of substitution/merge data
|
||||
* Normalized inbound email processing through your ESP
|
||||
|
||||
Anymail is released under the BSD license. It is extensively tested against Django 1.8--1.9
|
||||
|
||||
@@ -197,6 +197,9 @@ class BasePayload(object):
|
||||
('tags', combine, None),
|
||||
('track_clicks', last, None),
|
||||
('track_opens', last, None),
|
||||
('template_id', last, None),
|
||||
('merge_data', combine, None),
|
||||
('merge_global_data', combine, None),
|
||||
('esp_extra', combine, None),
|
||||
)
|
||||
esp_message_attrs = () # subclasses can override
|
||||
@@ -356,6 +359,15 @@ class BasePayload(object):
|
||||
def set_track_opens(self, track_opens):
|
||||
self.unsupported_feature("track_opens")
|
||||
|
||||
def set_template_id(self, template_id):
|
||||
self.unsupported_feature("template_id")
|
||||
|
||||
def set_merge_data(self, merge_data):
|
||||
self.unsupported_feature("merge_data")
|
||||
|
||||
def set_merge_global_data(self, merge_global_data):
|
||||
self.unsupported_feature("merge_global_data")
|
||||
|
||||
# ESP-specific payload construction
|
||||
def set_esp_extra(self, extra):
|
||||
self.unsupported_feature("esp_extra")
|
||||
|
||||
@@ -56,6 +56,12 @@ class MailgunPayload(RequestsPayload):
|
||||
auth = ("api", backend.api_key)
|
||||
self.sender_domain = None
|
||||
self.all_recipients = [] # used for backend.parse_recipient_status
|
||||
|
||||
# late-binding of recipient-variables:
|
||||
self.merge_data = None
|
||||
self.merge_global_data = None
|
||||
self.to_emails = []
|
||||
|
||||
super(MailgunPayload, self).__init__(message, defaults, backend, auth=auth, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
@@ -66,6 +72,34 @@ class MailgunPayload(RequestsPayload):
|
||||
backend=self.backend, email_message=self.message, payload=self)
|
||||
return "%s/messages" % self.sender_domain
|
||||
|
||||
def serialize_data(self):
|
||||
self.populate_recipient_variables()
|
||||
return self.data
|
||||
|
||||
def populate_recipient_variables(self):
|
||||
"""Populate Mailgun recipient-variables header from merge data"""
|
||||
merge_data = self.merge_data
|
||||
|
||||
if self.merge_global_data is not None:
|
||||
# Mailgun doesn't support global variables.
|
||||
# We emulate them by populating recipient-variables for all recipients.
|
||||
if merge_data is not None:
|
||||
merge_data = merge_data.copy() # don't modify the original, which doesn't belong to us
|
||||
else:
|
||||
merge_data = {}
|
||||
for email in self.to_emails:
|
||||
try:
|
||||
recipient_data = merge_data[email]
|
||||
except KeyError:
|
||||
merge_data[email] = self.merge_global_data
|
||||
else:
|
||||
# Merge globals (recipient_data wins in conflict)
|
||||
merge_data[email] = self.merge_global_data.copy()
|
||||
merge_data[email].update(recipient_data)
|
||||
|
||||
if merge_data is not None:
|
||||
self.data['recipient-variables'] = self.serialize_json(merge_data)
|
||||
|
||||
#
|
||||
# Payload construction
|
||||
#
|
||||
@@ -87,8 +121,10 @@ class MailgunPayload(RequestsPayload):
|
||||
def set_recipients(self, recipient_type, emails):
|
||||
assert recipient_type in ["to", "cc", "bcc"]
|
||||
if emails:
|
||||
self.data[recipient_type] = [str(email) for email in emails]
|
||||
self.data[recipient_type] = [email.address for email in emails]
|
||||
self.all_recipients += emails # used for backend.parse_recipient_status
|
||||
if recipient_type == 'to':
|
||||
self.to_emails = [email.email for email in emails] # used for populate_recipient_variables
|
||||
|
||||
def set_subject(self, subject):
|
||||
self.data["subject"] = subject
|
||||
@@ -145,6 +181,17 @@ class MailgunPayload(RequestsPayload):
|
||||
def set_track_opens(self, track_opens):
|
||||
self.data["o:tracking-opens"] = "yes" if track_opens else "no"
|
||||
|
||||
# template_id: Mailgun doesn't offer stored templates.
|
||||
# (The message body and other fields *are* the template content.)
|
||||
|
||||
def set_merge_data(self, merge_data):
|
||||
# Processed at serialization time (to allow merging global data)
|
||||
self.merge_data = merge_data
|
||||
|
||||
def set_merge_global_data(self, merge_global_data):
|
||||
# Processed at serialization time (to allow merging global data)
|
||||
self.merge_global_data = merge_global_data
|
||||
|
||||
def set_esp_extra(self, extra):
|
||||
self.data.update(extra)
|
||||
# Allow override of sender_domain via esp_extra
|
||||
|
||||
@@ -147,6 +147,24 @@ class MandrillPayload(RequestsPayload):
|
||||
def set_track_opens(self, track_opens):
|
||||
self.data["message"]["track_opens"] = track_opens
|
||||
|
||||
def set_template_id(self, template_id):
|
||||
self.data["template_name"] = template_id
|
||||
self.data.setdefault("template_content", []) # Mandrill requires something here
|
||||
|
||||
def set_merge_data(self, merge_data):
|
||||
self.data['message']['preserve_recipients'] = False # if merge, hide recipients from each other
|
||||
self.data['message']['merge_vars'] = [
|
||||
{'rcpt': rcpt, 'vars': [{'name': key, 'content': rcpt_data[key]}
|
||||
for key in sorted(rcpt_data.keys())]} # sort for testing reproducibility
|
||||
for rcpt, rcpt_data in merge_data.items()
|
||||
]
|
||||
|
||||
def set_merge_global_data(self, merge_global_data):
|
||||
self.data['message']['global_merge_vars'] = [
|
||||
{'name': var, 'content': value}
|
||||
for var, value in merge_global_data.items()
|
||||
]
|
||||
|
||||
def set_esp_extra(self, extra):
|
||||
pass
|
||||
|
||||
@@ -170,10 +188,7 @@ class MandrillPayload(RequestsPayload):
|
||||
('subaccount', last, None),
|
||||
('google_analytics_domains', last, None),
|
||||
('google_analytics_campaign', last, None),
|
||||
('global_merge_vars', combine, _expand_merge_vars),
|
||||
('merge_vars', combine, None),
|
||||
('recipient_metadata', combine, None),
|
||||
('template_name', last, None),
|
||||
('template_content', combine, _expand_merge_vars),
|
||||
)
|
||||
|
||||
@@ -183,20 +198,9 @@ class MandrillPayload(RequestsPayload):
|
||||
def set_ip_pool(self, ip_pool):
|
||||
self.data["ip_pool"] = ip_pool
|
||||
|
||||
def set_template_name(self, template_name):
|
||||
self.data["template_name"] = template_name
|
||||
self.data.setdefault("template_content", []) # Mandrill requires something here
|
||||
|
||||
def set_template_content(self, template_content):
|
||||
self.data["template_content"] = template_content
|
||||
|
||||
def set_merge_vars(self, merge_vars):
|
||||
# For testing reproducibility, we sort the recipients
|
||||
self.data['message']['merge_vars'] = [
|
||||
{'rcpt': rcpt, 'vars': _expand_merge_vars(merge_vars[rcpt])}
|
||||
for rcpt in sorted(merge_vars.keys())
|
||||
]
|
||||
|
||||
def set_recipient_metadata(self, recipient_metadata):
|
||||
# For testing reproducibility, we sort the recipients
|
||||
self.data['message']['recipient_metadata'] = [
|
||||
|
||||
@@ -100,6 +100,10 @@ class PostmarkPayload(RequestsPayload):
|
||||
super(PostmarkPayload, self).__init__(message, defaults, backend, headers=headers, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
if 'TemplateId' in self.data or 'TemplateModel' in self.data:
|
||||
# This is the one Postmark API documented to have a trailing slash. (Typo?)
|
||||
return "email/withTemplate/"
|
||||
else:
|
||||
return "email"
|
||||
|
||||
def get_request_params(self, api_url):
|
||||
@@ -185,6 +189,14 @@ class PostmarkPayload(RequestsPayload):
|
||||
def set_track_opens(self, track_opens):
|
||||
self.data["TrackOpens"] = track_opens
|
||||
|
||||
def set_template_id(self, template_id):
|
||||
self.data["TemplateId"] = template_id
|
||||
|
||||
# merge_data: Postmark doesn't support per-recipient substitutions
|
||||
|
||||
def set_merge_global_data(self, merge_global_data):
|
||||
self.data["TemplateModel"] = merge_global_data
|
||||
|
||||
def set_esp_extra(self, extra):
|
||||
self.data.update(extra)
|
||||
# Special handling for 'server_token':
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import warnings
|
||||
|
||||
from django.core.mail import make_msgid
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError
|
||||
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning
|
||||
from ..message import AnymailRecipientStatus
|
||||
from ..utils import get_anymail_setting, timestamp
|
||||
|
||||
@@ -31,6 +33,8 @@ class SendGridBackend(AnymailRequestsBackend):
|
||||
|
||||
self.generate_message_id = get_anymail_setting('generate_message_id', esp_name=esp_name,
|
||||
kwargs=kwargs, default=True)
|
||||
self.merge_field_format = get_anymail_setting('merge_field_format', esp_name=esp_name,
|
||||
kwargs=kwargs, default=None)
|
||||
|
||||
# This is SendGrid's Web API v2 (because the Web API v3 doesn't support sending)
|
||||
api_url = get_anymail_setting('api_url', esp_name=esp_name, kwargs=kwargs,
|
||||
@@ -65,6 +69,10 @@ class SendGridPayload(RequestsPayload):
|
||||
self.generate_message_id = backend.generate_message_id
|
||||
self.message_id = None # Message-ID -- assigned in serialize_data unless provided in headers
|
||||
self.smtpapi = {} # SendGrid x-smtpapi field
|
||||
self.to_list = [] # late-bound 'to' field
|
||||
self.merge_field_format = backend.merge_field_format
|
||||
self.merge_data = None # late-bound per-recipient data
|
||||
self.merge_global_data = None
|
||||
|
||||
http_headers = kwargs.pop('headers', {})
|
||||
query_params = kwargs.pop('params', {})
|
||||
@@ -86,6 +94,15 @@ class SendGridPayload(RequestsPayload):
|
||||
if self.generate_message_id:
|
||||
self.ensure_message_id()
|
||||
|
||||
self.build_merge_data()
|
||||
if self.merge_data is None:
|
||||
# Standard 'to' and 'toname' headers
|
||||
self.set_recipients('to', self.to_list)
|
||||
else:
|
||||
# Merge-friendly smtpapi 'to' field
|
||||
self.smtpapi['to'] = [email.address for email in self.to_list]
|
||||
self.all_recipients += self.to_list
|
||||
|
||||
# Serialize x-smtpapi to json:
|
||||
if len(self.smtpapi) > 0:
|
||||
# If esp_extra was also used to set x-smtpapi, need to merge it
|
||||
@@ -132,6 +149,41 @@ class SendGridPayload(RequestsPayload):
|
||||
domain = None
|
||||
return make_msgid(domain=domain)
|
||||
|
||||
def build_merge_data(self):
|
||||
"""Set smtpapi['sub'] and ['section']"""
|
||||
if self.merge_data is not None:
|
||||
# Convert from {to1: {a: A1, b: B1}, to2: {a: A2}} (merge_data format)
|
||||
# to {a: [A1, A2], b: [B1, ""]} ({field: [data in to-list order], ...})
|
||||
all_fields = set()
|
||||
for recipient_data in self.merge_data.values():
|
||||
all_fields = all_fields.union(recipient_data.keys())
|
||||
recipients = [email.email for email in self.to_list]
|
||||
|
||||
if self.merge_field_format is None and all(field.isalnum() for field in all_fields):
|
||||
warnings.warn(
|
||||
"Your SendGrid merge fields don't seem to have delimiters, "
|
||||
"which can cause unexpected results with Anymail's merge_data. "
|
||||
"Search SENDGRID_MERGE_FIELD_FORMAT in the Anymail docs for more info.",
|
||||
AnymailWarning)
|
||||
|
||||
sub_field_fmt = self.merge_field_format or '{}'
|
||||
sub_fields = {field: sub_field_fmt.format(field) for field in all_fields}
|
||||
|
||||
self.smtpapi['sub'] = {
|
||||
# If field data is missing for recipient, use (formatted) field as the substitution.
|
||||
# (This allows default to resolve from global "section" substitutions.)
|
||||
sub_fields[field]: [self.merge_data.get(recipient, {}).get(field, sub_fields[field])
|
||||
for recipient in recipients]
|
||||
for field in all_fields
|
||||
}
|
||||
|
||||
if self.merge_global_data is not None:
|
||||
section_field_fmt = self.merge_field_format or '{}'
|
||||
self.smtpapi['section'] = {
|
||||
section_field_fmt.format(field): data
|
||||
for field, data in self.merge_global_data.items()
|
||||
}
|
||||
|
||||
#
|
||||
# Payload construction
|
||||
#
|
||||
@@ -146,6 +198,11 @@ class SendGridPayload(RequestsPayload):
|
||||
if email.name:
|
||||
self.data["fromname"] = email.name
|
||||
|
||||
def set_to(self, emails):
|
||||
# late-bind in self.serialize_data, because whether it goes in smtpapi
|
||||
# depends on whether there is merge_data
|
||||
self.to_list = emails
|
||||
|
||||
def set_recipients(self, recipient_type, emails):
|
||||
assert recipient_type in ["to", "cc", "bcc"]
|
||||
if emails:
|
||||
@@ -229,5 +286,18 @@ class SendGridPayload(RequestsPayload):
|
||||
# (You could add it through esp_extra.)
|
||||
self.add_filter('opentrack', 'enable', int(track_opens))
|
||||
|
||||
def set_template_id(self, template_id):
|
||||
self.add_filter('templates', 'enable', 1)
|
||||
self.add_filter('templates', 'template_id', template_id)
|
||||
|
||||
def set_merge_data(self, merge_data):
|
||||
# Becomes smtpapi['sub'] in build_merge_data, after we know recipients and merge_field_format.
|
||||
self.merge_data = merge_data
|
||||
|
||||
def set_merge_global_data(self, merge_global_data):
|
||||
# Becomes smtpapi['section'] in build_merge_data, after we know merge_field_format.
|
||||
self.merge_global_data = merge_global_data
|
||||
|
||||
def set_esp_extra(self, extra):
|
||||
self.merge_field_format = extra.pop('merge_field_format', self.merge_field_format)
|
||||
self.data.update(extra)
|
||||
|
||||
@@ -25,6 +25,9 @@ class AnymailMessageMixin(object):
|
||||
self.tags = kwargs.pop('tags', UNSET)
|
||||
self.track_clicks = kwargs.pop('track_clicks', UNSET)
|
||||
self.track_opens = kwargs.pop('track_opens', UNSET)
|
||||
self.template_id = kwargs.pop('template_id', UNSET)
|
||||
self.merge_data = kwargs.pop('merge_data', UNSET)
|
||||
self.merge_global_data = kwargs.pop('merge_global_data', UNSET)
|
||||
self.anymail_status = None
|
||||
|
||||
# noinspection PyArgumentList
|
||||
|
||||
@@ -25,22 +25,28 @@ The table below summarizes the Anymail features supported for each ESP.
|
||||
|
||||
.. currentmodule:: anymail.message
|
||||
|
||||
=========================================== ========= ========== ========== ==========
|
||||
============================================ ========== ========== ========== ==========
|
||||
Email Service Provider |Mailgun| |Mandrill| |Postmark| |SendGrid|
|
||||
=========================================== ========= ========== ========== ==========
|
||||
============================================ ========== ========== ========== ==========
|
||||
.. rubric:: :ref:`Anymail send options <anymail-send-options>`
|
||||
-------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.metadata` Yes Yes No Yes
|
||||
:attr:`~AnymailMessage.send_at` Yes Yes No Yes
|
||||
:attr:`~AnymailMessage.tags` Yes Yes Max 1 tag Yes
|
||||
:attr:`~AnymailMessage.track_clicks` Yes Yes No Yes
|
||||
:attr:`~AnymailMessage.track_opens` Yes Yes Yes Yes
|
||||
|
||||
.. rubric:: :ref:`templates-and-merge`
|
||||
--------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.template_id` No Yes Yes Yes
|
||||
:attr:`~AnymailMessage.merge_data` Yes Yes No Yes
|
||||
:attr:`~AnymailMessage.merge_global_data` (emulated) Yes Yes Yes
|
||||
|
||||
.. rubric:: :ref:`Status <esp-send-status>` and :ref:`event tracking <event-tracking>`
|
||||
-------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.anymail_status` Yes Yes Yes Yes
|
||||
|AnymailTrackingEvent| from webhooks Yes Yes Yes Yes
|
||||
=========================================== ========= ========== ========== ==========
|
||||
============================================ ========== ========== ========== ==========
|
||||
|
||||
|
||||
.. .. rubric:: :ref:`inbound`
|
||||
|
||||
@@ -105,6 +105,51 @@ values directly to Mailgun. You can use any of the (non-file) parameters listed
|
||||
.. _Mailgun sending docs: https://documentation.mailgun.com/api-sending.html#sending
|
||||
|
||||
|
||||
.. _mailgun-templates:
|
||||
|
||||
Batch sending/merge and ESP templates
|
||||
-------------------------------------
|
||||
|
||||
Mailgun does not offer :ref:`ESP stored templates <esp-stored-templates>`,
|
||||
so Anymail's :attr:`~anymail.message.AnymailMessage.template_id` message
|
||||
attribute is not supported with the Mailgun backend.
|
||||
|
||||
Mailgun *does* support :ref:`batch sending <batch-send>` with per-recipient
|
||||
merge data. You can refer to Mailgun "recipient variables" in your
|
||||
message subject and body, and supply the values with Anymail's
|
||||
normalized :attr:`~anymail.message.AnymailMessage.merge_data`
|
||||
and :attr:`~anymail.message.AnymailMessage.merge_global_data`
|
||||
message attributes:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message = EmailMessage(
|
||||
...
|
||||
subject="Your order %recipient.order_no% has shipped",
|
||||
body="""Hi %recipient.name%,
|
||||
We shipped your order %recipient.order_no%
|
||||
on %recipient.ship_date%.""",
|
||||
to=["alice@example.com", "Bob <bob@example.com>"]
|
||||
)
|
||||
# (you'd probably also set a similar html body with %recipient.___% variables)
|
||||
message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'order_no': "12345"},
|
||||
'bob@example.com': {'name': "Bob", 'order_no': "54321"},
|
||||
}
|
||||
message.merge_global_data = {
|
||||
'ship_date': "May 15" # Anymail maps globals to all recipients
|
||||
}
|
||||
|
||||
Mailgun does not natively support global merge data. Anymail emulates
|
||||
the capability by copying any `merge_global_data` values to each
|
||||
recipient's section in Mailgun's "recipient-variables" API parameter.
|
||||
|
||||
See the `Mailgun batch sending`_ docs for more information.
|
||||
|
||||
.. _Mailgun batch sending:
|
||||
https://documentation.mailgun.com/user_manual.html#batch-sending
|
||||
|
||||
|
||||
.. _mailgun-webhooks:
|
||||
|
||||
Status tracking webhooks
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Mandrill
|
||||
========
|
||||
|
||||
Anymail integrates with the `Mandrill <http://mandrill.com/>`_
|
||||
Anymail integrates with the `Mandrill <http://mandrill.com/>`__
|
||||
transactional email service from MailChimp.
|
||||
|
||||
.. note:: **Limited Support for Mandrill**
|
||||
@@ -101,6 +101,61 @@ Anymail's Mandrill backend does not yet implement the
|
||||
:attr:`~anymail.message.AnymailMessage.esp_extra` feature.
|
||||
|
||||
|
||||
.. _mandrill-templates:
|
||||
|
||||
Batch sending/merge and ESP templates
|
||||
-------------------------------------
|
||||
|
||||
Mandrill offers both :ref:`ESP stored templates <esp-stored-templates>`
|
||||
and :ref:`batch sending <batch-send>` with per-recipient merge data.
|
||||
|
||||
You can use a Mandrill stored template by setting a message's
|
||||
:attr:`~anymail.message.AnymailMessage.template_id` to the
|
||||
template's name. Alternatively, you can refer to merge fields
|
||||
directly in an EmailMessage's subject and body---the message itself
|
||||
is used as an on-the-fly template.
|
||||
|
||||
In either case, supply the merge data values with Anymail's
|
||||
normalized :attr:`~anymail.message.AnymailMessage.merge_data`
|
||||
and :attr:`~anymail.message.AnymailMessage.merge_global_data`
|
||||
message attributes.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# This example defines the template inline, using Mandrill's
|
||||
# default MailChimp merge *|field|* syntax.
|
||||
# You could use a stored template, instead, with:
|
||||
# message.template_id = "template name"
|
||||
message = EmailMessage(
|
||||
...
|
||||
subject="Your order *|order_no|* has shipped",
|
||||
body="""Hi *|name|*,
|
||||
We shipped your order *|order_no|*
|
||||
on *|ship_date|*.""",
|
||||
to=["alice@example.com", "Bob <bob@example.com>"]
|
||||
)
|
||||
# (you'd probably also set a similar html body with merge fields)
|
||||
message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'order_no': "12345"},
|
||||
'bob@example.com': {'name': "Bob", 'order_no': "54321"},
|
||||
}
|
||||
message.merge_global_data = {
|
||||
'ship_date': "May 15",
|
||||
}
|
||||
|
||||
When you supply per-recipient :attr:`~anymail.message.AnymailMessage.merge_data`,
|
||||
Anymail automatically forces Mandrill's `preserve_recipients` option to false,
|
||||
so that each person in the message's "to" list sees only their own email address.
|
||||
|
||||
To use the subject or from address defined with a Mandrill template, set the message's
|
||||
`subject` or `from_email` attribute to `None`.
|
||||
|
||||
See the `Mandrill's template docs`_ for more information.
|
||||
|
||||
.. _Mandrill's template docs:
|
||||
https://mandrill.zendesk.com/hc/en-us/articles/205582507-Getting-Started-with-Templates
|
||||
|
||||
|
||||
.. _mandrill-webhooks:
|
||||
|
||||
Status tracking webhooks
|
||||
@@ -198,7 +253,8 @@ Changes to EmailMessage attributes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``message.send_at``
|
||||
If you are using an aware datetime for :attr:`send_at`,
|
||||
If you are using an aware datetime for
|
||||
:attr:`~anymail.message.AnymailMessage.send_at`,
|
||||
it will keep working unchanged with Anymail.
|
||||
|
||||
If you are using a date (without a time), or a naive datetime,
|
||||
@@ -211,7 +267,8 @@ Changes to EmailMessage attributes
|
||||
|
||||
``message.mandrill_response``
|
||||
Anymail normalizes ESP responses, so you don't have to be familiar
|
||||
with the format of Mandrill's JSON. See :attr:`anymail_status`.
|
||||
with the format of Mandrill's JSON.
|
||||
See :attr:`~anymail.message.AnymailMessage.anymail_status`.
|
||||
|
||||
The *raw* ESP response is attached to a sent message as
|
||||
``anymail_status.esp_response``, so the direct replacement
|
||||
@@ -221,14 +278,16 @@ Changes to EmailMessage attributes
|
||||
|
||||
mandrill_response = message.anymail_status.esp_response.json()
|
||||
|
||||
**Templates and merge variables**
|
||||
Coming to Anymail soon.
|
||||
``message.template_name``
|
||||
Anymail renames this to :attr:`~anymail.message.AnymailMessage.template_id`.
|
||||
|
||||
However, no other ESPs support MailChimp's templating language, so
|
||||
you'll need to rewrite your templates as you switch ESPs.
|
||||
``message.merge_vars`` and ``message.global_merge_vars``
|
||||
Anymail renames these to :attr:`~anymail.message.AnymailMessage.merge_data`
|
||||
and :attr:`~anymail.message.AnymailMessage.merge_global_data`, respectively.
|
||||
|
||||
Consider converting to :ref:`Django templates <django-templates>`
|
||||
instead, as these can be used with any email backend.
|
||||
``message.use_template_from`` and ``message.use_template_subject``
|
||||
With Anymail, set ``message.from_email = None`` or ``message.subject = None``
|
||||
to use the values from the stored template.
|
||||
|
||||
**Other Mandrill-specific attributes**
|
||||
Are currently still supported by Anymail's Mandrill backend,
|
||||
|
||||
@@ -115,6 +115,52 @@ see :ref:`unsupported-features`.
|
||||
Postmark does not support :attr:`~anymail.message.AnymailMessage.send_at`.
|
||||
|
||||
|
||||
.. _postmark-templates:
|
||||
|
||||
Batch sending/merge and ESP templates
|
||||
-------------------------------------
|
||||
|
||||
Postmark supports :ref:`ESP stored templates <esp-stored-templates>`
|
||||
populated with global merge data for all recipients, but does not
|
||||
offer :ref:`batch sending <batch-send>` with per-recipient merge data.
|
||||
Anymail's :attr:`~anymail.message.AnymailMessage.merge_data`
|
||||
message attribute is not supported with the Postmark backend.
|
||||
|
||||
To use a Postmark template, set the message's
|
||||
:attr:`~anymail.message.AnymailMessage.template_id` to the numeric
|
||||
Postmark "TemplateID" and supply the "TemplateModel" using
|
||||
the :attr:`~anymail.message.AnymailMessage.merge_global_data`
|
||||
message attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message = EmailMessage(
|
||||
...
|
||||
subject=None, # use template subject
|
||||
to=["alice@example.com"] # single recipient...
|
||||
# ...multiple to emails would all get the same message
|
||||
# (and would all see each other's emails in the "to" header)
|
||||
)
|
||||
message.template_id = 80801 # use this Postmark template
|
||||
message.merge_global_data = {
|
||||
'name': "Alice",
|
||||
'order_no': "12345",
|
||||
'ship_date': "May 15",
|
||||
'items': [
|
||||
{'product': "Widget", 'price': "9.99"},
|
||||
{'product': "Gadget", 'price': "17.99"},
|
||||
],
|
||||
}
|
||||
|
||||
Set the EmailMessage's subject to `None` to use the subject from
|
||||
your Postmark template, or supply a subject with the message to override
|
||||
the template value.
|
||||
|
||||
See this `Postmark blog post on templates`_ for more information.
|
||||
|
||||
.. _Postmark blog post on templates:
|
||||
https://postmarkapp.com/blog/special-delivery-postmark-templates
|
||||
|
||||
|
||||
.. _postmark-webhooks:
|
||||
|
||||
|
||||
@@ -89,6 +89,34 @@ Default ``True``. You can set to ``False`` to disable this behavior.
|
||||
See :ref:`Message-ID quirks <sendgrid-message-id>` below.
|
||||
|
||||
|
||||
.. setting:: ANYMAIL_SENDGRID_MERGE_FIELD_FORMAT
|
||||
|
||||
.. rubric:: SENDGRID_MERGE_FIELD_FORMAT
|
||||
|
||||
If you use :ref:`merge data <merge-data>`, set this to a :meth:`str.format`
|
||||
formatting string that indicates how merge fields are delimited
|
||||
in your SendGrid templates.
|
||||
For example, if your templates use the ``-field-`` hyphen delimiters
|
||||
suggested in some SendGrid docs, you would set:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ANYMAIL = {
|
||||
...
|
||||
"SENDGRID_MERGE_FIELD_FORMAT": "-{}-",
|
||||
}
|
||||
|
||||
The placeholder `{}` will become the merge field name. If you need to include
|
||||
a literal brace character, double it up. (For example, Handlebars-style
|
||||
``{{field}}`` delimiters would take the format string `"{{{{{}}}}}"`.)
|
||||
|
||||
The default `None` requires you include the delimiters directly in your
|
||||
:attr:`~anymail.message.AnymailMessage.merge_data` keys.
|
||||
You can also override this setting for individual messages.
|
||||
See the notes on SendGrid :ref:`templates and merge <sendgrid-templates>`
|
||||
below.
|
||||
|
||||
|
||||
.. setting:: ANYMAIL_SENDGRID_API_URL
|
||||
|
||||
.. rubric:: SENDGRID_API_URL
|
||||
@@ -185,6 +213,84 @@ Limitations and quirks
|
||||
(Tested March, 2016)
|
||||
|
||||
|
||||
.. _sendgrid-templates:
|
||||
|
||||
Batch sending/merge and ESP templates
|
||||
-------------------------------------
|
||||
|
||||
SendGrid offers both :ref:`ESP stored templates <esp-stored-templates>`
|
||||
and :ref:`batch sending <batch-send>` with per-recipient merge data.
|
||||
|
||||
You can use a SendGrid stored template by setting a message's
|
||||
:attr:`~anymail.message.AnymailMessage.template_id` to the
|
||||
template's unique id. Alternatively, you can refer to merge fields
|
||||
directly in an EmailMessage's subject and body---the message itself
|
||||
is used as an on-the-fly template.
|
||||
|
||||
In either case, supply the merge data values with Anymail's
|
||||
normalized :attr:`~anymail.message.AnymailMessage.merge_data`
|
||||
and :attr:`~anymail.message.AnymailMessage.merge_global_data`
|
||||
message attributes.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message = EmailMessage(
|
||||
...
|
||||
subject="", # don't add any additional subject content to the template
|
||||
body="", # (same thing for additional body content)
|
||||
to=["alice@example.com", "Bob <bob@example.com>"]
|
||||
)
|
||||
message.template_id = "5997fcf6-2b9f-484d-acd5-7e9a99f0dc1f" # SendGrid id
|
||||
message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'order_no': "12345"},
|
||||
'bob@example.com': {'name': "Bob", 'order_no': "54321"},
|
||||
}
|
||||
message.merge_global_data = {
|
||||
'ship_date': "May 15",
|
||||
}
|
||||
message.esp_extra = {
|
||||
# Tell Anymail this SendGrid template uses "-field-" to refer to merge fields.
|
||||
# (We could also just set SENDGRID_MERGE_FIELD_FORMAT in our ANYMAIL settings.)
|
||||
'merge_field_format': "-{}-"
|
||||
}
|
||||
|
||||
SendGrid doesn't have a pre-defined merge field syntax, so you
|
||||
must tell Anymail how substitution fields are delimited in your templates.
|
||||
There are three ways you can do this:
|
||||
|
||||
* Set `'merge_field_format'` in the message's
|
||||
:attr:`~anymail.message.AnymailMessage.esp_extra` to a python :meth:`str.format`
|
||||
string, as shown in the example above. (This applies only to that
|
||||
particular EmailMessage.)
|
||||
* *Or* set :setting:`SENDGRID_MERGE_FIELD_FORMAT <ANYMAIL_SENDGRID_MERGE_FIELD_FORMAT>`
|
||||
in your Anymail settings. This is usually the best approach, and will apply to all messages
|
||||
sent through SendGrid. (You can still use esp_extra to override for individual messages.)
|
||||
* *Or* include the field delimiters directly in *all* your
|
||||
:attr:`~anymail.message.AnymailMessage.merge_data` and
|
||||
:attr:`~anymail.message.AnymailMessage.merge_global_data` keys.
|
||||
E.g.: ``{'-name-': "Alice", '-order_no-': "12345"}``.
|
||||
(This can be error-prone, and difficult to move to other ESPs.)
|
||||
|
||||
When you supply per-recipient :attr:`~anymail.message.AnymailMessage.merge_data`,
|
||||
Anymail automatically changes how it communicates the "to" list to SendGrid, so that
|
||||
so that each recipient sees only their own email address. (Anymail moves the recipients
|
||||
from top-level "to" and "toname" API parameters into the "x-smtpapi" section "to" list.)
|
||||
|
||||
SendGrid templates allow you to mix your EmailMessage's `subject` and `body`
|
||||
with the template subject and body (by using `<%subject%>` and `<%body%>` in
|
||||
your SendGrid template definition where you want the message-specific versions
|
||||
to appear). If you don't want to supply any additional subject or body content
|
||||
from your Django app, set those EmailMessage attributes to empty strings.
|
||||
|
||||
See the `SendGrid's template overview`_ and `transactional template docs`_
|
||||
for more information.
|
||||
|
||||
.. _SendGrid's template overview:
|
||||
https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html
|
||||
.. _transactional template docs:
|
||||
https://sendgrid.com/docs/API_Reference/Web_API_v3/Transactional_Templates/smtpapi.html
|
||||
|
||||
|
||||
.. _sendgrid-webhooks:
|
||||
|
||||
Status tracking webhooks
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -27,14 +27,14 @@ Example that builds an email from the templates ``message_subject.txt``,
|
||||
from django.template import Context
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
template_data = {
|
||||
merge_data = {
|
||||
'ORDERNO': "12345", 'TRACKINGNO': "1Z987"
|
||||
}
|
||||
|
||||
plaintext_context = Context(autoescape=False) # HTML escaping not appropriate in plaintext
|
||||
subject = render_to_string("message_subject.txt", template_data, plaintext_context)
|
||||
text_body = render_to_string("message_body.txt", template_data, plaintext_context)
|
||||
html_body = render_to_string("message_body.html", template_data)
|
||||
subject = render_to_string("message_subject.txt", merge_data, plaintext_context)
|
||||
text_body = render_to_string("message_body.txt", merge_data, plaintext_context)
|
||||
html_body = render_to_string("message_body.html", merge_data)
|
||||
|
||||
msg = EmailMultiAlternatives(subject=subject, from_email="store@example.com",
|
||||
to=["customer@example.com"], body=text_body)
|
||||
|
||||
@@ -329,6 +329,44 @@ class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase):
|
||||
self.assertEqual(data['o:tracking-opens'], 'no')
|
||||
self.assertEqual(data['o:tracking-clicks'], 'yes')
|
||||
|
||||
# template_id: Mailgun doesn't support stored templates
|
||||
|
||||
def test_merge_data(self):
|
||||
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
|
||||
self.message.body = "Hi %recipient.name%. Welcome to %recipient.group% at %recipient.site%."
|
||||
self.message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
'bob@example.com': {'name': "Bob"}, # and leave group undefined
|
||||
}
|
||||
self.message.merge_global_data = {
|
||||
'group': "Users", # default
|
||||
'site': "ExampleCo",
|
||||
}
|
||||
self.message.send()
|
||||
data = self.get_api_call_data()
|
||||
self.assertJSONEqual(data['recipient-variables'], {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers", 'site': "ExampleCo"},
|
||||
'bob@example.com': {'name': "Bob", 'group': "Users", 'site': "ExampleCo"},
|
||||
})
|
||||
# Make sure we didn't modify original dicts on message:
|
||||
self.assertEqual(self.message.merge_data, {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
'bob@example.com': {'name': "Bob"},
|
||||
})
|
||||
self.assertEqual(self.message.merge_global_data, {'group': "Users", 'site': "ExampleCo"})
|
||||
|
||||
def test_only_merge_global_data(self):
|
||||
# Make sure merge_global_data distributed to recipient-variables
|
||||
# even when merge_data not set
|
||||
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
|
||||
self.message.merge_global_data = {'test': "value"}
|
||||
self.message.send()
|
||||
data = self.get_api_call_data()
|
||||
self.assertJSONEqual(data['recipient-variables'], {
|
||||
'alice@example.com': {'test': "value"},
|
||||
'bob@example.com': {'test': "value"},
|
||||
})
|
||||
|
||||
def test_sender_domain(self):
|
||||
"""Mailgun send domain can come from from_email or esp_extra"""
|
||||
# You could also use ANYMAIL_SEND_DEFAULTS={'esp_extra': {'sender_domain': 'your-domain.com'}}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from email.mime.base import MIMEBase
|
||||
@@ -340,6 +339,68 @@ class MandrillBackendAnymailFeatureTests(MandrillBackendMockAPITestCase):
|
||||
self.assertEqual(data['message']['track_opens'], False)
|
||||
self.assertEqual(data['message']['track_clicks'], True)
|
||||
|
||||
def test_template_id(self):
|
||||
self.message.template_id = "welcome_template"
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assert_esp_called("/messages/send-template.json") # template requires different send API
|
||||
self.assertEqual(data['template_name'], "welcome_template")
|
||||
self.assertEqual(data['template_content'], []) # Mandrill requires this field with send-template
|
||||
|
||||
def test_merge_data(self):
|
||||
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
|
||||
# Mandrill template_id is not required to use merge.
|
||||
# You can just supply template content as the message (e.g.):
|
||||
self.message.body = "Hi *|name|*. Welcome to *|group|* at *|site|*."
|
||||
self.message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
'bob@example.com': {'name': "Bob"}, # and leave :group undefined
|
||||
}
|
||||
self.message.merge_global_data = {
|
||||
'group': "Users",
|
||||
'site': "ExampleCo",
|
||||
}
|
||||
self.message.send()
|
||||
self.assert_esp_called("/messages/send.json") # didn't specify template_id, so use normal send
|
||||
data = self.get_api_call_json()
|
||||
self.assertCountEqual(data['message']['merge_vars'], [
|
||||
{'rcpt': "alice@example.com", 'vars': [
|
||||
{'name': "group", 'content': "Developers"},
|
||||
{'name': "name", 'content': "Alice"}
|
||||
]},
|
||||
{'rcpt': "bob@example.com", 'vars': [
|
||||
{'name': "name", 'content': "Bob"}
|
||||
]},
|
||||
])
|
||||
self.assertCountEqual(data['message']['global_merge_vars'], [
|
||||
{'name': "group", 'content': "Users"},
|
||||
{'name': "site", 'content': "ExampleCo"},
|
||||
])
|
||||
self.assertEqual(data['message']['preserve_recipients'], False) # we force with merge_data
|
||||
|
||||
def test_missing_from(self):
|
||||
"""Make sure a missing from_email omits from* from API call.
|
||||
|
||||
(Allows use of from email/name from template)
|
||||
"""
|
||||
# You must set from_email=None after constructing the EmailMessage
|
||||
# (or you will end up with Django's settings.DEFAULT_FROM_EMAIL instead)
|
||||
self.message.from_email = None
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertNotIn('from_email', data['message'])
|
||||
self.assertNotIn('from_name', data['message'])
|
||||
|
||||
def test_missing_subject(self):
|
||||
"""Make sure a missing subject omits subject from API call.
|
||||
|
||||
(Allows use of template subject)
|
||||
"""
|
||||
self.message.subject = None
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertNotIn('subject', data['message'])
|
||||
|
||||
def test_default_omits_options(self):
|
||||
"""Make sure by default we don't send any ESP-specific options.
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ class MandrillBackendDjrillFeatureTests(MandrillBackendMockAPITestCase):
|
||||
'google_analytics_domains': ['example.com/test'],
|
||||
'google_analytics_campaign': ['UA-00000000-1'],
|
||||
'merge_language': 'mailchimp',
|
||||
'global_merge_vars': {'TEST': 'djrill'},
|
||||
'async': True,
|
||||
'ip_pool': 'Pool1',
|
||||
'invalid': 'invalid',
|
||||
@@ -170,9 +169,6 @@ class MandrillBackendDjrillSendDefaultsTests(MandrillBackendMockAPITestCase):
|
||||
self.assertEqual(data['message']['google_analytics_domains'], ['example.com/test'])
|
||||
self.assertEqual(data['message']['google_analytics_campaign'], ['UA-00000000-1'])
|
||||
self.assertEqual(data['message']['merge_language'], 'mailchimp')
|
||||
self.assertEqual(data['message']['global_merge_vars'],
|
||||
[{'name': 'TEST', 'content': 'djrill'}])
|
||||
self.assertFalse('merge_vars' in data['message'])
|
||||
self.assertFalse('recipient_metadata' in data['message'])
|
||||
# Options at top level of api params (not in message dict):
|
||||
self.assertTrue(data['async'])
|
||||
@@ -217,69 +213,29 @@ class MandrillBackendDjrillSendDefaultsTests(MandrillBackendMockAPITestCase):
|
||||
self.assertEqual(data['message']['google_analytics_domains'], ['override.example.com'])
|
||||
self.assertEqual(data['message']['google_analytics_campaign'], ['UA-99999999-1'])
|
||||
self.assertEqual(data['message']['merge_language'], 'handlebars')
|
||||
self.assertEqual(data['message']['global_merge_vars'], [{'name': 'TEST', 'content': 'djrill'}])
|
||||
# Options at top level of api params (not in message dict):
|
||||
self.assertFalse(data['async'])
|
||||
self.assertEqual(data['ip_pool'], 'Bulk Pool')
|
||||
|
||||
def test_global_merge(self):
|
||||
# Test that global settings merge in
|
||||
self.message.global_merge_vars = {'GREETING': "Hello"}
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['message']['global_merge_vars'],
|
||||
[{'name': "GREETING", 'content': "Hello"},
|
||||
{'name': 'TEST', 'content': 'djrill'}])
|
||||
|
||||
def test_global_merge_overwrite(self):
|
||||
# Test that global merge settings are overwritten
|
||||
self.message.global_merge_vars = {'TEST': "Hello"}
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['message']['global_merge_vars'],
|
||||
[{'name': 'TEST', 'content': 'Hello'}])
|
||||
|
||||
|
||||
class MandrillBackendDjrillTemplateTests(MandrillBackendMockAPITestCase):
|
||||
"""Test backend support for ESP templating features"""
|
||||
|
||||
# Holdovers from Djrill, until we design Anymail's normalized esp-template support
|
||||
|
||||
def test_merge_data(self):
|
||||
# Anymail expands simple python dicts into the more-verbose name/content
|
||||
# structures the Mandrill API uses
|
||||
def test_merge_language(self):
|
||||
self.message.merge_language = "mailchimp"
|
||||
self.message.global_merge_vars = {'GREETING': "Hello",
|
||||
'ACCOUNT_TYPE': "Basic"}
|
||||
self.message.merge_vars = {
|
||||
"customer@example.com": {'GREETING': "Dear Customer",
|
||||
'ACCOUNT_TYPE': "Premium"},
|
||||
"guest@example.com": {'GREETING': "Dear Guest"},
|
||||
}
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['message']['merge_language'], "mailchimp")
|
||||
self.assertEqual(data['message']['global_merge_vars'],
|
||||
[{'name': 'ACCOUNT_TYPE', 'content': "Basic"},
|
||||
{'name': "GREETING", 'content': "Hello"}])
|
||||
self.assertEqual(data['message']['merge_vars'],
|
||||
[{'rcpt': "customer@example.com",
|
||||
'vars': [{'name': 'ACCOUNT_TYPE', 'content': "Premium"},
|
||||
{'name': "GREETING", 'content': "Dear Customer"}]},
|
||||
{'rcpt': "guest@example.com",
|
||||
'vars': [{'name': "GREETING", 'content': "Dear Guest"}]}
|
||||
])
|
||||
|
||||
def test_send_template(self):
|
||||
self.message.template_name = "PERSONALIZED_SPECIALS"
|
||||
def test_template_content(self):
|
||||
self.message.template_content = {
|
||||
'HEADLINE': "<h1>Specials Just For *|FNAME|*</h1>",
|
||||
'OFFER_BLOCK': "<p><em>Half off</em> all fruit</p>"
|
||||
}
|
||||
self.message.send()
|
||||
self.assert_esp_called("/messages/send-template.json")
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['template_name'], "PERSONALIZED_SPECIALS")
|
||||
# Anymail expands simple python dicts into the more-verbose name/content
|
||||
# structures the Mandrill API uses
|
||||
self.assertEqual(data['template_content'],
|
||||
@@ -287,47 +243,3 @@ class MandrillBackendDjrillTemplateTests(MandrillBackendMockAPITestCase):
|
||||
'content': "<h1>Specials Just For *|FNAME|*</h1>"},
|
||||
{'name': "OFFER_BLOCK",
|
||||
'content': "<p><em>Half off</em> all fruit</p>"}])
|
||||
|
||||
def test_send_template_without_from_field(self):
|
||||
self.message.template_name = "PERSONALIZED_SPECIALS"
|
||||
self.message.use_template_from = True
|
||||
self.message.send()
|
||||
self.assert_esp_called("/messages/send-template.json")
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['template_name'], "PERSONALIZED_SPECIALS")
|
||||
self.assertFalse('from_email' in data['message'])
|
||||
self.assertFalse('from_name' in data['message'])
|
||||
|
||||
def test_send_template_without_from_field_api_failure(self):
|
||||
self.set_mock_response(status_code=400)
|
||||
self.message.template_name = "PERSONALIZED_SPECIALS"
|
||||
self.message.use_template_from = True
|
||||
with self.assertRaises(AnymailAPIError):
|
||||
self.message.send()
|
||||
|
||||
def test_send_template_without_subject_field(self):
|
||||
self.message.template_name = "PERSONALIZED_SPECIALS"
|
||||
self.message.use_template_subject = True
|
||||
self.message.send()
|
||||
self.assert_esp_called("/messages/send-template.json")
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['template_name'], "PERSONALIZED_SPECIALS")
|
||||
self.assertFalse('subject' in data['message'])
|
||||
|
||||
def test_no_template_content(self):
|
||||
# Just a template, without any template_content to be merged
|
||||
self.message.template_name = "WELCOME_MESSAGE"
|
||||
self.message.send()
|
||||
self.assert_esp_called("/messages/send-template.json")
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['template_name'], "WELCOME_MESSAGE")
|
||||
self.assertEqual(data['template_content'], []) # Mandrill requires this field
|
||||
|
||||
def test_non_template_send(self):
|
||||
# Make sure the non-template case still uses /messages/send.json
|
||||
self.message.send()
|
||||
self.assert_esp_called("/messages/send.json")
|
||||
data = self.get_api_call_json()
|
||||
self.assertFalse('template_name' in data)
|
||||
self.assertFalse('template_content' in data)
|
||||
self.assertFalse('async' in data)
|
||||
|
||||
@@ -332,6 +332,34 @@ class PostmarkBackendAnymailFeatureTests(PostmarkBackendMockAPITestCase):
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'track_clicks'):
|
||||
self.message.send()
|
||||
|
||||
def test_template(self):
|
||||
self.message.template_id = 1234567
|
||||
# Postmark doesn't support per-recipient merge_data
|
||||
self.message.merge_global_data = {'name': "Alice", 'group': "Developers"}
|
||||
self.message.send()
|
||||
self.assert_esp_called('/email/withTemplate/')
|
||||
data = self.get_api_call_json()
|
||||
self.assertEqual(data['TemplateId'], 1234567)
|
||||
self.assertEqual(data['TemplateModel'], {'name': "Alice", 'group': "Developers"})
|
||||
|
||||
def test_merge_data(self):
|
||||
self.message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
}
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'merge_data'):
|
||||
self.message.send()
|
||||
|
||||
def test_missing_subject(self):
|
||||
"""Make sure a missing subject omits Subject from API call.
|
||||
|
||||
(Allows use of template subject)
|
||||
"""
|
||||
self.message.template_id = 1234567
|
||||
self.message.subject = None
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertNotIn('Subject', data)
|
||||
|
||||
def test_default_omits_options(self):
|
||||
"""Make sure by default we don't send any ESP-specific options.
|
||||
|
||||
@@ -342,6 +370,8 @@ class PostmarkBackendAnymailFeatureTests(PostmarkBackendMockAPITestCase):
|
||||
self.message.send()
|
||||
data = self.get_api_call_json()
|
||||
self.assertNotIn('Tag', data)
|
||||
self.assertNotIn('TemplateId', data)
|
||||
self.assertNotIn('TemplateModel', data)
|
||||
self.assertNotIn('TrackOpens', data)
|
||||
|
||||
def test_esp_extra(self):
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
|
||||
|
||||
from anymail.exceptions import AnymailAPIError, AnymailSerializationError, AnymailUnsupportedFeature
|
||||
from anymail.exceptions import AnymailAPIError, AnymailSerializationError, AnymailUnsupportedFeature, AnymailWarning
|
||||
from anymail.message import attach_inline_image_file
|
||||
|
||||
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
|
||||
@@ -403,6 +403,91 @@ class SendGridBackendAnymailFeatureTests(SendGridBackendMockAPITestCase):
|
||||
self.assertEqual(smtpapi['filters']['clicktrack'], {'settings': {'enable': 1}})
|
||||
self.assertEqual(smtpapi['filters']['opentrack'], {'settings': {'enable': 0}})
|
||||
|
||||
def test_template_id(self):
|
||||
self.message.template_id = "5997fcf6-2b9f-484d-acd5-7e9a99f0dc1f"
|
||||
self.message.send()
|
||||
smtpapi = self.get_smtpapi()
|
||||
self.assertEqual(smtpapi['filters']['templates'], {
|
||||
'settings': {'enable': 1,
|
||||
'template_id': "5997fcf6-2b9f-484d-acd5-7e9a99f0dc1f"}
|
||||
})
|
||||
|
||||
def test_merge_data(self):
|
||||
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
|
||||
# SendGrid template_id is not required to use merge.
|
||||
# You can just supply template content as the message (e.g.):
|
||||
self.message.body = "Hi :name. Welcome to :group at :site."
|
||||
self.message.merge_data = {
|
||||
# You must either include merge field delimiters in the keys (':name' rather than just 'name')
|
||||
# as shown here, or use one of the merge_field_format options shown in the test cases below
|
||||
'alice@example.com': {':name': "Alice", ':group': "Developers"},
|
||||
'bob@example.com': {':name': "Bob"}, # and leave :group undefined
|
||||
}
|
||||
self.message.merge_global_data = {
|
||||
':group': "Users",
|
||||
':site': "ExampleCo",
|
||||
}
|
||||
self.message.send()
|
||||
|
||||
data = self.get_api_call_data()
|
||||
smtpapi = self.get_smtpapi()
|
||||
self.assertNotIn('to', data) # recipients should be moved to smtpapi-to with merge_data
|
||||
self.assertNotIn('toname', data)
|
||||
self.assertEqual(smtpapi['to'], ['alice@example.com', 'Bob <bob@example.com>'])
|
||||
self.assertEqual(smtpapi['sub'], {
|
||||
':name': ["Alice", "Bob"],
|
||||
':group': ["Developers", ":group"], # missing value gets replaced with var name...
|
||||
})
|
||||
self.assertEqual(smtpapi['section'], {
|
||||
':group': "Users", # ... which SG should then try to resolve from here
|
||||
':site': "ExampleCo",
|
||||
})
|
||||
|
||||
@override_settings(ANYMAIL_SENDGRID_MERGE_FIELD_FORMAT=":{}") # :field as shown in SG examples
|
||||
def test_merge_field_format_setting(self):
|
||||
# Provide merge field delimiters in settings.py
|
||||
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
|
||||
self.message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
'bob@example.com': {'name': "Bob"}, # and leave group undefined
|
||||
}
|
||||
self.message.merge_global_data = {'site': "ExampleCo"}
|
||||
self.message.send()
|
||||
smtpapi = self.get_smtpapi()
|
||||
self.assertEqual(smtpapi['sub'], {
|
||||
':name': ["Alice", "Bob"],
|
||||
':group': ["Developers", ":group"] # substitutes formatted field name if missing for recipient
|
||||
})
|
||||
self.assertEqual(smtpapi['section'], {':site': "ExampleCo"})
|
||||
|
||||
def test_merge_field_format_esp_extra(self):
|
||||
# Provide merge field delimiters for an individual message
|
||||
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
|
||||
self.message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
'bob@example.com': {'name': "Bob"}, # and leave group undefined
|
||||
}
|
||||
self.message.merge_global_data = {'site': "ExampleCo"}
|
||||
self.message.esp_extra = {'merge_field_format': '*|{}|*'} # match Mandrill/MailChimp delimiters
|
||||
self.message.send()
|
||||
smtpapi = self.get_smtpapi()
|
||||
self.assertEqual(smtpapi['sub'], {
|
||||
'*|name|*': ["Alice", "Bob"],
|
||||
'*|group|*': ["Developers", '*|group|*'] # substitutes formatted field name if missing for recipient
|
||||
})
|
||||
self.assertEqual(smtpapi['section'], {'*|site|*': "ExampleCo"})
|
||||
# Make sure our esp_extra merge_field_format doesn't get sent to SendGrid API:
|
||||
data = self.get_api_call_data()
|
||||
self.assertNotIn('merge_field_format', data)
|
||||
|
||||
def test_warn_if_no_merge_field_delimiters(self):
|
||||
self.message.to = ['alice@example.com']
|
||||
self.message.merge_data = {
|
||||
'alice@example.com': {'name': "Alice", 'group': "Developers"},
|
||||
}
|
||||
with self.assertWarnsRegex(AnymailWarning, r'SENDGRID_MERGE_FIELD_FORMAT'):
|
||||
self.message.send()
|
||||
|
||||
@override_settings(ANYMAIL_SENDGRID_GENERATE_MESSAGE_ID=False) # else we force unique_args
|
||||
def test_default_omits_options(self):
|
||||
"""Make sure by default we don't send any ESP-specific options.
|
||||
|
||||
Reference in New Issue
Block a user