Skip to content

Commit

Permalink
Brevo: Rename SendinBlue to Brevo
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
medmunds committed Mar 12, 2024
1 parent 14d4516 commit c7ee59c
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 197 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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" }

Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~

Expand Down
24 changes: 12 additions & 12 deletions anymail/backends/sendinblue.py → anymail/backends/brevo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

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"""
Expand All @@ -33,11 +33,11 @@ def __init__(self, **kwargs):
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 = []

Expand All @@ -51,7 +51,7 @@ def parse_recipient_status(self, response, payload, message):
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,
Expand All @@ -70,7 +70,7 @@ def parse_recipient_status(self, response, payload, message):
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
Expand Down Expand Up @@ -124,7 +124,7 @@ def serialize_data(self):

@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:
Expand All @@ -147,14 +147,14 @@ def set_subject(self, subject):
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(
{
Expand Down Expand Up @@ -182,7 +182,7 @@ def set_html_body(self, body):
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,
Expand All @@ -204,7 +204,7 @@ def set_merge_global_data(self, merge_global_data):
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

Expand Down
25 changes: 11 additions & 14 deletions anymail/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
AmazonSESInboundWebhookView,
AmazonSESTrackingWebhookView,
)
from .webhooks.brevo import BrevoInboundWebhookView, BrevoTrackingWebhookView
from .webhooks.mailersend import (
MailerSendInboundWebhookView,
MailerSendTrackingWebhookView,
Expand All @@ -15,10 +16,6 @@
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,
Expand All @@ -32,6 +29,11 @@
AmazonSESInboundWebhookView.as_view(),
name="amazon_ses_inbound_webhook",
),
path(
"brevo/inbound/",
BrevoInboundWebhookView.as_view(),
name="brevo_inbound_webhook",
),
path(
"mailersend/inbound/",
MailerSendInboundWebhookView.as_view(),
Expand Down Expand Up @@ -66,11 +68,6 @@
SendGridInboundWebhookView.as_view(),
name="sendgrid_inbound_webhook",
),
path(
"sendinblue/inbound/",
SendinBlueInboundWebhookView.as_view(),
name="sendinblue_inbound_webhook",
),
path(
"sparkpost/inbound/",
SparkPostInboundWebhookView.as_view(),
Expand All @@ -81,6 +78,11 @@
AmazonSESTrackingWebhookView.as_view(),
name="amazon_ses_tracking_webhook",
),
path(
"brevo/tracking/",
BrevoTrackingWebhookView.as_view(),
name="brevo_tracking_webhook",
),
path(
"mailersend/tracking/",
MailerSendTrackingWebhookView.as_view(),
Expand Down Expand Up @@ -116,11 +118,6 @@
SendGridTrackingWebhookView.as_view(),
name="sendgrid_tracking_webhook",
),
path(
"sendinblue/tracking/",
SendinBlueTrackingWebhookView.as_view(),
name="sendinblue_tracking_webhook",
),
path(
"sparkpost/tracking/",
SparkPostTrackingWebhookView.as_view(),
Expand Down
36 changes: 19 additions & 17 deletions anymail/webhooks/sendinblue.py → anymail/webhooks/brevo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
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

Expand All @@ -33,15 +35,13 @@ def parse_events(self, request):
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),
Expand All @@ -67,7 +67,7 @@ def esp_to_anymail_event(self, esp_event):
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
Expand Down Expand Up @@ -98,7 +98,7 @@ def esp_to_anymail_event(self, esp_event):
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"),
Expand All @@ -113,8 +113,10 @@ def esp_to_anymail_event(self, esp_event):
)


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

Expand All @@ -141,10 +143,10 @@ def parse_events(self, request):
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]
Expand Down Expand Up @@ -199,7 +201,7 @@ def esp_to_anymail_event(self, esp_event):
)

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='')}")
Expand Down
Loading

0 comments on commit c7ee59c

Please sign in to comment.