Use specific ESP name in error messages.

Change  (e.g.,) "ESP API response 400"
to "Mailgun API response 400".
This commit is contained in:
medmunds
2017-01-22 10:21:19 -08:00
parent 79288603fb
commit 56d2b53c2b
14 changed files with 54 additions and 35 deletions

View File

@@ -183,7 +183,8 @@ class AnymailBaseBackend(BaseEmailBackend):
# Error if *all* recipients are invalid or refused # Error if *all* recipients are invalid or refused
# (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend) # (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend)
if anymail_status.status.issubset({"invalid", "rejected"}): if anymail_status.status.issubset({"invalid", "rejected"}):
raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response) raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response,
backend=self)
@property @property
def esp_name(self): def esp_name(self):

View File

@@ -85,7 +85,8 @@ class AnymailRequestsBackend(AnymailBaseBackend):
parse_recipient_status) parse_recipient_status)
""" """
if response.status_code != 200: if response.status_code != 200:
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response) raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response,
backend=self)
def deserialize_json_response(self, response, payload, message): def deserialize_json_response(self, response, payload, message):
"""Deserialize an ESP API response that's in json. """Deserialize an ESP API response that's in json.
@@ -96,7 +97,8 @@ class AnymailRequestsBackend(AnymailBaseBackend):
return response.json() return response.json()
except ValueError: except ValueError:
raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name, raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name,
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
class RequestsPayload(BasePayload): class RequestsPayload(BasePayload):

View File

