diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c1eece..8e449b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ~~~~~ diff --git a/anymail/backends/resend.py b/anymail/backends/resend.py index 9c19930..65e0850 100644 --- a/anymail/backends/resend.py +++ b/anymail/backends/resend.py @@ -266,8 +266,16 @@ def set_metadata(self, metadata): ) 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. diff --git a/docs/esps/esp-feature-matrix.csv b/docs/esps/esp-feature-matrix.csv index 48925b2..406fa00 100644 --- a/docs/esps/esp-feature-matrix.csv +++ b/docs/esps/esp-feature-matrix.csv @@ -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 diff --git a/docs/esps/resend.rst b/docs/esps/resend.rst index 7a981f7..89f98e8 100644 --- a/docs/esps/resend.rst +++ b/docs/esps/resend.rst @@ -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 diff --git a/tests/test_resend_backend.py b/tests/test_resend_backend.py index 66e3a82..66da761 100644 --- a/tests/test_resend_backend.py +++ b/tests/test_resend_backend.py @@ -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 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 @@ def test_metadata(self): ) 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"] diff --git a/tests/test_resend_integration.py b/tests/test_resend_integration.py index 6b9e2bb..89f9369 100644 --- a/tests/test_resend_integration.py +++ b/tests/test_resend_integration.py @@ -73,6 +73,8 @@ def test_all_options(self): 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("

HTML content

", "text/html")