-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for `merge_metadata` and new Resend email/batch API.
- Loading branch information
Showing
6 changed files
with
227 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -176,16 +176,6 @@ anyway---see :ref:`unsupported-features`. | |
tracking webhook using :ref:`esp_event <resend-esp-event>`. (The linked | ||
sections below include examples.) | ||
|
||
**No stored templates or batch sending** | ||
Resend does not currently offer ESP stored templates or merge capabilities, | ||
including Anymail's | ||
:attr:`~anymail.message.AnymailMessage.merge_data`, | ||
:attr:`~anymail.message.AnymailMessage.merge_global_data`, | ||
:attr:`~anymail.message.AnymailMessage.merge_metadata`, and | ||
:attr:`~anymail.message.AnymailMessage.template_id` features. | ||
(Resend's current template feature is only supported in node.js, | ||
using templates that are rendered in their API client.) | ||
|
||
**No click/open tracking overrides** | ||
Resend does not support :attr:`~anymail.message.AnymailMessage.track_clicks` | ||
or :attr:`~anymail.message.AnymailMessage.track_opens`. Its | ||
|
@@ -242,6 +232,47 @@ values directly to Resend's `send-email API`_. Example: | |
} | ||
.. _resend-templates: | ||
|
||
Batch sending/merge and ESP templates | ||
------------------------------------- | ||
|
||
.. versionadded:: 10.3 | ||
|
||
Support for batch sending with | ||
:attr:`~anymail.message.AnymailMessage.merge_metadata`. | ||
|
||
Resend supports :ref:`batch sending <batch-send>` (where each *To* | ||
recipient sees only their own email address). It also supports | ||
per-recipient metadata with batch sending. | ||
|
||
Set Anymail's normalized :attr:`~anymail.message.AnymailMessage.merge_metadata` | ||
attribute to use Resend's batch-send API: | ||
|
||
.. code-block:: python | ||
message = EmailMessage( | ||
to=["[email protected]", "Bob <[email protected]>"], | ||
from_email="...", subject="...", body="..." | ||
) | ||
message.merge_metadata = { | ||
'[email protected]': {'user_id': "12345"}, | ||
'[email protected]': {'user_id': "54321"}, | ||
} | ||
Resend does not currently offer :ref:`ESP stored templates <esp-stored-templates>` | ||
or merge capabilities, so does not support Anymail's | ||
:attr:`~anymail.message.AnymailMessage.merge_data`, | ||
:attr:`~anymail.message.AnymailMessage.merge_global_data`, or | ||
:attr:`~anymail.message.AnymailMessage.template_id` message attributes. | ||
(Resend's current template feature is only supported in node.js, | ||
using templates that are rendered in their API client.) | ||
|
||
(Setting :attr:`~anymail.message.AnymailMessage.merge_data` to an empty | ||
dict will also invoke batch send, but trying to supply merge data for | ||
any recipient will raise an :exc:`~anymail.exceptions.AnymailUnsupportedFeature` error.) | ||
|
||
|
||
.. _resend-webhooks: | ||
|
||
Status tracking webhooks | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
AnymailSerializationError, | ||
AnymailUnsupportedFeature, | ||
) | ||
from anymail.message import attach_inline_image_file | ||
from anymail.message import AnymailMessage, attach_inline_image_file | ||
|
||
from .mock_requests_backend import ( | ||
RequestsBackendMockAPITestCase, | ||
|
@@ -445,6 +445,94 @@ def test_headers_metadata_tags_interaction(self): | |
}, | ||
) | ||
|
||
_mock_batch_response = { | ||
"data": [ | ||
{"id": "ae2014de-c168-4c61-8267-70d2662a1ce1"}, | ||
{"id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"}, | ||
] | ||
} | ||
|
||
def test_merge_data(self): | ||
self.message.merge_data = {"[email protected]": {"customer_id": 3}} | ||
with self.assertRaisesMessage(AnymailUnsupportedFeature, "merge_data"): | ||
self.message.send() | ||
|
||
def test_empty_merge_data(self): | ||
# `merge_data = {}` triggers batch send | ||
self.set_mock_response(json_data=self._mock_batch_response) | ||
message = AnymailMessage( | ||
from_email="[email protected]", | ||
to=["[email protected]", "Bob <[email protected]>"], | ||
cc=["[email protected]"], | ||
merge_data={ | ||
"[email protected]": {}, | ||
"[email protected]": {}, | ||
}, | ||
) | ||
message.send() | ||
self.assert_esp_called("/emails/batch") | ||
data = self.get_api_call_json() | ||
self.assertEqual(len(data), 2) | ||
self.assertEqual(data[0]["to"], ["[email protected]"]) | ||
self.assertEqual(data[1]["to"], ["Bob <[email protected]>"]) | ||
|
||
recipients = message.anymail_status.recipients | ||
self.assertEqual(recipients["[email protected]"].status, "queued") | ||
self.assertEqual( | ||
recipients["[email protected]"].message_id, | ||
"ae2014de-c168-4c61-8267-70d2662a1ce1", | ||
) | ||
self.assertEqual(recipients["[email protected]"].status, "queued") | ||
self.assertEqual( | ||
recipients["[email protected]"].message_id, | ||
"faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb", | ||
) | ||
# No message_id for cc/bcc recipients in a batch send | ||
self.assertEqual(recipients["[email protected]"].status, "queued") | ||
self.assertIsNone(recipients["[email protected]"].message_id) | ||
|
||
def test_merge_metadata(self): | ||
self.set_mock_response(json_data=self._mock_batch_response) | ||
message = AnymailMessage( | ||
from_email="[email protected]", | ||
to=["[email protected]", "Bob <[email protected]>"], | ||
merge_metadata={ | ||
"[email protected]": {"order_id": 123, "tier": "premium"}, | ||
"[email protected]": {"order_id": 678}, | ||
}, | ||
metadata={"notification_batch": "zx912"}, | ||
) | ||
message.send() | ||
|
||
# merge_metadata forces batch send API: | ||
self.assert_esp_called("/emails/batch") | ||
|
||
data = self.get_api_call_json() | ||
self.assertEqual(len(data), 2) | ||
self.assertEqual(data[0]["to"], ["[email protected]"]) | ||
# metadata and merge_metadata[recipient] are combined: | ||
self.assertEqual( | ||
json.loads(data[0]["headers"]["X-Metadata"]), | ||
{"order_id": 123, "tier": "premium", "notification_batch": "zx912"}, | ||
) | ||
self.assertEqual(data[1]["to"], ["Bob <[email protected]>"]) | ||
self.assertEqual( | ||
json.loads(data[1]["headers"]["X-Metadata"]), | ||
{"order_id": 678, "notification_batch": "zx912"}, | ||
) | ||
|
||
recipients = message.anymail_status.recipients | ||
self.assertEqual(recipients["[email protected]"].status, "queued") | ||
self.assertEqual( | ||
recipients["[email protected]"].message_id, | ||
"ae2014de-c168-4c61-8267-70d2662a1ce1", | ||
) | ||
self.assertEqual(recipients["[email protected]"].status, "queued") | ||
self.assertEqual( | ||
recipients["[email protected]"].message_id, | ||
"faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb", | ||
) | ||
|
||
def test_track_opens(self): | ||
self.message.track_opens = True | ||
with self.assertRaisesMessage(AnymailUnsupportedFeature, "track_opens"): | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,6 +86,37 @@ def test_all_options(self): | |
len(message.anymail_status.message_id), 0 | ||
) # non-empty string | ||
|
||
def test_batch_send(self): | ||
# merge_metadata or merge_data will use batch send API | ||
message = AnymailMessage( | ||
subject="Anymail Resend batch sendintegration test", | ||
body="This is the text body", | ||
from_email=self.from_email, | ||
to=["[email protected]", '"Recipient 2" <[email protected]>'], | ||
metadata={"meta1": "simple string", "meta2": 2}, | ||
merge_metadata={ | ||
"[email protected]": {"meta3": "recipient 1"}, | ||
"[email protected]": {"meta3": "recipient 2"}, | ||
}, | ||
tags=["tag 1", "tag 2"], | ||
) | ||
message.attach_alternative("<p>HTML content</p>", "text/html") | ||
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain") | ||
|
||
message.send() | ||
# Resend always queues: | ||
self.assertEqual(message.anymail_status.status, {"queued"}) | ||
recipient_status = message.anymail_status.recipients | ||
self.assertEqual(recipient_status["[email protected]"].status, "queued") | ||
self.assertEqual(recipient_status["[email protected]"].status, "queued") | ||
self.assertRegex(recipient_status["[email protected]"].message_id, r".+") | ||
self.assertRegex(recipient_status["[email protected]"].message_id, r".+") | ||
# Each recipient gets their own message_id: | ||
self.assertNotEqual( | ||
recipient_status["[email protected]"].message_id, | ||
recipient_status["[email protected]"].message_id, | ||
) | ||
|
||
@unittest.skip("Resend has stopped responding to bad/missing API keys (12/2023)") | ||
@override_settings(ANYMAIL_RESEND_API_KEY="Hey, that's not an API key!") | ||
def test_invalid_api_key(self): | ||
|