From d7c06bb5769b773bad867ab6dc698ffbf5be0b5c Mon Sep 17 00:00:00 2001 From: medmunds Date: Sat, 25 Jan 2014 12:35:23 -0800 Subject: [PATCH] Better handling for cc and bcc recipients. Fixes #59. --- djrill/exceptions.py | 11 +++++---- djrill/mail/backends/djrill.py | 28 +++++++++++----------- djrill/tests/test_mandrill_send.py | 38 +++++++++++++++--------------- docs/history.rst | 1 + docs/usage/sending_mail.rst | 30 +++++++---------------- 5 files changed, 49 insertions(+), 59 deletions(-) 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: