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

@@ -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='')}")