diff --git a/README.rst b/README.rst
index 368749f..ea2bdbe 100644
--- a/README.rst
+++ b/README.rst
@@ -144,7 +144,10 @@ Djrill supports most of the functionality of Django's `EmailMessage`_ and
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 ``preserve_recipients`` set to False.)
-* **Attachments:** Djrill includes a message's attachments.
+* **Attachments:** Djrill includes a message's attachments. Also, if an image
+ attachment has a Content-ID header, Djrill will tell Mandrill to treat that
+ as an embedded image rather than an ordinary attachment. (For an example,
+ see ``test_embedded_images`` in tests/test_mandrill_send.py.)
* **Headers:** Djrill accepts additional headers, but only ``Reply-To`` and
``X-*`` (since that is all that Mandrill accepts). Any other extra headers
will raise ``djrill.NotSupportedByMandrillError`` when you attempt to send the
diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py
index 74e835d..5a31d7a 100644
--- a/djrill/mail/backends/djrill.py
+++ b/djrill/mail/backends/djrill.py
@@ -207,29 +207,46 @@ class DjrillBackend(BaseEmailBackend):
"""Extend msg_dict to include any attachments in message"""
if message.attachments:
str_encoding = message.encoding or settings.DEFAULT_CHARSET
- attachments = [
- self._make_mandrill_attachment(attachment, str_encoding)
- for attachment in message.attachments
- ]
- if len(attachments) > 0:
- msg_dict['attachments'] = attachments
+ mandrill_attachments = []
+ mandrill_embedded_images = []
+ for attachment in message.attachments:
+ att_dict, is_embedded = self._make_mandrill_attachment(attachment, str_encoding)
+ if is_embedded:
+ mandrill_embedded_images.append(att_dict)
+ else:
+ mandrill_attachments.append(att_dict)
+ if len(mandrill_attachments) > 0:
+ msg_dict['attachments'] = mandrill_attachments
+ if len(mandrill_embedded_images) > 0:
+ msg_dict['images'] = mandrill_embedded_images
def _make_mandrill_attachment(self, attachment, str_encoding=None):
- """Return a Mandrill dict for an EmailMessage.attachments item"""
+ """Returns EmailMessage.attachments item formatted for sending with Mandrill.
+
+ Returns mandrill_dict, is_embedded_image:
+ mandrill_dict: {"type":..., "name":..., "content":...}
+ is_embedded_image: True if the attachment should instead be handled as an inline image.
+
+ """
# 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.)
+ is_embedded_image = False
if isinstance(attachment, MIMEBase):
- filename = attachment.get_filename()
+ name = attachment.get_filename()
content = attachment.get_payload(decode=True)
mimetype = attachment.get_content_type()
+ # Treat image attachments that have content ids as embedded:
+ if attachment.get_content_maintype() == "image" and attachment["Content-ID"] is not None:
+ is_embedded_image = True
+ name = attachment["Content-ID"]
else:
- (filename, content, mimetype) = attachment
+ (name, content, mimetype) = attachment
- # Guess missing mimetype, borrowed from
+ # Guess missing mimetype from filename, 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 and name is not None:
+ mimetype, _ = mimetypes.guess_type(name)
if mimetype is None:
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
@@ -243,9 +260,10 @@ class DjrillBackend(BaseEmailBackend):
else:
raise
- return {
+ mandrill_attachment = {
'type': mimetype,
- 'name': filename or "",
+ 'name': name or "",
'content': content_b64.decode('ascii'),
}
+ return mandrill_attachment, is_embedded_image
diff --git a/djrill/tests/sample_image.png b/djrill/tests/sample_image.png
new file mode 100644
index 0000000..b23c10b
Binary files /dev/null and b/djrill/tests/sample_image.png differ
diff --git a/djrill/tests/test_mandrill_send.py b/djrill/tests/test_mandrill_send.py
index 43ea351..a70be22 100644
--- a/djrill/tests/test_mandrill_send.py
+++ b/djrill/tests/test_mandrill_send.py
@@ -1,17 +1,39 @@
from base64 import b64decode
from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
+import os
from django.conf import settings
from django.core import mail
from django.core.exceptions import ImproperlyConfigured
+from django.core.mail import make_msgid
from djrill import MandrillAPIError, NotSupportedByMandrillError
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
+def decode_att(att):
+ """Returns the original data from base64-encoded attachment content"""
+ return b64decode(att.encode('ascii'))
+
+
class DjrillBackendTests(DjrillBackendMockAPITestCase):
"""Test Djrill backend support for Django mail wrappers"""
+ sample_image_filename = "sample_image.png"
+
+ def sample_image_pathname(self):
+ """Returns path to an actual image file in the tests directory"""
+ test_dir = os.path.dirname(os.path.abspath(__file__))
+ path = os.path.join(test_dir, self.sample_image_filename)
+ return path
+
+ def sample_image_content(self):
+ """Returns contents of an actual image file from the tests directory"""
+ filename = self.sample_image_pathname()
+ with open(filename, "rb") as f:
+ return f.read()
+
def test_send_mail(self):
mail.send_mail('Subject here', 'Here is the message.',
'from@example.com', ['to@example.com'], fail_silently=False)
@@ -97,12 +119,10 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
self.assertFalse('attachments' in data['message'])
def test_attachments(self):
- email = mail.EmailMessage('Subject', 'Body goes here',
- 'from@example.com', ['to1@example.com'])
+ email = mail.EmailMessage('Subject', 'Body goes here', 'from@example.com', ['to1@example.com'])
text_content = "* Item one\n* Item two\n* Item three"
- email.attach(filename="test.txt", content=text_content,
- mimetype="text/plain")
+ email.attach(filename="test.txt", content=text_content, mimetype="text/plain")
# Should guess mimetype if not provided...
png_content = b"PNG\xb4 pretend this is the contents of a png file"
@@ -120,27 +140,70 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
mimetype="application/vnd.ms-powerpoint")
email.send()
-
- def decode_att(att):
- return b64decode(att.encode('ascii'))
-
data = self.get_api_call_data()
attachments = data['message']['attachments']
self.assertEqual(len(attachments), 4)
self.assertEqual(attachments[0]["type"], "text/plain")
self.assertEqual(attachments[0]["name"], "test.txt")
- self.assertEqual(decode_att(attachments[0]["content"]).decode('ascii'),
- text_content)
- self.assertEqual(attachments[1]["type"], "image/png") # inferred
+ self.assertEqual(decode_att(attachments[0]["content"]).decode('ascii'), text_content)
+ self.assertEqual(attachments[1]["type"], "image/png") # inferred from filename
self.assertEqual(attachments[1]["name"], "test.png")
self.assertEqual(decode_att(attachments[1]["content"]), png_content)
self.assertEqual(attachments[2]["type"], "application/pdf")
- self.assertEqual(attachments[2]["name"], "") # none
+ self.assertEqual(attachments[2]["name"], "") # none
self.assertEqual(decode_att(attachments[2]["content"]), pdf_content)
- self.assertEqual(attachments[3]["type"],
- "application/vnd.ms-powerpoint")
+ self.assertEqual(attachments[3]["type"], "application/vnd.ms-powerpoint")
self.assertEqual(attachments[3]["name"], "presentation.ppt")
self.assertEqual(decode_att(attachments[3]["content"]), ppt_content)
+ # Make sure the image attachment is not treated as embedded:
+ self.assertFalse('images' in data['message'])
+
+ def test_embedded_images(self):
+ image_data = self.sample_image_content() # Read from a png file
+ image_cid = make_msgid("img") # Content ID per RFC 2045 section 7 (with <...>)
+ image_cid_no_brackets = image_cid[1:-1] # Without <...>, for use as the tag src
+
+ text_content = 'This has an inline image.'
+ html_content = '
This has an image.