also consider Content-ID when marking attachment as inline (#126)

Handle MIME attachments with Content-ID as inline by default.

Treat MIME attachments that have a *Content-ID* but no explicit *Content-Disposition*
header as inline, matching the behavior of many email clients.
This commit is contained in:
Leo Antunes
2018-10-11 23:29:00 +02:00
committed by Mike Edmunds
parent 4028eda583
commit 64f7d31d14
4 changed files with 62 additions and 1 deletions

View File

@@ -33,6 +33,10 @@ v4.3
Features
~~~~~~~~
* Treat MIME attachments that have a *Content-ID* but no explicit *Content-Disposition*
header as inline, matching the behavior of many email clients. For maximum
compatibility, you should always set both (or use Anymail's inline helper functions).
(Thanks `@costela`_.)
* Add (undocumented) DEBUG_API_REQUESTS Anymail setting. When enabled, prints raw
API request and response during send. Currently implemented only for Requests-based
backends (all but Amazon SES and SparkPost). Because this can expose API keys and

View File

@@ -291,7 +291,8 @@ class Attachment(object):
self.content = attachment.as_string().encode(self.encoding)
self.mimetype = attachment.get_content_type()
if get_content_disposition(attachment) == 'inline':
content_disposition = get_content_disposition(attachment)
if content_disposition == 'inline' or (not content_disposition and 'Content-ID' in attachment):
self.inline = True
self.content_id = attachment["Content-ID"] # probably including the <...>
if self.content_id is not None:

View File

@@ -88,6 +88,15 @@ and :meth:`~email.message.Message.add_header` should be helpful.)
Even if you mark an attachment as inline, some email clients may decide to also
display it as an attachment. This is largely outside your control.
.. versionchanged:: 4.3
For convenience, Anymail will treat an attachment with a :mailheader:`Content-ID`
but no :mailheader:`Content-Disposition` as inline. (Many---though not all---email
clients make the same assumption. But to ensure consistent behavior with non-Anymail
email backends, you should always set *both* :mailheader:`Content-ID` and
:mailheader:`Content-Disposition: inline` headers for inline images. Or just use
Anymail's :ref:`inline image helpers <inline-images>`, which handle this for you.)
.. _message-headers:

View File

@@ -1,6 +1,7 @@
# Tests for the anymail/utils.py module
# (not to be confused with utilities for testing found in in tests/utils.py)
import base64
from email.mime.image import MIMEImage
from unittest import skipIf
import six
@@ -21,6 +22,7 @@ except ImportError:
from anymail.exceptions import AnymailInvalidAddress, _LazyError
from anymail.utils import (
parse_address_list, parse_single_address, EmailAddress,
Attachment,
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
update_deep,
get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst)
@@ -161,6 +163,51 @@ class ParseAddressListTests(SimpleTestCase):
parse_single_address(" ")
class NormalizedAttachmentTests(SimpleTestCase):
"""Test utils.Attachment"""
# (Several basic tests could be added here)
def test_content_disposition_attachment(self):
image = MIMEImage(b";-)", "x-emoticon")
image["Content-Disposition"] = 'attachment; filename="emoticon.txt"'
att = Attachment(image, "ascii")
self.assertEqual(att.name, "emoticon.txt")
self.assertEqual(att.content, b";-)")
self.assertFalse(att.inline)
self.assertIsNone(att.content_id)
self.assertEqual(att.cid, "")
def test_content_disposition_inline(self):
image = MIMEImage(b";-)", "x-emoticon")
image["Content-Disposition"] = 'inline'
att = Attachment(image, "ascii")
self.assertIsNone(att.name)
self.assertEqual(att.content, b";-)")
self.assertTrue(att.inline) # even without the Content-ID
self.assertIsNone(att.content_id)
self.assertEqual(att.cid, "")
image["Content-ID"] = "<abc123@example.net>"
att = Attachment(image, "ascii")
self.assertEqual(att.content_id, "<abc123@example.net>")
self.assertEqual(att.cid, "abc123@example.net")
def test_content_id_implies_inline(self):
"""A MIME object with a Content-ID should be assumed to be inline"""
image = MIMEImage(b";-)", "x-emoticon")
image["Content-ID"] = "<abc123@example.net>"
att = Attachment(image, "ascii")
self.assertTrue(att.inline)
self.assertEqual(att.content_id, "<abc123@example.net>")
# ... but not if explicit Content-Disposition says otherwise
image["Content-Disposition"] = "attachment"
att = Attachment(image, "ascii")
self.assertFalse(att.inline)
self.assertIsNone(att.content_id) # ignored for non-inline Attachment
class LazyCoercionTests(SimpleTestCase):
"""Test utils.is_lazy and force_non_lazy*"""