Sendinblue: Support send_at

Add support for delayed sending via
Sendinblue's public beta "scheduledAt"
parameter.

Closes #280
This commit is contained in:
medmunds
2022-12-18 15:44:25 -08:00
committed by Mike Edmunds
parent a8cfb2e5eb
commit 287c2175f4
6 changed files with 58 additions and 9 deletions

View File

@@ -40,6 +40,9 @@ Breaking changes
Features Features
~~~~~~~~ ~~~~~~~~
* **Sendinblue:** Support delayed sending using Anymail's `send_at` option.
(Thanks to `@dimitrisor`_ for noting Sendinblue's public beta release
of this capability.)
* Support customizing the requests.Session for requests-based backends, * Support customizing the requests.Session for requests-based backends,
and document how this can be used to mount an adapter that simplifies and document how this can be used to mount an adapter that simplifies
automatic retry logic. (Thanks to `@dgilmanAIDENTIFIED`_.) automatic retry logic. (Thanks to `@dgilmanAIDENTIFIED`_.)
@@ -1366,6 +1369,7 @@ Features
.. _@coupa-anya: https://github.com/coupa-anya .. _@coupa-anya: https://github.com/coupa-anya
.. _@decibyte: https://github.com/decibyte .. _@decibyte: https://github.com/decibyte
.. _@dgilmanAIDENTIFIED: https://github.com/dgilmanAIDENTIFIED .. _@dgilmanAIDENTIFIED: https://github.com/dgilmanAIDENTIFIED
.. _@dimitrisor: https://github.com/dimitrisor
.. _@dominik-lekse: https://github.com/dominik-lekse .. _@dominik-lekse: https://github.com/dominik-lekse
.. _@erikdrums: https://github.com/erikdrums .. _@erikdrums: https://github.com/erikdrums
.. _@ewingrj: https://github.com/ewingrj .. _@ewingrj: https://github.com/ewingrj

View File

@@ -162,3 +162,10 @@ class SendinBluePayload(RequestsPayload):
def set_metadata(self, metadata): def set_metadata(self, metadata):
# SendinBlue expects a single string payload # SendinBlue expects a single string payload
self.data['headers']["X-Mailin-custom"] = self.serialize_json(metadata) self.data['headers']["X-Mailin-custom"] = self.serialize_json(metadata)
def set_send_at(self, send_at):
try:
start_time_iso = send_at.isoformat(timespec="milliseconds")
except (AttributeError, TypeError):
start_time_iso = send_at # assume user already formatted
self.data['scheduledAt'] = start_time_iso

View File

@@ -40,7 +40,7 @@ Email Service Provider |Amazon SES| |Mailgun| |Mailje
:attr:`~AnymailMessage.envelope_sender` Yes Domain only Yes Domain only Yes No No No Yes :attr:`~AnymailMessage.envelope_sender` Yes Domain only Yes Domain only Yes No No No Yes
:attr:`~AnymailMessage.metadata` Yes Yes Yes Yes No Yes Yes Yes Yes :attr:`~AnymailMessage.metadata` Yes Yes Yes Yes No Yes Yes Yes Yes
:attr:`~AnymailMessage.merge_metadata` No Yes Yes Yes No Yes Yes No Yes :attr:`~AnymailMessage.merge_metadata` No Yes Yes Yes No Yes Yes No Yes
:attr:`~AnymailMessage.send_at` No Yes No Yes No No Yes No Yes :attr:`~AnymailMessage.send_at` No Yes No Yes No No Yes Yes Yes
:attr:`~AnymailMessage.tags` Yes Yes Max 1 tag Yes Max 1 tag Max 1 tag Yes Yes Max 1 tag :attr:`~AnymailMessage.tags` Yes Yes Max 1 tag Yes Max 1 tag Max 1 tag Yes Yes Max 1 tag
:attr:`~AnymailMessage.track_clicks` No Yes Yes Yes No Yes Yes No Yes :attr:`~AnymailMessage.track_clicks` No Yes Yes Yes No Yes Yes No Yes
:attr:`~AnymailMessage.track_opens` No Yes Yes Yes No Yes Yes No Yes :attr:`~AnymailMessage.track_opens` No Yes Yes Yes No Yes Yes No Yes

View File

