diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da9a87a6..ffd931a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -43,9 +43,16 @@ Fixes * **Mailjet:** Avoid a Mailjet API error when sending an inline image without a filename. (Anymail now substitutes ``"attachment"`` for the missing filename.) (Thanks to `@chickahoona`_ for reporting the issue.) + * **Mailjet:** Fix a JSON parsing error on Mailjet 429 "too many requests" API responses. (Thanks to `@rodrigondec`_ for reporting the issue.) +* **Postmark:** Fix a parsing error when Postmark indicates a sent message has + been delayed, which can occur if your message stream is paused or throttled or + when Postmark is experiencing service issues. These messages will now report + "queued" in the ``anymail_status`` (rather than throwing an error or reporting + "sent"). (Thanks to `@jmduke`_ for reporting the issue.) + v12.0 ----- diff --git a/anymail/backends/postmark.py b/anymail/backends/postmark.py index 8c380622..6a445828 100644 --- a/anymail/backends/postmark.py +++ b/anymail/backends/postmark.py @@ -64,7 +64,8 @@ def parse_recipient_status(self, response, payload, message): for one_response in parsed_response: try: # these fields should always be present - error_code = one_response["ErrorCode"] + # (but ErrorCode may be missing for Postmark service delays) + error_code = one_response.get("ErrorCode", 0) msg = one_response["Message"] except (KeyError, TypeError) as err: raise AnymailRequestsAPIError( @@ -88,6 +89,11 @@ def parse_recipient_status(self, response, payload, message): backend=self, ) from err + status = "sent" + # "Message accepted, but delivery may be delayed." (See #392.) + if "delivery may be delayed" in msg: + status = "queued" + # Assume all To recipients are "sent" unless proven otherwise below. # (Must use "To" from API response to get correct individual MessageIDs # in batch send.) @@ -98,7 +104,7 @@ def parse_recipient_status(self, response, payload, message): else: for to in parse_address_list(to_header): recipient_status[to.addr_spec] = AnymailRecipientStatus( - message_id=message_id, status="sent" + message_id=message_id, status=status ) # Assume all Cc and Bcc recipients are "sent" unless proven otherwise @@ -106,7 +112,7 @@ def parse_recipient_status(self, response, payload, message): # original payload values.) for recip in payload.cc_and_bcc_emails: recipient_status[recip.addr_spec] = AnymailRecipientStatus( - message_id=message_id, status="sent" + message_id=message_id, status=status ) # Change "sent" to "rejected" if Postmark reported an address as diff --git a/tests/test_postmark_backend.py b/tests/test_postmark_backend.py index 7d64f0f3..06f9730a 100644 --- a/tests/test_postmark_backend.py +++ b/tests/test_postmark_backend.py @@ -1079,6 +1079,22 @@ def test_mixed_response(self): self.assertEqual(status.recipients["valid@example.com"].status, "sent") self.assertEqual(status.recipients["spam@example.com"].status, "rejected") + def test_delivery_delayed_response(self): + # Postmark's "delivery may be delayed" response doesn't have an ErrorCode + # field set to 0 (despite their API docs). + response_data = { + "Message": "Message accepted, but delivery may be delayed.", + "MessageID": "38360f97-ff7f-44b2-bcd1-5ea94ff2af00", + "SubmittedAt": "2024-08-05T02:03:37.0951168Z", + "To": "to@example.com", + } + self.set_mock_response(json_data=response_data) + sent = self.message.send() + self.assertEqual(sent, 1) + status = self.message.anymail_status + self.assertEqual(status.recipients["to@example.com"].status, "queued") + self.assertEqual(status.message_id, "38360f97-ff7f-44b2-bcd1-5ea94ff2af00") + @tag("postmark") class PostmarkBackendSessionSharingTestCase(