Skip to content

Commit c7ee59c

Browse files
committed
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.
1 parent 14d4516 commit c7ee59c

File tree

12 files changed

+326
-197
lines changed

12 files changed

+326
-197
lines changed

.github/workflows/integration-test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
# combination, to avoid rapidly consuming the testing accounts' entire send allotments.
4141
config:
4242
- { tox: django41-py310-amazon_ses, python: "3.10" }
43+
- { tox: django41-py310-brevo, python: "3.10" }
4344
- { tox: django41-py310-mailersend, python: "3.10" }
4445
- { tox: django41-py310-mailgun, python: "3.10" }
4546
- { tox: django41-py310-mailjet, python: "3.10" }
@@ -48,7 +49,6 @@ jobs:
4849
- { tox: django41-py310-postmark, python: "3.10" }
4950
- { tox: django41-py310-resend, python: "3.10" }
5051
- { tox: django41-py310-sendgrid, python: "3.10" }
51-
- { tox: django41-py310-sendinblue, python: "3.10" }
5252
- { tox: django41-py310-sparkpost, python: "3.10" }
5353
- { tox: django41-py310-unisender_go, python: "3.10" }
5454

@@ -77,6 +77,8 @@ jobs:
7777
ANYMAIL_TEST_AMAZON_SES_DOMAIN: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_DOMAIN }}
7878
ANYMAIL_TEST_AMAZON_SES_REGION_NAME: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_REGION_NAME }}
7979
ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY }}
80+
ANYMAIL_TEST_BREVO_API_KEY: ${{ secrets.ANYMAIL_TEST_BREVO_API_KEY }}
81+
ANYMAIL_TEST_BREVO_DOMAIN: ${{ vars.ANYMAIL_TEST_BREVO_DOMAIN }}
8082
ANYMAIL_TEST_MAILERSEND_API_TOKEN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_API_TOKEN }}
8183
ANYMAIL_TEST_MAILERSEND_DOMAIN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_DOMAIN }}
8284
ANYMAIL_TEST_MAILGUN_API_KEY: ${{ secrets.ANYMAIL_TEST_MAILGUN_API_KEY }}
@@ -94,8 +96,6 @@ jobs:
9496
ANYMAIL_TEST_SENDGRID_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDGRID_API_KEY }}
9597
ANYMAIL_TEST_SENDGRID_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDGRID_DOMAIN }}
9698
ANYMAIL_TEST_SENDGRID_TEMPLATE_ID: ${{ secrets.ANYMAIL_TEST_SENDGRID_TEMPLATE_ID }}
97-
ANYMAIL_TEST_SENDINBLUE_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDINBLUE_API_KEY }}
98-
ANYMAIL_TEST_SENDINBLUE_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDINBLUE_DOMAIN }}
9999
ANYMAIL_TEST_SPARKPOST_API_KEY: ${{ secrets.ANYMAIL_TEST_SPARKPOST_API_KEY }}
100100
ANYMAIL_TEST_SPARKPOST_DOMAIN: ${{ secrets.ANYMAIL_TEST_SPARKPOST_DOMAIN }}
101101
ANYMAIL_TEST_UNISENDER_GO_API_KEY: ${{ secrets.ANYMAIL_TEST_UNISENDER_GO_API_KEY }}

CHANGELOG.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ vNext
3030

3131
*unreleased changes*
3232

33+
Deprecations
34+
~~~~~~~~~~~~
35+
36+
* **SendinBlue:** Rename "SendinBlue" to "Brevo" throughout Anymail's code.
37+
This affects the email backend name, settings names, and webhook URLs.
38+
The old names will continue to work for now, but are deprecated. See
39+
`Updating code from SendinBlue to Brevo <https://anymail.dev/en/latest/esps/brevo/#brevo-rename>`__
40+
for details.
41+
42+
3343
Features
3444
~~~~~~~~
3545

anymail/backends/sendinblue.py renamed to anymail/backends/brevo.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
class EmailBackend(AnymailRequestsBackend):
1010
"""
11-
SendinBlue v3 API Email Backend
11+
Brevo v3 API Email Backend
1212
"""
1313

14-
esp_name = "SendinBlue"
14+
esp_name = "Brevo"
1515

1616
def __init__(self, **kwargs):
1717
"""Init options from Django settings"""
@@ -33,11 +33,11 @@ def __init__(self, **kwargs):
3333
super().__init__(api_url, **kwargs)
3434

3535
def build_message_payload(self, message, defaults):
36-
return SendinBluePayload(message, defaults, self)
36+
return BrevoPayload(message, defaults, self)
3737

3838
def parse_recipient_status(self, response, payload, message):
39-
# SendinBlue doesn't give any detail on a success
40-
# https://developers.sendinblue.com/docs/responses
39+
# Brevo doesn't give any detail on a success, other than messageId
40+
# https://developers.brevo.com/reference/sendtransacemail
4141
message_id = None
4242
message_ids = []
4343

