diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 4005d1ac..2f161ccb 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -40,6 +40,7 @@ jobs: # combination, to avoid rapidly consuming the testing accounts' entire send allotments. config: - { tox: django41-py310-amazon_ses, python: "3.10" } + - { tox: django41-py310-brevo, python: "3.10" } - { tox: django41-py310-mailersend, python: "3.10" } - { tox: django41-py310-mailgun, python: "3.10" } - { tox: django41-py310-mailjet, python: "3.10" } @@ -48,7 +49,6 @@ jobs: - { tox: django41-py310-postmark, python: "3.10" } - { tox: django41-py310-resend, python: "3.10" } - { tox: django41-py310-sendgrid, python: "3.10" } - - { tox: django41-py310-sendinblue, python: "3.10" } - { tox: django41-py310-sparkpost, python: "3.10" } - { tox: django41-py310-unisender_go, python: "3.10" } @@ -77,6 +77,8 @@ jobs: ANYMAIL_TEST_AMAZON_SES_DOMAIN: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_DOMAIN }} ANYMAIL_TEST_AMAZON_SES_REGION_NAME: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_REGION_NAME }} ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY: ${{ secrets.ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY }} + ANYMAIL_TEST_BREVO_API_KEY: ${{ secrets.ANYMAIL_TEST_BREVO_API_KEY }} + ANYMAIL_TEST_BREVO_DOMAIN: ${{ vars.ANYMAIL_TEST_BREVO_DOMAIN }} ANYMAIL_TEST_MAILERSEND_API_TOKEN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_API_TOKEN }} ANYMAIL_TEST_MAILERSEND_DOMAIN: ${{ secrets.ANYMAIL_TEST_MAILERSEND_DOMAIN }} ANYMAIL_TEST_MAILGUN_API_KEY: ${{ secrets.ANYMAIL_TEST_MAILGUN_API_KEY }} @@ -94,8 +96,6 @@ jobs: ANYMAIL_TEST_SENDGRID_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDGRID_API_KEY }} ANYMAIL_TEST_SENDGRID_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDGRID_DOMAIN }} ANYMAIL_TEST_SENDGRID_TEMPLATE_ID: ${{ secrets.ANYMAIL_TEST_SENDGRID_TEMPLATE_ID }} - ANYMAIL_TEST_SENDINBLUE_API_KEY: ${{ secrets.ANYMAIL_TEST_SENDINBLUE_API_KEY }} - ANYMAIL_TEST_SENDINBLUE_DOMAIN: ${{ secrets.ANYMAIL_TEST_SENDINBLUE_DOMAIN }} ANYMAIL_TEST_SPARKPOST_API_KEY: ${{ secrets.ANYMAIL_TEST_SPARKPOST_API_KEY }} ANYMAIL_TEST_SPARKPOST_DOMAIN: ${{ secrets.ANYMAIL_TEST_SPARKPOST_DOMAIN }} ANYMAIL_TEST_UNISENDER_GO_API_KEY: ${{ secrets.ANYMAIL_TEST_UNISENDER_GO_API_KEY }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 050786ec..f6a5c493 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `__ + for details. + + Features ~~~~~~~~ diff --git a/anymail/backends/sendinblue.py b/anymail/backends/brevo.py similarity index 91% rename from anymail/backends/sendinblue.py rename to anymail/backends/brevo.py index e469c915..55124236 100644 --- a/anymail/backends/sendinblue.py +++ b/anymail/backends/brevo.py @@ -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""" @@ -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 = [] @@ -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, @@ -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 @@ -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: @@ -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( { @@ -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, @@ -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 diff --git a/anymail/urls.py b/anymail/urls.py index 28647b4e..8642b9b7 100644 --- a/anymail/urls.py +++ b/anymail/urls.py @@ -4,6 +4,7 @@ AmazonSESInboundWebhookView, AmazonSESTrackingWebhookView, ) +from .webhooks.brevo import BrevoInboundWebhookView, BrevoTrackingWebhookView from .webhooks.mailersend import ( MailerSendInboundWebhookView, MailerSendTrackingWebhookView, @@ -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, @@ -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(), @@ -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(), @@ -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(), @@ -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(), diff --git a/anymail/webhooks/sendinblue.py b/anymail/webhooks/brevo.py similarity index 86% rename from anymail/webhooks/sendinblue.py rename to anymail/webhooks/brevo.py index 6cdd4350..38e91b2f 100644 --- a/anymail/webhooks/sendinblue.py +++ b/anymail/webhooks/brevo.py @@ -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 @@ -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), @@ -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 @@ -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"), @@ -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 @@ -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] @@ -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='')}") diff --git a/docs/esps/brevo.rst b/docs/esps/brevo.rst index acba2773..1e1be7e0 100644 --- a/docs/esps/brevo.rst +++ b/docs/esps/brevo.rst @@ -4,14 +4,24 @@ Brevo ===== +.. Docs note: esps/sendinblue is redirected to esps/brevo in ReadTheDocs config. + Please preserve existing _sendinblue-* ref labels, so that redirected link + anchors work properly (in old links from external sites). E.g.: + an old link: https://anymail.dev/en/stable/esps/sendinblue#sendinblue-templates + redirects to: https://anymail.dev/en/stable/esps/brevo#sendinblue-templates + which is also: https://anymail.dev/en/stable/esps/brevo#brevo-templates + (There's no need to create _sendinblue-* duplicates of any new _brevo-* labels.) + Anymail integrates with the `Brevo`_ email service (formerly Sendinblue), using their `API v3`_. Brevo's transactional API does not support some basic email features, such as -inline images. Be sure to review the :ref:`limitations ` below. +inline images. Be sure to review the :ref:`limitations ` below. -.. versionchanged:: 10.1 +.. versionchanged:: 10.3 - Brevo was called "Sendinblue" until May, 2023. To avoid unnecessary code changes, - Anymail still uses the old name in code (settings, backend, webhook urls, etc.). + SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 uses the new + name throughout its code; earlier versions used the old name. Code that + refers to "SendinBlue" should continue to work, but is now deprecated. + See :ref:`brevo-rename` for details. .. important:: @@ -36,14 +46,14 @@ To use Anymail's Brevo backend, set: .. code-block:: python - EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend" + EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend" in your settings.py. -.. setting:: ANYMAIL_SENDINBLUE_API_KEY +.. setting:: ANYMAIL_BREVO_API_KEY -.. rubric:: SENDINBLUE_API_KEY +.. rubric:: BREVO_API_KEY The API key can be retrieved from your Brevo `SMTP & API settings`_ on the "API Keys" tab (don't try to use an SMTP key). Required. @@ -55,23 +65,23 @@ Anymail. If you don't see a v3 key listed, use "Create a New API Key".) ANYMAIL = { ... - "SENDINBLUE_API_KEY": "", + "BREVO_API_KEY": "", } -Anymail will also look for ``SENDINBLUE_API_KEY`` at the -root of the settings file if neither ``ANYMAIL["SENDINBLUE_API_KEY"]`` -nor ``ANYMAIL_SENDINBLUE_API_KEY`` is set. +Anymail will also look for ``BREVO_API_KEY`` at the +root of the settings file if neither ``ANYMAIL["BREVO_API_KEY"]`` +nor ``ANYMAIL_BREVO_API_KEY`` is set. .. _SMTP & API settings: https://app.brevo.com/settings/keys/api -.. setting:: ANYMAIL_SENDINBLUE_API_URL +.. setting:: ANYMAIL_BREVO_API_URL -.. rubric:: SENDINBLUE_API_URL +.. rubric:: BREVO_API_URL The base url for calling the Brevo API. -The default is ``SENDINBLUE_API_URL = "https://api.brevo.com/v3/"`` +The default is ``BREVO_API_URL = "https://api.brevo.com/v3/"`` (It's unlikely you would need to change this.) .. versionchanged:: 10.1 @@ -79,6 +89,7 @@ The default is ``SENDINBLUE_API_URL = "https://api.brevo.com/v3/"`` Earlier Anymail releases used ``https://api.sendinblue.com/v3/``. +.. _brevo-esp-extra: .. _sendinblue-esp-extra: esp_extra support @@ -106,6 +117,7 @@ to apply it to all messages.) .. _smtp/email API: https://developers.brevo.com/reference/sendtransacemail +.. _brevo-limitations: .. _sendinblue-limitations: Limitations and quirks @@ -192,6 +204,7 @@ Brevo can handle. on individual messages. +.. _brevo-templates: .. _sendinblue-templates: Batch sending/merge and ESP templates @@ -267,9 +280,9 @@ message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``. .. caution:: - **Sendinblue "old template language" not supported** + **"Old template language" not supported** - Sendinblue once supported two different template styles: a "new" template + Brevo once supported two different template styles: a "new" template language that uses Django-like template syntax (with ``{{ param.NAME }}`` substitutions), and an "old" template language that used percent-delimited ``%NAME%`` substitutions. @@ -299,17 +312,18 @@ message's headers: ``message.extra_headers = {"idempotencyKey": "...uuid..."}``. https://help.brevo.com/hc/en-us/articles/360000991960 +.. _brevo-webhooks: .. _sendinblue-webhooks: Status tracking webhooks ------------------------ If you are using Anymail's normalized :ref:`status tracking `, add -the url at Brevo's site under `Transactional > Email > Settings > Webhook`_. +the url at Brevo's site under `Transactional > Email > Settings > Webhook`_. The "URL to call" is: - :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendinblue/tracking/` + :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/brevo/tracking/` * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret * *yoursite.example.com* is your Django site @@ -336,10 +350,17 @@ For example, it's not uncommon to receive a "delivered" event before the corresp The event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event` field will be a `dict` of raw webhook data received from Brevo. +.. versionchanged:: 10.3 + + Older Anymail versions used a tracking webhook URL containing "sendinblue" rather + than "brevo". The old URL will still work, but is deprecated. See :ref:`brevo-rename` + below. + .. _Transactional > Email > Settings > Webhook: https://app-smtp.brevo.com/webhook +.. _brevo-inbound: .. _sendinblue-inbound: Inbound webhook @@ -353,7 +374,7 @@ guide to enable inbound service and add Anymail's inbound webhook. At the "Creating the webhook" step, set the ``"url"`` param to: - :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendinblue/inbound/` + :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/brevo/inbound/` * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret * *yoursite.example.com* is your Django site @@ -364,6 +385,12 @@ by entering your API key in "Header" field above the example, and then clicking "Try It!". The `webhooks management APIs`_ and `inbound events list API`_ can be helpful for diagnosing inbound issues. +.. versionchanged:: 10.3 + + Older Anymail versions used an inbound webhook URL containing "sendinblue" rather + than "brevo". The old URL will still work, but is deprecated. See :ref:`brevo-rename` + below. + .. _Inbound parsing webhooks: https://developers.brevo.com/docs/inbound-parse-webhooks @@ -371,3 +398,101 @@ be helpful for diagnosing inbound issues. https://developers.brevo.com/reference/getwebhooks-1 .. _inbound events list API: https://developers.brevo.com/reference/getinboundemailevents + + +.. _brevo-rename: + +Updating code from SendinBlue to Brevo +-------------------------------------- + +SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 has switched +to the new name. + +If your code refers to the old "sendinblue" name +(in :setting:`!EMAIL_BACKEND` and :setting:`!ANYMAIL` settings, :attr:`!esp_name` +checks, or elsewhere) you should update it to use "brevo" instead. +If you are using Anymail's tracking or inbound webhooks, you should +also update the webhook URLs you've configured at Brevo. + +For compatibility, code and URLs using the old name are still functional in Anymail. +But they will generate deprecation warnings, and may be removed in a future release. + +To update your code: + +.. setting:: ANYMAIL_SENDINBLUE_API_KEY +.. setting:: ANYMAIL_SENDINBLUE_API_URL + +1. In your settings.py, update the :setting:`!EMAIL_BACKEND` + and rename any ``"SENDINBLUE_..."`` settings to ``"BREVO_..."``: + + .. code-block:: diff + + - EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend" # old + + EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend" # new + + ANYMAIL = { + ... + - "SENDINBLUE_API_KEY": "", # old + + "BREVO_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 `, + 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 `, + 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 = "" + + 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 diff --git a/pyproject.toml b/pyproject.toml index 1d50510b..d91c3294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ dependencies = [ # (For simplicity, requests is included in the base dependencies.) # (Do not use underscores in extra names: they get normalized to hyphens.) amazon-ses = ["boto3"] +brevo = [] mailersend = [] mailgun = [] mailjet = [] diff --git a/tests/test_sendinblue_backend.py b/tests/test_brevo_backend.py similarity index 93% rename from tests/test_sendinblue_backend.py rename to tests/test_brevo_backend.py index d1290c78..2d5fa0f7 100644 --- a/tests/test_sendinblue_backend.py +++ b/tests/test_brevo_backend.py @@ -32,19 +32,16 @@ ) -@tag("sendinblue") +@tag("brevo") @override_settings( - EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend", - ANYMAIL={"SENDINBLUE_API_KEY": "test_api_key"}, + EMAIL_BACKEND="anymail.backends.brevo.EmailBackend", + ANYMAIL={"BREVO_API_KEY": "test_api_key"}, ) -class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase): - # SendinBlue v3 success responses are empty +class BrevoBackendMockAPITestCase(RequestsBackendMockAPITestCase): DEFAULT_RAW_RESPONSE = ( b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}' ) - DEFAULT_STATUS_CODE = ( - 201 # SendinBlue v3 uses '201 Created' for success (in most cases) - ) + DEFAULT_STATUS_CODE = 201 # Brevo v3 uses '201 Created' for success (in most cases) def setUp(self): super().setUp() @@ -54,8 +51,8 @@ def setUp(self): ) -@tag("sendinblue") -class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase): +@tag("brevo") +class BrevoBackendStandardEmailTests(BrevoBackendMockAPITestCase): """Test backend support for Django standard email features""" def test_send_mail(self): @@ -204,7 +201,7 @@ def test_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 = [ '"Reply recipient" "], reply_to=["Recipient "], ) - # SendinBlue uses per-account numeric ID to identify templates: + # Brevo uses per-account numeric ID to identify templates: message.template_id = 12 message.send() data = self.get_api_call_json() @@ -603,7 +600,7 @@ def test_esp_extra(self): # noinspection PyUnresolvedReferences def test_send_attaches_anymail_status(self): """The anymail_status should be attached to the message when it is sent""" - # the DEFAULT_RAW_RESPONSE above is the *only* success response SendinBlue + # the DEFAULT_RAW_RESPONSE above is the *only* success response Brevo # returns, so no need to override it here msg = mail.EmailMessage( "Subject", @@ -652,39 +649,37 @@ def test_json_serialization_errors(self): err = cm.exception self.assertIsInstance(err, TypeError) # compatibility with json.dumps # our added context: - self.assertIn("Don't know how to send this data to SendinBlue", str(err)) + self.assertIn("Don't know how to send this data to Brevo", str(err)) # original message self.assertRegex(str(err), r"Decimal.*is not JSON serializable") -@tag("sendinblue") -class SendinBlueBackendRecipientsRefusedTests(SendinBlueBackendMockAPITestCase): +@tag("brevo") +class BrevoBackendRecipientsRefusedTests(BrevoBackendMockAPITestCase): """ Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid """ - # SendinBlue doesn't check email bounce or complaint lists at time of send -- + # Brevo doesn't check email bounce or complaint lists at time of send -- # it always just queues the message. You'll need to listen for the "rejected" # and "failed" events to detect refused recipients. pass # not applicable to this backend -@tag("sendinblue") -class SendinBlueBackendSessionSharingTestCase( - SessionSharingTestCases, SendinBlueBackendMockAPITestCase +@tag("brevo") +class BrevoBackendSessionSharingTestCase( + SessionSharingTestCases, BrevoBackendMockAPITestCase ): """Requests session sharing tests""" pass # tests are defined in SessionSharingTestCases -@tag("sendinblue") -@override_settings(EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend") -class SendinBlueBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase): +@tag("brevo") +@override_settings(EMAIL_BACKEND="anymail.backends.brevo.EmailBackend") +class BrevoBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase): """Test ESP backend without required settings in place""" def test_missing_auth(self): - with self.assertRaisesRegex( - AnymailConfigurationError, r"\bSENDINBLUE_API_KEY\b" - ): + with self.assertRaisesRegex(AnymailConfigurationError, r"\bBREVO_API_KEY\b"): mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"]) diff --git a/tests/test_sendinblue_inbound.py b/tests/test_brevo_inbound.py similarity index 92% rename from tests/test_sendinblue_inbound.py rename to tests/test_brevo_inbound.py index 0e7e65eb..7dddc6eb 100644 --- a/tests/test_sendinblue_inbound.py +++ b/tests/test_brevo_inbound.py @@ -7,15 +7,15 @@ from anymail.exceptions import AnymailConfigurationError from anymail.inbound import AnymailInboundMessage from anymail.signals import AnymailInboundEvent -from anymail.webhooks.sendinblue import SendinBlueInboundWebhookView +from anymail.webhooks.brevo import BrevoInboundWebhookView from .utils import sample_email_content, sample_image_content from .webhook_cases import WebhookTestCase -@tag("sendinblue") -@override_settings(ANYMAIL_SENDINBLUE_API_KEY="test-api-key") -class SendinBlueInboundTestCase(WebhookTestCase): +@tag("brevo") +@override_settings(ANYMAIL_BREVO_API_KEY="test-api-key") +class BrevoInboundTestCase(WebhookTestCase): def test_inbound_basics(self): # Actual (sanitized) Brevo inbound message payload 7/2023 raw_event = { @@ -54,16 +54,16 @@ def test_inbound_basics(self): } response = self.client.post( - "/anymail/sendinblue/inbound/", + "/anymail/brevo/inbound/", content_type="application/json", data={"items": [raw_event]}, ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.inbound_handler, - sender=SendinBlueInboundWebhookView, + sender=BrevoInboundWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) # AnymailInboundEvent event = kwargs["event"] @@ -123,15 +123,15 @@ def test_envelope_attrs(self): } } self.client.post( - "/anymail/sendinblue/inbound/", + "/anymail/brevo/inbound/", content_type="application/json", data={"items": [raw_event]}, ) kwargs = self.assert_handler_called_once_with( self.inbound_handler, - sender=SendinBlueInboundWebhookView, + sender=BrevoInboundWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] message = event.message @@ -203,16 +203,16 @@ def test_attachments(self): ) response = self.client.post( - "/anymail/sendinblue/inbound/", + "/anymail/brevo/inbound/", content_type="application/json", data={"items": [raw_event]}, ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.inbound_handler, - sender=SendinBlueInboundWebhookView, + sender=BrevoInboundWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] message = event.message @@ -235,12 +235,12 @@ def test_attachments(self): def test_misconfigured_tracking(self): errmsg = ( - "You seem to have set SendinBlue's *tracking* webhook URL" - " to Anymail's SendinBlue *inbound* webhook URL." + "You seem to have set Brevo's *tracking* webhook URL" + " to Anymail's Brevo *inbound* webhook URL." ) with self.assertRaisesMessage(AnymailConfigurationError, errmsg): self.client.post( - "/anymail/sendinblue/inbound/", + "/anymail/brevo/inbound/", content_type="application/json", data={"event": "delivered"}, ) diff --git a/tests/test_sendinblue_integration.py b/tests/test_brevo_integration.py similarity index 73% rename from tests/test_sendinblue_integration.py rename to tests/test_brevo_integration.py index 0acdb2dd..c9d472f5 100644 --- a/tests/test_sendinblue_integration.py +++ b/tests/test_brevo_integration.py @@ -10,39 +10,39 @@ from .utils import AnymailTestMixin -ANYMAIL_TEST_SENDINBLUE_API_KEY = os.getenv("ANYMAIL_TEST_SENDINBLUE_API_KEY") -ANYMAIL_TEST_SENDINBLUE_DOMAIN = os.getenv("ANYMAIL_TEST_SENDINBLUE_DOMAIN") +ANYMAIL_TEST_BREVO_API_KEY = os.getenv("ANYMAIL_TEST_BREVO_API_KEY") +ANYMAIL_TEST_BREVO_DOMAIN = os.getenv("ANYMAIL_TEST_BREVO_DOMAIN") -@tag("sendinblue", "live") +@tag("brevo", "live") @unittest.skipUnless( - ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN, - "Set ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN " - "environment variables to run SendinBlue integration tests", + ANYMAIL_TEST_BREVO_API_KEY and ANYMAIL_TEST_BREVO_DOMAIN, + "Set ANYMAIL_TEST_BREVO_API_KEY and ANYMAIL_TEST_BREVO_DOMAIN " + "environment variables to run Brevo integration tests", ) @override_settings( - ANYMAIL_SENDINBLUE_API_KEY=ANYMAIL_TEST_SENDINBLUE_API_KEY, - ANYMAIL_SENDINBLUE_SEND_DEFAULTS=dict(), - EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend", + ANYMAIL_BREVO_API_KEY=ANYMAIL_TEST_BREVO_API_KEY, + ANYMAIL_BREVO_SEND_DEFAULTS=dict(), + EMAIL_BACKEND="anymail.backends.brevo.EmailBackend", ) -class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase): - """SendinBlue v3 API integration tests +class BrevoBackendIntegrationTests(AnymailTestMixin, SimpleTestCase): + """Brevo v3 API integration tests - SendinBlue doesn't have sandbox so these tests run - against the **live** SendinBlue API, using the - environment variable `ANYMAIL_TEST_SENDINBLUE_API_KEY` as the API key, - and `ANYMAIL_TEST_SENDINBLUE_DOMAIN` to construct sender addresses. + Brevo doesn't have sandbox so these tests run + against the **live** Brevo API, using the + environment variable `ANYMAIL_TEST_BREVO_API_KEY` as the API key, + and `ANYMAIL_TEST_BREVO_DOMAIN` to construct sender addresses. If those variables are not set, these tests won't run. - https://developers.sendinblue.com/docs/faq#section-how-can-i-test-the-api- + https://developers.brevo.com/docs/faq#how-can-i-test-the-api """ def setUp(self): super().setUp() - self.from_email = "from@%s" % ANYMAIL_TEST_SENDINBLUE_DOMAIN + self.from_email = "from@%s" % ANYMAIL_TEST_BREVO_DOMAIN self.message = AnymailMessage( - "Anymail SendinBlue integration test", + "Anymail Brevo integration test", "Text content", self.from_email, ["test+to1@anymail.dev"], @@ -50,7 +50,7 @@ def setUp(self): self.message.attach_alternative("

