Brevo: Rename SendinBlue to Brevo

- Replace "SendinBlue" with "Brevo"
  throughout the code.
- Maintain deprecated compatibility
  versions on the old names/URLs.
  (Split into separate commit
  to make renamed files more
  obvious.)
- Update docs to reflect change,
  provide migration advice.
- Update integration workflow.
This commit is contained in:
Mike Edmunds
2024-03-11 18:46:52 -07:00
parent 14d451659e
commit c7ee59c3ca
12 changed files with 326 additions and 197 deletions

View File

@@ -40,6 +40,7 @@ jobs:
# combination, to avoid rapidly consuming the testing accounts' entire send allotments.
config:
- { tox: django41-py310-amazon_ses, python: "3.10" }
- { tox: django41-py310-brevo, python: "3.10" }
- { tox: django41-py310-mailersend, python: "3.10" }
- { tox: django41-py310-mailgun, python: "3.10" }
- { tox: django41-py310-mailjet, python: "3.10" }
@@ -48,7 +49,6 @@ jobs:
- { tox: django41-py310-postmark, python: "3.10" }
- { tox: django41-py310-resend, python: "3.10" }
- { tox: django41-py310-sendgrid, python: "3.10" }
- { tox: django41-py310-sendinblue, python: "3.10" }
- { tox: django41-py310-sparkpost, python: "3.10" }
- { tox: django41-py310-unisender_go, python: "3.10" }
@@ -77,6 +77,8 @@ jobs:
ANYMAIL_TEST_AMAZON_SES_DOMAIN: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_DOMAIN }}
ANYMAIL_TEST_AMAZON_SES_REGION_NAME: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_REGION_NAME }}
ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY }}
ANYMAIL_TEST_BREVO_API_KEY: ${{ secrets.ANYMAIL_TEST_BREVO_API_KEY }}
ANYMAIL_TEST_BREVO_DOMAIN: ${{ vars.ANYMAIL_TEST_BREVO_DOMAIN }}
ANYMAIL_TEST_MAILERSEND_API_TOKEN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_API_TOKEN }}
ANYMAIL_TEST_MAILERSEND_DOMAIN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_DOMAIN }}
ANYMAIL_TEST_MAILGUN_API_KEY: ${{ secrets.ANYMAIL_TEST_MAILGUN_API_KEY }}
@@ -94,8 +96,6 @@ jobs:
ANYMAIL_TEST_SENDGRID_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDGRID_API_KEY }}
ANYMAIL_TEST_SENDGRID_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDGRID_DOMAIN }}
ANYMAIL_TEST_SENDGRID_TEMPLATE_ID: ${{ secrets.ANYMAIL_TEST_SENDGRID_TEMPLATE_ID }}
ANYMAIL_TEST_SENDINBLUE_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDINBLUE_API_KEY }}
ANYMAIL_TEST_SENDINBLUE_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDINBLUE_DOMAIN }}
ANYMAIL_TEST_SPARKPOST_API_KEY: ${{ secrets.ANYMAIL_TEST_SPARKPOST_API_KEY }}
ANYMAIL_TEST_SPARKPOST_DOMAIN: ${{ secrets.ANYMAIL_TEST_SPARKPOST_DOMAIN }}
ANYMAIL_TEST_UNISENDER_GO_API_KEY: ${{ secrets.ANYMAIL_TEST_UNISENDER_GO_API_KEY }}

View File

@@ -30,6 +30,16 @@ vNext
*unreleased changes*
Deprecations
~~~~~~~~~~~~
* **SendinBlue:** Rename "SendinBlue" to "Brevo" throughout Anymail's code.
This affects the email backend name, settings names, and webhook URLs.
The old names will continue to work for now, but are deprecated. See
`Updating code from SendinBlue to Brevo <https://anymail.dev/en/latest/esps/brevo/#brevo-rename>`__
for details.
Features
~~~~~~~~

View File

