diff --git a/anymail/backends/sendinblue.py b/anymail/backends/sendinblue.py new file mode 100644 index 00000000..15bd3d90 --- /dev/null +++ b/anymail/backends/sendinblue.py @@ -0,0 +1,20 @@ +import warnings + +from ..exceptions import AnymailDeprecationWarning +from .brevo import EmailBackend as BrevoEmailBackend + + +class EmailBackend(BrevoEmailBackend): + """ + Deprecated compatibility backend for old Brevo name "SendinBlue". + """ + + esp_name = "SendinBlue" + + def __init__(self, **kwargs): + warnings.warn( + "`anymail.backends.sendinblue.EmailBackend` has been renamed" + " `anymail.backends.brevo.EmailBackend`.", + AnymailDeprecationWarning, + ) + super().__init__(**kwargs) diff --git a/anymail/urls.py b/anymail/urls.py index 8642b9b7..050d9b76 100644 --- a/anymail/urls.py +++ b/anymail/urls.py @@ -16,6 +16,10 @@ 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, @@ -68,6 +72,12 @@ SendGridInboundWebhookView.as_view(), name="sendgrid_inbound_webhook", ), + path( + # Compatibility for old SendinBlue esp_name; use Brevo in new code + "sendinblue/inbound/", + SendinBlueInboundWebhookView.as_view(), + name="sendinblue_inbound_webhook", + ), path( "sparkpost/inbound/", SparkPostInboundWebhookView.as_view(), @@ -118,6 +128,12 @@ SendGridTrackingWebhookView.as_view(), name="sendgrid_tracking_webhook", ), + path( + # Compatibility for old SendinBlue esp_name; use Brevo in new code + "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/sendinblue.py new file mode 100644 index 00000000..61d180c8 --- /dev/null +++ b/anymail/webhooks/sendinblue.py @@ -0,0 +1,38 @@ +import warnings + +from ..exceptions import AnymailDeprecationWarning +from .brevo import BrevoInboundWebhookView, BrevoTrackingWebhookView + + +class SendinBlueTrackingWebhookView(BrevoTrackingWebhookView): + """ + Deprecated compatibility tracking webhook for old Brevo name "SendinBlue". + """ + + esp_name = "SendinBlue" + + def __init__(self, **kwargs): + warnings.warn( + "Anymail's SendinBlue webhook URLs are deprecated." + " Update your Brevo transactional email webhook URL to change" + " 'anymail/sendinblue' to 'anymail/brevo'.", + AnymailDeprecationWarning, + ) + super().__init__(**kwargs) + + +class SendinBlueInboundWebhookView(BrevoInboundWebhookView): + """ + Deprecated compatibility inbound webhook for old Brevo name "SendinBlue". + """ + + esp_name = "SendinBlue" + + def __init__(self, **kwargs): + warnings.warn( + "Anymail's SendinBlue webhook URLs are deprecated." + " Update your Brevo inbound webhook URL to change" + " 'anymail/sendinblue' to 'anymail/brevo'.", + AnymailDeprecationWarning, + ) + super().__init__(**kwargs) diff --git a/tests/test_sendinblue_deprecations.py b/tests/test_sendinblue_deprecations.py new file mode 100644 index 00000000..9f401820 --- /dev/null +++ b/tests/test_sendinblue_deprecations.py @@ -0,0 +1,117 @@ +from unittest.mock import ANY + +from django.core.mail import EmailMessage, send_mail +from django.test import ignore_warnings, override_settings, tag + +from anymail.exceptions import AnymailConfigurationError, AnymailDeprecationWarning +from anymail.webhooks.sendinblue import ( + SendinBlueInboundWebhookView, + SendinBlueTrackingWebhookView, +) + +from .mock_requests_backend import RequestsBackendMockAPITestCase +from .webhook_cases import WebhookTestCase + + +@tag("brevo", "sendinblue") +@override_settings( + EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend", + ANYMAIL={"SENDINBLUE_API_KEY": "test_api_key"}, +) +@ignore_warnings(category=AnymailDeprecationWarning) +class SendinBlueBackendDeprecationTests(RequestsBackendMockAPITestCase): + DEFAULT_RAW_RESPONSE = ( + b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}' + ) + DEFAULT_STATUS_CODE = 201 # Brevo v3 uses '201 Created' for success (in most cases) + + def test_deprecation_warning(self): + message = EmailMessage( + "Subject", "Body", "from@example.com", ["to@example.com"] + ) + with self.assertWarnsMessage( + AnymailDeprecationWarning, + "`anymail.backends.sendinblue.EmailBackend` has been renamed" + " `anymail.backends.brevo.EmailBackend`.", + ): + message.send() + self.assert_esp_called("https://api.brevo.com/v3/smtp/email") + + @override_settings(ANYMAIL={"BREVO_API_KEY": "test_api_key"}) + def test_missing_api_key_error_uses_correct_setting_name(self): + # The sendinblue.EmailBackend requires SENDINBLUE_ settings names + with self.assertRaisesMessage(AnymailConfigurationError, "SENDINBLUE_API_KEY"): + send_mail("Subject", "Body", "from@example.com", ["to@example.com"]) + + +@tag("brevo", "sendinblue") +@ignore_warnings(category=AnymailDeprecationWarning) +class SendinBlueTrackingWebhookDeprecationTests(WebhookTestCase): + def test_deprecation_warning(self): + with self.assertWarnsMessage( + AnymailDeprecationWarning, + "Anymail's SendinBlue webhook URLs are deprecated.", + ): + response = self.client.post( + "/anymail/sendinblue/tracking/", + content_type="application/json", + data="{}", + ) + self.assertEqual(response.status_code, 200) + # Old url uses old names to preserve compatibility: + self.assert_handler_called_once_with( + self.tracking_handler, + sender=SendinBlueTrackingWebhookView, # *not* BrevoTrackingWebhookView + event=ANY, + esp_name="SendinBlue", # *not* "Brevo" + ) + + def test_misconfigured_inbound(self): + # Uses old esp_name when called on old URL + errmsg = ( + "You seem to have set Brevo's *inbound* webhook URL" + " to Anymail's SendinBlue *tracking* webhook URL." + ) + with self.assertRaisesMessage(AnymailConfigurationError, errmsg): + self.client.post( + "/anymail/sendinblue/tracking/", + content_type="application/json", + data={"items": []}, + ) + + +@tag("brevo", "sendinblue") +@override_settings(ANYMAIL_SENDINBLUE_API_KEY="test-api-key") +@ignore_warnings(category=AnymailDeprecationWarning) +class SendinBlueInboundWebhookDeprecationTests(WebhookTestCase): + def test_deprecation_warning(self): + with self.assertWarnsMessage( + AnymailDeprecationWarning, + "Anymail's SendinBlue webhook URLs are deprecated.", + ): + response = self.client.post( + "/anymail/sendinblue/inbound/", + content_type="application/json", + data='{"items":[{}]}', + ) + self.assertEqual(response.status_code, 200) + # Old url uses old names to preserve compatibility: + self.assert_handler_called_once_with( + self.inbound_handler, + sender=SendinBlueInboundWebhookView, # *not* BrevoInboundWebhookView + event=ANY, + esp_name="SendinBlue", # *not* "Brevo" + ) + + def test_misconfigured_tracking(self): + # Uses old esp_name when called on old URL + errmsg = ( + "You seem to have set Brevo's *tracking* webhook URL" + " to Anymail's SendinBlue *inbound* webhook URL." + ) + with self.assertRaisesMessage(AnymailConfigurationError, errmsg): + self.client.post( + "/anymail/sendinblue/inbound/", + content_type="application/json", + data={"event": "delivered"}, + )