mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Sendinblue: use latest API improvements (templates, tags)
Track Sendinblue API updates: * Multiple tags are now supported * When using a template, display name is now supported on 'to', 'bcc', 'cc' and 'replyTo' * Templates now support overriding 'from_email' and 'subject' * Templates no longer require separate API endpoint * 'merge_global_data' can be used without templates
This commit is contained in:
committed by
Mike Edmunds
parent
73a73ea01f
commit
989d56bd85
@@ -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,9 +74,6 @@ 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"
|
||||
|
||||
def init_payload(self):
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <sendinblue-templates>`
|
||||
If you are sending using a SendinBlue template, their API doesn't support overriding the template's
|
||||
body. See the :ref:`templates <sendinblue-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 <alice@example.com>"]`
|
||||
will result in an unsupported feature error. (SendinBlue supports display names
|
||||
only in *non*-template sends.)
|
||||
|
||||
|
||||
.. _sendinblue-webhooks:
|
||||
|
||||
|
||||
@@ -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 <to@example.com>'], # single 'to' recommended (all 'to' get the same message)
|
||||
cc=['Recipient <cc1@example.com>', 'Recipient <cc2@example.com>'],
|
||||
bcc=['Recipient <bcc@example.com>'],
|
||||
reply_to=['Recipient <reply@example.com>'],
|
||||
)
|
||||
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 <to@example.com>"]
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, "display names in to when sending with a template"):
|
||||
message.send()
|
||||
message.to = ["to@example.com"]
|
||||
|
||||
message.cc = ["Recipient <cc@example.com>"]
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, "display names in cc when sending with a template"):
|
||||
message.send()
|
||||
message.cc = []
|
||||
|
||||
message.bcc = ["Recipient <bcc@example.com>"]
|
||||
with self.assertRaisesMessage(AnymailUnsupportedFeature, "display names in bcc when sending with a template"):
|
||||
message.send()
|
||||
message.bcc = []
|
||||
|
||||
message.reply_to = ["Reply-To <reply@example.com>"]
|
||||
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 = {
|
||||
@@ -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'})
|
||||
|
||||
Reference in New Issue
Block a user