@@ -8,10 +8,10 @@ from .base_requests import AnymailRequestsBackend, RequestsPayload
class EmailBackend(AnymailRequestsBackend):
"""
SendinBlue v3 API Email Backend
Brevo v3 API Email Backend
"""
esp_name = "SendinBlue"
esp_name = "Brevo"
def __init__(self, **kwargs):
"""Init options from Django settings"""
@@ -33,11 +33,11 @@ class EmailBackend(AnymailRequestsBackend):
super().__init__(api_url, **kwargs)
def build_message_payload(self, message, defaults):
return SendinBluePayload(message, defaults, self)
return BrevoPayload(message, defaults, self)
def parse_recipient_status(self, response, payload, message):
# SendinBlue doesn't give any detail on a success
# https://developers.sendinblue.com/docs/responses
# Brevo doesn't give any detail on a success, other than messageId
# https://developers.brevo.com/reference/sendtransacemail
message_id = None
message_ids = []
@@ -51,7 +51,7 @@ class EmailBackend(AnymailRequestsBackend):
message_ids = parsed_response["messageIds"]
except (KeyError, TypeError) as err:
raise AnymailRequestsAPIError(
"Invalid SendinBlue API response format",
"Invalid Brevo API response format",
email_message=message,
payload=payload,
response=response,
@@ -70,7 +70,7 @@ class EmailBackend(AnymailRequestsBackend):
return recipient_status
class SendinBluePayload(RequestsPayload):
class BrevoPayload(RequestsPayload):
def __init__(self, message, defaults, backend, *args, **kwargs):
self.all_recipients = [] # used for backend.parse_recipient_status
self.to_recipients = [] # used for backend.parse_recipient_status
@@ -124,7 +124,7 @@ class SendinBluePayload(RequestsPayload):
@staticmethod
def email_object(email):
"""Converts EmailAddress to SendinBlue API array"""
"""Converts EmailAddress to Brevo API array"""
email_object = dict()
email_object["email"] = email.addr_spec
if email.display_name:
@@ -147,14 +147,14 @@ class SendinBluePayload(RequestsPayload):
self.data["subject"] = subject
def set_reply_to(self, emails):
# SendinBlue only supports a single address in the reply_to API param.
# Brevo only supports a single address in the reply_to API param.
if len(emails) > 1:
self.unsupported_feature("multiple reply_to addresses")
if len(emails) > 0:
self.data["replyTo"] = self.email_object(emails[0])
def set_extra_headers(self, headers):
# SendinBlue requires header values to be strings (not integers) as of 11/2022.
# Brevo requires header values to be strings (not integers) as of 11/2022.
# Stringify ints and floats; anything else is the caller's responsibility.
self.data["headers"].update(
{
@@ -182,7 +182,7 @@ class SendinBluePayload(RequestsPayload):
self.data["htmlContent"] = body
def add_attachment(self, attachment):
"""Converts attachments to SendinBlue API {name, base64} array"""
"""Converts attachments to Brevo API {name, base64} array"""
att = {
"name": attachment.name or "",
"content": attachment.b64content,
@@ -204,7 +204,7 @@ class SendinBluePayload(RequestsPayload):
self.data["params"] = merge_global_data
def set_metadata(self, metadata):
# SendinBlue expects a single string payload
# Brevo expects a single string payload
self.data["headers"]["X-Mailin-custom"] = self.serialize_json(metadata)
self.metadata = metadata # needed in serialize_data for batch send

View File

@@ -4,6 +4,7 @@ from .webhooks.amazon_ses import (
AmazonSESInboundWebhookView,
AmazonSESTrackingWebhookView,
)
from .webhooks.brevo import BrevoInboundWebhookView, BrevoTrackingWebhookView
from .webhooks.mailersend import (
MailerSendInboundWebhookView,
MailerSendTrackingWebhookView,
@@ -15,10 +16,6 @@ from .webhooks.postal import PostalInboundWebhookView, PostalTrackingWebhookView
from .webhooks.postmark import PostmarkInboundWebhookView, PostmarkTrackingWebhookView
from .webhooks.resend import ResendTrackingWebhookView
from .webhooks.sendgrid import SendGridInboundWebhookView, SendGridTrackingWebhookView
from .webhooks.sendinblue import (
SendinBlueInboundWebhookView,
SendinBlueTrackingWebhookView,
)
from .webhooks.sparkpost import (
SparkPostInboundWebhookView,
SparkPostTrackingWebhookView,
@@ -32,6 +29,11 @@ urlpatterns = [
AmazonSESInboundWebhookView.as_view(),
name="amazon_ses_inbound_webhook",
),
path(
"brevo/inbound/",
BrevoInboundWebhookView.as_view(),
name="brevo_inbound_webhook",
),
path(
"mailersend/inbound/",
MailerSendInboundWebhookView.as_view(),
@@ -66,11 +68,6 @@ urlpatterns = [
SendGridInboundWebhookView.as_view(),
name="sendgrid_inbound_webhook",
),
path(
"sendinblue/inbound/",
SendinBlueInboundWebhookView.as_view(),
name="sendinblue_inbound_webhook",
),
path(
"sparkpost/inbound/",
SparkPostInboundWebhookView.as_view(),
@@ -81,6 +78,11 @@ urlpatterns = [
AmazonSESTrackingWebhookView.as_view(),
name="amazon_ses_tracking_webhook",
),
path(
"brevo/tracking/",
BrevoTrackingWebhookView.as_view(),
name="brevo_tracking_webhook",
),
path(
"mailersend/tracking/",
MailerSendTrackingWebhookView.as_view(),
@@ -116,11 +118,6 @@ urlpatterns = [
SendGridTrackingWebhookView.as_view(),
name="sendgrid_tracking_webhook",
),
path(
"sendinblue/tracking/",
SendinBlueTrackingWebhookView.as_view(),
name="sendinblue_tracking_webhook",
),
path(
"sparkpost/tracking/",
SparkPostTrackingWebhookView.as_view(),

View File

@@ -19,12 +19,14 @@ from ..utils import get_anymail_setting
from .base import AnymailBaseWebhookView
class SendinBlueBaseWebhookView(AnymailBaseWebhookView):
esp_name = "SendinBlue"
class BrevoBaseWebhookView(AnymailBaseWebhookView):
esp_name = "Brevo"
class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
"""Handler for SendinBlue delivery and engagement tracking webhooks"""
class BrevoTrackingWebhookView(BrevoBaseWebhookView):
"""Handler for Brevo delivery and engagement tracking webhooks"""
# https://developers.brevo.com/docs/transactional-webhooks
signal = tracking
@@ -33,15 +35,13 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
if "items" in esp_event:
# This is an inbound webhook post
raise AnymailConfigurationError(
"You seem to have set SendinBlue's *inbound* webhook URL "
"to Anymail's SendinBlue *tracking* webhook URL."
f"You seem to have set Brevo's *inbound* webhook URL "
f"to Anymail's {self.esp_name} *tracking* webhook URL."
)
return [self.esp_to_anymail_event(esp_event)]
# SendinBlue's webhook payload data doesn't seem to be documented anywhere.
# There's a list of webhook events at https://apidocs.sendinblue.com/webhooks/#3.
event_types = {
# Map SendinBlue event type: Anymail normalized (event type, reject reason)
# Map Brevo event type: Anymail normalized (event type, reject reason)
# received even if message won't be sent (e.g., before "blocked"):
"request": (EventType.QUEUED, None),
"delivered": (EventType.DELIVERED, None),
@@ -67,7 +67,7 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
recipient = esp_event.get("email")
try:
# SendinBlue supplies "ts", "ts_event" and "date" fields, which seem to be
# Brevo supplies "ts", "ts_event" and "date" fields, which seem to be
# based on the timezone set in the account preferences (and possibly with
# inconsistent DST adjustment). "ts_epoch" is the only field that seems to
# be consistently UTC; it's in milliseconds
@@ -98,7 +98,7 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
return AnymailTrackingEvent(
description=None,
esp_event=esp_event,
# SendinBlue doesn't provide a unique event id:
# Brevo doesn't provide a unique event id:
event_id=None,
event_type=event_type,
message_id=esp_event.get("message-id"),
@@ -113,8 +113,10 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
)
class SendinBlueInboundWebhookView(SendinBlueBaseWebhookView):
"""Handler for SendinBlue inbound email webhooks"""
class BrevoInboundWebhookView(BrevoBaseWebhookView):
"""Handler for Brevo inbound email webhooks"""
# https://developers.brevo.com/docs/inbound-parse-webhooks#parsed-email-payload
signal = inbound
@@ -141,10 +143,10 @@ class SendinBlueInboundWebhookView(SendinBlueBaseWebhookView):
try:
esp_events = payload["items"]
except KeyError:
# This is not n inbound webhook post
# This is not an inbound webhook post
raise AnymailConfigurationError(
"You seem to have set SendinBlue's *tracking* webhook URL "
"to Anymail's SendinBlue *inbound* webhook URL."
f"You seem to have set Brevo's *tracking* webhook URL "
f"to Anymail's {self.esp_name} *inbound* webhook URL."
)
else:
return [self.esp_to_anymail_event(esp_event) for esp_event in esp_events]
@@ -199,7 +201,7 @@ class SendinBlueInboundWebhookView(SendinBlueBaseWebhookView):
)
def _fetch_attachment(self, attachment):
# Download attachment content from SendinBlue API.
# Download attachment content from Brevo API.
# FUTURE: somehow defer download until attachment is accessed?
token = attachment["DownloadToken"]
url = urljoin(self.api_url, f"inbound/attachments/{quote(token, safe='')}")

View File

@@ -4,14 +4,24 @@
Brevo
=====
.. Docs note: esps/sendinblue is redirected to esps/brevo in ReadTheDocs config.
Please preserve existing _sendinblue-* ref labels, so that redirected link
anchors work properly (in old links from external sites). E.g.:
an old link: https://anymail.dev/en/stable/esps/sendinblue#sendinblue-templates
redirects to: https://anymail.dev/en/stable/esps/brevo#sendinblue-templates
which is also: https://anymail.dev/en/stable/esps/brevo#brevo-templates
(There's no need to create _sendinblue-* duplicates of any new _brevo-* labels.)
Anymail integrates with the `Brevo`_ email service (formerly Sendinblue), using their `API v3`_.
Brevo's transactional API does not support some basic email features, such as
inline images. Be sure to review the :ref:`limitations <sendinblue-limitations>` below.
inline images. Be sure to review the :ref:`limitations <brevo-limitations>` below.
.. versionchanged:: 10.1
.. versionchanged:: 10.3
Brevo was called "Sendinblue" until May, 2023. To avoid unnecessary code changes,
Anymail still uses the old name in code (settings, backend, webhook urls, etc.).
SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 uses the new
name throughout its code; earlier versions used the old name. Code that
refers to "SendinBlue" should continue to work, but is now deprecated.
See :ref:`brevo-rename` for details.
.. important::
@@ -36,14 +46,14 @@ To use Anymail's Brevo backend, set:
.. code-block:: python
EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend"
EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend"
in your settings.py.
.. setting:: ANYMAIL_SENDINBLUE_API_KEY
.. setting:: ANYMAIL_BREVO_API_KEY
.. rubric:: SENDINBLUE_API_KEY
.. rubric:: BREVO_API_KEY
The API key can be retrieved from your Brevo `SMTP & API settings`_ on the
"API Keys" tab (don't try to use an SMTP key). Required.
@@ -55,23 +65,23 @@ Anymail. If you don't see a v3 key listed, use "Create a New API Key".)
ANYMAIL = {
...
"SENDINBLUE_API_KEY": "<your v3 API key>",
"BREVO_API_KEY": "<your v3 API key>",
}
Anymail will also look for ``SENDINBLUE_API_KEY`` at the
root of the settings file if neither ``ANYMAIL["SENDINBLUE_API_KEY"]``
nor ``ANYMAIL_SENDINBLUE_API_KEY`` is set.
Anymail will also look for ``BREVO_API_KEY`` at the
root of the settings file if neither ``ANYMAIL["BREVO_API_KEY"]``
nor ``ANYMAIL_BREVO_API_KEY`` is set.
.. _SMTP & API settings: https://app.brevo.com/settings/keys/api
.. setting:: ANYMAIL_SENDINBLUE_API_URL
.. setting:: ANYMAIL_BREVO_API_URL
.. rubric:: SENDINBLUE_API_URL
.. rubric:: BREVO_API_URL
The base url for calling the Brevo API.
The default is ``SENDINBLUE_API_URL = "https://api.brevo.com/v3/"``
The default is ``BREVO_API_URL = "https://api.brevo.com/v3/"``
(It's unlikely you would need to change this.)
.. versionchanged:: 10.1
@@ -79,6 +89,7 @@ The default is ``SENDINBLUE_API_URL = "https://api.brevo.com/v3/"``
Earlier Anymail releases used ``https://api.sendinblue.com/v3/``.
.. _brevo-esp-extra:
.. _sendinblue-esp-extra:
esp_extra support
@@ -106,6 +117,7 @@ to apply it to all messages.)
.. _smtp/email API: https://developers.brevo.com/reference/sendtransacemail
.. _brevo-limitations:
.. _sendinblue-limitations:
Limitations and quirks
@@ -192,6 +204,7 @@ Brevo can handle.
on individual messages.
.. _brevo-templates:
.. _sendinblue-templates:
Batch sending/merge and ESP templates
@@ -267,9 +280,9 @@ message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``.
.. caution::
**Sendinblue "old template language" not supported**
**"Old template language" not supported**
Sendinblue once supported two different template styles: a "new" template
Brevo once supported two different template styles: a "new" template
language that uses Django-like template syntax (with ``{{ param.NAME }}``
substitutions), and an "old" template language that used percent-delimited
``%NAME%`` substitutions.
@@ -299,6 +312,7 @@ message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``.
https://help.brevo.com/hc/en-us/articles/360000991960
.. _brevo-webhooks:
.. _sendinblue-webhooks:
Status tracking webhooks
@@ -309,7 +323,7 @@ the url at Brevo's site under `Transactional > Email > Settings > Webhook`_.
The "URL to call" is:
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendinblue/tracking/`
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/brevo/tracking/`
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
* *yoursite.example.com* is your Django site
@@ -336,10 +350,17 @@ For example, it's not uncommon to receive a "delivered" event before the corresp
The event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event` field will be
a `dict` of raw webhook data received from Brevo.
.. versionchanged:: 10.3
Older Anymail versions used a tracking webhook URL containing "sendinblue" rather
than "brevo". The old URL will still work, but is deprecated. See :ref:`brevo-rename`
below.
.. _Transactional > Email > Settings > Webhook: https://app-smtp.brevo.com/webhook
.. _brevo-inbound:
.. _sendinblue-inbound:
Inbound webhook
@@ -353,7 +374,7 @@ guide to enable inbound service and add Anymail's inbound webhook.
At the "Creating the webhook" step, set the ``"url"`` param to:
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendinblue/inbound/`
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/brevo/inbound/`
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
* *yoursite.example.com* is your Django site
@@ -364,6 +385,12 @@ by entering your API key in "Header" field above the example, and then clicking
"Try It!". The `webhooks management APIs`_ and `inbound events list API`_ can
be helpful for diagnosing inbound issues.
.. versionchanged:: 10.3
Older Anymail versions used an inbound webhook URL containing "sendinblue" rather
than "brevo". The old URL will still work, but is deprecated. See :ref:`brevo-rename`
below.
.. _Inbound parsing webhooks:
https://developers.brevo.com/docs/inbound-parse-webhooks
@@ -371,3 +398,101 @@ be helpful for diagnosing inbound issues.
https://developers.brevo.com/reference/getwebhooks-1
.. _inbound events list API:
https://developers.brevo.com/reference/getinboundemailevents
.. _brevo-rename:
Updating code from SendinBlue to Brevo
--------------------------------------
SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 has switched
to the new name.
If your code refers to the old "sendinblue" name
(in :setting:`!EMAIL_BACKEND` and :setting:`!ANYMAIL` settings, :attr:`!esp_name`
checks, or elsewhere) you should update it to use "brevo" instead.
If you are using Anymail's tracking or inbound webhooks, you should
also update the webhook URLs you've configured at Brevo.
For compatibility, code and URLs using the old name are still functional in Anymail.
But they will generate deprecation warnings, and may be removed in a future release.
To update your code:
.. setting:: ANYMAIL_SENDINBLUE_API_KEY
.. setting:: ANYMAIL_SENDINBLUE_API_URL
1. In your settings.py, update the :setting:`!EMAIL_BACKEND`
and rename any ``"SENDINBLUE_..."`` settings to ``"BREVO_..."``:
.. code-block:: diff
- EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend" # old
+ EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend" # new
ANYMAIL = {
...
- "SENDINBLUE_API_KEY": "<your v3 API key>", # old
+ "BREVO_API_KEY": "<your v3 API key>", # new
# (Also change "SENDINBLUE_API_URL" to "BREVO_API_URL" if present)
# If you are using Brevo-specific global send defaults, change:
- "SENDINBLUE_SEND_DEFAULTS" = {...}, # old
+ "BREVO_SEND_DEFAULTS" = {...}, # new
}
2. If you are using Anymail's status tracking webhook,
go to Brevo's dashboard (under `Transactional > Email > Settings > Webhook`_),
and change the end or the URL from ``.../anymail/sendinblue/tracking/``
to ``.../anymail/brevo/tracking/``. (Or use the code below to automate this.)
In your :ref:`tracking signal receiver function <signal-receivers>`,
if you are examining the ``esp_name`` parameter, the name will change
once you have updated the webhook URL. If you had been checking
whether ``esp_name == "SendinBlue"``, change that to check if
``esp_name == "Brevo"``.
3. If you are using Anymail's inbound handling, update the inbound webhook
URL to change ``.../anymail/sendinblue/inbound/`` to ``.../anymail/brevo/inbound/``.
You will need to use Brevo's webhooks API to make the change---see below.
In your :ref:`inbound signal receiver function <inbound-signal-receivers>`,
if you are examining the ``esp_name`` parameter, the name will change
once you have updated the webhook URL. If you had been checking
whether ``esp_name == "SendinBlue"``, change that to check if
``esp_name == "Brevo"``.
That should be everything, but to double check you may want to search your
code for any remaining references to "sendinblue" (case-insensitive).
(E.g., ``grep -r -i sendinblue``.)
To update both the tracking and inbound webhook URLs using Brevo's `webhooks API`_,
you could run something like this Python code:
.. code-block:: python
# Update Brevo webhook URLs to replace "anymail/sendinblue" with "anymail/brevo".
import requests
BREVO_API_KEY = "<your API key>"
headers = {
"accept": "application/json",
"api-key": BREVO_API_KEY,
}
response = requests.get("https://api.brevo.com/v3/webhooks", headers=headers)
response.raise_for_status()
webhooks = response.json()
for webhook in webhooks:
if "anymail/sendinblue" in webhook["url"]:
response = requests.put(
f"https://api.brevo.com/v3/webhooks/{webhook['id']}",
headers=headers,
json={
"url": webhook["url"].replace("anymail/sendinblue", "anymail/brevo")
}
)
response.raise_for_status()
.. _webhooks API: https://developers.brevo.com/reference/updatewebhook-1

View File

@@ -66,6 +66,7 @@ dependencies = [
# (For simplicity, requests is included in the base dependencies.)
# (Do not use underscores in extra names: they get normalized to hyphens.)
amazon-ses = ["boto3"]
brevo = []
mailersend = []
mailgun = []
mailjet = []

View File

@@ -32,19 +32,16 @@ from .utils import (
)
@tag("sendinblue")
@tag("brevo")
@override_settings(
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend",
ANYMAIL={"SENDINBLUE_API_KEY": "test_api_key"},
EMAIL_BACKEND="anymail.backends.brevo.EmailBackend",
ANYMAIL={"BREVO_API_KEY": "test_api_key"},
)
class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase):
# SendinBlue v3 success responses are empty
class BrevoBackendMockAPITestCase(RequestsBackendMockAPITestCase):
DEFAULT_RAW_RESPONSE = (
b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}'
)
DEFAULT_STATUS_CODE = (
201 # SendinBlue v3 uses '201 Created' for success (in most cases)
)
DEFAULT_STATUS_CODE = 201 # Brevo v3 uses '201 Created' for success (in most cases)
def setUp(self):
super().setUp()
@@ -54,8 +51,8 @@ class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase):
)
@tag("sendinblue")
class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
@tag("brevo")
class BrevoBackendStandardEmailTests(BrevoBackendMockAPITestCase):
"""Test backend support for Django standard email features"""
def test_send_mail(self):
@@ -204,7 +201,7 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
)
def test_multiple_reply_to(self):
# SendinBlue v3 only allows a single reply address
# Brevo v3 only allows a single reply address
self.message.reply_to = [
'"Reply recipient" <reply@example.com',
"reply2@example.com",
@@ -274,7 +271,7 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
)
def test_embedded_images(self):
# SendinBlue doesn't support inline image
# Brevo doesn't support inline image
# inline image are just added as a content attachment
image_filename = SAMPLE_IMAGE_FILENAME
@@ -339,7 +336,7 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
def test_api_failure(self):
self.set_mock_response(status_code=400)
with self.assertRaisesMessage(AnymailAPIError, "SendinBlue API response 400"):
with self.assertRaisesMessage(AnymailAPIError, "Brevo API response 400"):
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
# Make sure fail_silently is respected
@@ -373,12 +370,12 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
self.message.send()
@tag("sendinblue")
class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
@tag("brevo")
class BrevoBackendAnymailFeatureTests(BrevoBackendMockAPITestCase):
"""Test backend support for Anymail added features"""
def test_envelope_sender(self):
# SendinBlue does not have a way to change envelope sender.
# Brevo does not have a way to change envelope sender.
self.message.envelope_sender = "anything@bounces.example.com"
with self.assertRaisesMessage(AnymailUnsupportedFeature, "envelope_sender"):
self.message.send()
@@ -459,7 +456,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
self.message.send()
def test_template_id(self):
# subject, body, and from_email must be None for SendinBlue template send:
# subject, body, and from_email must be None for Brevo template send:
message = mail.EmailMessage(
subject="My Subject",
body=None,
@@ -470,7 +467,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
bcc=["Recipient <bcc@example.com>"],
reply_to=["Recipient <reply@example.com>"],
)
# SendinBlue uses per-account numeric ID to identify templates:
# Brevo uses per-account numeric ID to identify templates:
message.template_id = 12
message.send()
data = self.get_api_call_json()
@@ -603,7 +600,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
# noinspection PyUnresolvedReferences
def test_send_attaches_anymail_status(self):
"""The anymail_status should be attached to the message when it is sent"""
# the DEFAULT_RAW_RESPONSE above is the *only* success response SendinBlue
# the DEFAULT_RAW_RESPONSE above is the *only* success response Brevo
# returns, so no need to override it here
msg = mail.EmailMessage(
"Subject",
@@ -652,39 +649,37 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
err = cm.exception
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
# our added context:
self.assertIn("Don't know how to send this data to SendinBlue", str(err))
self.assertIn("Don't know how to send this data to Brevo", str(err))
# original message
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
@tag("sendinblue")
class SendinBlueBackendRecipientsRefusedTests(SendinBlueBackendMockAPITestCase):
@tag("brevo")
class BrevoBackendRecipientsRefusedTests(BrevoBackendMockAPITestCase):
"""
Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid
"""
# SendinBlue doesn't check email bounce or complaint lists at time of send --
# Brevo doesn't check email bounce or complaint lists at time of send --
# it always just queues the message. You'll need to listen for the "rejected"
# and "failed" events to detect refused recipients.
pass # not applicable to this backend
@tag("sendinblue")
class SendinBlueBackendSessionSharingTestCase(
SessionSharingTestCases, SendinBlueBackendMockAPITestCase
@tag("brevo")
class BrevoBackendSessionSharingTestCase(
SessionSharingTestCases, BrevoBackendMockAPITestCase
):
"""Requests session sharing tests"""
pass # tests are defined in SessionSharingTestCases
@tag("sendinblue")
@override_settings(EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend")
class SendinBlueBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
@tag("brevo")
@override_settings(EMAIL_BACKEND="anymail.backends.brevo.EmailBackend")
class BrevoBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_auth(self):
with self.assertRaisesRegex(
AnymailConfigurationError, r"\bSENDINBLUE_API_KEY\b"
):
with self.assertRaisesRegex(AnymailConfigurationError, r"\bBREVO_API_KEY\b"):
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])

View File

@@ -7,15 +7,15 @@ from responses.matchers import header_matcher
from anymail.exceptions import AnymailConfigurationError
from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.sendinblue import SendinBlueInboundWebhookView
from anymail.webhooks.brevo import BrevoInboundWebhookView
from .utils import sample_email_content, sample_image_content
from .webhook_cases import WebhookTestCase
@tag("sendinblue")
@override_settings(ANYMAIL_SENDINBLUE_API_KEY="test-api-key")
class SendinBlueInboundTestCase(WebhookTestCase):
@tag("brevo")
@override_settings(ANYMAIL_BREVO_API_KEY="test-api-key")
class BrevoInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
# Actual (sanitized) Brevo inbound message payload 7/2023
raw_event = {
@@ -54,16 +54,16 @@ class SendinBlueInboundTestCase(WebhookTestCase):
}
response = self.client.post(
"/anymail/sendinblue/inbound/",
"/anymail/brevo/inbound/",
content_type="application/json",
data={"items": [raw_event]},
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendinBlueInboundWebhookView,
sender=BrevoInboundWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
# AnymailInboundEvent
event = kwargs["event"]
@@ -123,15 +123,15 @@ class SendinBlueInboundTestCase(WebhookTestCase):
}
}
self.client.post(
"/anymail/sendinblue/inbound/",
"/anymail/brevo/inbound/",
content_type="application/json",
data={"items": [raw_event]},
)
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendinBlueInboundWebhookView,
sender=BrevoInboundWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
message = event.message
@@ -203,16 +203,16 @@ class SendinBlueInboundTestCase(WebhookTestCase):
)
response = self.client.post(
"/anymail/sendinblue/inbound/",
"/anymail/brevo/inbound/",
content_type="application/json",
data={"items": [raw_event]},
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendinBlueInboundWebhookView,
sender=BrevoInboundWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
message = event.message
@@ -235,12 +235,12 @@ class SendinBlueInboundTestCase(WebhookTestCase):
def test_misconfigured_tracking(self):
errmsg = (
"You seem to have set SendinBlue's *tracking* webhook URL"
" to Anymail's SendinBlue *inbound* webhook URL."
"You seem to have set Brevo's *tracking* webhook URL"
" to Anymail's Brevo *inbound* webhook URL."
)
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post(
"/anymail/sendinblue/inbound/",
"/anymail/brevo/inbound/",
content_type="application/json",
data={"event": "delivered"},
)

View File

@@ -10,39 +10,39 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin
ANYMAIL_TEST_SENDINBLUE_API_KEY = os.getenv("ANYMAIL_TEST_SENDINBLUE_API_KEY")
ANYMAIL_TEST_SENDINBLUE_DOMAIN = os.getenv("ANYMAIL_TEST_SENDINBLUE_DOMAIN")
ANYMAIL_TEST_BREVO_API_KEY = os.getenv("ANYMAIL_TEST_BREVO_API_KEY")
ANYMAIL_TEST_BREVO_DOMAIN = os.getenv("ANYMAIL_TEST_BREVO_DOMAIN")
@tag("sendinblue", "live")
@tag("brevo", "live")
@unittest.skipUnless(
ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN,
"Set ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN "
"environment variables to run SendinBlue integration tests",
ANYMAIL_TEST_BREVO_API_KEY and ANYMAIL_TEST_BREVO_DOMAIN,
"Set ANYMAIL_TEST_BREVO_API_KEY and ANYMAIL_TEST_BREVO_DOMAIN "
"environment variables to run Brevo integration tests",
)
@override_settings(
ANYMAIL_SENDINBLUE_API_KEY=ANYMAIL_TEST_SENDINBLUE_API_KEY,
ANYMAIL_SENDINBLUE_SEND_DEFAULTS=dict(),
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend",
ANYMAIL_BREVO_API_KEY=ANYMAIL_TEST_BREVO_API_KEY,
ANYMAIL_BREVO_SEND_DEFAULTS=dict(),
EMAIL_BACKEND="anymail.backends.brevo.EmailBackend",
)
class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SendinBlue v3 API integration tests
class BrevoBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Brevo v3 API integration tests
SendinBlue doesn't have sandbox so these tests run
against the **live** SendinBlue API, using the
environment variable `ANYMAIL_TEST_SENDINBLUE_API_KEY` as the API key,
and `ANYMAIL_TEST_SENDINBLUE_DOMAIN` to construct sender addresses.
Brevo doesn't have sandbox so these tests run
against the **live** Brevo API, using the
environment variable `ANYMAIL_TEST_BREVO_API_KEY` as the API key,
and `ANYMAIL_TEST_BREVO_DOMAIN` to construct sender addresses.
If those variables are not set, these tests won't run.
https://developers.sendinblue.com/docs/faq#section-how-can-i-test-the-api-
https://developers.brevo.com/docs/faq#how-can-i-test-the-api
"""
def setUp(self):
super().setUp()
self.from_email = "from@%s" % ANYMAIL_TEST_SENDINBLUE_DOMAIN
self.from_email = "from@%s" % ANYMAIL_TEST_BREVO_DOMAIN
self.message = AnymailMessage(
"Anymail SendinBlue integration test",
"Anymail Brevo integration test",
"Text content",
self.from_email,
["test+to1@anymail.dev"],
@@ -50,7 +50,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the SendinBlue send status and message id from the message
# Example of getting the Brevo send status and message id from the message
sent_count = self.message.send()
self.assertEqual(sent_count, 1)
@@ -58,7 +58,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
self.assertEqual(sent_status, "queued") # SendinBlue always queues
self.assertEqual(sent_status, "queued") # Brevo always queues
# Message-ID can be ...@smtp-relay.mail.fr or .sendinblue.com:
self.assertRegex(message_id, r"\<.+@.+\>")
# set of all recipient statuses:
@@ -68,27 +68,27 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def test_all_options(self):
send_at = datetime.now() + timedelta(minutes=2)
message = AnymailMessage(
subject="Anymail SendinBlue all-options integration test",
subject="Anymail Brevo all-options integration test",
body="This is the text body",
from_email=formataddr(("Test From, with comma", self.from_email)),
to=["test+to1@anymail.dev", '"Recipient 2, OK?" <test+to2@anymail.dev>'],
cc=["test+cc1@anymail.dev", "Copy 2 <test+cc2@anymail.dev>"],
bcc=["test+bcc1@anymail.dev", "Blind Copy 2 <test+bcc2@anymail.dev>"],
# SendinBlue API v3 only supports single reply-to
# Brevo API v3 only supports single reply-to
reply_to=['"Reply, with comma" <reply@example.com>'],
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1", "tag 2"],
)
# SendinBlue requires an HTML body:
# Brevo requires an HTML body:
message.attach_alternative("<p>HTML content</p>", "text/html")
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
message.send()
# SendinBlue always queues:
# Brevo always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
self.assertRegex(message.anymail_status.message_id, r"\<.+@.+\>")
@@ -118,7 +118,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach("attachment1.txt", "Here is some\ntext", "text/plain")
message.send()
# SendinBlue always queues:
# Brevo always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "queued")
@@ -135,11 +135,11 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
recipient_status["test+to2@anymail.dev"].message_id,
)
@override_settings(ANYMAIL_SENDINBLUE_API_KEY="Hey, that's not an API key!")
@override_settings(ANYMAIL_BREVO_API_KEY="Hey, that's not an API key!")
def test_invalid_api_key(self):
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()
err = cm.exception
self.assertEqual(err.status_code, 401)
# Make sure the exception message includes SendinBlue's response:
# Make sure the exception message includes Brevo's response:
self.assertIn("Key not found", str(err))

View File

@@ -6,16 +6,16 @@ from django.test import tag
from anymail.exceptions import AnymailConfigurationError
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.sendinblue import SendinBlueTrackingWebhookView
from anymail.webhooks.brevo import BrevoTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag("sendinblue")
class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase):
@tag("brevo")
class BrevoWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps({}),
)
@@ -23,23 +23,22 @@ class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase):
# Actual tests are in WebhookBasicAuthTestCase
@tag("sendinblue")
class SendinBlueDeliveryTestCase(WebhookTestCase):
# SendinBlue's webhook payload data is partially documented at
# https://help.sendinblue.com/hc/en-us/articles/360007666479,
# but it's not completely up to date.
@tag("brevo")
class BrevoDeliveryTestCase(WebhookTestCase):
# Brevo's webhook payload data is documented at
# https://developers.brevo.com/docs/transactional-webhooks.
# The payloads below were obtained through live testing.
def test_sent_event(self):
raw_event = {
"event": "request",
"email": "recipient@example.com",
"id": 9999999, # this seems to be SendinBlue account id (not an event id)
"id": 9999999, # this seems to be Brevo account id (not an event id)
"message-id": "<201803062010.27287306012@smtp-relay.mailin.fr>",
"subject": "Test subject",
# From a message sent at 2018-03-06 11:10:23-08:00
# (2018-03-06 19:10:23+00:00)...
"date": "2018-03-06 11:10:23", # tz from SendinBlue account's preferences
"date": "2018-03-06 11:10:23", # tz from Brevo account's preferences
"ts": 1520331023, # 2018-03-06 10:10:23 -- what time zone is this?
"ts_event": 1520331023, # unclear if this ever differs from "ts"
"ts_epoch": 1520363423000, # 2018-03-06 19:10:23.000+00:00 -- UTC (msec)
@@ -55,16 +54,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"sending_ip": "333.33.33.33",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
@@ -77,7 +76,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
self.assertEqual(
event.message_id, "<201803062010.27287306012@smtp-relay.mailin.fr>"
)
# SendinBlue does not provide a unique event id:
# Brevo does not provide a unique event id:
self.assertIsNone(event.event_id)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.metadata, {"meta": "data"})
@@ -93,16 +92,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
@@ -128,16 +127,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"tag": "header-tag",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
@@ -158,16 +157,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"reason": "undefined Unable to find MX of domain no-mx.example.com",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
@@ -188,16 +187,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"reason": "blocked : due to blacklist user",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "rejected")
@@ -214,16 +213,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "complained")
@@ -231,7 +230,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
def test_invalid_email(self):
# "If a ISP again indicated us that the email is not valid or if we discovered
# that the email is not valid." (unclear whether this error originates with the
# receiving MTA or with SendinBlue pre-send) (haven't observed "invalid_email"
# receiving MTA or with Brevo pre-send) (haven't observed "invalid_email"
# event in actual testing; payload below is a guess)
raw_event = {
"event": "invalid_email",
@@ -241,16 +240,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"reason": "(guessing invalid_email includes a reason)",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
@@ -262,7 +261,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
def test_deferred_event(self):
# Note: the example below is an actual event capture (with 'example.com'
# substituted for the real receiving domain). It's pretty clearly a bounce, not
# a deferral. It looks like SendinBlue mis-categorizes this SMTP response code.
# a deferral. It looks like Brevo mis-categorizes this SMTP response code.
raw_event = {
"event": "deferred",
"email": "notauser@example.com",
@@ -272,16 +271,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
" address rejected: User unknown in virtual alias table",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "deferred")
@@ -294,7 +293,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
)
def test_opened_event(self):
# SendinBlue delivers 'unique_opened' only on the first open, and 'opened'
# Brevo delivers 'unique_opened' only on the first open, and 'opened'
# only on the second or later tracking pixel views. (But they used to deliver
# both on the first open.)
raw_event = {
@@ -304,20 +303,20 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
self.assertIsNone(event.user_agent) # SendinBlue doesn't report user agent
self.assertIsNone(event.user_agent) # Brevo doesn't report user agent
def test_unique_opened_event(self):
# See note in test_opened_event above
@@ -328,16 +327,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
@@ -351,21 +350,21 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"link": "https://example.com/click/me",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.click_url, "https://example.com/click/me")
self.assertIsNone(event.user_agent) # SendinBlue doesn't report user agent
self.assertIsNone(event.user_agent) # Brevo doesn't report user agent
def test_unsubscribe(self):
# "When a person unsubscribes from the email received."
@@ -378,28 +377,28 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
sender=BrevoTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
esp_name="Brevo",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "unsubscribed")
def test_misconfigured_inbound(self):
errmsg = (
"You seem to have set SendinBlue's *inbound* webhook URL"
" to Anymail's SendinBlue *tracking* webhook URL."
"You seem to have set Brevo's *inbound* webhook URL"
" to Anymail's Brevo *tracking* webhook URL."
)
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post(
"/anymail/sendinblue/tracking/",
"/anymail/brevo/tracking/",
content_type="application/json",
data={"items": []},
)

View File

@@ -59,6 +59,7 @@ setenv =
# (resend should work with or without its extras, so it isn't in `none`)
none: ANYMAIL_SKIP_TESTS=amazon_ses,postal
amazon_ses: ANYMAIL_ONLY_TEST=amazon_ses
brevo: ANYMAIL_ONLY_TEST=brevo
mailersend: ANYMAIL_ONLY_TEST=mailersend
mailgun: ANYMAIL_ONLY_TEST=mailgun
mailjet: ANYMAIL_ONLY_TEST=mailjet
@@ -68,7 +69,6 @@ setenv =
resend: ANYMAIL_ONLY_TEST=resend
sendgrid: ANYMAIL_ONLY_TEST=sendgrid
unisender_go: ANYMAIL_ONLY_TEST=unisender_go
sendinblue: ANYMAIL_ONLY_TEST=sendinblue
sparkpost: ANYMAIL_ONLY_TEST=sparkpost
ignore_outcome =
# CI that wants to handle errors itself can set TOX_OVERRIDE_IGNORE_OUTCOME=false