Exception cleanup

Introduce djrill.NotSupportedByMandrillError for unsupported functionality (previously used generic ValueError).
Change to djrill.MandrillAPIError, derived from requests.HTTPError, for API error responses (previously used djrill.mail.backends.djrill.DjrillBackendHTTPError -- retained as equivalent for backwards compatibility).
This commit is contained in:
medmunds
2013-01-11 16:59:42 -08:00
parent fac078ee18
commit 18d27fdb21
6 changed files with 86 additions and 37 deletions

View File

@@ -110,9 +110,14 @@ Example, sending HTML email with Mandrill tags and metadata:
# Send it:
msg.send()
If the Mandrill API returns an error response for any reason, the send call will
raise a ``djrill.mail.backends.djrill.DjrillBackendHTTPError`` exception
(unless called with fail_silently=True).
If the email tries to use features that aren't supported by Mandrill, the send
call will raise a ``djrill.NotSupportedByMandrillError`` exception (a subclass
of ValueError).
If the Mandrill API fails or returns an error response, the send call will
raise a ``djrill.MandrillAPIError`` exception (a subclass of
requests.HTTPError).
Django EmailMessage Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -122,12 +127,13 @@ Djrill supports most of the functionality of Django's `EmailMessage`_ and
* Djrill accepts additional headers, but only ``Reply-To`` and ``X-*`` (since
that is all that Mandrill accepts). Any other extra headers will raise a
``ValueError`` exception when you attempt to send the message.
``djrill.NotSupportedByMandrillError`` exception when you attempt to send the
message.
* Djrill requires that if you ``attach_alternative`` to a message, there must be
only one alternative type, and it must be text/html. Otherwise, Djrill will
raise a ``ValueError`` exception when you attempt to send the message.
(Mandrill doesn't support sending multiple html alternative parts, or any
non-html alternatives.)
raise a ``djrill.NotSupportedByMandrillError`` exception when you attempt to
send the message. (Mandrill doesn't support sending multiple html alternative
parts, or any non-html alternatives.)
* Djrill attempts to include a message's attachments, but Mandrill will
(silently) ignore any attachment types it doesn't allow. According to
Mandrill's docs, attachments are only allowed with the mimetypes "text/\*",

View File

