diff --git a/djrill/exceptions.py b/djrill/exceptions.py
index 4c986ff..10d7c9d 100644
--- a/djrill/exceptions.py
+++ b/djrill/exceptions.py
@@ -1,11 +1,12 @@
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.response = response # often contains helpful Mandrill info
self.log_message = log_message
def __str__(self):
@@ -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
diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py
index 52266cc..a75c2bf 100644
--- a/djrill/mail/backends/djrill.py
+++ b/djrill/mail/backends/djrill.py
@@ -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
" 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:
diff --git a/djrill/tests/test_mandrill_send.py b/djrill/tests/test_mandrill_send.py
index 453aabf..d6a5cea 100644
--- a/djrill/tests/test_mandrill_send.py
+++ b/djrill/tests/test_mandrill_send.py
@@ -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 ',
['Recipient #1 ', 'to2@example.com'],
cc=['Carbon Copy ', 'cc2@example.com'],
- bcc=['Blind Copy '])
+ bcc=['Blind Copy ', '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 '],
- bcc=['bcc@example.com'],
+ bcc=['bcc1@example.com', 'Also BCC '],
cc=['cc1@example.com', 'Also CC '],
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):
diff --git a/docs/history.rst b/docs/history.rst
index ab485d2..cc75916 100644
--- a/docs/history.rst
+++ b/docs/history.rst
@@ -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.)
diff --git a/docs/usage/sending_mail.rst b/docs/usage/sending_mail.rst
index b4e7b90..789db12 100644
--- a/docs/usage/sending_mail.rst
+++ b/docs/usage/sending_mail.rst
@@ -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 ").
-**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: