Better handling for cc and bcc recipients.

Fixes #59.
This commit is contained in:
medmunds
2014-01-25 12:35:23 -08:00
parent 1e44392b13
commit d7c06bb576
5 changed files with 49 additions and 59 deletions

View File

@@ -1,9 +1,10 @@
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__()
def __init__(self, status_code, response=None, log_message=None, *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
@@ -22,8 +23,8 @@ class NotSupportedByMandrillError(ValueError):
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.)
ignored by or can't be communicated to Mandrill's API. (E.g., non-HTML
alternative parts.)
It's generally *not* raised for Mandrill-specific features, like limitations
on Mandrill tag names or restrictions on from emails. (Djrill expects

View File

@@ -135,11 +135,9 @@ class DjrillBackend(BaseEmailBackend):
sender = sanitize_address(message.from_email, message.encoding)
from_name, from_email = parseaddr(sender)
recipients = message.to + message.cc # message.recipients() w/o bcc
parsed_rcpts = [parseaddr(sanitize_address(addr, message.encoding))
for addr in recipients]
to_list = [{"email": to_email, "name": to_name}
for (to_name, to_email) in parsed_rcpts]
to_list = self._make_mandrill_to_list(message, message.to, "to")
to_list += self._make_mandrill_to_list(message, message.cc, "cc")
to_list += self._make_mandrill_to_list(message, message.bcc, "bcc")
content = "html" if message.content_subtype == "html" else "text"
msg_dict = {
@@ -151,15 +149,6 @@ class DjrillBackend(BaseEmailBackend):
if from_name:
msg_dict["from_name"] = from_name
if len(message.bcc) == 1:
bcc = message.bcc[0]
_, bcc_addr = parseaddr(sanitize_address(bcc, message.encoding))
msg_dict['bcc_address'] = bcc_addr
elif len(message.bcc) > 1:
raise NotSupportedByMandrillError(
"Too many bcc addresses (%d) - Mandrill only allows one"
% len(message.bcc))
if message.extra_headers:
msg_dict["headers"] = message.extra_headers
@@ -175,6 +164,17 @@ class DjrillBackend(BaseEmailBackend):
if hasattr(message, attr):
api_params[attr] = getattr(message, attr)
def _make_mandrill_to_list(self, message, recipients, recipient_type="to"):
"""Create a Mandrill 'to' field from a list of emails.
Parses "Real Name <address@example.com>" format emails.
Sanitizes all email addresses.
"""
parsed_rcpts = [parseaddr(sanitize_address(addr, message.encoding))
for addr in recipients]
return [{"email": to_email, "name": to_name, "type": recipient_type}
for (to_name, to_email) in parsed_rcpts]
def _add_mandrill_options(self, message, msg_dict):
"""Extend msg_dict to include Mandrill per-message options set on message"""
# Mandrill attributes that can be copied directly:

View File

@@ -10,7 +10,6 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.mail import make_msgid
from djrill import MandrillAPIError, NotSupportedByMandrillError
from djrill.mail.backends.djrill import DjrillBackend
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
def decode_att(att):
@@ -62,12 +61,12 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
'From Name <from@example.com>',
['Recipient #1 <to1@example.com>', 'to2@example.com'],
cc=['Carbon Copy <cc1@example.com>', 'cc2@example.com'],
bcc=['Blind Copy <bcc@example.com>'])
bcc=['Blind Copy <bcc1@example.com>', 'bcc2@example.com'])
msg.send()
data = self.get_api_call_data()
self.assertEqual(data['message']['from_name'], "From Name")
self.assertEqual(data['message']['from_email'], "from@example.com")
self.assertEqual(len(data['message']['to']), 4)
self.assertEqual(len(data['message']['to']), 6)
self.assertEqual(data['message']['to'][0]['name'], "Recipient #1")
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
self.assertEqual(data['message']['to'][1]['name'], "")
@@ -76,14 +75,16 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
self.assertEqual(data['message']['to'][2]['email'], "cc1@example.com")
self.assertEqual(data['message']['to'][3]['name'], "")
self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com")
# Mandrill only supports email, not name, for bcc:
self.assertEqual(data['message']['bcc_address'], "bcc@example.com")
self.assertEqual(data['message']['to'][4]['name'], "Blind Copy")
self.assertEqual(data['message']['to'][4]['email'], "bcc1@example.com")
self.assertEqual(data['message']['to'][5]['name'], "")
self.assertEqual(data['message']['to'][5]['email'], "bcc2@example.com")
def test_email_message(self):
email = mail.EmailMessage('Subject', 'Body goes here',
'from@example.com',
['to1@example.com', 'Also To <to2@example.com>'],
bcc=['bcc@example.com'],
bcc=['bcc1@example.com', 'Also BCC <bcc2@example.com>'],
cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
headers={'Reply-To': 'another@example.com',
'X-MyHeader': 'my value',
@@ -98,15 +99,22 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
{'Reply-To': 'another@example.com',
'X-MyHeader': 'my value',
'Message-ID': 'mycustommsgid@example.com'})
# Mandrill doesn't have a notion of cc.
# Djrill just treats cc as additional "to" addresses,
# which may or may not be what you want.
self.assertEqual(len(data['message']['to']), 4)
# Verify recipients correctly identified as "to", "cc", or "bcc"
self.assertEqual(len(data['message']['to']), 6)
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
self.assertEqual(data['message']['to'][0]['type'], "to")
self.assertEqual(data['message']['to'][1]['email'], "to2@example.com")
self.assertEqual(data['message']['to'][1]['type'], "to")
self.assertEqual(data['message']['to'][2]['email'], "cc1@example.com")
self.assertEqual(data['message']['to'][2]['type'], "cc")
self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com")
self.assertEqual(data['message']['bcc_address'], "bcc@example.com")
self.assertEqual(data['message']['to'][3]['type'], "cc")
self.assertEqual(data['message']['to'][4]['email'], "bcc1@example.com")
self.assertEqual(data['message']['to'][4]['type'], "bcc")
self.assertEqual(data['message']['to'][5]['email'], "bcc2@example.com")
self.assertEqual(data['message']['to'][5]['type'], "bcc")
# Don't use Mandrill's bcc_address "logging" feature for bcc's:
self.assertNotIn('bcc_address', data['message'])
def test_html_message(self):
text_content = 'This is an important message.'
@@ -245,14 +253,6 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
msg="Mandrill API should not be called when send fails silently")
self.assertEqual(sent, 0)
def test_bcc_errors(self):
# Mandrill only allows a single bcc address
with self.assertRaises(NotSupportedByMandrillError):
msg = mail.EmailMessage('Subject', 'Body',
'from@example.com', ['to@example.com'],
bcc=['bcc1@example.com>', 'bcc2@example.com'])
msg.send()
def test_mandrill_api_failure(self):
self.mock_post.return_value = self.MockResponse(status_code=400)
with self.assertRaises(MandrillAPIError):

View File

@@ -3,6 +3,7 @@ Release Notes
Version 0.9 (development):
* Better handling for "cc" and "bcc" recipients.
* Allow all extra message headers in send.
(Mandrill has relaxed previous API restrictions on headers.)

View File

@@ -21,32 +21,20 @@ and :class:`~django.core.mail.EmailMultiAlternatives` classes.
Some notes and limitations:
**Display Names**
All email addresses (from, to, cc) can be simple
All email addresses (from, to, cc, bcc) can be simple
("email\@example.com") or can include a display name
("Real Name <email\@example.com>").
**CC Recipients**
Djrill treats all "cc" recipients as if they were
additional "to" addresses. (Mandrill does not distinguish "cc" from "to".)
**CC and BCC Recipients**
Djrill properly identifies "cc" and "bcc" recipients to Mandrill.
.. note::
Note that you may need to set the Mandrill option :attr:`preserve_recipients`
to `!True` if you want recipients to be able to see who else was included
in the "to" list.
By default, Mandrill hides all recipients from each other. If you want the
headers to list everyone who was sent the message, you'll also need to set the
Mandrill option :attr:`preserve_recipients` to `!True`
**BCC Recipients**
Mandrill does not permit more than one "bcc" address.
Djrill raises :exc:`~djrill.NotSupportedByMandrillError` if you attempt to send a
message with multiple bcc's.
(Mandrill's bcc option seems intended primarily
for logging. To send a single message to multiple recipients without exposing
their email addresses to each other, simply include them all in the "to" list
and leave Mandrill's :attr:`preserve_recipients` set to `!False`.)
.. versionadded:: 0.3
Previously "bcc" was treated as "cc"
.. versionchanged:: 0.9
Previously, Djrill (and Mandrill) didn't distinguish "cc" from "to",
and allowed only a single "bcc" recipient.
.. _sending-html: