diff --git a/README.rst b/README.rst index c84f8dd..c9113b6 100644 --- a/README.rst +++ b/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/\*", diff --git a/djrill/__init__.py b/djrill/__init__.py index 1e21173..f74604c 100644 --- a/djrill/__init__.py +++ b/djrill/__init__.py @@ -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('', diff --git a/djrill/exceptions.py b/djrill/exceptions.py new file mode 100644 index 0000000..4c986ff --- /dev/null +++ b/djrill/exceptions.py @@ -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.) + + """ diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index ac210ba..73b8a42 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -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 diff --git a/djrill/tests/test_legacy.py b/djrill/tests/test_legacy.py index c8b39c6..64a12f7 100644 --- a/djrill/tests/test_legacy.py +++ b/djrill/tests/test_legacy.py @@ -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) diff --git a/djrill/tests/test_mandrill_send.py b/djrill/tests/test_mandrill_send.py index 96c4231..2565ac1 100644 --- a/djrill/tests/test_mandrill_send.py +++ b/djrill/tests/test_mandrill_send.py @@ -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("
First html is OK
", "text/html") email.attach_alternative("But not second html
", "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)