Skip to content

Commit 0f2eef7

Browse files
committed
Amazon SES: support headers with template
Use new SES v2 SendBulkEmail ReplacementHeaders param to support features that require custom headers, including `extra_headers`, `metadata`, `merge_metadata` and `tags`. Update integration tests and docs Closes #375
1 parent 1cdadda commit 0f2eef7

File tree

5 files changed

+188
-138
lines changed

5 files changed

+188
-138
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ Features
4848
headers with template sends. (Requires boto3 >= 1.34.98.)
4949
(Thanks to `@carrerasrodrigo`_ the implementation.)
5050

51+
* **Amazon SES:** Allow extra headers, ``metadata``, ``merge_metadata``,
52+
and ``tags`` when sending with a ``template_id``.
53+
(Requires boto3 v1.34.98 or later.)
54+
5155

5256
v10.3
5357
-----

anymail/backends/amazon_ses.py

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import email.encoders
33
import email.policy
44

5+
from requests.structures import CaseInsensitiveDict
6+
57
from .. import __version__ as ANYMAIL_VERSION
68
from ..exceptions import AnymailAPIError, AnymailImproperlyInstalled
79
from ..message import AnymailRecipientStatus
@@ -339,10 +341,14 @@ class AmazonSESV2SendBulkEmailPayload(AmazonSESBasePayload):
339341

340342
def init_payload(self):
341343
super().init_payload()
342-
# late-bind recipients and merge_data in finalize_payload
344+
# late-bind in finalize_payload:
343345
self.recipients = {"to": [], "cc": [], "bcc": []}
344346
self.merge_data = {}
347+
self.headers = {}
345348
self.merge_headers = {}
349+
self.metadata = {}
350+
self.merge_metadata = {}
351+
self.tags = []
346352

347353
def finalize_payload(self):
348354
# Build BulkEmailEntries from recipients and merge_data.
@@ -372,11 +378,26 @@ def finalize_payload(self):
372378
},
373379
}
374380

375-
if len(self.merge_headers) > 0:
376-
entry["ReplacementHeaders"] = [
377-
{"Name": key, "Value": value}
378-
for key, value in self.merge_headers.get(to.addr_spec, {}).items()
381+
replacement_headers = []
382+
if self.headers or to.addr_spec in self.merge_headers:
383+
headers = CaseInsensitiveDict(self.headers)
384+
headers.update(self.merge_headers.get(to.addr_spec, {}))
385+
replacement_headers += [
386+
{"Name": key, "Value": value} for key, value in headers.items()
387+
]
388+
if self.metadata or to.addr_spec in self.merge_metadata:
389+
metadata = self.metadata.copy()
390+
metadata.update(self.merge_metadata.get(to.addr_spec, {}))
391+
if metadata:
392+
replacement_headers.append(
393+
{"Name": "X-Metadata", "Value": self.serialize_json(metadata)}
394+
)
395+
if self.tags:
396+
replacement_headers += [
397+
{"Name": "X-Tag", "Value": tag} for tag in self.tags
379398
]
399+
if replacement_headers:
400+
entry["ReplacementHeaders"] = replacement_headers
380401
self.params["BulkEmailEntries"].append(entry)
381402

382403
def parse_recipient_status(self, response):
@@ -446,7 +467,7 @@ def set_reply_to(self, emails):
446467
self.params["ReplyToAddresses"] = [email.address for email in emails]
447468

448469
def set_extra_headers(self, headers):
449-
self.unsupported_feature("extra_headers with template")
470+
self.headers = headers
450471

451472
def set_text_body(self, body):
452473
if body:
@@ -468,27 +489,26 @@ def set_envelope_sender(self, email):
468489
self.params["FeedbackForwardingEmailAddress"] = email.addr_spec
469490

470491
def set_metadata(self, metadata):
471-
# no custom headers with SendBulkEmail
472-
self.unsupported_feature("metadata with template")
492+
self.metadata = metadata
493+
494+
def set_merge_metadata(self, merge_metadata):
495+
self.merge_metadata = merge_metadata
473496

474497
def set_tags(self, tags):
475-
# no custom headers with SendBulkEmail, but support
476-
# AMAZON_SES_MESSAGE_TAG_NAME if used (see tags/metadata in
477-
# AmazonSESV2SendEmailPayload for more info)
478-
if tags:
479-
if self.backend.message_tag_name is not None:
480-
if len(tags) > 1:
481-
self.unsupported_feature(
482-
"multiple tags with the AMAZON_SES_MESSAGE_TAG_NAME setting"
483-
)
484-
self.params["DefaultEmailTags"] = [
485-
{"Name": self.backend.message_tag_name, "Value": tags[0]}
486-
]
487-
else:
498+
self.tags = tags
499+
500+
# Also *optionally* pass a single Message Tag if the AMAZON_SES_MESSAGE_TAG_NAME
501+
# Anymail setting is set (default no). The AWS API restricts tag content in this
502+
# case. (This is useful for dashboard segmentation; use esp_extra["Tags"] for
503+
# anything more complex.)
504+
if tags and self.backend.message_tag_name is not None:
505+
if len(tags) > 1:
488506
self.unsupported_feature(
489-
"tags with template (unless using the"
490-
" AMAZON_SES_MESSAGE_TAG_NAME setting)"
507+
"multiple tags with the AMAZON_SES_MESSAGE_TAG_NAME setting"
491508
)
509+
self.params["DefaultEmailTags"] = [
510+
{"Name": self.backend.message_tag_name, "Value": tags[0]}
511+
]
492512

493513
def set_template_id(self, template_id):
494514
# DefaultContent.Template.TemplateName

docs/esps/amazon_ses.rst

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ setting to customize the Boto session.
6868
Limitations and quirks
6969
----------------------
7070

71+
.. versionchanged:: 11.0
72+
73+
Anymail's :attr:`~anymail.message.AnymailMessage.merge_metadata`
74+
is now supported.
75+
7176
**Hard throttling**
7277
Like most ESPs, Amazon SES `throttles sending`_ for new customers. But unlike
7378
most ESPs, SES does not queue and slowly release throttled messages. Instead, it
@@ -80,11 +85,6 @@ Limitations and quirks
8085
:attr:`~anymail.message.AnymailMessage.tags` feature. See :ref:`amazon-ses-tags`
8186
below for more information and additional options.
8287

83-
**No merge_metadata**
84-
Amazon SES's batch sending API does not support the custom headers Anymail uses
85-
for metadata, so Anymail's :attr:`~anymail.message.AnymailMessage.merge_metadata`
86-
feature is not available. (See :ref:`amazon-ses-tags` below for more information.)
87-
8888
**Open and click tracking overrides**
8989
Anymail's :attr:`~anymail.message.AnymailMessage.track_opens` and
9090
:attr:`~anymail.message.AnymailMessage.track_clicks` are not supported.
@@ -126,7 +126,7 @@ Limitations and quirks
126126
signal, and using it will likely prevent delivery of your email.)
127127