@@ -78,18 +78,20 @@ set a message's :attr:`~anymail.message.AnymailMessage.esp_extra` to
a `dict` that will be merged into the json sent to Sendinblue's a `dict` that will be merged into the json sent to Sendinblue's
`smtp/email API`_. `smtp/email API`_.
Example: For example, you could set Sendinblue's *batchId* for use with
their `batched scheduled sending`_:
.. code-block:: python .. code-block:: python
message.esp_extra = { message.esp_extra = {
'hypotheticalFutureSendinblueParam': '2022', # merged into send params 'batchId': '275d3289-d5cb-4768-9460-a990054b6c81', # merged into send params
} }
(You can also set `"esp_extra"` in Anymail's :ref:`global send defaults <send-defaults>` (You can also set `"esp_extra"` in Anymail's :ref:`global send defaults <send-defaults>`
to apply it to all messages.) to apply it to all messages.)
.. _batched scheduled sending: https://developers.sendinblue.com/docs/schedule-batch-sendings
.. _smtp/email API: https://developers.sendinblue.com/v3.0/reference#sendtransacemail .. _smtp/email API: https://developers.sendinblue.com/v3.0/reference#sendtransacemail
@@ -141,8 +143,10 @@ Sendinblue can handle.
as a JSON-encoded string using their :mailheader:`X-Mailin-custom` email header. as a JSON-encoded string using their :mailheader:`X-Mailin-custom` email header.
The metadata is available in tracking webhooks. The metadata is available in tracking webhooks.
**No delayed sending** **Delayed sending**
Sendinblue does not support :attr:`~anymail.message.AnymailMessage.send_at`. .. versionadded:: 9.0
Earlier versions of Anymail did not support :attr:`~anymail.message.AnymailMessage.send_at`
with Sendinblue.
**No click-tracking or open-tracking options** **No click-tracking or open-tracking options**
Sendinblue does not provide a way to control open or click tracking for individual Sendinblue does not provide a way to control open or click tracking for individual

View File

@@ -1,6 +1,6 @@
import json import json
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from datetime import datetime from datetime import date, datetime, timezone
from decimal import Decimal from decimal import Decimal
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
@@ -294,10 +294,41 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
with override_current_timezone(utc_plus_6): with override_current_timezone(utc_plus_6):
# Timezone-aware datetime converted to UTC: # Timezone-aware datetime converted to UTC:
self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=utc_minus_8) self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, 8000, tzinfo=utc_minus_8)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2016-03-04T05:06:07.008-08:00")
with self.assertRaises(AnymailUnsupportedFeature): # Explicit UTC:
self.message.send() self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=timezone.utc)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2016-03-04T05:06:07.000+00:00")
# Timezone-naive datetime assumed to be Django current_timezone
# (also checks stripping microseconds)
self.message.send_at = datetime(2022, 10, 11, 12, 13, 14, 567)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-10-11T12:13:14.000+06:00")
# Date-only treated as midnight in current timezone
self.message.send_at = date(2022, 10, 22)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-10-22T00:00:00.000+06:00")
# POSIX timestamp
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-05-06T07:08:09.000+00:00")
# String passed unchanged (this is *not* portable between ESPs)
self.message.send_at = "2022-10-13T18:02:00.123-11:30"
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-10-13T18:02:00.123-11:30")
def test_tag(self): def test_tag(self):
self.message.tags = ["receipt", "multiple"] self.message.tags = ["receipt", "multiple"]

View File

@@ -1,5 +1,6 @@
import os import os
import unittest import unittest
from datetime import datetime, timedelta
from email.utils import formataddr from email.utils import formataddr
from django.test import SimpleTestCase, override_settings, tag from django.test import SimpleTestCase, override_settings, tag
@@ -55,6 +56,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(anymail_status.message_id, message_id) self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self): def test_all_options(self):
send_at = datetime.now() + timedelta(minutes=2)
message = AnymailMessage( message = AnymailMessage(
subject="Anymail SendinBlue all-options integration test", subject="Anymail SendinBlue all-options integration test",
body="This is the text body", body="This is the text body",
@@ -66,6 +68,7 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3}, headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
metadata={"meta1": "simple string", "meta2": 2}, metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1", "tag 2"], tags=["tag 1", "tag 2"],
) )
message.attach_alternative('<p>HTML content</p>', "text/html") # SendinBlue requires an HTML body message.attach_alternative('<p>HTML content</p>', "text/html") # SendinBlue requires an HTML body