Resend: add support for send_at

Resend's new `scheduled_at` API field allows delayed sending
(though not with attachments or batch sending).

Closes #396.
This commit is contained in:
Mike Edmunds
2024-09-06 11:29:01 -07:00
parent af6eaea565
commit 2f2a888f61
6 changed files with 63 additions and 7 deletions

View File

@@ -36,6 +36,11 @@ Breaking changes
* Require **Django 4.0 or later** and Python 3.8 or later. * Require **Django 4.0 or later** and Python 3.8 or later.
Features
~~~~~~~~
* **Resend:** Add support for ``send_at``.
Other Other
~~~~~ ~~~~~

View File

@@ -266,8 +266,16 @@ class ResendPayload(RequestsPayload):
) )
self.metadata = metadata # may be needed for batch send in serialize_data self.metadata = metadata # may be needed for batch send in serialize_data
# Resend doesn't support delayed sending def set_send_at(self, send_at):
# def set_send_at(self, send_at): try:
# Resend can't handle microseconds; truncate to milliseconds if necessary.
send_at = send_at.isoformat(
timespec="milliseconds" if send_at.microsecond else "seconds"
)
except AttributeError:
# User is responsible for formatting their own string
pass
self.data["scheduled_at"] = send_at
def set_tags(self, tags): def set_tags(self, tags):
# Send tags using a custom X-Tags header. # Send tags using a custom X-Tags header.

View File

