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 from requests import HTTPError
class MandrillAPIError(HTTPError): class MandrillAPIError(HTTPError):
"""Exception for unsuccessful response from Mandrill API.""" """Exception for unsuccessful response from Mandrill API."""
def __init__(self, status_code, response=None, log_message=None): def __init__(self, status_code, response=None, log_message=None, *args, **kwargs):
super(MandrillAPIError, self).__init__() super(MandrillAPIError, self).__init__(*args, **kwargs)
self.status_code = status_code self.status_code = status_code
self.response = response # often contains helpful Mandrill info self.response = response # often contains helpful Mandrill info
self.log_message = log_message self.log_message = log_message
@@ -22,8 +23,8 @@ class NotSupportedByMandrillError(ValueError):
This is typically raised when attempting to send a Django EmailMessage that 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 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 ignored by or can't be communicated to Mandrill's API. (E.g., non-HTML
attachment types, multiple bcc recipients.) alternative parts.)
It's generally *not* raised for Mandrill-specific features, like limitations It's generally *not* raised for Mandrill-specific features, like limitations
on Mandrill tag names or restrictions on from emails. (Djrill expects 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) sender = sanitize_address(message.from_email, message.encoding)
from_name, from_email = parseaddr(sender) from_name, from_email = parseaddr(sender)
recipients = message.to + message.cc # message.recipients() w/o bcc to_list = self._make_mandrill_to_list(message, message.to, "to")
parsed_rcpts = [parseaddr(sanitize_address(addr, message.encoding)) to_list += self._make_mandrill_to_list(message, message.cc, "cc")
for addr in recipients] to_list += self._make_mandrill_to_list(message, message.bcc, "bcc")
to_list = [{"email": to_email, "name": to_name}
for (to_name, to_email) in parsed_rcpts]
content = "html" if message.content_subtype == "html" else "text" content = "html" if message.content_subtype == "html" else "text"
msg_dict = { msg_dict = {
@@ -151,15 +149,6 @@ class DjrillBackend(BaseEmailBackend):
if from_name: if from_name:
msg_dict["from_name"] = 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: if message.extra_headers:
msg_dict["headers"] = message.extra_headers msg_dict["headers"] = message.extra_headers
@@ -175,6 +164,17 @@ class DjrillBackend(BaseEmailBackend):
if hasattr(message, attr): if hasattr(message, attr):
api_params[attr] = getattr(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): def _add_mandrill_options(self, message, msg_dict):
"""Extend msg_dict to include Mandrill per-message options set on message""" """Extend msg_dict to include Mandrill per-message options set on message"""
# Mandrill attributes that can be copied directly: # 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 django.core.mail import make_msgid
from djrill import MandrillAPIError, NotSupportedByMandrillError from djrill import MandrillAPIError, NotSupportedByMandrillError
from djrill.mail.backends.djrill import DjrillBackend
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
def decode_att(att): def decode_att(att):
@@ -62,12 +61,12 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
'From Name <from@example.com>', 'From Name <from@example.com>',
['Recipient #1 <to1@example.com>', 'to2@example.com'], ['Recipient #1 <to1@example.com>', 'to2@example.com'],
cc=['Carbon Copy <cc1@example.com>', 'cc2@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() msg.send()
data = self.get_api_call_data() data = self.get_api_call_data()
self.assertEqual(data['message']['from_name'], "From Name") self.assertEqual(data['message']['from_name'], "From Name")
self.assertEqual(data['message']['from_email'], "from@example.com") 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]['name'], "Recipient #1")
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com") self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
self.assertEqual(data['message']['to'][1]['name'], "") 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'][2]['email'], "cc1@example.com")
self.assertEqual(data['message']['to'][3]['name'], "") self.assertEqual(data['message']['to'][3]['name'], "")
self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com") self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com")
# Mandrill only supports email, not name, for bcc: self.assertEqual(data['message']['to'][4]['name'], "Blind Copy")
self.assertEqual(data['message']['bcc_address'], "bcc@example.com") 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): def test_email_message(self):
email = mail.EmailMessage('Subject', 'Body goes here', email = mail.EmailMessage('Subject', 'Body goes here',
'from@example.com', 'from@example.com',
['to1@example.com', 'Also To <to2@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>'], cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
headers={'Reply-To': 'another@example.com', headers={'Reply-To': 'another@example.com',
'X-MyHeader': 'my value', 'X-MyHeader': 'my value',
@@ -98,15 +99,22 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
{'Reply-To': 'another@example.com', {'Reply-To': 'another@example.com',
'X-MyHeader': 'my value', 'X-MyHeader': 'my value',
'Message-ID': 'mycustommsgid@example.com'}) 'Message-ID': 'mycustommsgid@example.com'})
# Mandrill doesn't have a notion of cc. # Verify recipients correctly identified as "to", "cc", or "bcc"
# Djrill just treats cc as additional "to" addresses, self.assertEqual(len(data['message']['to']), 6)
# which may or may not be what you want.
self.assertEqual(len(data['message']['to']), 4)
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com") 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]['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]['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']['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): def test_html_message(self):
text_content = 'This is an important message.' 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") msg="Mandrill API should not be called when send fails silently")
self.assertEqual(sent, 0) 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): def test_mandrill_api_failure(self):
self.mock_post.return_value = self.MockResponse(status_code=400) self.mock_post.return_value = self.MockResponse(status_code=400)
with self.assertRaises(MandrillAPIError): with self.assertRaises(MandrillAPIError):

View File

@@ -3,6 +3,7 @@ Release Notes
Version 0.9 (development): Version 0.9 (development):
* Better handling for "cc" and "bcc" recipients.
* Allow all extra message headers in send. * Allow all extra message headers in send.
(Mandrill has relaxed previous API restrictions on headers.) (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: Some notes and limitations:
**Display Names** **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 ("email\@example.com") or can include a display name
("Real Name <email\@example.com>"). ("Real Name <email\@example.com>").
**CC Recipients** **CC and BCC Recipients**
Djrill treats all "cc" recipients as if they were Djrill properly identifies "cc" and "bcc" recipients to Mandrill.
additional "to" addresses. (Mandrill does not distinguish "cc" from "to".)
.. 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 .. versionchanged:: 0.9
headers to list everyone who was sent the message, you'll also need to set the Previously, Djrill (and Mandrill) didn't distinguish "cc" from "to",
Mandrill option :attr:`preserve_recipients` to `!True` and allowed only a single "bcc" recipient.
**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"
.. _sending-html: .. _sending-html: