diff --git a/anymail/backends/sendinblue.py b/anymail/backends/sendinblue.py index 35caf3c..4553d0b 100644 --- a/anymail/backends/sendinblue.py +++ b/anymail/backends/sendinblue.py @@ -1,5 +1,4 @@ from requests.structures import CaseInsensitiveDict -from six.moves.urllib.parse import quote from .base_requests import AnymailRequestsBackend, RequestsPayload from ..exceptions import AnymailRequestsAPIError @@ -67,7 +66,6 @@ class SendinBluePayload(RequestsPayload): def __init__(self, message, defaults, backend, *args, **kwargs): self.all_recipients = [] # used for backend.parse_recipient_status - self.template_id = None http_headers = kwargs.pop('headers', {}) http_headers['api-key'] = backend.api_key @@ -76,10 +74,7 @@ class SendinBluePayload(RequestsPayload): super(SendinBluePayload, self).__init__(message, defaults, backend, headers=http_headers, *args, **kwargs) def get_api_endpoint(self): - if self.template_id: - return "smtp/templates/%s/send" % quote(str(self.template_id), safe='') - else: - return "smtp/email" + return "smtp/email" def init_payload(self): self.data = { # becomes json @@ -91,47 +86,10 @@ class SendinBluePayload(RequestsPayload): if not self.data['headers']: del self.data['headers'] # don't send empty headers - - # SendinBlue use different argument's name if we use template functionality - if self.template_id: - data = self._transform_data_for_templated_email(self.data) - else: - data = self.data - - return self.serialize_json(data) - - def _transform_data_for_templated_email(self, data): - """ - Transform the default Payload's data (used for basic transactional email) to - the data used by SendinBlue in case of a templated transactional email. - :param data: The data we want to transform - :return: The transformed data - """ - if data.pop('subject', False): - self.unsupported_feature("overriding template subject") - if data.pop('sender', False): - self.unsupported_feature("overriding template from_email") - if data.pop('textContent', False) or data.pop('htmlContent', False): + if self.data.get('templateId') and (self.data.pop('textContent', False) or self.data.pop('htmlContent', False)): self.unsupported_feature("overriding template body content") - transformation = { - 'to': 'emailTo', - 'cc': 'emailCc', - 'bcc': 'emailBcc', - } - for key, new_key in transformation.items(): - if key in data: - if any(email.get('name') for email in data[key]): - self.unsupported_feature("display names in %s when sending with a template" % key) - data[new_key] = [email['email'] for email in data[key]] - del data[key] - - if 'replyTo' in data: - if data['replyTo'].get('name'): - self.unsupported_feature("display names in reply_to when sending with a template") - data['replyTo'] = data['replyTo']['email'] - - return data + return self.serialize_json(self.data) # # Payload construction @@ -171,12 +129,10 @@ class SendinBluePayload(RequestsPayload): def set_tags(self, tags): if len(tags) > 0: - self.data['headers']["X-Mailin-tag"] = tags[0] - if len(tags) > 1: - self.unsupported_feature('multiple tags (%r)' % tags) + self.data['tags'] = tags def set_template_id(self, template_id): - self.template_id = template_id + self.data['templateId'] = template_id def set_text_body(self, body): if body: @@ -209,7 +165,7 @@ class SendinBluePayload(RequestsPayload): self.unsupported_feature("merge_data") def set_merge_global_data(self, merge_global_data): - self.data['attributes'] = merge_global_data + self.data['params'] = merge_global_data def set_metadata(self, metadata): # SendinBlue expects a single string payload diff --git a/docs/esps/index.rst b/docs/esps/index.rst index 6a5fdbf..4480534 100644 --- a/docs/esps/index.rst +++ b/docs/esps/index.rst @@ -40,7 +40,7 @@ Email Service Provider |Amazon SES| |Mailgun| |Mailje :attr:`~AnymailMessage.metadata` Yes Yes Yes Yes Yes Yes Yes Yes :attr:`~AnymailMessage.merge_metadata` No Yes Yes Yes Yes Yes No Yes :attr:`~AnymailMessage.send_at` No Yes No Yes No Yes No Yes -:attr:`~AnymailMessage.tags` Yes Yes Max 1 tag Yes Max 1 tag Yes Max 1 tag Max 1 tag +:attr:`~AnymailMessage.tags` Yes Yes Max 1 tag Yes Max 1 tag Yes Yes Max 1 tag :attr:`~AnymailMessage.track_clicks` No Yes Yes Yes Yes Yes No Yes :attr:`~AnymailMessage.track_opens` No Yes Yes Yes Yes Yes No Yes diff --git a/docs/esps/sendinblue.rst b/docs/esps/sendinblue.rst index fb6eda6..39ac4a0 100644 --- a/docs/esps/sendinblue.rst +++ b/docs/esps/sendinblue.rst @@ -131,9 +131,8 @@ SendinBlue can handle. to the SendinBlue API if the name is not set correctly. **Additional template limitations** - If you are sending using a SendinBlue template, their API doesn't allow display - names in recipient or reply-to emails, and doesn't support overriding the template's - from_email, subject, or body. See the :ref:`templates ` + If you are sending using a SendinBlue template, their API doesn't support overriding the template's + body. See the :ref:`templates ` section below. **Single Reply-To** @@ -142,15 +141,6 @@ SendinBlue can handle. If you are ignoring unsupported features and have multiple reply addresses, Anymail will use only the first one. -**Single tag** - SendinBlue supports a single message tag, which can be used for filtering in their - dashboard statistics and logs panels, and is available in tracking webhooks. - Anymail will pass the first of a message's :attr:`~anymail.message.AnymailMessage.tags` - to SendinBlue, using their :mailheader:`X-Mailin-tag` email header. - - Trying to send a message with more than one tag will result in an error unless you - are ignoring unsupported features. - **Metadata** Anymail passes :attr:`~anymail.message.AnymailMessage.metadata` to SendinBlue as a JSON-encoded string using their :mailheader:`X-Mailin-custom` email header. @@ -189,7 +179,7 @@ the messages's :attr:`~anymail.message.AnymailMessage.merge_global_data`: .. code-block:: python message = EmailMessage( - subject=None, # required for SendinBlue templates + subject="My Subject", # optional for SendinBlue templates body=None, # required for SendinBlue templates to=["alice@example.com"] # single recipient... # ...multiple to emails would all get the same message @@ -208,15 +198,10 @@ variables using %-delimited names, e.g., `%order_no%` or `%ship_date%` from the example above. Note that SendinBlue's API does not permit overriding a template's -subject, body, or from_email. You *must* set them to `None` as shown above, +body. You *must* set it to `None` as shown above, or Anymail will raise an :exc:`~anymail.exceptions.AnymailUnsupportedFeature` error (if you are not ignoring unsupported features). -Also, SendinBlue's API does not permit display names in recipient or reply-to -emails when sending with a template. Code like `to=["Alice "]` -will result in an unsupported feature error. (SendinBlue supports display names -only in *non*-template sends.) - .. _sendinblue-webhooks: diff --git a/tests/test_sendinblue_backend.py b/tests/test_sendinblue_backend.py index f516855..e5f88e6 100644 --- a/tests/test_sendinblue_backend.py +++ b/tests/test_sendinblue_backend.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import json - from base64 import b64encode, b64decode from datetime import datetime from decimal import Decimal @@ -16,7 +15,6 @@ from django.utils.timezone import get_fixed_timezone, override as override_curre from anymail.exceptions import (AnymailAPIError, AnymailConfigurationError, AnymailSerializationError, AnymailUnsupportedFeature) from anymail.message import attach_inline_image_file - from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin @@ -309,15 +307,10 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): self.message.send() def test_tag(self): - self.message.tags = ["receipt"] + self.message.tags = ["receipt", "multiple"] self.message.send() data = self.get_api_call_json() - self.assertCountEqual(data['headers']["X-Mailin-tag"], "receipt") - - def test_multiple_tags(self): - self.message.tags = ["receipt", "repeat-user"] - with self.assertRaises(AnymailUnsupportedFeature): - self.message.send() + self.assertEqual(data['tags'], ["receipt", "multiple"]) def test_tracking(self): # Test one way... @@ -336,56 +329,26 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): def test_template_id(self): # subject, body, and from_email must be None for SendinBlue template send: message = mail.EmailMessage( - subject=None, + subject='My Subject', body=None, - # recipient and reply_to display names are not allowed - to=['alice@example.com'], # single 'to' recommended (all 'to' get the same message) - cc=['cc1@example.com', 'cc2@example.com'], - bcc=['bcc@example.com'], - reply_to=['reply@example.com'], + from_email='from@example.com', + to=['Recipient '], # single 'to' recommended (all 'to' get the same message) + cc=['Recipient ', 'Recipient '], + bcc=['Recipient '], + reply_to=['Recipient '], ) - message.from_email = None # from_email must be cleared after constructing EmailMessage - message.template_id = 12 # SendinBlue uses per-account numeric ID to identify templates - message.merge_global_data = { - 'buttonUrl': 'https://example.com', - } - message.send() - - # Template API call uses different endpoint and payload format from normal send: - self.assert_esp_called('/v3/smtp/templates/12/send') data = self.get_api_call_json() - self.assertEqual(data['emailTo'], ["alice@example.com"]) - self.assertEqual(data['emailCc'], ["cc1@example.com", "cc2@example.com"]) - self.assertEqual(data['emailBcc'], ["bcc@example.com"]) - self.assertEqual(data['replyTo'], 'reply@example.com') - self.assertEqual(data['attributes'], {'buttonUrl': 'https://example.com'}) - - # Make sure these normal-send parameters didn't get left in: - self.assertNotIn('to', data) - self.assertNotIn('cc', data) - self.assertNotIn('bcc', data) - self.assertNotIn('sender', data) - self.assertNotIn('subject', data) - self.assertNotIn('textContent', data) - self.assertNotIn('htmlContent', data) + self.assertEqual(data['templateId'], 12) + self.assertEqual(data['subject'], 'My Subject') + self.assertEqual(data['to'], [{'email': "to@example.com", 'name': 'Recipient'}]) def test_unsupported_template_overrides(self): # SendinBlue doesn't allow overriding any template headers/content message = mail.EmailMessage(to=['to@example.com']) message.template_id = "9" - message.from_email = "from@example.com" - with self.assertRaisesMessage(AnymailUnsupportedFeature, "overriding template from_email"): - message.send() - message.from_email = None - - message.subject = "nope, can't change template subject" - with self.assertRaisesMessage(AnymailUnsupportedFeature, "overriding template subject"): - message.send() - message.subject = None - message.body = "nope, can't change text body" with self.assertRaisesMessage(AnymailUnsupportedFeature, "overriding template body content"): message.send() @@ -394,33 +357,6 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): message.send() message.body = None - def test_unsupported_template_display_names(self): - # SendinBlue doesn't support display names in template recipients or reply-to - message = mail.EmailMessage() - message.from_email = None - message.template_id = "9" - - message.to = ["Recipient "] - with self.assertRaisesMessage(AnymailUnsupportedFeature, "display names in to when sending with a template"): - message.send() - message.to = ["to@example.com"] - - message.cc = ["Recipient "] - with self.assertRaisesMessage(AnymailUnsupportedFeature, "display names in cc when sending with a template"): - message.send() - message.cc = [] - - message.bcc = ["Recipient "] - with self.assertRaisesMessage(AnymailUnsupportedFeature, "display names in bcc when sending with a template"): - message.send() - message.bcc = [] - - message.reply_to = ["Reply-To "] - with self.assertRaisesMessage(AnymailUnsupportedFeature, - "display names in reply_to when sending with a template"): - message.send() - message.reply_to = [] - def test_merge_data(self): self.message.merge_data = { 'alice@example.com': {':name': "Alice", ':group': "Developers"}, @@ -429,6 +365,14 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): with self.assertRaises(AnymailUnsupportedFeature): self.message.send() + def test_merge_global_data(self): + self.message.merge_global_data = { + 'a': 'b' + } + self.message.send() + data = self.get_api_call_json() + self.assertEqual(data['params'], {'a': 'b'}) + def test_default_omits_options(self): """Make sure by default we don't send any ESP-specific options. @@ -445,7 +389,6 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): self.assertNotIn('atributes', data) def test_esp_extra(self): - self.message.tags = ["tag"] # SendinBlue doesn't offer any esp-extra but we will test # with some extra of SendGrid to see if it's work in the future self.message.esp_extra = { @@ -455,8 +398,8 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): }, 'tracking_settings': { 'subscription_tracking': { - 'enable': True, - 'substitution_tag': '[unsubscribe_url]', + 'enable': True, + 'substitution_tag': '[unsubscribe_url]', }, }, } @@ -467,15 +410,13 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase): self.assertEqual(data['asm'], {'group_id': 1}) self.assertEqual(data['tracking_settings']['subscription_tracking'], {'enable': True, 'substitution_tag': "[unsubscribe_url]"}) - # make sure we didn't overwrite Anymail message options: - self.assertEqual(data['headers']["X-Mailin-tag"], "tag") # 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 returns, # so no need to override it here - msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],) + msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'], ) sent = msg.send() self.assertEqual(sent, 1) self.assertEqual(msg.anymail_status.status, {'queued'})