mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
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:
20
README.rst
20
README.rst
@@ -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/\*",
|
||||
|
||||
@@ -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
33
djrill/exceptions.py
Normal 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.)
|
||||
|
||||
"""
|
||||
@@ -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,8 +126,9 @@ 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 "
|
||||
"only allows Reply-To and X-* headers" % k)
|
||||
raise NotSupportedByMandrillError(
|
||||
"Invalid message header '%s' - Mandrill "
|
||||
"only allows Reply-To and X-* headers" % k)
|
||||
msg_dict["headers"] = message.extra_headers
|
||||
|
||||
return msg_dict
|
||||
@@ -192,15 +184,16 @@ 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'. "
|
||||
"Mandrill only accepts plain text and html emails."
|
||||
% mimetype)
|
||||
raise NotSupportedByMandrillError(
|
||||
"Invalid alternative mimetype '%s'. "
|
||||
"Mandrill only accepts plain text and html emails."
|
||||
% mimetype)
|
||||
|
||||
msg_dict['html'] = content
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user