@@ -4,6 +4,7 @@ from django.utils.text import capfirst
VERSION = (0, 2, 0)
__version__ = '.'.join([str(x) for x in VERSION])
from exceptions import MandrillAPIError, NotSupportedByMandrillError
class DjrillAdminSite(AdminSite):
index_template = "djrill/index.html"
@@ -31,6 +32,7 @@ class DjrillAdminSite(AdminSite):
from django.conf.urls import include, patterns, url
except ImportError:
# Django 1.3
#noinspection PyDeprecation
from django.conf.urls.defaults import include, patterns, url
for path, view, name, display_name in self.custom_views:
urls += patterns('',

33
djrill/exceptions.py Normal file
View File

@@ -0,0 +1,33 @@
from requests import HTTPError
class MandrillAPIError(HTTPError):
"""Exception for unsuccessful response from Mandrill API."""
def __init__(self, status_code, response=None, log_message=None):
super(MandrillAPIError, self).__init__()
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
if self.response:
message += "\nResponse: " + getattr(self.response, 'content', "")
return message
class NotSupportedByMandrillError(ValueError):
"""Exception for email features that Mandrill doesn't support.
This is typically raised when attempting to send a Django EmailMessage that
uses options or values you might expect to work, but that are silently
ignored by or can't be communicated to Mandrill's API. (E.g., unsupported
attachment types, multiple bcc recipients.)
It's generally *not* raised for Mandrill-specific features, like limitations
on Mandrill tag names or restrictions on from emails. (Djrill expects
Mandrill to return an API error for these where appropriate, and tries to
avoid duplicating Mandrill's validation logic locally.)
"""

View File

@@ -4,6 +4,10 @@ from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
from django.utils import simplejson as json
# Oops: this file has the same name as our app, and cannot be renamed.
#from djrill import MandrillAPIError, NotSupportedByMandrillError
from ... import MandrillAPIError, NotSupportedByMandrillError
from base64 import b64encode
from email.mime.base import MIMEBase
from email.utils import parseaddr
@@ -14,21 +18,7 @@ import requests
# You can override in settings.py, if desired.
MANDRILL_API_URL = "http://mandrillapp.com/api/1.0"
class DjrillBackendHTTPError(Exception):
"""An exception that will turn into an HTTP error response."""
def __init__(self, status_code, response=None, log_message=None):
super(DjrillBackendHTTPError, self).__init__()
self.status_code = status_code
self.response = response # often contains helpful Mandrill info
self.log_message = log_message
def __str__(self):
message = "DjrillBackendHTTP %d" % self.status_code
if self.log_message:
return message + " " + self.log_message
else:
return message
DjrillBackendHTTPError = MandrillAPIError # Backwards-compat Djrill<=0.2.0
class DjrillBackend(BaseEmailBackend):
"""
@@ -73,7 +63,7 @@ class DjrillBackend(BaseEmailBackend):
if getattr(message, 'alternatives', None):
self._add_alternatives(message, msg_dict)
self._add_attachments(message, msg_dict)
except ValueError:
except NotSupportedByMandrillError:
if not self.fail_silently:
raise
return False
@@ -97,7 +87,7 @@ class DjrillBackend(BaseEmailBackend):
if response.status_code != 200:
if not self.fail_silently:
raise DjrillBackendHTTPError(
raise MandrillAPIError(
status_code=response.status_code,
response=response,
log_message="Failed to send a message to %s, from %s" %
@@ -112,8 +102,9 @@ class DjrillBackend(BaseEmailBackend):
use by default. Standard text email messages sent through Django will
still work through Mandrill.
Raises ValueError for any standard EmailMessage features that cannot be
accurately communicated to Mandrill (e.g., prohibited headers).
Raises NotSupportedByMandrillError for any standard EmailMessage
features that cannot be accurately communicated to Mandrill
(e.g., prohibited headers).
"""
sender = sanitize_address(message.from_email, message.encoding)
from_name, from_email = parseaddr(sender)
@@ -135,7 +126,8 @@ class DjrillBackend(BaseEmailBackend):
if message.extra_headers:
for k in message.extra_headers.keys():
if k != "Reply-To" and not k.startswith("X-"):
raise ValueError("Invalid message header '%s' - Mandrill "
raise NotSupportedByMandrillError(
"Invalid message header '%s' - Mandrill "
"only allows Reply-To and X-* headers" % k)
msg_dict["headers"] = message.extra_headers
@@ -192,13 +184,14 @@ class DjrillBackend(BaseEmailBackend):
the HTML output for your email.
"""
if len(message.alternatives) > 1:
raise ValueError(
raise NotSupportedByMandrillError(
"Too many alternatives attached to the message. "
"Mandrill only accepts plain text and html emails.")
(content, mimetype) = message.alternatives[0]
if mimetype != 'text/html':
raise ValueError("Invalid alternative mimetype '%s'. "
raise NotSupportedByMandrillError(
"Invalid alternative mimetype '%s'. "
"Mandrill only accepts plain text and html emails."
% mimetype)

View File

@@ -3,6 +3,7 @@
from django.test import TestCase
from djrill.mail import DjrillMessage
from djrill import MandrillAPIError, NotSupportedByMandrillError
class DjrillMessageTests(TestCase):
@@ -72,3 +73,17 @@ class DjrillMessageTests(TestCase):
self.assertFalse(hasattr(msg, 'tags'))
self.assertFalse(hasattr(msg, 'from_name'))
self.assertFalse(hasattr(msg, 'preserve_recipients'))
class DjrillLegacyExceptionTests(TestCase):
def test_DjrillBackendHTTPError(self):
"""MandrillApiError was DjrillBackendHTTPError in 0.2.0"""
# ... and had to be imported from deep in the package:
from djrill.mail.backends.djrill import DjrillBackendHTTPError
ex = MandrillAPIError("testing")
self.assertIsInstance(ex, DjrillBackendHTTPError)
def test_NotSupportedByMandrillError(self):
"""Unsupported features used to just raise ValueError in 0.2.0"""
ex = NotSupportedByMandrillError("testing")
self.assertIsInstance(ex, ValueError)

View File

@@ -5,7 +5,7 @@ from django.conf import settings
from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from djrill.mail.backends.djrill import DjrillBackendHTTPError
from djrill import MandrillAPIError, NotSupportedByMandrillError
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
@@ -123,7 +123,7 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
email = mail.EmailMessage('Subject', 'Body', 'from@example.com',
['to@example.com'],
headers={'Non-X-Non-Reply-To-Header': 'not permitted'})
with self.assertRaises(ValueError):
with self.assertRaises(NotSupportedByMandrillError):
email.send()
# Make sure fail_silently is respected
@@ -141,14 +141,14 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
'from@example.com', ['to@example.com'])
email.attach_alternative("<p>First html is OK</p>", "text/html")
email.attach_alternative("<p>But not second html</p>", "text/html")
with self.assertRaises(ValueError):
with self.assertRaises(NotSupportedByMandrillError):
email.send()
# Only html alternatives allowed
email = mail.EmailMultiAlternatives('Subject', 'Body',
'from@example.com', ['to@example.com'])
email.attach_alternative("{'not': 'allowed'}", "application/json")
with self.assertRaises(ValueError):
with self.assertRaises(NotSupportedByMandrillError):
email.send()
# Make sure fail_silently is respected
@@ -162,7 +162,7 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
def test_mandrill_api_failure(self):
self.mock_post.return_value = self.MockResponse(status_code=400)
with self.assertRaises(DjrillBackendHTTPError):
with self.assertRaises(MandrillAPIError):
sent = mail.send_mail('Subject', 'Body', 'from@example.com',
['to@example.com'])
self.assertEqual(sent, 0)