diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index d9997cf..e59a98e 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -26,7 +26,7 @@ class DjrillBackend(BaseEmailBackend): Mandrill API Email Backend """ - def __init__(self, fail_silently=False, **kwargs): + def __init__(self, **kwargs): """ Set the API key, API url and set the action url. """ @@ -71,8 +71,13 @@ class DjrillBackend(BaseEmailBackend): if getattr(message, "alternative_subtype", None): if message.alternative_subtype == "mandrill": self._build_advanced_message_dict(message) - if message.alternatives: - self._add_alternatives(message) + try: + if getattr(message, 'alternatives', None): + self._add_alternatives(message) + except ValueError: + if not self.fail_silently: + raise + return False djrill_it = requests.post(self.api_action, data=json.dumps({ "key": self.api_key, @@ -96,46 +101,54 @@ class DjrillBackend(BaseEmailBackend): use by default. Standard text email messages sent through Django will still work through Mandrill. """ - return { + msg_dict = { "text": message.body, "subject": message.subject, "from_email": self.sender, "to": self.recipients } + if message.extra_headers: + accepted_headers = {} + for k in message.extra_headers.keys(): + if k.startswith("X-") or k == "Reply-To": + accepted_headers.update( + {"%s" % k: message.extra_headers[k]}) + msg_dict.update({"headers": accepted_headers}) + + return msg_dict + def _build_advanced_message_dict(self, message): """ - Builds advanced message dict and attaches any accepted extra headers. + Builds advanced message dict """ self.msg_dict.update({ "from_name": message.from_name, "tags": message.tags, "track_opens": message.track_opens, + "track_clicks": message.track_clicks }) - if message.extra_headers: - accepted_headers = {} - - for k in message.extra_headers.keys(): - if k.startswith("X-") or k == "Reply-To": - accepted_headers.update( - {"%s" % k: message.extra_headers[k]}) - self.msg_dict.update({"headers": accepted_headers}) def _add_alternatives(self, message): """ - There can be only one! ... alternative attachment. + There can be only one! ... alternative attachment, and it must be text/html. Since mandrill does not accept image attachments or anything other than HTML, the assumption is the only thing you are attaching is the HTML output for your email. """ if len(message.alternatives) > 1: - raise ImproperlyConfigured( - "Mandrill only accepts plain text and html emails. Please " - "check the alternatives you have attached to your message.") + raise ValueError( + "Too many alternatives attached to the message. " + "Mandrill only accepts plain text and html emails.") + + (content, mimetype) = message.alternatives[0] + if mimetype != 'text/html': + raise ValueError("Invalid alternative mimetype '%s'. " + "Mandrill only accepts plain text and html emails." + % mimetype) self.msg_dict.update({ - "html": message.alternatives[0][0], - "track_clicks": message.track_clicks + "html": content }) diff --git a/djrill/tests.py b/djrill/tests.py index eebba74..e3ee158 100644 --- a/djrill/tests.py +++ b/djrill/tests.py @@ -65,6 +65,69 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase): mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com']) + def test_email_message(self): + email = mail.EmailMessage('Subject', 'Body goes here', + 'from@example.com', + ['to1@example.com', 'Also To '], + bcc=['bcc1@example.com', 'Also BCC '], + cc=['cc1@example.com', 'Also CC '], + headers={'Reply-To': 'another@example.com', + 'X-MyHeader': 'my value', + 'Errors-To': 'silently stripped'}) + email.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['subject'], "Subject") + self.assertEqual(data['message']['text'], "Body goes here") + self.assertEqual(data['message']['from_email'], "from@example.com") + self.assertEqual(data['message']['headers'], + { 'Reply-To': 'another@example.com', 'X-MyHeader': 'my value' }) + # Mandrill doesn't have a notion of cc, and only allows a single bcc. + # Djrill just treats cc and bcc as though they were "to" addresses, + # which may or may not be what you want. + self.assertEqual(len(data['message']['to']), 6) + self.assertEqual(data['message']['to'][0]['email'], "to1@example.com") + self.assertEqual(data['message']['to'][1]['email'], "to2@example.com") + self.assertEqual(data['message']['to'][2]['email'], "cc1@example.com") + self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com") + self.assertEqual(data['message']['to'][4]['email'], "bcc1@example.com") + self.assertEqual(data['message']['to'][5]['email'], "bcc2@example.com") + + def test_html_message(self): + text_content = 'This is an important message.' + html_content = '

This is an important message.

' + email = mail.EmailMultiAlternatives('Subject', text_content, + 'from@example.com', ['to@example.com']) + email.attach_alternative(html_content, "text/html") + email.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['text'], text_content) + self.assertEqual(data['message']['html'], html_content) + + def test_alternative_errors(self): + # Multiple alternatives not allowed + email = mail.EmailMultiAlternatives('Subject', 'Body', + 'from@example.com', ['to@example.com']) + email.attach_alternative("

First html is OK

", "text/html") + email.attach_alternative("

But not second html

", "text/html") + with self.assertRaises(ValueError): + email.send() + + # Only html alternatives allowed + email = mail.EmailMultiAlternatives('Subject', 'Body', + 'from@example.com', ['to@example.com']) + email.attach_alternative("{'not': 'allowed'}", "application/json") + with self.assertRaises(ValueError): + email.send() + + # Make sure fail_silently is respected + email = mail.EmailMultiAlternatives('Subject', 'Body', + 'from@example.com', ['to@example.com']) + email.attach_alternative("{'not': 'allowed'}", "application/json") + sent = email.send(fail_silently=True) + self.assertFalse(self.mock_post.called, + msg="Mandrill API should not be called when send fails silently") + self.assertEqual(sent, 0) + class DjrillMessageTests(TestCase): def setUp(self):