@@ -46,10 +46,12 @@ class EmailBackend(AnymailRequestsBackend):
mailgun_message = parsed_response["message"] mailgun_message = parsed_response["message"]
except (KeyError, TypeError): except (KeyError, TypeError):
raise AnymailRequestsAPIError("Invalid Mailgun API response format", raise AnymailRequestsAPIError("Invalid Mailgun API response format",
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
if not mailgun_message.startswith("Queued"): if not mailgun_message.startswith("Queued"):
raise AnymailRequestsAPIError("Unrecognized Mailgun API message '%s'" % mailgun_message, raise AnymailRequestsAPIError("Unrecognized Mailgun API message '%s'" % mailgun_message,
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
# Simulate a per-recipient status of "queued": # Simulate a per-recipient status of "queued":
status = AnymailRecipientStatus(message_id=message_id, status="queued") status = AnymailRecipientStatus(message_id=message_id, status="queued")
return {recipient.email: status for recipient in payload.all_recipients} return {recipient.email: status for recipient in payload.all_recipients}

View File

@@ -42,7 +42,8 @@ class EmailBackend(AnymailRequestsBackend):
recipient_status[email] = AnymailRecipientStatus(message_id=message_id, status=status) recipient_status[email] = AnymailRecipientStatus(message_id=message_id, status=status)
except (KeyError, TypeError): except (KeyError, TypeError):
raise AnymailRequestsAPIError("Invalid Mandrill API response format", raise AnymailRequestsAPIError("Invalid Mandrill API response format",
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
return recipient_status return recipient_status

View File

@@ -42,7 +42,8 @@ class EmailBackend(AnymailRequestsBackend):
msg = parsed_response["Message"] msg = parsed_response["Message"]
except (KeyError, TypeError): except (KeyError, TypeError):
raise AnymailRequestsAPIError("Invalid Postmark API response format", raise AnymailRequestsAPIError("Invalid Postmark API response format",
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
message_id = parsed_response.get("MessageID", None) message_id = parsed_response.get("MessageID", None)
rejected_emails = [] rejected_emails = []
@@ -51,7 +52,8 @@ class EmailBackend(AnymailRequestsBackend):
# Either the From address or at least one recipient was invalid. Email not sent. # Either the From address or at least one recipient was invalid. Email not sent.
if "'From' address" in msg: if "'From' address" in msg:
# Normal error # Normal error
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response) raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response,
backend=self)
else: else:
# Use AnymailRecipientsRefused logic # Use AnymailRecipientsRefused logic
default_status = 'invalid' default_status = 'invalid'
@@ -64,7 +66,8 @@ class EmailBackend(AnymailRequestsBackend):
default_status = 'sent' default_status = 'sent'
rejected_emails = self.parse_inactive_recipients(msg) rejected_emails = self.parse_inactive_recipients(msg)
else: else:
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response) raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response,
backend=self)
return { return {
recipient.email: AnymailRecipientStatus( recipient.email: AnymailRecipientStatus(

View File

@@ -55,7 +55,8 @@ class EmailBackend(AnymailRequestsBackend):
def raise_for_status(self, response, payload, message): def raise_for_status(self, response, payload, message):
if response.status_code < 200 or response.status_code >= 300: if response.status_code < 200 or response.status_code >= 300:
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response) raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response,
backend=self)
def parse_recipient_status(self, response, payload, message): def parse_recipient_status(self, response, payload, message):
# If we get here, the send call was successful. # If we get here, the send call was successful.

View File

@@ -54,11 +54,13 @@ class EmailBackend(AnymailRequestsBackend):
sendgrid_message = parsed_response["message"] sendgrid_message = parsed_response["message"]
except (KeyError, TypeError): except (KeyError, TypeError):
raise AnymailRequestsAPIError("Invalid SendGrid API response format", raise AnymailRequestsAPIError("Invalid SendGrid API response format",
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
if sendgrid_message != "success": if sendgrid_message != "success":
errors = parsed_response.get("errors", []) errors = parsed_response.get("errors", [])
raise AnymailRequestsAPIError("SendGrid send failed: '%s'" % "; ".join(errors), raise AnymailRequestsAPIError("SendGrid send failed: '%s'" % "; ".join(errors),
email_message=message, payload=payload, response=response) email_message=message, payload=payload, response=response,
backend=self)
# Simulate a per-recipient status of "queued": # Simulate a per-recipient status of "queued":
status = AnymailRecipientStatus(message_id=payload.message_id, status="queued") status = AnymailRecipientStatus(message_id=payload.message_id, status="queued")
return {recipient.email: status for recipient in payload.all_recipients} return {recipient.email: status for recipient in payload.all_recipients}

View File

@@ -17,15 +17,19 @@ class AnymailError(Exception):
Optional kwargs: Optional kwargs:
email_message: the original EmailMessage being sent email_message: the original EmailMessage being sent
status_code: HTTP status code of response to ESP send call status_code: HTTP status code of response to ESP send call
backend: the backend instance involved
payload: data arg (*not* json-stringified) for the ESP send call payload: data arg (*not* json-stringified) for the ESP send call
response: requests.Response from the send call response: requests.Response from the send call
raised_from: original/wrapped Exception raised_from: original/wrapped Exception
esp_name: what to call the ESP (read from backend if provided)
""" """
self.backend = kwargs.pop('backend', None) self.backend = kwargs.pop('backend', None)
self.email_message = kwargs.pop('email_message', None) self.email_message = kwargs.pop('email_message', None)
self.payload = kwargs.pop('payload', None) self.payload = kwargs.pop('payload', None)
self.status_code = kwargs.pop('status_code', None) self.status_code = kwargs.pop('status_code', None)
self.raised_from = kwargs.pop('raised_from', None) self.raised_from = kwargs.pop('raised_from', None)
self.esp_name = kwargs.pop('esp_name',
self.backend.esp_name if self.backend else None)
if isinstance(self, HTTPError): if isinstance(self, HTTPError):
# must leave response in kwargs for HTTPError # must leave response in kwargs for HTTPError
self.response = kwargs.get('response', None) self.response = kwargs.get('response', None)
@@ -61,7 +65,7 @@ class AnymailError(Exception):
"""Return a formatted string of self.status_code and response, or None""" """Return a formatted string of self.status_code and response, or None"""
if self.status_code is None: if self.status_code is None:
return None return None
description = "ESP API response %d:" % self.status_code description = "%s API response %d:" % (self.esp_name or "ESP", self.status_code)
try: try:
json_response = self.response.json() json_response = self.response.json()
description += "\n" + json.dumps(json_response, indent=2) description += "\n" + json.dumps(json_response, indent=2)
@@ -131,7 +135,9 @@ class AnymailSerializationError(AnymailError, TypeError):
def __init__(self, message=None, orig_err=None, *args, **kwargs): def __init__(self, message=None, orig_err=None, *args, **kwargs):
if message is None: if message is None:
esp_name = kwargs["backend"].esp_name if "backend" in kwargs else "the ESP" # self.esp_name not set until super init, so duplicate logic to get esp_name
backend = kwargs.get('backend', None)
esp_name = kwargs.get('esp_name', backend.esp_name if backend else "the ESP")
message = "Don't know how to send this data to %s. " \ message = "Don't know how to send this data to %s. " \
"Try converting it to a string or number first." % esp_name "Try converting it to a string or number first." % esp_name
if orig_err is not None: if orig_err is not None:

View File

@@ -227,9 +227,8 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
def test_api_failure(self): def test_api_failure(self):
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)
with self.assertRaises(AnymailAPIError): with self.assertRaisesMessage(AnymailAPIError, "Mailgun API response 400"):
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
self.assertEqual(sent, 0)
# Make sure fail_silently is respected # Make sure fail_silently is respected
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)

View File

@@ -236,9 +236,8 @@ class MandrillBackendStandardEmailTests(MandrillBackendMockAPITestCase):
def test_api_failure(self): def test_api_failure(self):
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)
with self.assertRaises(AnymailAPIError): with self.assertRaisesMessage(AnymailAPIError, "Mandrill API response 400"):
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
self.assertEqual(sent, 0)
# Make sure fail_silently is respected # Make sure fail_silently is respected
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)

View File

@@ -273,9 +273,8 @@ class PostmarkBackendStandardEmailTests(PostmarkBackendMockAPITestCase):
def test_api_failure(self): def test_api_failure(self):
self.set_mock_response(status_code=500) self.set_mock_response(status_code=500)
with self.assertRaises(AnymailAPIError): with self.assertRaisesMessage(AnymailAPIError, "Postmark API response 500"):
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
self.assertEqual(sent, 0)
# Make sure fail_silently is respected # Make sure fail_silently is respected
self.set_mock_response(status_code=500) self.set_mock_response(status_code=500)

View File

@@ -294,9 +294,8 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
def test_api_failure(self): def test_api_failure(self):
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)
with self.assertRaises(AnymailAPIError): with self.assertRaisesMessage(AnymailAPIError, "SendGrid API response 400"):
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
self.assertEqual(sent, 0)
# Make sure fail_silently is respected # Make sure fail_silently is respected
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)

View File

@@ -301,9 +301,8 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
def test_api_failure(self): def test_api_failure(self):
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)
with self.assertRaises(AnymailAPIError): with self.assertRaisesMessage(AnymailAPIError, "SendGrid API response 400"):
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
self.assertEqual(sent, 0)
# Make sure fail_silently is respected # Make sure fail_silently is respected
self.set_mock_response(status_code=400) self.set_mock_response(status_code=400)

View File

@@ -289,12 +289,8 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertNotIn('recipients', params) self.assertNotIn('recipients', params)
def test_api_failure(self): def test_api_failure(self):
failure_response = b"""{ "errors": [ { self.set_mock_failure(status_code=400)
"message": "Something went wrong", with self.assertRaisesMessage(AnymailAPIError, "SparkPost API response 400"):
"description": "Helpful explanation from your ESP"
} ] }"""
self.set_mock_failure(raw=failure_response)
with self.assertRaisesMessage(AnymailAPIError, "Helpful explanation from your ESP"):
self.message.send() self.message.send()
def test_api_failure_fail_silently(self): def test_api_failure_fail_silently(self):
@@ -303,6 +299,16 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
sent = self.message.send(fail_silently=True) sent = self.message.send(fail_silently=True)
self.assertEqual(sent, 0) self.assertEqual(sent, 0)
def test_api_error_includes_details(self):
"""AnymailAPIError should include ESP's error message"""
failure_response = b"""{ "errors": [ {
"message": "Something went wrong",
"description": "Helpful explanation from your ESP"
} ] }"""
self.set_mock_failure(raw=failure_response)
with self.assertRaisesMessage(AnymailAPIError, "Helpful explanation from your ESP"):
self.message.send()
class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase): class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
"""Test backend support for Anymail added features""" """Test backend support for Anymail added features"""