@@ -51,7 +51,7 @@ def parse_recipient_status(self, response, payload, message):
5151
message_ids = parsed_response["messageIds"]
5252
except (KeyError, TypeError) as err:
5353
raise AnymailRequestsAPIError(
54-
"Invalid SendinBlue API response format",
54+
"Invalid Brevo API response format",
5555
email_message=message,
5656
payload=payload,
5757
response=response,
@@ -70,7 +70,7 @@ def parse_recipient_status(self, response, payload, message):
7070
return recipient_status
7171

7272

73-
class SendinBluePayload(RequestsPayload):
73+
class BrevoPayload(RequestsPayload):
7474
def __init__(self, message, defaults, backend, *args, **kwargs):
7575
self.all_recipients = [] # used for backend.parse_recipient_status
7676
self.to_recipients = [] # used for backend.parse_recipient_status
@@ -124,7 +124,7 @@ def serialize_data(self):
124124

125125
@staticmethod
126126
def email_object(email):
127-
"""Converts EmailAddress to SendinBlue API array"""
127+
"""Converts EmailAddress to Brevo API array"""
128128
email_object = dict()
129129
email_object["email"] = email.addr_spec
130130
if email.display_name:
@@ -147,14 +147,14 @@ def set_subject(self, subject):
147147
self.data["subject"] = subject
148148

149149
def set_reply_to(self, emails):
150-
# SendinBlue only supports a single address in the reply_to API param.
150+
# Brevo only supports a single address in the reply_to API param.
151151
if len(emails) > 1:
152152
self.unsupported_feature("multiple reply_to addresses")
153153
if len(emails) > 0:
154154
self.data["replyTo"] = self.email_object(emails[0])
155155

156156
def set_extra_headers(self, headers):
157-
# SendinBlue requires header values to be strings (not integers) as of 11/2022.
157+
# Brevo requires header values to be strings (not integers) as of 11/2022.
158158
# Stringify ints and floats; anything else is the caller's responsibility.
159159
self.data["headers"].update(
160160
{
@@ -182,7 +182,7 @@ def set_html_body(self, body):
182182
self.data["htmlContent"] = body
183183

184184
def add_attachment(self, attachment):
185-
"""Converts attachments to SendinBlue API {name, base64} array"""
185+
"""Converts attachments to Brevo API {name, base64} array"""
186186
att = {
187187
"name": attachment.name or "",
188188
"content": attachment.b64content,
@@ -204,7 +204,7 @@ def set_merge_global_data(self, merge_global_data):
204204
self.data["params"] = merge_global_data
205205

206206
def set_metadata(self, metadata):
207-
# SendinBlue expects a single string payload
207+
# Brevo expects a single string payload
208208
self.data["headers"]["X-Mailin-custom"] = self.serialize_json(metadata)
209209
self.metadata = metadata # needed in serialize_data for batch send
210210

anymail/urls.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
AmazonSESInboundWebhookView,
55
AmazonSESTrackingWebhookView,
66
)
7+
from .webhooks.brevo import BrevoInboundWebhookView, BrevoTrackingWebhookView
78
from .webhooks.mailersend import (
89
MailerSendInboundWebhookView,
910
MailerSendTrackingWebhookView,
@@ -15,10 +16,6 @@
1516
from .webhooks.postmark import PostmarkInboundWebhookView, PostmarkTrackingWebhookView
1617
from .webhooks.resend import ResendTrackingWebhookView
1718
from .webhooks.sendgrid import SendGridInboundWebhookView, SendGridTrackingWebhookView
18-
from .webhooks.sendinblue import (
19-
SendinBlueInboundWebhookView,
20-
SendinBlueTrackingWebhookView,
21-
)
2219
from .webhooks.sparkpost import (
2320
SparkPostInboundWebhookView,
2421
SparkPostTrackingWebhookView,
@@ -32,6 +29,11 @@
3229
AmazonSESInboundWebhookView.as_view(),
3330
name="amazon_ses_inbound_webhook",
3431
),
32+
path(
33+
"brevo/inbound/",
34+
BrevoInboundWebhookView.as_view(),
35+
name="brevo_inbound_webhook",
36+
),
3537
path(
3638
"mailersend/inbound/",
3739
MailerSendInboundWebhookView.as_view(),
@@ -66,11 +68,6 @@
6668
SendGridInboundWebhookView.as_view(),
6769
name="sendgrid_inbound_webhook",
6870
),
69-
path(
70-
"sendinblue/inbound/",
71-
SendinBlueInboundWebhookView.as_view(),
72-
name="sendinblue_inbound_webhook",
73-
),
7471
path(
7572
"sparkpost/inbound/",
7673
SparkPostInboundWebhookView.as_view(),
@@ -81,6 +78,11 @@
8178
AmazonSESTrackingWebhookView.as_view(),
8279
name="amazon_ses_tracking_webhook",
8380
),
81+
path(
82+
"brevo/tracking/",
83+
BrevoTrackingWebhookView.as_view(),
84+
name="brevo_tracking_webhook",
85+
),
8486
path(
8587
"mailersend/tracking/",
8688
MailerSendTrackingWebhookView.as_view(),
@@ -116,11 +118,6 @@
116118
SendGridTrackingWebhookView.as_view(),
117119
name="sendgrid_tracking_webhook",
118120
),
119-
path(
120-
"sendinblue/tracking/",
121-
SendinBlueTrackingWebhookView.as_view(),
122-
name="sendinblue_tracking_webhook",
123-
),
124121
path(
125122
"sparkpost/tracking/",
126123
SparkPostTrackingWebhookView.as_view(),

anymail/webhooks/sendinblue.py renamed to anymail/webhooks/brevo.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
from .base import AnymailBaseWebhookView
2020

2121

22-
class SendinBlueBaseWebhookView(AnymailBaseWebhookView):
23-
esp_name = "SendinBlue"
22+
class BrevoBaseWebhookView(AnymailBaseWebhookView):
23+
esp_name = "Brevo"
2424

2525

26-
class SendinBlueTrackingWebhookView(SendinBlueBaseWebhookView):
27-
"""Handler for SendinBlue delivery and engagement tracking webhooks"""
26+
class BrevoTrackingWebhookView(BrevoBaseWebhookView):
27+
"""Handler for Brevo delivery and engagement tracking webhooks"""
28+
29+
# https://developers.brevo.com/docs/transactional-webhooks
2830

2931
signal = tracking
3032

@@ -33,15 +35,13 @@ def parse_events(self, request):
3335
if "items" in esp_event:
3436
# This is an inbound webhook post
3537
raise AnymailConfigurationError(
36-
"You seem to have set SendinBlue's *inbound* webhook URL "
37-
"to Anymail's SendinBlue *tracking* webhook URL."
38+
f"You seem to have set Brevo's *inbound* webhook URL "
39+
f"to Anymail's {self.esp_name} *tracking* webhook URL."
3840
)
3941
return [self.esp_to_anymail_event(esp_event)]
4042

41-
# SendinBlue's webhook payload data doesn't seem to be documented anywhere.
42-
# There's a list of webhook events at https://apidocs.sendinblue.com/webhooks/#3.
4343
event_types = {
44-
# Map SendinBlue event type: Anymail normalized (event type, reject reason)
44+
# Map Brevo event type: Anymail normalized (event type, reject reason)
4545
# received even if message won't be sent (e.g., before "blocked"):
4646
"request": (EventType.QUEUED, None),
4747
"delivered": (EventType.DELIVERED, None),
@@ -67,7 +67,7 @@ def esp_to_anymail_event(self, esp_event):
6767
recipient = esp_event.get("email")
6868

6969
try:
70-
# SendinBlue supplies "ts", "ts_event" and "date" fields, which seem to be
70+
# Brevo supplies "ts", "ts_event" and "date" fields, which seem to be
7171
# based on the timezone set in the account preferences (and possibly with
7272
# inconsistent DST adjustment). "ts_epoch" is the only field that seems to
7373
# be consistently UTC; it's in milliseconds
@@ -98,7 +98,7 @@ def esp_to_anymail_event(self, esp_event):
9898
return AnymailTrackingEvent(
9999
description=None,
100100
esp_event=esp_event,
101-
# SendinBlue doesn't provide a unique event id:
101+
# Brevo doesn't provide a unique event id:
102102
event_id=None,
103103
event_type=event_type,
104104
message_id=esp_event.get("message-id"),
@@ -113,8 +113,10 @@ def esp_to_anymail_event(self, esp_event):
113113
)
114114

115115

116-
class SendinBlueInboundWebhookView(SendinBlueBaseWebhookView):
117-
"""Handler for SendinBlue inbound email webhooks"""
116+
class BrevoInboundWebhookView(BrevoBaseWebhookView):
117+
"""Handler for Brevo inbound email webhooks"""
118+
119+
# https://developers.brevo.com/docs/inbound-parse-webhooks#parsed-email-payload
118120

119121
signal = inbound
120122

@@ -141,10 +143,10 @@ def parse_events(self, request):
141143
try:
142144
esp_events = payload["items"]
143145
except KeyError:
144-
# This is not n inbound webhook post
146+
# This is not an inbound webhook post
145147
raise AnymailConfigurationError(
146-
"You seem to have set SendinBlue's *tracking* webhook URL "
147-
"to Anymail's SendinBlue *inbound* webhook URL."
148+
f"You seem to have set Brevo's *tracking* webhook URL "
149+
f"to Anymail's {self.esp_name} *inbound* webhook URL."
148150
)
149151
else:
150152
return [self.esp_to_anymail_event(esp_event) for esp_event in esp_events]
@@ -199,7 +201,7 @@ def esp_to_anymail_event(self, esp_event):
199201
)
200202

201203
def _fetch_attachment(self, attachment):
202-
# Download attachment content from SendinBlue API.
204+
# Download attachment content from Brevo API.
203205
# FUTURE: somehow defer download until attachment is accessed?
204206
token = attachment["DownloadToken"]
205207
url = urljoin(self.api_url, f"inbound/attachments/{quote(token, safe='')}")

0 commit comments

Comments
 (0)