HTML content

", "text/html") def test_simple_send(self): - # Example of getting the SendinBlue send status and message id from the message + # Example of getting the Brevo send status and message id from the message sent_count = self.message.send() self.assertEqual(sent_count, 1) @@ -58,7 +58,7 @@ def test_simple_send(self): sent_status = anymail_status.recipients["test+to1@anymail.dev"].status message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id - self.assertEqual(sent_status, "queued") # SendinBlue always queues + self.assertEqual(sent_status, "queued") # Brevo always queues # Message-ID can be ...@smtp-relay.mail.fr or .sendinblue.com: self.assertRegex(message_id, r"\<.+@.+\>") # set of all recipient statuses: @@ -68,27 +68,27 @@ def test_simple_send(self): def test_all_options(self): send_at = datetime.now() + timedelta(minutes=2) message = AnymailMessage( - subject="Anymail SendinBlue all-options integration test", + subject="Anymail Brevo all-options integration test", body="This is the text body", from_email=formataddr(("Test From, with comma", self.from_email)), to=["test+to1@anymail.dev", '"Recipient 2, OK?" '], cc=["test+cc1@anymail.dev", "Copy 2 "], bcc=["test+bcc1@anymail.dev", "Blind Copy 2 "], - # SendinBlue API v3 only supports single reply-to + # Brevo API v3 only supports single reply-to reply_to=['"Reply, with comma" '], headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3}, metadata={"meta1": "simple string", "meta2": 2}, send_at=send_at, tags=["tag 1", "tag 2"], ) - # SendinBlue requires an HTML body: + # Brevo requires an HTML body: message.attach_alternative("

