From 5b4f4c12cba1cfd9b1a9da085fc71c1790cd71f5 Mon Sep 17 00:00:00 2001 From: medmunds Date: Thu, 3 Jan 2013 13:52:41 -0800 Subject: [PATCH] Support sending attachments --- README.rst | 3 ++- djrill/mail/backends/djrill.py | 42 +++++++++++++++++++++++++++++- djrill/tests/test_mandrill_send.py | 37 ++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0e0c8da..e76dcfd 100644 --- a/README.rst +++ b/README.rst @@ -122,7 +122,8 @@ Djrill supports most of the functionality of Django's `EmailMessage`_ and raise a ``ValueError`` exception when you attempt to send the message. (Mandrill doesn't support sending multiple html alternative parts, or any non-html alternatives.) -* Djrill (currently) silently ignores all attachments on a message. +* Djrill attempts to include a message's attachments, though Mandrill may place + some restrictions on allowable attachment types. (See the Mandrill docs.) * Djrill treats all cc and bcc recipients as if they were additional "to" addresses. (Mandrill does not distinguish cc, and only allows a single bcc -- which Djrill doesn't use. *Caution:* depending on the ``preserve_recipients`` diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index fb63822..e82bd69 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -1,10 +1,13 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.mail.backends.base import BaseEmailBackend -from django.core.mail.message import sanitize_address +from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE from django.utils import simplejson as json +from base64 import b64encode +from email.mime.base import MIMEBase from email.utils import parseaddr +import mimetypes import requests # This backend was developed against this API endpoint. @@ -68,6 +71,7 @@ class DjrillBackend(BaseEmailBackend): self._add_mandrill_options(message, msg_dict) if getattr(message, 'alternatives', None): self._add_alternatives(message, msg_dict) + self._add_attachments(message, msg_dict) except ValueError: if not self.fail_silently: raise @@ -186,3 +190,39 @@ class DjrillBackend(BaseEmailBackend): % mimetype) msg_dict['html'] = content + + def _add_attachments(self, message, msg_dict): + """Extend msg_dict to include any attachments in message""" + if message.attachments: + attachments = [ + self._make_mandrill_attachment(attachment) + for attachment in message.attachments + ] + if len(attachments) > 0: + msg_dict['attachments'] = attachments + + def _make_mandrill_attachment(self, attachment): + """Return a Mandrill dict for an EmailMessage.attachments item""" + # Note that an attachment can be either a tuple of (filename, content, + # mimetype) or a MIMEBase object. (Also, both filename and mimetype may + # be missing.) + if isinstance(attachment, MIMEBase): + filename = attachment.get_filename() + content = attachment.get_payload(decode=True) + mimetype = attachment.get_content_type() + else: + (filename, content, mimetype) = attachment + + # Guess missing mimetype, borrowed from + # django.core.mail.EmailMessage._create_attachment() + if mimetype is None and filename is not None: + mimetype, _ = mimetypes.guess_type(filename) + if mimetype is None: + mimetype = DEFAULT_ATTACHMENT_MIME_TYPE + + return { + 'type': mimetype, + 'name': filename or "", + 'content': b64encode(content), + } + diff --git a/djrill/tests/test_mandrill_send.py b/djrill/tests/test_mandrill_send.py index 4b0da6e..9bce8be 100644 --- a/djrill/tests/test_mandrill_send.py +++ b/djrill/tests/test_mandrill_send.py @@ -1,3 +1,6 @@ +from base64 import b64encode +from email.mime.base import MIMEBase + from django.conf import settings from django.core import mail from django.core.exceptions import ImproperlyConfigured @@ -78,6 +81,40 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase): data = self.get_api_call_data() self.assertEqual(data['message']['text'], text_content) self.assertEqual(data['message']['html'], html_content) + # Don't accidentally send the html part as an attachment: + self.assertFalse('attachments' in data['message']) + + def test_attachments(self): + email = mail.EmailMessage('Subject', 'Body goes here', + 'from@example.com', ['to1@example.com']) + + content1 = "* Item one\n* Item two\n* Item three" + email.attach(filename="test.txt", content=content1, + mimetype="text/plain") + + # Should guess mimetype if not provided... + content2 = "PNG* pretend this is the contents of a png file" + email.attach(filename="test.png", content=content2) + + # Should work with a MIMEBase object (also tests no filename)... + content3 = "PDF* pretend this is valid pdf data" + mimeattachment = MIMEBase('application', 'pdf') + mimeattachment.set_payload(content3) + email.attach(mimeattachment) + + email.send() + data = self.get_api_call_data() + attachments = data['message']['attachments'] + self.assertEqual(len(attachments), 3) + self.assertEqual(attachments[0]["type"], "text/plain") + self.assertEqual(attachments[0]["name"], "test.txt") + self.assertEqual(attachments[0]["content"], b64encode(content1)) + self.assertEqual(attachments[1]["type"], "image/png") # inferred + self.assertEqual(attachments[1]["name"], "test.png") + self.assertEqual(attachments[1]["content"], b64encode(content2)) + self.assertEqual(attachments[2]["type"], "application/pdf") + self.assertEqual(attachments[2]["name"], "") # none + self.assertEqual(attachments[2]["content"], b64encode(content3)) def test_extra_header_errors(self): email = mail.EmailMessage('Subject', 'Body', 'from@example.com',