mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Sendinblue: Support send_at
Add support for delayed sending via Sendinblue's public beta "scheduledAt" parameter. Closes #280
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user