128128
**Template limitations**
129-
Messages sent with templates have a number of additional limitations, such as not
129+
Messages sent with templates have some additional limitations, such as not
130130
supporting attachments. See :ref:`amazon-ses-templates` below.
131131

132132

@@ -195,12 +195,7 @@ characters.
195195

196196
For more complex use cases, set the SES ``EmailTags`` parameter (or ``DefaultEmailTags``
197197
for template sends) directly in Anymail's :ref:`esp_extra <amazon-ses-esp-extra>`. See
198-
the example below. (Because custom headers do not work with SES's SendBulkEmail call,
199-
esp_extra ``DefaultEmailTags`` is the only way to attach data to SES messages also using
200-
Anymail's :attr:`~anymail.message.AnymailMessage.template_id` and
201-
:attr:`~anymail.message.AnymailMessage.merge_data` features, and
202-
:attr:`~anymail.message.AnymailMessage.merge_metadata` cannot be supported.)
203-
198+
the example below.
204199

205200
.. _Introducing Sending Metrics:
206201
https://aws.amazon.com/blogs/ses/introducing-sending-metrics/
@@ -264,9 +259,10 @@ See Amazon's `Sending personalized email`_ guide for more information.
264259
When you set a message's :attr:`~anymail.message.AnymailMessage.template_id`
265260
to the name of one of your SES templates, Anymail will use the SES v2 `SendBulkEmail`_
266261
call to send template messages personalized with data
267-
from Anymail's normalized :attr:`~anymail.message.AnymailMessage.merge_data`
268-
and :attr:`~anymail.message.AnymailMessage.merge_global_data`
269-
message attributes.
262+
from Anymail's normalized :attr:`~anymail.message.AnymailMessage.merge_data`,
263+
:attr:`~anymail.message.AnymailMessage.merge_global_data`,
264+
:attr:`~anymail.message.AnymailMessage.merge_metadata`, and
265+
:attr:`~anymail.message.AnymailMessage.merge_headers` message attributes.
270266

271267
.. code-block:: python
272268
@@ -284,17 +280,21 @@ message attributes.
284280
'ship_date': "May 15",
285281
}
286282
287-
Amazon's templated email APIs don't support several features available for regular email.
283+
Amazon's templated email APIs don't support a few features available for regular email.
288284
When :attr:`~anymail.message.AnymailMessage.template_id` is used:
289285

290-
* Attachments and alternative parts (including AMPHTML) are not supported
291-
* Extra headers are not supported
286+
* Attachments and inline images are not supported
287+
* Alternative parts (including AMPHTML) are not supported
292288
* Overriding the template's subject or body is not supported
293-
* Anymail's :attr:`~anymail.message.AnymailMessage.metadata` is not supported
294-
* Anymail's :attr:`~anymail.message.AnymailMessage.tags` are only supported
295-
with the :setting:`AMAZON_SES_MESSAGE_TAG_NAME <ANYMAIL_AMAZON_SES_MESSAGE_TAG_NAME>`
296-
setting; only a single tag is allowed, and the tag is not directly available
297-
to webhooks. (See :ref:`amazon-ses-tags` above.)
289+
290+
.. versionchanged:: 11.0
291+
292+
Extra headers, :attr:`~anymail.message.AnymailMessage.metadata`,
293+
:attr:`~anymail.message.AnymailMessage.merge_metadata`, and
294+
:attr:`~anymail.message.AnymailMessage.tags` are now fully supported
295+
when using :attr:`~anymail.message.AnymailMessage.template_id`.
296+
(This requires :pypi:`boto3` v1.34.98 or later, which enables the
297+
ReplacementHeaders parameter for SendBulkEmail.)
298298

299299
.. _Sending personalized email:
300300
https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-personalized-email-api.html

0 commit comments

Comments
 (0)