mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Cleanup Djrill exceptions
* Add common base DjrillException * Simplify backend by moving logic to describe errors into base DjrillException * Add NotSerializableForMandrillError for JSON serialization errors
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
from ._version import __version__, VERSION
|
||||
from .exceptions import MandrillAPIError, MandrillRecipientsRefused, NotSupportedByMandrillError
|
||||
from .exceptions import (MandrillAPIError, MandrillRecipientsRefused,
|
||||
NotSerializableForMandrillError, NotSupportedByMandrillError)
|
||||
|
||||
@@ -2,55 +2,86 @@ import json
|
||||
from requests import HTTPError
|
||||
|
||||
|
||||
def format_response(response):
|
||||
"""Return a string-formatted version of response
|
||||
class DjrillError(Exception):
|
||||
"""Base class for exceptions raised by Djrill
|
||||
|
||||
Format json if available, else just return text.
|
||||
Returns "" if neither json nor text available.
|
||||
Overrides __str__ to provide additional information about
|
||||
Mandrill API call and response.
|
||||
"""
|
||||
try:
|
||||
json_response = response.json()
|
||||
return "\n" + json.dumps(json_response, indent=2)
|
||||
except (AttributeError, KeyError, ValueError): # not JSON = ValueError
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Optional kwargs:
|
||||
payload: data arg (*not* json-stringified) for the Mandrill send call
|
||||
response: requests.Response from the send call
|
||||
"""
|
||||
self.payload = kwargs.pop('payload', None)
|
||||
if isinstance(self, HTTPError):
|
||||
# must leave response in kwargs for HTTPError
|
||||
self.response = kwargs.get('response', None)
|
||||
else:
|
||||
self.response = kwargs.pop('response', None)
|
||||
super(DjrillError, self).__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
parts = [
|
||||
" ".join([str(arg) for arg in self.args]),
|
||||
self.describe_send(),
|
||||
self.describe_response(),
|
||||
]
|
||||
return "\n".join(filter(None, parts))
|
||||
|
||||
def describe_send(self):
|
||||
"""Return a string describing the Mandrill send in self.payload, or None"""
|
||||
if self.payload is None:
|
||||
return None
|
||||
description = "Sending a message"
|
||||
try:
|
||||
return response.text
|
||||
except AttributeError:
|
||||
to_emails = [to['email'] for to in self.payload['message']['to']]
|
||||
description += " to %s" % ','.join(to_emails)
|
||||
except KeyError:
|
||||
pass
|
||||
return ""
|
||||
try:
|
||||
description += " from %s" % self.payload['message']['from_email']
|
||||
except KeyError:
|
||||
pass
|
||||
return description
|
||||
|
||||
def describe_response(self):
|
||||
"""Return a formatted string of self.response, or None"""
|
||||
if self.response is None:
|
||||
return None
|
||||
description = "Mandrill API response %d:" % self.response.status_code
|
||||
try:
|
||||
json_response = self.response.json()
|
||||
description += "\n" + json.dumps(json_response, indent=2)
|
||||
except (AttributeError, KeyError, ValueError): # not JSON = ValueError
|
||||
try:
|
||||
description += self.response.text
|
||||
except AttributeError:
|
||||
pass
|
||||
return description
|
||||
|
||||
|
||||
class MandrillAPIError(HTTPError):
|
||||
class MandrillAPIError(DjrillError, HTTPError):
|
||||
"""Exception for unsuccessful response from Mandrill API."""
|
||||
def __init__(self, status_code, response=None, log_message=None, *args, **kwargs):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MandrillAPIError, self).__init__(*args, **kwargs)
|
||||
self.status_code = status_code
|
||||
self.response = response # often contains helpful Mandrill info
|
||||
self.log_message = log_message
|
||||
|
||||
def __str__(self):
|
||||
message = "Mandrill API response %d" % self.status_code
|
||||
if self.log_message:
|
||||
message += "\n" + self.log_message
|
||||
# Include the Mandrill response, nicely formatted, if possible
|
||||
if self.response is not None:
|
||||
message += "\nMandrill response: " + format_response(self.response)
|
||||
return message
|
||||
self.status_code = self.response.status_code
|
||||
|
||||
|
||||
class MandrillRecipientsRefused(IOError):
|
||||
class MandrillRecipientsRefused(DjrillError):
|
||||
"""Exception for send where all recipients are invalid or rejected."""
|
||||
def __init__(self, message, response=None, *args, **kwargs):
|
||||
|
||||
def __init__(self, message=None, *args, **kwargs):
|
||||
if message is None:
|
||||
message = "All message recipients were rejected or invalid"
|
||||
super(MandrillRecipientsRefused, self).__init__(message, *args, **kwargs)
|
||||
self.response = response
|
||||
|
||||
def __str__(self):
|
||||
message = self.args[0]
|
||||
if self.response is not None:
|
||||
message += "\nMandrill response: " + format_response(self.response)
|
||||
return message
|
||||
|
||||
|
||||
class NotSupportedByMandrillError(ValueError):
|
||||
class NotSupportedByMandrillError(DjrillError, ValueError):
|
||||
"""Exception for email features that Mandrill doesn't support.
|
||||
|
||||
This is typically raised when attempting to send a Django EmailMessage that
|
||||
@@ -64,3 +95,21 @@ class NotSupportedByMandrillError(ValueError):
|
||||
avoid duplicating Mandrill's validation logic locally.)
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class NotSerializableForMandrillError(DjrillError, TypeError):
|
||||
"""Exception for data that Djrill doesn't know how to convert to JSON.
|
||||
|
||||
This typically results from including something like a date or Decimal
|
||||
in your merge_vars (or other Mandrill-specific EmailMessage option).
|
||||
|
||||
"""
|
||||
# inherits from TypeError for backwards compatibility with Djrill 1.x
|
||||
|
||||
def __init__(self, message=None, orig_err=None, *args, **kwargs):
|
||||
if message is None:
|
||||
message = "Don't know how to send this data to Mandrill. " \
|
||||
"Try converting it to a string or number first."
|
||||
if orig_err is not None:
|
||||
message += "\n%s" % str(orig_err)
|
||||
super(NotSerializableForMandrillError, self).__init__(message, *args, **kwargs)
|
||||
|
||||
@@ -12,7 +12,8 @@ from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
|
||||
from ..._version import __version__
|
||||
from ...exceptions import MandrillAPIError, MandrillRecipientsRefused, NotSupportedByMandrillError
|
||||
from ...exceptions import (MandrillAPIError, MandrillRecipientsRefused,
|
||||
NotSerializableForMandrillError, NotSupportedByMandrillError)
|
||||
|
||||
|
||||
def encode_date_for_mandrill(dt):
|
||||
@@ -164,28 +165,13 @@ class DjrillBackend(BaseEmailBackend):
|
||||
if self.fail_silently:
|
||||
return False
|
||||
# Add some context to the "not JSON serializable" message
|
||||
if not err.args:
|
||||
err.args = ('',)
|
||||
err.args = (
|
||||
err.args[0] + " in a Djrill message (perhaps it's a merge var?)."
|
||||
" Try converting it to a string or number first.",
|
||||
) + err.args[1:]
|
||||
raise err
|
||||
raise NotSerializableForMandrillError(orig_err=err)
|
||||
|
||||
response = self.session.post(api_url, data=api_data)
|
||||
|
||||
if response.status_code != 200:
|
||||
if not self.fail_silently:
|
||||
log_message = "Failed to send a message"
|
||||
if 'to' in msg_dict:
|
||||
log_message += " to " + ','.join(
|
||||
to['email'] for to in msg_dict.get('to', []) if 'email' in to)
|
||||
if 'from_email' in msg_dict:
|
||||
log_message += " from %s" % msg_dict['from_email']
|
||||
raise MandrillAPIError(
|
||||
status_code=response.status_code,
|
||||
response=response,
|
||||
log_message=log_message)
|
||||
raise MandrillAPIError(payload=api_params, response=response)
|
||||
return False
|
||||
|
||||
# add the response from mandrill to the EmailMessage so callers can inspect it
|
||||
@@ -194,10 +180,8 @@ class DjrillBackend(BaseEmailBackend):
|
||||
recipient_status = [item["status"] for item in message.mandrill_response]
|
||||
except (ValueError, KeyError):
|
||||
if not self.fail_silently:
|
||||
raise MandrillAPIError(
|
||||
status_code=response.status_code,
|
||||
response=response,
|
||||
log_message="Error parsing Mandrill API response")
|
||||
raise MandrillAPIError("Error parsing Mandrill API response",
|
||||
payload=api_params, response=response)
|
||||
return False
|
||||
|
||||
# Error if *all* recipients are invalid or refused
|
||||
@@ -205,10 +189,7 @@ class DjrillBackend(BaseEmailBackend):
|
||||
if (not self.ignore_recipient_status and
|
||||
all([status in ('invalid', 'rejected') for status in recipient_status])):
|
||||
if not self.fail_silently:
|
||||
raise MandrillRecipientsRefused(
|
||||
"All message recipients were rejected or invalid",
|
||||
response=response
|
||||
)
|
||||
raise MandrillRecipientsRefused(payload=api_params, response=response)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import unittest
|
||||
from base64 import b64decode
|
||||
@@ -18,7 +19,8 @@ from django.core.mail import make_msgid
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from djrill import MandrillAPIError, MandrillRecipientsRefused, NotSupportedByMandrillError
|
||||
from djrill import (MandrillAPIError, MandrillRecipientsRefused,
|
||||
NotSerializableForMandrillError, NotSupportedByMandrillError)
|
||||
|
||||
from .mock_backend import DjrillBackendMockAPITestCase
|
||||
|
||||
@@ -339,6 +341,9 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
||||
self.message = mail.EmailMessage('Subject', 'Text Body',
|
||||
'from@example.com', ['to@example.com'])
|
||||
|
||||
def assertStrContains(self, haystack, needle, msg=None):
|
||||
six.assertRegex(self, haystack, re.escape(needle), msg)
|
||||
|
||||
def test_tracking(self):
|
||||
# First make sure we're not setting the API param if the track_click
|
||||
# attr isn't there. (The Mandrill account option of True for html,
|
||||
@@ -541,17 +546,17 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
||||
def test_json_serialization_errors(self):
|
||||
"""Try to provide more information about non-json-serializable data"""
|
||||
self.message.global_merge_vars = {'PRICE': Decimal('19.99')}
|
||||
with self.assertRaisesMessage(
|
||||
TypeError,
|
||||
"Decimal('19.99') is not JSON serializable in a Djrill message (perhaps "
|
||||
"it's a merge var?). Try converting it to a string or number first."
|
||||
):
|
||||
with self.assertRaises(NotSerializableForMandrillError) as cm:
|
||||
self.message.send()
|
||||
err = cm.exception
|
||||
self.assertTrue(isinstance(err, TypeError)) # Djrill 1.x re-raised TypeError from json.dumps
|
||||
self.assertStrContains(str(err), "Don't know how to send this data to Mandrill") # our added context
|
||||
self.assertStrContains(str(err), "Decimal('19.99') is not JSON serializable") # original message
|
||||
|
||||
def test_dates_not_serialized(self):
|
||||
"""Pre-2.0 Djrill accidentally serialized dates to ISO"""
|
||||
self.message.global_merge_vars = {'SHIP_DATE': date(2015, 12, 2)}
|
||||
with self.assertRaises(TypeError):
|
||||
with self.assertRaises(NotSerializableForMandrillError):
|
||||
self.message.send()
|
||||
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ Other Djrill 2.0 Changes
|
||||
(You can also directly manage your own long-lived Djrill connection across multiple sends,
|
||||
by calling open and close on :ref:`Django's email backend <django:topic-email-backends>`.)
|
||||
|
||||
* Add :exc:`djrill.NotSerializableForMandrillError`
|
||||
|
||||
|
||||
Older Releases
|
||||
--------------
|
||||
|
||||
@@ -392,3 +392,15 @@ Exceptions
|
||||
`API error log <https://mandrillapp.com/settings/api>`_ to view the full API
|
||||
request and error response.)
|
||||
|
||||
|
||||
.. exception:: djrill.NotSerializableForMandrillError
|
||||
|
||||
The send call will raise a :exc:`~!djrill.NotSerializableForMandrillError` exception
|
||||
if the message has attached data which cannot be serialized to JSON for the Mandrill API.
|
||||
|
||||
See :ref:`formatting-merge-data` for more information.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
Djrill 1.x raised a generic `TypeError` in this case.
|
||||
:exc:`~!djrill.NotSerializableForMandrillError` is a subclass of `TypeError`
|
||||
for compatibility with existing code.
|
||||
|
||||
@@ -75,6 +75,9 @@ which means advanced template users can include dicts and lists as merge vars
|
||||
(for templates designed to handle objects and arrays).
|
||||
See the Python :class:`json.JSONEncoder` docs for a list of allowable types.
|
||||
|
||||
Djrill will raise :exc:`djrill.NotSerializableForMandrillError` if you attempt
|
||||
to send a message with non-json-serializable data.
|
||||
|
||||
|
||||
How To Use Default Mandrill Subject and From fields
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Reference in New Issue
Block a user