mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
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:
committed by
Mike Edmunds
parent
4028eda583
commit
64f7d31d14
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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*"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user