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.
Features
~~~~~~~~
* **Resend:** Add support for ``send_at``.
Other
~~~~~

View File

@@ -266,8 +266,16 @@ class ResendPayload(RequestsPayload):
)
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):
# 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.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.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.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
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
in Resend's control panel.
**No delayed sending**
Resend does not support :attr:`~anymail.message.AnymailMessage.send_at`.
**No attachments with delayed sending**
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**
Resend does not support specifying the

View File

@@ -1,5 +1,6 @@
import json
from base64 import b64encode
from datetime import date, datetime
from decimal import Decimal
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
@@ -8,6 +9,10 @@ from email.utils import formataddr
from django.core import mail
from django.core.exceptions import ImproperlyConfigured
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 (
AnymailAPIError,
@@ -416,9 +421,41 @@ class ResendBackendAnymailFeatureTests(ResendBackendMockAPITestCase):
)
def test_send_at(self):
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC
with self.assertRaisesMessage(AnymailUnsupportedFeature, "send_at"):
utc_plus_6 = get_fixed_timezone(6 * 60)
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()
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):
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},
metadata={"meta1": "simple string", "meta2": 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")