Skip to content

Commit

Permalink
Sendinblue: Support send_at
Browse files Browse the repository at this point in the history
Add support for delayed sending via
Sendinblue's public beta "scheduledAt"
parameter.

Closes #280
  • Loading branch information
medmunds committed Dec 18, 2022
1 parent a8cfb2e commit 287c217
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Breaking changes
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,
and document how this can be used to mount an adapter that simplifies
automatic retry logic. (Thanks to `@dgilmanAIDENTIFIED`_.)
Expand Down Expand Up @@ -1366,6 +1369,7 @@ Features
.. _@coupa-anya: https://github.com/coupa-anya
.. _@decibyte: https://github.com/decibyte
.. _@dgilmanAIDENTIFIED: https://github.com/dgilmanAIDENTIFIED
.. _@dimitrisor: https://github.com/dimitrisor
.. _@dominik-lekse: https://github.com/dominik-lekse
.. _@erikdrums: https://github.com/erikdrums
.. _@ewingrj: https://github.com/ewingrj
Expand Down
7 changes: 7 additions & 0 deletions anymail/backends/sendinblue.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,10 @@ def set_merge_global_data(self, merge_global_data):
def set_metadata(self, metadata):
# SendinBlue expects a single string payload
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
2 changes: 1 addition & 1 deletion docs/esps/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.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.track_clicks` No Yes Yes Yes No Yes Yes No Yes
:attr:`~AnymailMessage.track_opens` No Yes Yes Yes No Yes Yes No Yes
Expand Down
12 changes: 8 additions & 4 deletions docs/esps/sendinblue.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
`smtp/email API`_.

Example:
For example, you could set Sendinblue's *batchId* for use with
their `batched scheduled sending`_:

.. code-block:: python
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>`
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


Expand Down Expand Up @@ -141,8 +143,10 @@ Sendinblue can handle.
as a JSON-encoded string using their :mailheader:`X-Mailin-custom` email header.
The metadata is available in tracking webhooks.

**No delayed sending**
Sendinblue does not support :attr:`~anymail.message.AnymailMessage.send_at`.
**Delayed sending**
.. 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**
Sendinblue does not provide a way to control open or click tracking for individual
Expand Down
39 changes: 35 additions & 4 deletions tests/test_sendinblue_backend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from base64 import b64encode, b64decode
from datetime import datetime
from datetime import date, datetime, timezone
from decimal import Decimal
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
Expand Down Expand Up @@ -294,10 +294,41 @@ def test_send_at(self):

with override_current_timezone(utc_plus_6):
# 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")

# Explicit UTC:
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")

with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()
# 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):
self.message.tags = ["receipt", "multiple"]
Expand Down
3 changes: 3 additions & 0 deletions tests/test_sendinblue_integration.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import unittest
from datetime import datetime, timedelta
from email.utils import formataddr

from django.test import SimpleTestCase, override_settings, tag
Expand Down Expand Up @@ -55,6 +56,7 @@ def test_simple_send(self):
self.assertEqual(anymail_status.message_id, message_id)

def test_all_options(self):
send_at = datetime.now() + timedelta(minutes=2)
message = AnymailMessage(
subject="Anymail SendinBlue all-options integration test",
body="This is the text body",
Expand All @@ -66,6 +68,7 @@ def test_all_options(self):
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},

metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1", "tag 2"],
)
message.attach_alternative('<p>HTML content</p>', "text/html") # SendinBlue requires an HTML body
Expand Down

0 comments on commit 287c217

Please sign in to comment.