@@ -4,7 +4,7 @@ Email Service Provider,:ref:`amazon-ses-backend`,:ref:`brevo-backend`,:ref:`mail
:attr:`~AnymailMessage.merge_headers`,Yes [#caveats]_,Yes,No,Yes,Yes,No,No,Yes,Yes,Yes,Yes [#caveats]_,Yes [#caveats]_ :attr:`~AnymailMessage.merge_headers`,Yes [#caveats]_,Yes,No,Yes,Yes,No,No,Yes,Yes,Yes,Yes [#caveats]_,Yes [#caveats]_
:attr:`~AnymailMessage.metadata`,Yes,Yes,No,Yes,Yes,Yes,No,Yes,Yes,Yes,Yes,Yes :attr:`~AnymailMessage.metadata`,Yes,Yes,No,Yes,Yes,Yes,No,Yes,Yes,Yes,Yes,Yes
:attr:`~AnymailMessage.merge_metadata`,Yes [#caveats]_,Yes,No,Yes,Yes,Yes,No,Yes,Yes,Yes,Yes,Yes :attr:`~AnymailMessage.merge_metadata`,Yes [#caveats]_,Yes,No,Yes,Yes,Yes,No,Yes,Yes,Yes,Yes,Yes
:attr:`~AnymailMessage.send_at`,No,Yes,Yes,Yes,No,Yes,No,No,No,Yes,Yes,Yes :attr:`~AnymailMessage.send_at`,No,Yes,Yes,Yes,No,Yes,No,No,Yes,Yes,Yes,Yes
:attr:`~AnymailMessage.tags`,Yes,Yes,Yes,Yes,Max 1 tag,Yes,Max 1 tag,Max 1 tag,Yes,Yes,Max 1 tag,Yes :attr:`~AnymailMessage.tags`,Yes,Yes,Yes,Yes,Max 1 tag,Yes,Max 1 tag,Max 1 tag,Yes,Yes,Max 1 tag,Yes
:attr:`~AnymailMessage.track_clicks`,No [#nocontrol]_,No [#nocontrol]_,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes,Yes :attr:`~AnymailMessage.track_clicks`,No [#nocontrol]_,No [#nocontrol]_,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes,Yes
:attr:`~AnymailMessage.track_opens`,No [#nocontrol]_,No [#nocontrol]_,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes,Yes :attr:`~AnymailMessage.track_opens`,No [#nocontrol]_,No [#nocontrol]_,Yes,Yes,Yes,Yes,No,Yes,No,Yes,Yes,Yes
1 Email Service Provider :ref:`amazon-ses-backend` :ref:`brevo-backend` :ref:`mailersend-backend` :ref:`mailgun-backend` :ref:`mailjet-backend` :ref:`mandrill-backend` :ref:`postal-backend` :ref:`postmark-backend` :ref:`resend-backend` :ref:`sendgrid-backend` :ref:`sparkpost-backend` :ref:`unisender-go-backend`
4 :attr:`~AnymailMessage.merge_headers` Yes [#caveats]_ Yes No Yes Yes No No Yes Yes Yes Yes [#caveats]_ Yes [#caveats]_
5 :attr:`~AnymailMessage.metadata` Yes Yes No Yes Yes Yes No Yes Yes Yes Yes Yes
6 :attr:`~AnymailMessage.merge_metadata` Yes [#caveats]_ Yes No Yes Yes Yes No Yes Yes Yes Yes Yes
7 :attr:`~AnymailMessage.send_at` No Yes Yes Yes No Yes No No No Yes Yes Yes Yes
8 :attr:`~AnymailMessage.tags` Yes Yes Yes Yes Max 1 tag Yes Max 1 tag Max 1 tag Yes Yes Max 1 tag Yes
9 :attr:`~AnymailMessage.track_clicks` No [#nocontrol]_ No [#nocontrol]_ Yes Yes Yes Yes No Yes No Yes Yes Yes
10 :attr:`~AnymailMessage.track_opens` No [#nocontrol]_ No [#nocontrol]_ Yes Yes Yes Yes No Yes No Yes Yes Yes

View File

@@ -182,8 +182,12 @@ anyway---see :ref:`unsupported-features`.
tracking features can only be configured at the domain level tracking features can only be configured at the domain level
in Resend's control panel. in Resend's control panel.
**No delayed sending** **No attachments with delayed sending**
Resend does not support :attr:`~anymail.message.AnymailMessage.send_at`. Resend does not support attachments or batch sending features when using
:attr:`~anymail.message.AnymailMessage.send_at`.
.. versionchanged:: 12.0
Resend now supports :attr:`~anymail.message.AnymailMessage.send_at`.
**No envelope sender** **No envelope sender**
Resend does not support specifying the Resend does not support specifying the

View File

@@ -1,5 +1,6 @@
import json import json
from base64 import b64encode from base64 import b64encode
from datetime import date, datetime
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
@@ -8,6 +9,10 @@ from email.utils import formataddr
from django.core import mail from django.core import mail
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase, override_settings, tag from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import (
get_fixed_timezone,
override as override_current_timezone,
)
from anymail.exceptions import ( from anymail.exceptions import (
AnymailAPIError, AnymailAPIError,
@@ -416,9 +421,41 @@ class ResendBackendAnymailFeatureTests(ResendBackendMockAPITestCase):
) )
def test_send_at(self): def test_send_at(self):
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC utc_plus_6 = get_fixed_timezone(6 * 60)
with self.assertRaisesMessage(AnymailUnsupportedFeature, "send_at"): utc_minus_8 = get_fixed_timezone(-8 * 60)
with override_current_timezone(utc_plus_6):
# Timezone-naive datetime assumed to be Django current_timezone
self.message.send_at = datetime(2022, 10, 11, 12, 13, 14, 123456)
self.message.send() self.message.send()
data = self.get_api_call_json()
# (Resend can't handle microseconds; truncate to milliseconds.)
self.assertEqual(data["scheduled_at"], "2022-10-11T12:13:14.123+06:00")
# Timezone-aware datetime converted to UTC:
self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=utc_minus_8)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["scheduled_at"], "2016-03-04T05:06:07-08:00")
# Date-only treated as midnight in current timezone
# (which probably won't send since it's not in the future)
self.message.send_at = date(2022, 10, 22)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["scheduled_at"], "2022-10-22T00:00:00+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["scheduled_at"], "2022-05-06T07:08:09+00:00")
# String passed unchanged (this is *not* portable between ESPs)
self.message.send_at = "2013-11-12T01:02:03Z"
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["scheduled_at"], "2013-11-12T01:02:03Z")
def test_tags(self): def test_tags(self):
self.message.tags = ["receipt", "reorder test 12"] self.message.tags = ["receipt", "reorder test 12"]

View File

@@ -73,6 +73,8 @@ class ResendBackendIntegrationTests(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},
tags=["tag 1", "tag 2"], tags=["tag 1", "tag 2"],
# Resend supports send_at or attachments, but not both at once.
# send_at=datetime.now() + timedelta(minutes=2),
) )
message.attach_alternative("<p>HTML content</p>", "text/html") message.attach_alternative("<p>HTML content</p>", "text/html")