mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
@@ -1,11 +1,12 @@
|
|||||||
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
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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.)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user