mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
@@ -25,6 +25,18 @@ Release history
|
|||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
.. This extra heading level keeps the ToC from becoming unmanageably long
|
.. This extra heading level keeps the ToC from becoming unmanageably long
|
||||||
|
|
||||||
|
vNext
|
||||||
|
-----
|
||||||
|
|
||||||
|
*unreleased changes*
|
||||||
|
|
||||||
|
Features
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
* **Brevo:** Add support for batch sending
|
||||||
|
(`docs <https://anymail.dev/en/latest/esps/brevo/#batch-sending-merge-and-esp-templates>`__).
|
||||||
|
|
||||||
|
|
||||||
v10.2
|
v10.2
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -39,11 +39,16 @@ class EmailBackend(AnymailRequestsBackend):
|
|||||||
# SendinBlue doesn't give any detail on a success
|
# SendinBlue doesn't give any detail on a success
|
||||||
# https://developers.sendinblue.com/docs/responses
|
# https://developers.sendinblue.com/docs/responses
|
||||||
message_id = None
|
message_id = None
|
||||||
|
message_ids = []
|
||||||
|
|
||||||
if response.content != b"":
|
if response.content != b"":
|
||||||
parsed_response = self.deserialize_json_response(response, payload, message)
|
parsed_response = self.deserialize_json_response(response, payload, message)
|
||||||
try:
|
try:
|
||||||
message_id = parsed_response["messageId"]
|
message_id = parsed_response["messageId"]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
try:
|
||||||
|
# batch send
|
||||||
|
message_ids = parsed_response["messageIds"]
|
||||||
except (KeyError, TypeError) as err:
|
except (KeyError, TypeError) as err:
|
||||||
raise AnymailRequestsAPIError(
|
raise AnymailRequestsAPIError(
|
||||||
"Invalid SendinBlue API response format",
|
"Invalid SendinBlue API response format",
|
||||||
@@ -54,12 +59,21 @@ class EmailBackend(AnymailRequestsBackend):
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
status = AnymailRecipientStatus(message_id=message_id, status="queued")
|
status = AnymailRecipientStatus(message_id=message_id, status="queued")
|
||||||
return {recipient.addr_spec: status for recipient in payload.all_recipients}
|
recipient_status = {
|
||||||
|
recipient.addr_spec: status for recipient in payload.all_recipients
|
||||||
|
}
|
||||||
|
if message_ids:
|
||||||
|
for to, message_id in zip(payload.to_recipients, message_ids):
|
||||||
|
recipient_status[to.addr_spec] = AnymailRecipientStatus(
|
||||||
|
message_id=message_id, status="queued"
|
||||||
|
)
|
||||||
|
return recipient_status
|
||||||
|
|
||||||
|
|
||||||
class SendinBluePayload(RequestsPayload):
|
class SendinBluePayload(RequestsPayload):
|
||||||
def __init__(self, message, defaults, backend, *args, **kwargs):
|
def __init__(self, message, defaults, backend, *args, **kwargs):
|
||||||
self.all_recipients = [] # used for backend.parse_recipient_status
|
self.all_recipients = [] # used for backend.parse_recipient_status
|
||||||
|
self.to_recipients = [] # used for backend.parse_recipient_status
|
||||||
|
|
||||||
http_headers = kwargs.pop("headers", {})
|
http_headers = kwargs.pop("headers", {})
|
||||||
http_headers["api-key"] = backend.api_key
|
http_headers["api-key"] = backend.api_key
|
||||||
@@ -74,9 +88,32 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
|
|
||||||
def init_payload(self):
|
def init_payload(self):
|
||||||
self.data = {"headers": CaseInsensitiveDict()} # becomes json
|
self.data = {"headers": CaseInsensitiveDict()} # becomes json
|
||||||
|
self.merge_data = {}
|
||||||
|
self.metadata = {}
|
||||||
|
self.merge_metadata = {}
|
||||||
|
|
||||||
def serialize_data(self):
|
def serialize_data(self):
|
||||||
"""Performs any necessary serialization on self.data, and returns the result."""
|
"""Performs any necessary serialization on self.data, and returns the result."""
|
||||||
|
if self.is_batch():
|
||||||
|
# Burst data["to"] into data["messageVersions"]
|
||||||
|
to_list = self.data.pop("to", [])
|
||||||
|
self.data["messageVersions"] = [
|
||||||
|
{"to": [to], "params": self.merge_data.get(to["email"])}
|
||||||
|
for to in to_list
|
||||||
|
]
|
||||||
|
if self.merge_metadata:
|
||||||
|
# Merge global metadata with any per-recipient metadata.
|
||||||
|
# (Top-level X-Mailin-custom header is already set to global metadata,
|
||||||
|
# and will apply for recipients without a "headers" override.)
|
||||||
|
for version in self.data["messageVersions"]:
|
||||||
|
to_email = version["to"][0]["email"]
|
||||||
|
if to_email in self.merge_metadata:
|
||||||
|
recipient_metadata = self.metadata.copy()
|
||||||
|
recipient_metadata.update(self.merge_metadata[to_email])
|
||||||
|
version["headers"] = {
|
||||||
|
"X-Mailin-custom": self.serialize_json(recipient_metadata)
|
||||||
|
}
|
||||||
|
|
||||||
if not self.data["headers"]:
|
if not self.data["headers"]:
|
||||||
del self.data["headers"] # don't send empty headers
|
del self.data["headers"] # don't send empty headers
|
||||||
return self.serialize_json(self.data)
|
return self.serialize_json(self.data)
|
||||||
@@ -102,6 +139,8 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
if emails:
|
if emails:
|
||||||
self.data[recipient_type] = [self.email_object(email) for email in emails]
|
self.data[recipient_type] = [self.email_object(email) for email in emails]
|
||||||
self.all_recipients += emails # used for backend.parse_recipient_status
|
self.all_recipients += emails # used for backend.parse_recipient_status
|
||||||
|
if recipient_type == "to":
|
||||||
|
self.to_recipients = emails # used for backend.parse_recipient_status
|
||||||
|
|
||||||
def set_subject(self, subject):
|
def set_subject(self, subject):
|
||||||
if subject != "": # see note in set_text_body about template rendering
|
if subject != "": # see note in set_text_body about template rendering
|
||||||
@@ -158,8 +197,8 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
self.data.update(extra)
|
self.data.update(extra)
|
||||||
|
|
||||||
def set_merge_data(self, merge_data):
|
def set_merge_data(self, merge_data):
|
||||||
"""SendinBlue doesn't support special attributes for each recipient"""
|
# Late bound in serialize_data:
|
||||||
self.unsupported_feature("merge_data")
|
self.merge_data = merge_data
|
||||||
|
|
||||||
def set_merge_global_data(self, merge_global_data):
|
def set_merge_global_data(self, merge_global_data):
|
||||||
self.data["params"] = merge_global_data
|
self.data["params"] = merge_global_data
|
||||||
@@ -167,6 +206,11 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
def set_metadata(self, metadata):
|
def set_metadata(self, metadata):
|
||||||
# SendinBlue expects a single string payload
|
# SendinBlue expects a single string payload
|
||||||
self.data["headers"]["X-Mailin-custom"] = self.serialize_json(metadata)
|
self.data["headers"]["X-Mailin-custom"] = self.serialize_json(metadata)
|
||||||
|
self.metadata = metadata # needed in serialize_data for batch send
|
||||||
|
|
||||||
|
def set_merge_metadata(self, merge_metadata):
|
||||||
|
# Late-bound in serialize_data:
|
||||||
|
self.merge_metadata = merge_metadata
|
||||||
|
|
||||||
def set_send_at(self, send_at):
|
def set_send_at(self, send_at):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -149,10 +149,33 @@ Brevo can handle.
|
|||||||
If you are ignoring unsupported features and have multiple reply addresses,
|
If you are ignoring unsupported features and have multiple reply addresses,
|
||||||
Anymail will use only the first one.
|
Anymail will use only the first one.
|
||||||
|
|
||||||
**Metadata**
|
**Metadata exposed in message headers**
|
||||||
Anymail passes :attr:`~anymail.message.AnymailMessage.metadata` to Brevo
|
Anymail passes :attr:`~anymail.message.AnymailMessage.metadata` to Brevo
|
||||||
as a JSON-encoded string using their :mailheader:`X-Mailin-custom` email header.
|
as a JSON-encoded string using their :mailheader:`X-Mailin-custom` email header.
|
||||||
The metadata is available in tracking webhooks.
|
This header is included in the sent message, so **metadata will be visible to
|
||||||
|
message recipients** if they view the raw message source.
|
||||||
|
|
||||||
|
**Special headers**
|
||||||
|
Brevo uses special email headers to control certain features.
|
||||||
|
You can set these using Django's
|
||||||
|
:class:`EmailMessage.headers <django.core.mail.EmailMessage>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
message = EmailMessage(
|
||||||
|
...,
|
||||||
|
headers = {
|
||||||
|
"sender.ip": "10.10.1.150", # use a dedicated IP
|
||||||
|
"idempotencyKey": "...uuid...", # batch send deduplication
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note the constructor param is called `headers`, but the
|
||||||
|
# corresponding attribute is named `extra_headers`:
|
||||||
|
message.extra_headers = {
|
||||||
|
"sender.ip": "10.10.1.222",
|
||||||
|
"idempotencyKey": "...uuid...",
|
||||||
|
}
|
||||||
|
|
||||||
**Delayed sending**
|
**Delayed sending**
|
||||||
.. versionadded:: 9.0
|
.. versionadded:: 9.0
|
||||||
@@ -174,30 +197,33 @@ Brevo can handle.
|
|||||||
Batch sending/merge and ESP templates
|
Batch sending/merge and ESP templates
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
Brevo supports :ref:`ESP stored templates <esp-stored-templates>` populated with
|
.. versionchanged:: 10.3
|
||||||
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`
|
Added support for batch sending with :attr:`~anymail.message.AnymailMessage.merge_data`
|
||||||
and :attr:`~anymail.message.AnymailMessage.merge_metadata` message attributes are not
|
and :attr:`~anymail.message.AnymailMessage.merge_metadata`.
|
||||||
supported with the Brevo backend, but you can use Anymail's
|
|
||||||
:attr:`~anymail.message.AnymailMessage.merge_global_data` with Brevo templates.
|
Brevo supports :ref:`ESP stored templates <esp-stored-templates>` and
|
||||||
|
:ref:`batch sending <batch-send>` with per-recipient merge data.
|
||||||
|
|
||||||
To use a Brevo template, set the message's
|
To use a Brevo template, set the message's
|
||||||
:attr:`~anymail.message.AnymailMessage.template_id` to the numeric
|
:attr:`~anymail.message.AnymailMessage.template_id` to the numeric
|
||||||
Brevo template ID, and supply substitution attributes using
|
Brevo template ID, and supply substitution params using Anymail's normalized
|
||||||
the message's :attr:`~anymail.message.AnymailMessage.merge_global_data`:
|
:attr:`~anymail.message.AnymailMessage.merge_data` and
|
||||||
|
:attr:`~anymail.message.AnymailMessage.merge_global_data` message attributes:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
message = EmailMessage(
|
message = EmailMessage(
|
||||||
to=["alice@example.com"] # single recipient...
|
# (subject and body come from the template, so don't include those)
|
||||||
# ...multiple to emails would all get the same message
|
to=["alice@example.com", "Bob <bob@example.com>"]
|
||||||
# (and would all see each other's emails in the "to" header)
|
|
||||||
)
|
)
|
||||||
message.template_id = 3 # use this Brevo template
|
message.template_id = 3 # use this Brevo template
|
||||||
message.from_email = None # to use the template's default sender
|
message.from_email = None # to use the template's default sender
|
||||||
|
message.merge_data = {
|
||||||
|
'alice@example.com': {'name': "Alice", 'order_no': "12345"},
|
||||||
|
'bob@example.com': {'name': "Bob", 'order_no': "54321"},
|
||||||
|
}
|
||||||
message.merge_global_data = {
|
message.merge_global_data = {
|
||||||
'name': "Alice",
|
|
||||||
'order_no': "12345",
|
|
||||||
'ship_date': "May 15",
|
'ship_date': "May 15",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +240,31 @@ If you want to use the template's sender, be sure to set ``from_email`` to ``Non
|
|||||||
You can also override the template's subject and reply-to address (but not body)
|
You can also override the template's subject and reply-to address (but not body)
|
||||||
using standard :class:`~django.core.mail.EmailMessage` attributes.
|
using standard :class:`~django.core.mail.EmailMessage` attributes.
|
||||||
|
|
||||||
|
Brevo also supports batch-sending without using an ESP-stored template. In this
|
||||||
|
case, each recipient will receive the same content (Brevo doesn't support inline
|
||||||
|
templates) but will see only their own *To* email address. Setting either of
|
||||||
|
:attr:`~anymail.message.AnymailMessage.merge_data` or
|
||||||
|
:attr:`~anymail.message.AnymailMessage.merge_metadata`---even to an empty
|
||||||
|
dict---will cause Anymail to use Brevo's batch send option (``"messageVersions"``).
|
||||||
|
|
||||||
|
You can use Anymail's
|
||||||
|
:attr:`~anymail.message.AnymailMessage.merge_metadata` to supply custom tracking
|
||||||
|
data for each recipient:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
message = EmailMessage(
|
||||||
|
to=["alice@example.com", "Bob <bob@example.com>"],
|
||||||
|
from_email="...", subject="...", body="..."
|
||||||
|
)
|
||||||
|
message.merge_metadata = {
|
||||||
|
'alice@example.com': {'user_id': "12345"},
|
||||||
|
'bob@example.com': {'user_id': "54321"},
|
||||||
|
}
|
||||||
|
|
||||||
|
To use Brevo's "`idempotencyKey`_" with a batch send, set it in the
|
||||||
|
message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``.
|
||||||
|
|
||||||
.. caution::
|
.. caution::
|
||||||
|
|
||||||
**Sendinblue "old template language" not supported**
|
**Sendinblue "old template language" not supported**
|
||||||
@@ -241,6 +292,9 @@ using standard :class:`~django.core.mail.EmailMessage` attributes.
|
|||||||
.. _Brevo Template Language:
|
.. _Brevo Template Language:
|
||||||
https://help.brevo.com/hc/en-us/articles/360000946299
|
https://help.brevo.com/hc/en-us/articles/360000946299
|
||||||
|
|
||||||
|
.. _idempotencyKey:
|
||||||
|
https://developers.brevo.com/docs/heterogenous-versions-batch-emails
|
||||||
|
|
||||||
.. _convert each old template:
|
.. _convert each old template:
|
||||||
https://help.brevo.com/hc/en-us/articles/360000991960
|
https://help.brevo.com/hc/en-us/articles/360000991960
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from anymail.exceptions import (
|
|||||||
AnymailSerializationError,
|
AnymailSerializationError,
|
||||||
AnymailUnsupportedFeature,
|
AnymailUnsupportedFeature,
|
||||||
)
|
)
|
||||||
from anymail.message import attach_inline_image_file
|
from anymail.message import AnymailMessage, attach_inline_image_file
|
||||||
|
|
||||||
from .mock_requests_backend import (
|
from .mock_requests_backend import (
|
||||||
RequestsBackendMockAPITestCase,
|
RequestsBackendMockAPITestCase,
|
||||||
@@ -478,13 +478,60 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
self.assertEqual(data["subject"], "My Subject")
|
self.assertEqual(data["subject"], "My Subject")
|
||||||
self.assertEqual(data["to"], [{"email": "to@example.com", "name": "Recipient"}])
|
self.assertEqual(data["to"], [{"email": "to@example.com", "name": "Recipient"}])
|
||||||
|
|
||||||
def test_merge_data(self):
|
_mock_batch_response = {
|
||||||
self.message.merge_data = {
|
"messageIds": [
|
||||||
"alice@example.com": {":name": "Alice", ":group": "Developers"},
|
"<202403182259.64789700810.1@smtp-relay.mailin.fr>",
|
||||||
"bob@example.com": {":name": "Bob"}, # and leave :group undefined
|
"<202403182259.64789700810.2@smtp-relay.mailin.fr>",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
with self.assertRaises(AnymailUnsupportedFeature):
|
|
||||||
self.message.send()
|
def test_merge_data(self):
|
||||||
|
self.set_mock_response(json_data=self._mock_batch_response)
|
||||||
|
message = AnymailMessage(
|
||||||
|
from_email="from@example.com",
|
||||||
|
template_id=1234567,
|
||||||
|
to=["alice@example.com", "Bob <bob@example.com>"],
|
||||||
|
merge_data={
|
||||||
|
"alice@example.com": {"name": "Alice", "group": "Developers"},
|
||||||
|
"bob@example.com": {"name": "Bob"}, # and leave group undefined
|
||||||
|
"nobody@example.com": {"name": "Not a recipient for this message"},
|
||||||
|
},
|
||||||
|
merge_global_data={"group": "Users", "site": "ExampleCo"},
|
||||||
|
)
|
||||||
|
message.send()
|
||||||
|
|
||||||
|
# batch send uses same API endpoint as regular send:
|
||||||
|
self.assert_esp_called("/v3/smtp/email")
|
||||||
|
data = self.get_api_call_json()
|
||||||
|
versions = data["messageVersions"]
|
||||||
|
self.assertEqual(len(versions), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
versions[0],
|
||||||
|
{
|
||||||
|
"to": [{"email": "alice@example.com"}],
|
||||||
|
"params": {"name": "Alice", "group": "Developers"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
versions[1],
|
||||||
|
{
|
||||||
|
"to": [{"email": "bob@example.com", "name": "Bob"}],
|
||||||
|
"params": {"name": "Bob"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(data["params"], {"group": "Users", "site": "ExampleCo"})
|
||||||
|
|
||||||
|
recipients = message.anymail_status.recipients
|
||||||
|
self.assertEqual(recipients["alice@example.com"].status, "queued")
|
||||||
|
self.assertEqual(
|
||||||
|
recipients["alice@example.com"].message_id,
|
||||||
|
"<202403182259.64789700810.1@smtp-relay.mailin.fr>",
|
||||||
|
)
|
||||||
|
self.assertEqual(recipients["bob@example.com"].status, "queued")
|
||||||
|
self.assertEqual(
|
||||||
|
recipients["bob@example.com"].message_id,
|
||||||
|
"<202403182259.64789700810.2@smtp-relay.mailin.fr>",
|
||||||
|
)
|
||||||
|
|
||||||
def test_merge_global_data(self):
|
def test_merge_global_data(self):
|
||||||
self.message.merge_global_data = {"a": "b"}
|
self.message.merge_global_data = {"a": "b"}
|
||||||
@@ -492,6 +539,38 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
data = self.get_api_call_json()
|
data = self.get_api_call_json()
|
||||||
self.assertEqual(data["params"], {"a": "b"})
|
self.assertEqual(data["params"], {"a": "b"})
|
||||||
|
|
||||||
|
def test_merge_metadata(self):
|
||||||
|
self.set_mock_response(json_data=self._mock_batch_response)
|
||||||
|
self.message.to = ["alice@example.com", "Bob <bob@example.com>"]
|
||||||
|
self.message.merge_metadata = {
|
||||||
|
"alice@example.com": {"order_id": 123, "tier": "premium"},
|
||||||
|
"bob@example.com": {"order_id": 678},
|
||||||
|
}
|
||||||
|
self.message.metadata = {"notification_batch": "zx912"}
|
||||||
|
self.message.send()
|
||||||
|
|
||||||
|
data = self.get_api_call_json()
|
||||||
|
versions = data["messageVersions"]
|
||||||
|
self.assertEqual(len(versions), 2)
|
||||||
|
self.assertEqual(versions[0]["to"], [{"email": "alice@example.com"}])
|
||||||
|
# metadata and merge_metadata[recipient] are combined:
|
||||||
|
self.assertEqual(
|
||||||
|
json.loads(versions[0]["headers"]["X-Mailin-custom"]),
|
||||||
|
{"order_id": 123, "tier": "premium", "notification_batch": "zx912"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
versions[1]["to"], [{"name": "Bob", "email": "bob@example.com"}]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
json.loads(versions[1]["headers"]["X-Mailin-custom"]),
|
||||||
|
{"order_id": 678, "notification_batch": "zx912"},
|
||||||
|
)
|
||||||
|
# default metadata still sent in base headers:
|
||||||
|
self.assertEqual(
|
||||||
|
json.loads(data["headers"]["X-Mailin-custom"]),
|
||||||
|
{"notification_batch": "zx912"},
|
||||||
|
)
|
||||||
|
|
||||||
def test_default_omits_options(self):
|
def test_default_omits_options(self):
|
||||||
"""Make sure by default we don't send any ESP-specific options.
|
"""Make sure by default we don't send any ESP-specific options.
|
||||||
|
|
||||||
@@ -502,35 +581,24 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
self.message.send()
|
self.message.send()
|
||||||
data = self.get_api_call_json()
|
data = self.get_api_call_json()
|
||||||
self.assertNotIn("attachment", data)
|
self.assertNotIn("attachment", data)
|
||||||
self.assertNotIn("tag", data)
|
self.assertNotIn("bcc", data)
|
||||||
|
self.assertNotIn("cc", data)
|
||||||
self.assertNotIn("headers", data)
|
self.assertNotIn("headers", data)
|
||||||
|
self.assertNotIn("messageVersions", data)
|
||||||
|
self.assertNotIn("params", data)
|
||||||
self.assertNotIn("replyTo", data)
|
self.assertNotIn("replyTo", data)
|
||||||
self.assertNotIn("atributes", data)
|
self.assertNotIn("schedule", data)
|
||||||
|
self.assertNotIn("tags", data)
|
||||||
|
self.assertNotIn("templateId", data)
|
||||||
|
|
||||||
def test_esp_extra(self):
|
def test_esp_extra(self):
|
||||||
# SendinBlue doesn't offer any esp-extra but we will test
|
|
||||||
# with some extra of SendGrid to see if it's work in the future
|
|
||||||
self.message.esp_extra = {
|
self.message.esp_extra = {
|
||||||
"ip_pool_name": "transactional",
|
"batchId": "5c6cfa04-eed9-42c2-8b5c-6d470d978e9d",
|
||||||
"asm": { # subscription management
|
|
||||||
"group_id": 1,
|
|
||||||
},
|
|
||||||
"tracking_settings": {
|
|
||||||
"subscription_tracking": {
|
|
||||||
"enable": True,
|
|
||||||
"substitution_tag": "[unsubscribe_url]",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
self.message.send()
|
self.message.send()
|
||||||
data = self.get_api_call_json()
|
data = self.get_api_call_json()
|
||||||
# merged from esp_extra:
|
# merged from esp_extra:
|
||||||
self.assertEqual(data["ip_pool_name"], "transactional")
|
self.assertEqual(data["batchId"], "5c6cfa04-eed9-42c2-8b5c-6d470d978e9d")
|
||||||
self.assertEqual(data["asm"], {"group_id": 1})
|
|
||||||
self.assertEqual(
|
|
||||||
data["tracking_settings"]["subscription_tracking"],
|
|
||||||
{"enable": True, "substitution_tag": "[unsubscribe_url]"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
def test_send_attaches_anymail_status(self):
|
def test_send_attaches_anymail_status(self):
|
||||||
|
|||||||
@@ -98,40 +98,42 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
|||||||
template_id=5,
|
template_id=5,
|
||||||
# Override template sender:
|
# Override template sender:
|
||||||
from_email=formataddr(("Sender", self.from_email)),
|
from_email=formataddr(("Sender", self.from_email)),
|
||||||
# No batch send (so max one recipient suggested):
|
to=["Recipient 1 <test+to1@anymail.dev>", "test+to2@anymail.dev"],
|
||||||
to=["Recipient <test+to1@anymail.dev>"],
|
|
||||||
reply_to=["Do not reply <reply@example.dev>"],
|
reply_to=["Do not reply <reply@example.dev>"],
|
||||||
tags=["using-template"],
|
tags=["using-template"],
|
||||||
headers={"X-Anymail-Test": "group: A, variation: C"},
|
|
||||||
merge_global_data={
|
|
||||||
# The Anymail test template includes `{{ params.SHIP_DATE }}`
|
# The Anymail test template includes `{{ params.SHIP_DATE }}`
|
||||||
# and `{{ params.ORDER_ID }}` substitutions
|
# and `{{ params.ORDER_ID }}` substitutions
|
||||||
"SHIP_DATE": "yesterday",
|
merge_data={
|
||||||
"ORDER_ID": "12345",
|
"test+to1@anymail.dev": {"ORDER_ID": "12345"},
|
||||||
|
"test+to2@anymail.dev": {"ORDER_ID": "23456"},
|
||||||
|
},
|
||||||
|
merge_global_data={"SHIP_DATE": "yesterday"},
|
||||||
|
metadata={"customer-id": "unknown", "meta2": 2},
|
||||||
|
merge_metadata={
|
||||||
|
"test+to1@anymail.dev": {"customer-id": "ZXK9123"},
|
||||||
|
"test+to2@anymail.dev": {"customer-id": "ZZT4192"},
|
||||||
},
|
},
|
||||||
metadata={"customer-id": "ZXK9123", "meta2": 2},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normal attachments don't work with Brevo templates:
|
message.attach("attachment1.txt", "Here is some\ntext", "text/plain")
|
||||||
# message.attach("attachment1.txt", "Here is some\ntext", "text/plain")
|
|
||||||
# If you can host the attachment content on some publicly-accessible URL,
|
|
||||||
# this *non-portable* alternative allows sending attachments with templates:
|
|
||||||
message.esp_extra = {
|
|
||||||
"attachment": [
|
|
||||||
{
|
|
||||||
"name": "attachment1.txt",
|
|
||||||
# URL where Brevo can download the attachment content while
|
|
||||||
# sending (must be content-type: text/plain):
|
|
||||||
"url": "https://raw.githubusercontent.com/anymail/django-anymail/"
|
|
||||||
"main/docs/_readme/template.txt",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
message.send()
|
message.send()
|
||||||
# SendinBlue always queues:
|
# SendinBlue always queues:
|
||||||
self.assertEqual(message.anymail_status.status, {"queued"})
|
self.assertEqual(message.anymail_status.status, {"queued"})
|
||||||
self.assertRegex(message.anymail_status.message_id, r"\<.+@.+\>")
|
recipient_status = message.anymail_status.recipients
|
||||||
|
self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "queued")
|
||||||
|
self.assertEqual(recipient_status["test+to2@anymail.dev"].status, "queued")
|
||||||
|
self.assertRegex(
|
||||||
|
recipient_status["test+to1@anymail.dev"].message_id, r"\<.+@.+\>"
|
||||||
|
)
|
||||||
|
self.assertRegex(
|
||||||
|
recipient_status["test+to2@anymail.dev"].message_id, r"\<.+@.+\>"
|
||||||
|
)
|
||||||
|
# Each recipient gets their own message_id:
|
||||||
|
self.assertNotEqual(
|
||||||
|
recipient_status["test+to1@anymail.dev"].message_id,
|
||||||
|
recipient_status["test+to2@anymail.dev"].message_id,
|
||||||
|
)
|
||||||
|
|
||||||
@override_settings(ANYMAIL_SENDINBLUE_API_KEY="Hey, that's not an API key!")
|
@override_settings(ANYMAIL_SENDINBLUE_API_KEY="Hey, that's not an API key!")
|
||||||
def test_invalid_api_key(self):
|
def test_invalid_api_key(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user