mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
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:
6
.github/workflows/integration-test.yml
vendored
6
.github/workflows/integration-test.yml
vendored
@@ -40,6 +40,7 @@ jobs:
|
|||||||
# combination, to avoid rapidly consuming the testing accounts' entire send allotments.
|
# combination, to avoid rapidly consuming the testing accounts' entire send allotments.
|
||||||
config:
|
config:
|
||||||
- { tox: django41-py310-amazon_ses, python: "3.10" }
|
- { 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-mailersend, python: "3.10" }
|
||||||
- { tox: django41-py310-mailgun, python: "3.10" }
|
- { tox: django41-py310-mailgun, python: "3.10" }
|
||||||
- { tox: django41-py310-mailjet, 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-postmark, python: "3.10" }
|
||||||
- { tox: django41-py310-resend, python: "3.10" }
|
- { tox: django41-py310-resend, python: "3.10" }
|
||||||
- { tox: django41-py310-sendgrid, 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-sparkpost, python: "3.10" }
|
||||||
- { tox: django41-py310-unisender_go, 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_DOMAIN: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_DOMAIN }}
|
||||||
ANYMAIL_TEST_AMAZON_SES_REGION_NAME: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_REGION_NAME }}
|
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_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_API_TOKEN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_API_TOKEN }}
|
||||||
ANYMAIL_TEST_MAILERSEND_DOMAIN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_DOMAIN }}
|
ANYMAIL_TEST_MAILERSEND_DOMAIN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_DOMAIN }}
|
||||||
ANYMAIL_TEST_MAILGUN_API_KEY: ${{ secrets.ANYMAIL_TEST_MAILGUN_API_KEY }}
|
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_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDGRID_API_KEY }}
|
||||||
ANYMAIL_TEST_SENDGRID_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDGRID_DOMAIN }}
|
ANYMAIL_TEST_SENDGRID_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDGRID_DOMAIN }}
|
||||||
ANYMAIL_TEST_SENDGRID_TEMPLATE_ID: ${{ secrets.ANYMAIL_TEST_SENDGRID_TEMPLATE_ID }}
|
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_API_KEY: ${{ secrets.ANYMAIL_TEST_SPARKPOST_API_KEY }}
|
||||||
ANYMAIL_TEST_SPARKPOST_DOMAIN: ${{ secrets.ANYMAIL_TEST_SPARKPOST_DOMAIN }}
|
ANYMAIL_TEST_SPARKPOST_DOMAIN: ${{ secrets.ANYMAIL_TEST_SPARKPOST_DOMAIN }}
|
||||||
ANYMAIL_TEST_UNISENDER_GO_API_KEY: ${{ secrets.ANYMAIL_TEST_UNISENDER_GO_API_KEY }}
|
ANYMAIL_TEST_UNISENDER_GO_API_KEY: ${{ secrets.ANYMAIL_TEST_UNISENDER_GO_API_KEY }}
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ vNext
|
|||||||
|
|
||||||
*unreleased changes*
|
*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
|
Features
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from .base_requests import AnymailRequestsBackend, RequestsPayload
|
|||||||
|
|
||||||
class EmailBackend(AnymailRequestsBackend):
|
class EmailBackend(AnymailRequestsBackend):
|
||||||
"""
|
"""
|
||||||
SendinBlue v3 API Email Backend
|
Brevo v3 API Email Backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
esp_name = "SendinBlue"
|
esp_name = "Brevo"
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Init options from Django settings"""
|
"""Init options from Django settings"""
|
||||||
@@ -33,11 +33,11 @@ class EmailBackend(AnymailRequestsBackend):
|
|||||||
super().__init__(api_url, **kwargs)
|
super().__init__(api_url, **kwargs)
|
||||||
|
|
||||||
def build_message_payload(self, message, defaults):
|
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):
|
def parse_recipient_status(self, response, payload, message):
|
||||||
# SendinBlue doesn't give any detail on a success
|
# Brevo doesn't give any detail on a success, other than messageId
|
||||||
# https://developers.sendinblue.com/docs/responses
|
# https://developers.brevo.com/reference/sendtransacemail
|
||||||
message_id = None
|
message_id = None
|
||||||
message_ids = []
|
message_ids = []
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class EmailBackend(AnymailRequestsBackend):
|
|||||||
message_ids = parsed_response["messageIds"]
|
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 Brevo API response format",
|
||||||
email_message=message,
|
email_message=message,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
response=response,
|
response=response,
|
||||||
@@ -70,7 +70,7 @@ class EmailBackend(AnymailRequestsBackend):
|
|||||||
return recipient_status
|
return recipient_status
|
||||||
|
|
||||||
|
|
||||||
class SendinBluePayload(RequestsPayload):
|
class BrevoPayload(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
|
self.to_recipients = [] # used for backend.parse_recipient_status
|
||||||
@@ -124,7 +124,7 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def email_object(email):
|
def email_object(email):
|
||||||
"""Converts EmailAddress to SendinBlue API array"""
|
"""Converts EmailAddress to Brevo API array"""
|
||||||
email_object = dict()
|
email_object = dict()
|
||||||
email_object["email"] = email.addr_spec
|
email_object["email"] = email.addr_spec
|
||||||
if email.display_name:
|
if email.display_name:
|
||||||
@@ -147,14 +147,14 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
self.data["subject"] = subject
|
self.data["subject"] = subject
|
||||||
|
|
||||||
def set_reply_to(self, emails):
|
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:
|
if len(emails) > 1:
|
||||||
self.unsupported_feature("multiple reply_to addresses")
|
self.unsupported_feature("multiple reply_to addresses")
|
||||||
if len(emails) > 0:
|
if len(emails) > 0:
|
||||||
self.data["replyTo"] = self.email_object(emails[0])
|
self.data["replyTo"] = self.email_object(emails[0])
|
||||||
|
|
||||||
def set_extra_headers(self, headers):
|
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.
|
# Stringify ints and floats; anything else is the caller's responsibility.
|
||||||
self.data["headers"].update(
|
self.data["headers"].update(
|
||||||
{
|
{
|
||||||
@@ -182,7 +182,7 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
self.data["htmlContent"] = body
|
self.data["htmlContent"] = body
|
||||||
|
|
||||||
def add_attachment(self, attachment):
|
def add_attachment(self, attachment):
|
||||||
"""Converts attachments to SendinBlue API {name, base64} array"""
|
"""Converts attachments to Brevo API {name, base64} array"""
|
||||||
att = {
|
att = {
|
||||||
"name": attachment.name or "",
|
"name": attachment.name or "",
|
||||||
"content": attachment.b64content,
|
"content": attachment.b64content,
|
||||||
@@ -204,7 +204,7 @@ class SendinBluePayload(RequestsPayload):
|
|||||||
self.data["params"] = merge_global_data
|
self.data["params"] = merge_global_data
|
||||||
|
|
||||||
def set_metadata(self, metadata):
|
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.data["headers"]["X-Mailin-custom"] = self.serialize_json(metadata)
|
||||||
self.metadata = metadata # needed in serialize_data for batch send
|
self.metadata = metadata # needed in serialize_data for batch send
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ from .webhooks.amazon_ses import (
|
|||||||
AmazonSESInboundWebhookView,
|
AmazonSESInboundWebhookView,
|
||||||
AmazonSESTrackingWebhookView,
|
AmazonSESTrackingWebhookView,
|
||||||
)
|
)
|
||||||
|
from .webhooks.brevo import BrevoInboundWebhookView, BrevoTrackingWebhookView
|
||||||
from .webhooks.mailersend import (
|
from .webhooks.mailersend import (
|
||||||
MailerSendInboundWebhookView,
|
MailerSendInboundWebhookView,
|
||||||
MailerSendTrackingWebhookView,
|
MailerSendTrackingWebhookView,
|
||||||
@@ -15,10 +16,6 @@ from .webhooks.postal import PostalInboundWebhookView, PostalTrackingWebhookView
|
|||||||
from .webhooks.postmark import PostmarkInboundWebhookView, PostmarkTrackingWebhookView
|
from .webhooks.postmark import PostmarkInboundWebhookView, PostmarkTrackingWebhookView
|
||||||
from .webhooks.resend import ResendTrackingWebhookView
|
from .webhooks.resend import ResendTrackingWebhookView
|
||||||
from .webhooks.sendgrid import SendGridInboundWebhookView, SendGridTrackingWebhookView
|
from .webhooks.sendgrid import SendGridInboundWebhookView, SendGridTrackingWebhookView
|
||||||
from .webhooks.sendinblue import (
|
|
||||||
SendinBlueInboundWebhookView,
|
|
||||||
SendinBlueTrackingWebhookView,
|
|
||||||
)
|
|
||||||
from .webhooks.sparkpost import (
|
from .webhooks.sparkpost import (
|
||||||
SparkPostInboundWebhookView,
|
SparkPostInboundWebhookView,
|
||||||
SparkPostTrackingWebhookView,
|
SparkPostTrackingWebhookView,
|
||||||
@@ -32,6 +29,11 @@ urlpatterns = [
|
|||||||
AmazonSESInboundWebhookView.as_view(),
|
AmazonSESInboundWebhookView.as_view(),
|
||||||
name="amazon_ses_inbound_webhook",
|
name="amazon_ses_inbound_webhook",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"brevo/inbound/",
|
||||||
|
BrevoInboundWebhookView.as_view(),
|
||||||
|
name="brevo_inbound_webhook",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"mailersend/inbound/",
|
"mailersend/inbound/",
|
||||||
MailerSendInboundWebhookView.as_view(),
|
MailerSendInboundWebhookView.as_view(),
|
||||||
@@ -66,11 +68,6 @@ urlpatterns = [
|
|||||||
SendGridInboundWebhookView.as_view(),
|
SendGridInboundWebhookView.as_view(),
|
||||||
name="sendgrid_inbound_webhook",
|
name="sendgrid_inbound_webhook",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"sendinblue/inbound/",
|
|
||||||
SendinBlueInboundWebhookView.as_view(),
|
|
||||||
name="sendinblue_inbound_webhook",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"sparkpost/inbound/",
|
"sparkpost/inbound/",
|
||||||
SparkPostInboundWebhookView.as_view(),
|
SparkPostInboundWebhookView.as_view(),
|
||||||
@@ -81,6 +78,11 @@ urlpatterns = [
|
|||||||
AmazonSESTrackingWebhookView.as_view(),
|
AmazonSESTrackingWebhookView.as_view(),
|
||||||
name="amazon_ses_tracking_webhook",
|
name="amazon_ses_tracking_webhook",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"brevo/tracking/",
|
||||||
|
BrevoTrackingWebhookView.as_view(),
|
||||||
|
name="brevo_tracking_webhook",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"mailersend/tracking/",
|
"mailersend/tracking/",
|
||||||
MailerSendTrackingWebhookView.as_view(),
|
MailerSendTrackingWebhookView.as_view(),
|
||||||
@@ -116,11 +118,6 @@ urlpatterns = [
|
|||||||
SendGridTrackingWebhookView.as_view(),
|
SendGridTrackingWebhookView.as_view(),
|
||||||
name="sendgrid_tracking_webhook",
|
name="sendgrid_tracking_webhook",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"sendinblue/tracking/",
|
|
||||||
SendinBlueTrackingWebhookView.as_view(),
|
|
||||||
name="sendinblue_tracking_webhook",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"sparkpost/tracking/",
|
"sparkpost/tracking/",
|
||||||
SparkPostTrackingWebhookView.as_view(),
|
SparkPostTrackingWebhookView.as_view(),
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ from ..utils import get_anymail_setting
|
|||||||
from .base import AnymailBaseWebhookView
|
from .base import AnymailBaseWebhookView
|
||||||
|
|
||||||
|
|
||||||
class SendinBlueBaseWebhookView(AnymailBaseWebhookView):
|
class BrevoBaseWebhookView(AnymailBaseWebhookView):
|
||||||
esp_name = "SendinBlue"
|
esp_name = "Brevo"
|
||||||
|
|
||||||
|
|
||||||
class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
|
class BrevoTrackingWebhookView(BrevoBaseWebhookView):
|
||||||
"""Handler for SendinBlue delivery and engagement tracking webhooks"""
|
"""Handler for Brevo delivery and engagement tracking webhooks"""
|
||||||
|
|
||||||
|
# https://developers.brevo.com/docs/transactional-webhooks
|
||||||
|
|
||||||
signal = tracking
|
signal = tracking
|
||||||
|
|
||||||
@@ -33,15 +35,13 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
|
|||||||
if "items" in esp_event:
|
if "items" in esp_event:
|
||||||
# This is an inbound webhook post
|
# This is an inbound webhook post
|
||||||
raise AnymailConfigurationError(
|
raise AnymailConfigurationError(
|
||||||
"You seem to have set SendinBlue's *inbound* webhook URL "
|
f"You seem to have set Brevo's *inbound* webhook URL "
|
||||||
"to Anymail's SendinBlue *tracking* webhook URL."
|
f"to Anymail's {self.esp_name} *tracking* webhook URL."
|
||||||
)
|
)
|
||||||
return [self.esp_to_anymail_event(esp_event)]
|
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 = {
|
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"):
|
# received even if message won't be sent (e.g., before "blocked"):
|
||||||
"request": (EventType.QUEUED, None),
|
"request": (EventType.QUEUED, None),
|
||||||
"delivered": (EventType.DELIVERED, None),
|
"delivered": (EventType.DELIVERED, None),
|
||||||
@@ -67,7 +67,7 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
|
|||||||
recipient = esp_event.get("email")
|
recipient = esp_event.get("email")
|
||||||
|
|
||||||
try:
|
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
|
# based on the timezone set in the account preferences (and possibly with
|
||||||
# inconsistent DST adjustment). "ts_epoch" is the only field that seems to
|
# inconsistent DST adjustment). "ts_epoch" is the only field that seems to
|
||||||
# be consistently UTC; it's in milliseconds
|
# be consistently UTC; it's in milliseconds
|
||||||
@@ -98,7 +98,7 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
|
|||||||
return AnymailTrackingEvent(
|
return AnymailTrackingEvent(
|
||||||
description=None,
|
description=None,
|
||||||
esp_event=esp_event,
|
esp_event=esp_event,
|
||||||
# SendinBlue doesn't provide a unique event id:
|
# Brevo doesn't provide a unique event id:
|
||||||
event_id=None,
|
event_id=None,
|
||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
message_id=esp_event.get("message-id"),
|
message_id=esp_event.get("message-id"),
|
||||||
@@ -113,8 +113,10 @@ class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SendinBlueInboundWebhookView(SendinBlueBaseWebhookView):
|
class BrevoInboundWebhookView(BrevoBaseWebhookView):
|
||||||
"""Handler for SendinBlue inbound email webhooks"""
|
"""Handler for Brevo inbound email webhooks"""
|
||||||
|
|
||||||
|
# https://developers.brevo.com/docs/inbound-parse-webhooks#parsed-email-payload
|
||||||
|
|
||||||
signal = inbound
|
signal = inbound
|
||||||
|
|
||||||
@@ -141,10 +143,10 @@ class SendinBlueInboundWebhookView(SendinBlueBaseWebhookView):
|
|||||||
try:
|
try:
|
||||||
esp_events = payload["items"]
|
esp_events = payload["items"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# This is not n inbound webhook post
|
# This is not an inbound webhook post
|
||||||
raise AnymailConfigurationError(
|
raise AnymailConfigurationError(
|
||||||
"You seem to have set SendinBlue's *tracking* webhook URL "
|
f"You seem to have set Brevo's *tracking* webhook URL "
|
||||||
"to Anymail's SendinBlue *inbound* webhook URL."
|
f"to Anymail's {self.esp_name} *inbound* webhook URL."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return [self.esp_to_anymail_event(esp_event) for esp_event in esp_events]
|
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):
|
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?
|
# FUTURE: somehow defer download until attachment is accessed?
|
||||||
token = attachment["DownloadToken"]
|
token = attachment["DownloadToken"]
|
||||||
url = urljoin(self.api_url, f"inbound/attachments/{quote(token, safe='')}")
|
url = urljoin(self.api_url, f"inbound/attachments/{quote(token, safe='')}")
|
||||||
@@ -4,14 +4,24 @@
|
|||||||
Brevo
|
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`_.
|
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
|
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,
|
SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 uses the new
|
||||||
Anymail still uses the old name in code (settings, backend, webhook urls, etc.).
|
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::
|
.. important::
|
||||||
|
|
||||||
@@ -36,14 +46,14 @@ To use Anymail's Brevo backend, set:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend"
|
EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend"
|
||||||
|
|
||||||
in your settings.py.
|
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
|
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.
|
"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 = {
|
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
|
Anymail will also look for ``BREVO_API_KEY`` at the
|
||||||
root of the settings file if neither ``ANYMAIL["SENDINBLUE_API_KEY"]``
|
root of the settings file if neither ``ANYMAIL["BREVO_API_KEY"]``
|
||||||
nor ``ANYMAIL_SENDINBLUE_API_KEY`` is set.
|
nor ``ANYMAIL_BREVO_API_KEY`` is set.
|
||||||
|
|
||||||
.. _SMTP & API settings: https://app.brevo.com/settings/keys/api
|
.. _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 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.)
|
(It's unlikely you would need to change this.)
|
||||||
|
|
||||||
.. versionchanged:: 10.1
|
.. 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/``.
|
Earlier Anymail releases used ``https://api.sendinblue.com/v3/``.
|
||||||
|
|
||||||
|
|
||||||
|
.. _brevo-esp-extra:
|
||||||
.. _sendinblue-esp-extra:
|
.. _sendinblue-esp-extra:
|
||||||
|
|
||||||
esp_extra support
|
esp_extra support
|
||||||
@@ -106,6 +117,7 @@ to apply it to all messages.)
|
|||||||
.. _smtp/email API: https://developers.brevo.com/reference/sendtransacemail
|
.. _smtp/email API: https://developers.brevo.com/reference/sendtransacemail
|
||||||
|
|
||||||
|
|
||||||
|
.. _brevo-limitations:
|
||||||
.. _sendinblue-limitations:
|
.. _sendinblue-limitations:
|
||||||
|
|
||||||
Limitations and quirks
|
Limitations and quirks
|
||||||
@@ -192,6 +204,7 @@ Brevo can handle.
|
|||||||
on individual messages.
|
on individual messages.
|
||||||
|
|
||||||
|
|
||||||
|
.. _brevo-templates:
|
||||||
.. _sendinblue-templates:
|
.. _sendinblue-templates:
|
||||||
|
|
||||||
Batch sending/merge and ESP templates
|
Batch sending/merge and ESP templates
|
||||||
@@ -267,9 +280,9 @@ message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``.
|
|||||||
|
|
||||||
.. caution::
|
.. 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 }}``
|
language that uses Django-like template syntax (with ``{{ param.NAME }}``
|
||||||
substitutions), and an "old" template language that used percent-delimited
|
substitutions), and an "old" template language that used percent-delimited
|
||||||
``%NAME%`` substitutions.
|
``%NAME%`` substitutions.
|
||||||
@@ -299,6 +312,7 @@ message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``.
|
|||||||
https://help.brevo.com/hc/en-us/articles/360000991960
|
https://help.brevo.com/hc/en-us/articles/360000991960
|
||||||
|
|
||||||
|
|
||||||
|
.. _brevo-webhooks:
|
||||||
.. _sendinblue-webhooks:
|
.. _sendinblue-webhooks:
|
||||||
|
|
||||||
Status tracking webhooks
|
Status tracking webhooks
|
||||||
@@ -309,7 +323,7 @@ the url at Brevo's site under `Transactional > Email > Settings > Webhook`_.
|
|||||||
|
|
||||||
The "URL to call" is:
|
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
|
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
|
||||||
* *yoursite.example.com* is your Django site
|
* *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
|
The event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event` field will be
|
||||||
a `dict` of raw webhook data received from Brevo.
|
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
|
.. _Transactional > Email > Settings > Webhook: https://app-smtp.brevo.com/webhook
|
||||||
|
|
||||||
|
|
||||||
|
.. _brevo-inbound:
|
||||||
.. _sendinblue-inbound:
|
.. _sendinblue-inbound:
|
||||||
|
|
||||||
Inbound webhook
|
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:
|
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
|
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
|
||||||
* *yoursite.example.com* is your Django site
|
* *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
|
"Try It!". The `webhooks management APIs`_ and `inbound events list API`_ can
|
||||||
be helpful for diagnosing inbound issues.
|
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:
|
.. _Inbound parsing webhooks:
|
||||||
https://developers.brevo.com/docs/inbound-parse-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
|
https://developers.brevo.com/reference/getwebhooks-1
|
||||||
.. _inbound events list API:
|
.. _inbound events list API:
|
||||||
https://developers.brevo.com/reference/getinboundemailevents
|
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
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ dependencies = [
|
|||||||
# (For simplicity, requests is included in the base dependencies.)
|
# (For simplicity, requests is included in the base dependencies.)
|
||||||
# (Do not use underscores in extra names: they get normalized to hyphens.)
|
# (Do not use underscores in extra names: they get normalized to hyphens.)
|
||||||
amazon-ses = ["boto3"]
|
amazon-ses = ["boto3"]
|
||||||
|
brevo = []
|
||||||
mailersend = []
|
mailersend = []
|
||||||
mailgun = []
|
mailgun = []
|
||||||
mailjet = []
|
mailjet = []
|
||||||
|
|||||||
@@ -32,19 +32,16 @@ from .utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
@override_settings(
|
@override_settings(
|
||||||
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend",
|
EMAIL_BACKEND="anymail.backends.brevo.EmailBackend",
|
||||||
ANYMAIL={"SENDINBLUE_API_KEY": "test_api_key"},
|
ANYMAIL={"BREVO_API_KEY": "test_api_key"},
|
||||||
)
|
)
|
||||||
class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase):
|
class BrevoBackendMockAPITestCase(RequestsBackendMockAPITestCase):
|
||||||
# SendinBlue v3 success responses are empty
|
|
||||||
DEFAULT_RAW_RESPONSE = (
|
DEFAULT_RAW_RESPONSE = (
|
||||||
b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}'
|
b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}'
|
||||||
)
|
)
|
||||||
DEFAULT_STATUS_CODE = (
|
DEFAULT_STATUS_CODE = 201 # Brevo v3 uses '201 Created' for success (in most cases)
|
||||||
201 # SendinBlue v3 uses '201 Created' for success (in most cases)
|
|
||||||
)
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@@ -54,8 +51,8 @@ class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
|
class BrevoBackendStandardEmailTests(BrevoBackendMockAPITestCase):
|
||||||
"""Test backend support for Django standard email features"""
|
"""Test backend support for Django standard email features"""
|
||||||
|
|
||||||
def test_send_mail(self):
|
def test_send_mail(self):
|
||||||
@@ -204,7 +201,7 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_multiple_reply_to(self):
|
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 = [
|
self.message.reply_to = [
|
||||||
'"Reply recipient" <reply@example.com',
|
'"Reply recipient" <reply@example.com',
|
||||||
"reply2@example.com",
|
"reply2@example.com",
|
||||||
@@ -274,7 +271,7 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_embedded_images(self):
|
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
|
# inline image are just added as a content attachment
|
||||||
|
|
||||||
image_filename = SAMPLE_IMAGE_FILENAME
|
image_filename = SAMPLE_IMAGE_FILENAME
|
||||||
@@ -339,7 +336,7 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
|
|||||||
|
|
||||||
def test_api_failure(self):
|
def test_api_failure(self):
|
||||||
self.set_mock_response(status_code=400)
|
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"])
|
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
|
||||||
|
|
||||||
# Make sure fail_silently is respected
|
# Make sure fail_silently is respected
|
||||||
@@ -373,12 +370,12 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
|
|||||||
self.message.send()
|
self.message.send()
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
class BrevoBackendAnymailFeatureTests(BrevoBackendMockAPITestCase):
|
||||||
"""Test backend support for Anymail added features"""
|
"""Test backend support for Anymail added features"""
|
||||||
|
|
||||||
def test_envelope_sender(self):
|
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"
|
self.message.envelope_sender = "anything@bounces.example.com"
|
||||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, "envelope_sender"):
|
with self.assertRaisesMessage(AnymailUnsupportedFeature, "envelope_sender"):
|
||||||
self.message.send()
|
self.message.send()
|
||||||
@@ -459,7 +456,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
self.message.send()
|
self.message.send()
|
||||||
|
|
||||||
def test_template_id(self):
|
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(
|
message = mail.EmailMessage(
|
||||||
subject="My Subject",
|
subject="My Subject",
|
||||||
body=None,
|
body=None,
|
||||||
@@ -470,7 +467,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
bcc=["Recipient <bcc@example.com>"],
|
bcc=["Recipient <bcc@example.com>"],
|
||||||
reply_to=["Recipient <reply@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.template_id = 12
|
||||||
message.send()
|
message.send()
|
||||||
data = self.get_api_call_json()
|
data = self.get_api_call_json()
|
||||||
@@ -603,7 +600,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
def test_send_attaches_anymail_status(self):
|
def test_send_attaches_anymail_status(self):
|
||||||
"""The anymail_status should be attached to the message when it is sent"""
|
"""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
|
# returns, so no need to override it here
|
||||||
msg = mail.EmailMessage(
|
msg = mail.EmailMessage(
|
||||||
"Subject",
|
"Subject",
|
||||||
@@ -652,39 +649,37 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
|
|||||||
err = cm.exception
|
err = cm.exception
|
||||||
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
|
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
|
||||||
# our added context:
|
# 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
|
# original message
|
||||||
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
|
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
class SendinBlueBackendRecipientsRefusedTests(SendinBlueBackendMockAPITestCase):
|
class BrevoBackendRecipientsRefusedTests(BrevoBackendMockAPITestCase):
|
||||||
"""
|
"""
|
||||||
Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid
|
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"
|
# it always just queues the message. You'll need to listen for the "rejected"
|
||||||
# and "failed" events to detect refused recipients.
|
# and "failed" events to detect refused recipients.
|
||||||
pass # not applicable to this backend
|
pass # not applicable to this backend
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
class SendinBlueBackendSessionSharingTestCase(
|
class BrevoBackendSessionSharingTestCase(
|
||||||
SessionSharingTestCases, SendinBlueBackendMockAPITestCase
|
SessionSharingTestCases, BrevoBackendMockAPITestCase
|
||||||
):
|
):
|
||||||
"""Requests session sharing tests"""
|
"""Requests session sharing tests"""
|
||||||
|
|
||||||
pass # tests are defined in SessionSharingTestCases
|
pass # tests are defined in SessionSharingTestCases
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
@override_settings(EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend")
|
@override_settings(EMAIL_BACKEND="anymail.backends.brevo.EmailBackend")
|
||||||
class SendinBlueBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
|
class BrevoBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
|
||||||
"""Test ESP backend without required settings in place"""
|
"""Test ESP backend without required settings in place"""
|
||||||
|
|
||||||
def test_missing_auth(self):
|
def test_missing_auth(self):
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(AnymailConfigurationError, r"\bBREVO_API_KEY\b"):
|
||||||
AnymailConfigurationError, r"\bSENDINBLUE_API_KEY\b"
|
|
||||||
):
|
|
||||||
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
|
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
|
||||||
@@ -7,15 +7,15 @@ from responses.matchers import header_matcher
|
|||||||
from anymail.exceptions import AnymailConfigurationError
|
from anymail.exceptions import AnymailConfigurationError
|
||||||
from anymail.inbound import AnymailInboundMessage
|
from anymail.inbound import AnymailInboundMessage
|
||||||
from anymail.signals import AnymailInboundEvent
|
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 .utils import sample_email_content, sample_image_content
|
||||||
from .webhook_cases import WebhookTestCase
|
from .webhook_cases import WebhookTestCase
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
@override_settings(ANYMAIL_SENDINBLUE_API_KEY="test-api-key")
|
@override_settings(ANYMAIL_BREVO_API_KEY="test-api-key")
|
||||||
class SendinBlueInboundTestCase(WebhookTestCase):
|
class BrevoInboundTestCase(WebhookTestCase):
|
||||||
def test_inbound_basics(self):
|
def test_inbound_basics(self):
|
||||||
# Actual (sanitized) Brevo inbound message payload 7/2023
|
# Actual (sanitized) Brevo inbound message payload 7/2023
|
||||||
raw_event = {
|
raw_event = {
|
||||||
@@ -54,16 +54,16 @@ class SendinBlueInboundTestCase(WebhookTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/inbound/",
|
"/anymail/brevo/inbound/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data={"items": [raw_event]},
|
data={"items": [raw_event]},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.inbound_handler,
|
self.inbound_handler,
|
||||||
sender=SendinBlueInboundWebhookView,
|
sender=BrevoInboundWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
# AnymailInboundEvent
|
# AnymailInboundEvent
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
@@ -123,15 +123,15 @@ class SendinBlueInboundTestCase(WebhookTestCase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.client.post(
|
self.client.post(
|
||||||
"/anymail/sendinblue/inbound/",
|
"/anymail/brevo/inbound/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data={"items": [raw_event]},
|
data={"items": [raw_event]},
|
||||||
)
|
)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.inbound_handler,
|
self.inbound_handler,
|
||||||
sender=SendinBlueInboundWebhookView,
|
sender=BrevoInboundWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
message = event.message
|
message = event.message
|
||||||
@@ -203,16 +203,16 @@ class SendinBlueInboundTestCase(WebhookTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/inbound/",
|
"/anymail/brevo/inbound/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data={"items": [raw_event]},
|
data={"items": [raw_event]},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.inbound_handler,
|
self.inbound_handler,
|
||||||
sender=SendinBlueInboundWebhookView,
|
sender=BrevoInboundWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
message = event.message
|
message = event.message
|
||||||
@@ -235,12 +235,12 @@ class SendinBlueInboundTestCase(WebhookTestCase):
|
|||||||
|
|
||||||
def test_misconfigured_tracking(self):
|
def test_misconfigured_tracking(self):
|
||||||
errmsg = (
|
errmsg = (
|
||||||
"You seem to have set SendinBlue's *tracking* webhook URL"
|
"You seem to have set Brevo's *tracking* webhook URL"
|
||||||
" to Anymail's SendinBlue *inbound* webhook URL."
|
" to Anymail's Brevo *inbound* webhook URL."
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
|
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
|
||||||
self.client.post(
|
self.client.post(
|
||||||
"/anymail/sendinblue/inbound/",
|
"/anymail/brevo/inbound/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data={"event": "delivered"},
|
data={"event": "delivered"},
|
||||||
)
|
)
|
||||||
@@ -10,39 +10,39 @@ from anymail.message import AnymailMessage
|
|||||||
|
|
||||||
from .utils import AnymailTestMixin
|
from .utils import AnymailTestMixin
|
||||||
|
|
||||||
ANYMAIL_TEST_SENDINBLUE_API_KEY = os.getenv("ANYMAIL_TEST_SENDINBLUE_API_KEY")
|
ANYMAIL_TEST_BREVO_API_KEY = os.getenv("ANYMAIL_TEST_BREVO_API_KEY")
|
||||||
ANYMAIL_TEST_SENDINBLUE_DOMAIN = os.getenv("ANYMAIL_TEST_SENDINBLUE_DOMAIN")
|
ANYMAIL_TEST_BREVO_DOMAIN = os.getenv("ANYMAIL_TEST_BREVO_DOMAIN")
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue", "live")
|
@tag("brevo", "live")
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN,
|
ANYMAIL_TEST_BREVO_API_KEY and ANYMAIL_TEST_BREVO_DOMAIN,
|
||||||
"Set ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN "
|
"Set ANYMAIL_TEST_BREVO_API_KEY and ANYMAIL_TEST_BREVO_DOMAIN "
|
||||||
"environment variables to run SendinBlue integration tests",
|
"environment variables to run Brevo integration tests",
|
||||||
)
|
)
|
||||||
@override_settings(
|
@override_settings(
|
||||||
ANYMAIL_SENDINBLUE_API_KEY=ANYMAIL_TEST_SENDINBLUE_API_KEY,
|
ANYMAIL_BREVO_API_KEY=ANYMAIL_TEST_BREVO_API_KEY,
|
||||||
ANYMAIL_SENDINBLUE_SEND_DEFAULTS=dict(),
|
ANYMAIL_BREVO_SEND_DEFAULTS=dict(),
|
||||||
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend",
|
EMAIL_BACKEND="anymail.backends.brevo.EmailBackend",
|
||||||
)
|
)
|
||||||
class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
class BrevoBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
||||||
"""SendinBlue v3 API integration tests
|
"""Brevo v3 API integration tests
|
||||||
|
|
||||||
SendinBlue doesn't have sandbox so these tests run
|
Brevo doesn't have sandbox so these tests run
|
||||||
against the **live** SendinBlue API, using the
|
against the **live** Brevo API, using the
|
||||||
environment variable `ANYMAIL_TEST_SENDINBLUE_API_KEY` as the API key,
|
environment variable `ANYMAIL_TEST_BREVO_API_KEY` as the API key,
|
||||||
and `ANYMAIL_TEST_SENDINBLUE_DOMAIN` to construct sender addresses.
|
and `ANYMAIL_TEST_BREVO_DOMAIN` to construct sender addresses.
|
||||||
If those variables are not set, these tests won't run.
|
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):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.from_email = "from@%s" % ANYMAIL_TEST_SENDINBLUE_DOMAIN
|
self.from_email = "from@%s" % ANYMAIL_TEST_BREVO_DOMAIN
|
||||||
self.message = AnymailMessage(
|
self.message = AnymailMessage(
|
||||||
"Anymail SendinBlue integration test",
|
"Anymail Brevo integration test",
|
||||||
"Text content",
|
"Text content",
|
||||||
self.from_email,
|
self.from_email,
|
||||||
["test+to1@anymail.dev"],
|
["test+to1@anymail.dev"],
|
||||||
@@ -50,7 +50,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
|||||||
self.message.attach_alternative("<p>HTML content</p>", "text/html")
|
self.message.attach_alternative("<p>HTML content</p>", "text/html")
|
||||||
|
|
||||||
def test_simple_send(self):
|
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()
|
sent_count = self.message.send()
|
||||||
self.assertEqual(sent_count, 1)
|
self.assertEqual(sent_count, 1)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
|||||||
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
|
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
|
||||||
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
|
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:
|
# Message-ID can be ...@smtp-relay.mail.fr or .sendinblue.com:
|
||||||
self.assertRegex(message_id, r"\<.+@.+\>")
|
self.assertRegex(message_id, r"\<.+@.+\>")
|
||||||
# set of all recipient statuses:
|
# set of all recipient statuses:
|
||||||
@@ -68,27 +68,27 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
|||||||
def test_all_options(self):
|
def test_all_options(self):
|
||||||
send_at = datetime.now() + timedelta(minutes=2)
|
send_at = datetime.now() + timedelta(minutes=2)
|
||||||
message = AnymailMessage(
|
message = AnymailMessage(
|
||||||
subject="Anymail SendinBlue all-options integration test",
|
subject="Anymail Brevo all-options integration test",
|
||||||
body="This is the text body",
|
body="This is the text body",
|
||||||
from_email=formataddr(("Test From, with comma", self.from_email)),
|
from_email=formataddr(("Test From, with comma", self.from_email)),
|
||||||
to=["test+to1@anymail.dev", '"Recipient 2, OK?" <test+to2@anymail.dev>'],
|
to=["test+to1@anymail.dev", '"Recipient 2, OK?" <test+to2@anymail.dev>'],
|
||||||
cc=["test+cc1@anymail.dev", "Copy 2 <test+cc2@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>"],
|
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>'],
|
reply_to=['"Reply, with comma" <reply@example.com>'],
|
||||||
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
|
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
|
||||||
metadata={"meta1": "simple string", "meta2": 2},
|
metadata={"meta1": "simple string", "meta2": 2},
|
||||||
send_at=send_at,
|
send_at=send_at,
|
||||||
tags=["tag 1", "tag 2"],
|
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_alternative("<p>HTML content</p>", "text/html")
|
||||||
|
|
||||||
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
|
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
|
||||||
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
|
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
|
||||||
|
|
||||||
message.send()
|
message.send()
|
||||||
# SendinBlue always queues:
|
# Brevo always queues:
|
||||||
self.assertEqual(message.anymail_status.status, {"queued"})
|
self.assertEqual(message.anymail_status.status, {"queued"})
|
||||||
self.assertRegex(message.anymail_status.message_id, r"\<.+@.+\>")
|
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.attach("attachment1.txt", "Here is some\ntext", "text/plain")
|
||||||
|
|
||||||
message.send()
|
message.send()
|
||||||
# SendinBlue always queues:
|
# Brevo always queues:
|
||||||
self.assertEqual(message.anymail_status.status, {"queued"})
|
self.assertEqual(message.anymail_status.status, {"queued"})
|
||||||
recipient_status = message.anymail_status.recipients
|
recipient_status = message.anymail_status.recipients
|
||||||
self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "queued")
|
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,
|
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):
|
def test_invalid_api_key(self):
|
||||||
with self.assertRaises(AnymailAPIError) as cm:
|
with self.assertRaises(AnymailAPIError) as cm:
|
||||||
self.message.send()
|
self.message.send()
|
||||||
err = cm.exception
|
err = cm.exception
|
||||||
self.assertEqual(err.status_code, 401)
|
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))
|
self.assertIn("Key not found", str(err))
|
||||||
@@ -6,16 +6,16 @@ from django.test import tag
|
|||||||
|
|
||||||
from anymail.exceptions import AnymailConfigurationError
|
from anymail.exceptions import AnymailConfigurationError
|
||||||
from anymail.signals import AnymailTrackingEvent
|
from anymail.signals import AnymailTrackingEvent
|
||||||
from anymail.webhooks.sendinblue import SendinBlueTrackingWebhookView
|
from anymail.webhooks.brevo import BrevoTrackingWebhookView
|
||||||
|
|
||||||
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
|
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase):
|
class BrevoWebhookSecurityTestCase(WebhookBasicAuthTestCase):
|
||||||
def call_webhook(self):
|
def call_webhook(self):
|
||||||
return self.client.post(
|
return self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps({}),
|
data=json.dumps({}),
|
||||||
)
|
)
|
||||||
@@ -23,23 +23,22 @@ class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase):
|
|||||||
# Actual tests are in WebhookBasicAuthTestCase
|
# Actual tests are in WebhookBasicAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@tag("sendinblue")
|
@tag("brevo")
|
||||||
class SendinBlueDeliveryTestCase(WebhookTestCase):
|
class BrevoDeliveryTestCase(WebhookTestCase):
|
||||||
# SendinBlue's webhook payload data is partially documented at
|
# Brevo's webhook payload data is documented at
|
||||||
# https://help.sendinblue.com/hc/en-us/articles/360007666479,
|
# https://developers.brevo.com/docs/transactional-webhooks.
|
||||||
# but it's not completely up to date.
|
|
||||||
# The payloads below were obtained through live testing.
|
# The payloads below were obtained through live testing.
|
||||||
|
|
||||||
def test_sent_event(self):
|
def test_sent_event(self):
|
||||||
raw_event = {
|
raw_event = {
|
||||||
"event": "request",
|
"event": "request",
|
||||||
"email": "recipient@example.com",
|
"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>",
|
"message-id": "<201803062010.27287306012@smtp-relay.mailin.fr>",
|
||||||
"subject": "Test subject",
|
"subject": "Test subject",
|
||||||
# From a message sent at 2018-03-06 11:10:23-08:00
|
# From a message sent at 2018-03-06 11:10:23-08:00
|
||||||
# (2018-03-06 19:10:23+00: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": 1520331023, # 2018-03-06 10:10:23 -- what time zone is this?
|
||||||
"ts_event": 1520331023, # unclear if this ever differs from "ts"
|
"ts_event": 1520331023, # unclear if this ever differs from "ts"
|
||||||
"ts_epoch": 1520363423000, # 2018-03-06 19:10:23.000+00:00 -- UTC (msec)
|
"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",
|
"sending_ip": "333.33.33.33",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||||
@@ -77,7 +76,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
event.message_id, "<201803062010.27287306012@smtp-relay.mailin.fr>"
|
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.assertIsNone(event.event_id)
|
||||||
self.assertEqual(event.recipient, "recipient@example.com")
|
self.assertEqual(event.recipient, "recipient@example.com")
|
||||||
self.assertEqual(event.metadata, {"meta": "data"})
|
self.assertEqual(event.metadata, {"meta": "data"})
|
||||||
@@ -93,16 +92,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||||
@@ -128,16 +127,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"tag": "header-tag",
|
"tag": "header-tag",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "bounced")
|
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",
|
"reason": "undefined Unable to find MX of domain no-mx.example.com",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "bounced")
|
self.assertEqual(event.event_type, "bounced")
|
||||||
@@ -188,16 +187,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"reason": "blocked : due to blacklist user",
|
"reason": "blocked : due to blacklist user",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "rejected")
|
self.assertEqual(event.event_type, "rejected")
|
||||||
@@ -214,16 +213,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "complained")
|
self.assertEqual(event.event_type, "complained")
|
||||||
@@ -231,7 +230,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
def test_invalid_email(self):
|
def test_invalid_email(self):
|
||||||
# "If a ISP again indicated us that the email is not valid or if we discovered
|
# "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
|
# 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)
|
# event in actual testing; payload below is a guess)
|
||||||
raw_event = {
|
raw_event = {
|
||||||
"event": "invalid_email",
|
"event": "invalid_email",
|
||||||
@@ -241,16 +240,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"reason": "(guessing invalid_email includes a reason)",
|
"reason": "(guessing invalid_email includes a reason)",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "bounced")
|
self.assertEqual(event.event_type, "bounced")
|
||||||
@@ -262,7 +261,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
def test_deferred_event(self):
|
def test_deferred_event(self):
|
||||||
# Note: the example below is an actual event capture (with 'example.com'
|
# 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
|
# 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 = {
|
raw_event = {
|
||||||
"event": "deferred",
|
"event": "deferred",
|
||||||
"email": "notauser@example.com",
|
"email": "notauser@example.com",
|
||||||
@@ -272,16 +271,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
" address rejected: User unknown in virtual alias table",
|
" address rejected: User unknown in virtual alias table",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "deferred")
|
self.assertEqual(event.event_type, "deferred")
|
||||||
@@ -294,7 +293,7 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_opened_event(self):
|
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
|
# only on the second or later tracking pixel views. (But they used to deliver
|
||||||
# both on the first open.)
|
# both on the first open.)
|
||||||
raw_event = {
|
raw_event = {
|
||||||
@@ -304,20 +303,20 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "opened")
|
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):
|
def test_unique_opened_event(self):
|
||||||
# See note in test_opened_event above
|
# See note in test_opened_event above
|
||||||
@@ -328,16 +327,16 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "opened")
|
self.assertEqual(event.event_type, "opened")
|
||||||
@@ -351,21 +350,21 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"link": "https://example.com/click/me",
|
"link": "https://example.com/click/me",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "clicked")
|
self.assertEqual(event.event_type, "clicked")
|
||||||
self.assertEqual(event.click_url, "https://example.com/click/me")
|
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):
|
def test_unsubscribe(self):
|
||||||
# "When a person unsubscribes from the email received."
|
# "When a person unsubscribes from the email received."
|
||||||
@@ -378,28 +377,28 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
|
|||||||
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.tracking_handler,
|
self.tracking_handler,
|
||||||
sender=SendinBlueTrackingWebhookView,
|
sender=BrevoTrackingWebhookView,
|
||||||
event=ANY,
|
event=ANY,
|
||||||
esp_name="SendinBlue",
|
esp_name="Brevo",
|
||||||
)
|
)
|
||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
self.assertEqual(event.event_type, "unsubscribed")
|
self.assertEqual(event.event_type, "unsubscribed")
|
||||||
|
|
||||||
def test_misconfigured_inbound(self):
|
def test_misconfigured_inbound(self):
|
||||||
errmsg = (
|
errmsg = (
|
||||||
"You seem to have set SendinBlue's *inbound* webhook URL"
|
"You seem to have set Brevo's *inbound* webhook URL"
|
||||||
" to Anymail's SendinBlue *tracking* webhook URL."
|
" to Anymail's Brevo *tracking* webhook URL."
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
|
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
|
||||||
self.client.post(
|
self.client.post(
|
||||||
"/anymail/sendinblue/tracking/",
|
"/anymail/brevo/tracking/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data={"items": []},
|
data={"items": []},
|
||||||
)
|
)
|
||||||
2
tox.ini
2
tox.ini
@@ -59,6 +59,7 @@ setenv =
|
|||||||
# (resend should work with or without its extras, so it isn't in `none`)
|
# (resend should work with or without its extras, so it isn't in `none`)
|
||||||
none: ANYMAIL_SKIP_TESTS=amazon_ses,postal
|
none: ANYMAIL_SKIP_TESTS=amazon_ses,postal
|
||||||
amazon_ses: ANYMAIL_ONLY_TEST=amazon_ses
|
amazon_ses: ANYMAIL_ONLY_TEST=amazon_ses
|
||||||
|
brevo: ANYMAIL_ONLY_TEST=brevo
|
||||||
mailersend: ANYMAIL_ONLY_TEST=mailersend
|
mailersend: ANYMAIL_ONLY_TEST=mailersend
|
||||||
mailgun: ANYMAIL_ONLY_TEST=mailgun
|
mailgun: ANYMAIL_ONLY_TEST=mailgun
|
||||||
mailjet: ANYMAIL_ONLY_TEST=mailjet
|
mailjet: ANYMAIL_ONLY_TEST=mailjet
|
||||||
@@ -68,7 +69,6 @@ setenv =
|
|||||||
resend: ANYMAIL_ONLY_TEST=resend
|
resend: ANYMAIL_ONLY_TEST=resend
|
||||||
sendgrid: ANYMAIL_ONLY_TEST=sendgrid
|
sendgrid: ANYMAIL_ONLY_TEST=sendgrid
|
||||||
unisender_go: ANYMAIL_ONLY_TEST=unisender_go
|
unisender_go: ANYMAIL_ONLY_TEST=unisender_go
|
||||||
sendinblue: ANYMAIL_ONLY_TEST=sendinblue
|
|
||||||
sparkpost: ANYMAIL_ONLY_TEST=sparkpost
|
sparkpost: ANYMAIL_ONLY_TEST=sparkpost
|
||||||
ignore_outcome =
|
ignore_outcome =
|
||||||
# CI that wants to handle errors itself can set TOX_OVERRIDE_IGNORE_OUTCOME=false
|
# CI that wants to handle errors itself can set TOX_OVERRIDE_IGNORE_OUTCOME=false
|
||||||
|
|||||||
Reference in New Issue
Block a user