HTML content

", "text/html") message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain") message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv") message.send() - # SendinBlue always queues: + # Brevo always queues: self.assertEqual(message.anymail_status.status, {"queued"}) self.assertRegex(message.anymail_status.message_id, r"\<.+@.+\>") @@ -118,7 +118,7 @@ def test_template(self): message.attach("attachment1.txt", "Here is some\ntext", "text/plain") message.send() - # SendinBlue always queues: + # Brevo always queues: self.assertEqual(message.anymail_status.status, {"queued"}) recipient_status = message.anymail_status.recipients self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "queued") @@ -135,11 +135,11 @@ def test_template(self): recipient_status["test+to2@anymail.dev"].message_id, ) - @override_settings(ANYMAIL_SENDINBLUE_API_KEY="Hey, that's not an API key!") + @override_settings(ANYMAIL_BREVO_API_KEY="Hey, that's not an API key!") def test_invalid_api_key(self): with self.assertRaises(AnymailAPIError) as cm: self.message.send() err = cm.exception self.assertEqual(err.status_code, 401) - # Make sure the exception message includes SendinBlue's response: + # Make sure the exception message includes Brevo's response: self.assertIn("Key not found", str(err)) diff --git a/tests/test_sendinblue_webhooks.py b/tests/test_brevo_webhooks.py similarity index 82% rename from tests/test_sendinblue_webhooks.py rename to tests/test_brevo_webhooks.py index a2b224c6..c29539ac 100644 --- a/tests/test_sendinblue_webhooks.py +++ b/tests/test_brevo_webhooks.py @@ -6,16 +6,16 @@ from anymail.exceptions import AnymailConfigurationError from anymail.signals import AnymailTrackingEvent -from anymail.webhooks.sendinblue import SendinBlueTrackingWebhookView +from anymail.webhooks.brevo import BrevoTrackingWebhookView from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase -@tag("sendinblue") -class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase): +@tag("brevo") +class BrevoWebhookSecurityTestCase(WebhookBasicAuthTestCase): def call_webhook(self): return self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps({}), ) @@ -23,23 +23,22 @@ def call_webhook(self): # Actual tests are in WebhookBasicAuthTestCase -@tag("sendinblue") -class SendinBlueDeliveryTestCase(WebhookTestCase): - # SendinBlue's webhook payload data is partially documented at - # https://help.sendinblue.com/hc/en-us/articles/360007666479, - # but it's not completely up to date. +@tag("brevo") +class BrevoDeliveryTestCase(WebhookTestCase): + # Brevo's webhook payload data is documented at + # https://developers.brevo.com/docs/transactional-webhooks. # The payloads below were obtained through live testing. def test_sent_event(self): raw_event = { "event": "request", "email": "recipient@example.com", - "id": 9999999, # this seems to be SendinBlue account id (not an event id) + "id": 9999999, # this seems to be Brevo account id (not an event id) "message-id": "<201803062010.27287306012@smtp-relay.mailin.fr>", "subject": "Test subject", # From a message sent at 2018-03-06 11:10:23-08:00 # (2018-03-06 19:10:23+00:00)... - "date": "2018-03-06 11:10:23", # tz from SendinBlue account's preferences + "date": "2018-03-06 11:10:23", # tz from Brevo account's preferences "ts": 1520331023, # 2018-03-06 10:10:23 -- what time zone is this? "ts_event": 1520331023, # unclear if this ever differs from "ts" "ts_epoch": 1520363423000, # 2018-03-06 19:10:23.000+00:00 -- UTC (msec) @@ -55,16 +54,16 @@ def test_sent_event(self): "sending_ip": "333.33.33.33", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertIsInstance(event, AnymailTrackingEvent) @@ -77,7 +76,7 @@ def test_sent_event(self): self.assertEqual( event.message_id, "<201803062010.27287306012@smtp-relay.mailin.fr>" ) - # SendinBlue does not provide a unique event id: + # Brevo does not provide a unique event id: self.assertIsNone(event.event_id) self.assertEqual(event.recipient, "recipient@example.com") self.assertEqual(event.metadata, {"meta": "data"}) @@ -93,16 +92,16 @@ def test_delivered_event(self): "message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertIsInstance(event, AnymailTrackingEvent) @@ -128,16 +127,16 @@ def test_hard_bounce(self): "tag": "header-tag", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "bounced") @@ -158,16 +157,16 @@ def test_soft_bounce_event(self): "reason": "undefined Unable to find MX of domain no-mx.example.com", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "bounced") @@ -188,16 +187,16 @@ def test_blocked(self): "reason": "blocked : due to blacklist user", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "rejected") @@ -214,16 +213,16 @@ def test_spam(self): "message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "complained") @@ -231,7 +230,7 @@ def test_spam(self): def test_invalid_email(self): # "If a ISP again indicated us that the email is not valid or if we discovered # that the email is not valid." (unclear whether this error originates with the - # receiving MTA or with SendinBlue pre-send) (haven't observed "invalid_email" + # receiving MTA or with Brevo pre-send) (haven't observed "invalid_email" # event in actual testing; payload below is a guess) raw_event = { "event": "invalid_email", @@ -241,16 +240,16 @@ def test_invalid_email(self): "reason": "(guessing invalid_email includes a reason)", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "bounced") @@ -262,7 +261,7 @@ def test_invalid_email(self): def test_deferred_event(self): # Note: the example below is an actual event capture (with 'example.com' # substituted for the real receiving domain). It's pretty clearly a bounce, not - # a deferral. It looks like SendinBlue mis-categorizes this SMTP response code. + # a deferral. It looks like Brevo mis-categorizes this SMTP response code. raw_event = { "event": "deferred", "email": "notauser@example.com", @@ -272,16 +271,16 @@ def test_deferred_event(self): " address rejected: User unknown in virtual alias table", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "deferred") @@ -294,7 +293,7 @@ def test_deferred_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 # both on the first open.) raw_event = { @@ -304,20 +303,20 @@ def test_opened_event(self): "message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "opened") - self.assertIsNone(event.user_agent) # SendinBlue doesn't report user agent + self.assertIsNone(event.user_agent) # Brevo doesn't report user agent def test_unique_opened_event(self): # See note in test_opened_event above @@ -328,16 +327,16 @@ def test_unique_opened_event(self): "message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "opened") @@ -351,21 +350,21 @@ def test_clicked_event(self): "link": "https://example.com/click/me", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "clicked") self.assertEqual(event.click_url, "https://example.com/click/me") - self.assertIsNone(event.user_agent) # SendinBlue doesn't report user agent + self.assertIsNone(event.user_agent) # Brevo doesn't report user agent def test_unsubscribe(self): # "When a person unsubscribes from the email received." @@ -378,28 +377,28 @@ def test_unsubscribe(self): "message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>", } response = self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data=json.dumps(raw_event), ) self.assertEqual(response.status_code, 200) kwargs = self.assert_handler_called_once_with( self.tracking_handler, - sender=SendinBlueTrackingWebhookView, + sender=BrevoTrackingWebhookView, event=ANY, - esp_name="SendinBlue", + esp_name="Brevo", ) event = kwargs["event"] self.assertEqual(event.event_type, "unsubscribed") def test_misconfigured_inbound(self): errmsg = ( - "You seem to have set SendinBlue's *inbound* webhook URL" - " to Anymail's SendinBlue *tracking* webhook URL." + "You seem to have set Brevo's *inbound* webhook URL" + " to Anymail's Brevo *tracking* webhook URL." ) with self.assertRaisesMessage(AnymailConfigurationError, errmsg): self.client.post( - "/anymail/sendinblue/tracking/", + "/anymail/brevo/tracking/", content_type="application/json", data={"items": []}, ) diff --git a/tox.ini b/tox.ini index 97dd7061..1579487b 100644 --- a/tox.ini +++ b/tox.ini @@ -59,6 +59,7 @@ setenv = # (resend should work with or without its extras, so it isn't in `none`) none: ANYMAIL_SKIP_TESTS=amazon_ses,postal amazon_ses: ANYMAIL_ONLY_TEST=amazon_ses + brevo: ANYMAIL_ONLY_TEST=brevo mailersend: ANYMAIL_ONLY_TEST=mailersend mailgun: ANYMAIL_ONLY_TEST=mailgun mailjet: ANYMAIL_ONLY_TEST=mailjet @@ -68,7 +69,6 @@ setenv = resend: ANYMAIL_ONLY_TEST=resend sendgrid: ANYMAIL_ONLY_TEST=sendgrid unisender_go: ANYMAIL_ONLY_TEST=unisender_go - sendinblue: ANYMAIL_ONLY_TEST=sendinblue sparkpost: ANYMAIL_ONLY_TEST=sparkpost ignore_outcome = # CI that wants to handle errors itself can set TOX_OVERRIDE_IGNORE_OUTCOME=false