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
|
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
|
* Add (undocumented) DEBUG_API_REQUESTS Anymail setting. When enabled, prints raw
|
||||||
API request and response during send. Currently implemented only for Requests-based
|
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
|
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.content = attachment.as_string().encode(self.encoding)
|
||||||
self.mimetype = attachment.get_content_type()
|
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.inline = True
|
||||||
self.content_id = attachment["Content-ID"] # probably including the <...>
|
self.content_id = attachment["Content-ID"] # probably including the <...>
|
||||||
if self.content_id is not None:
|
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
|
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.
|
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:
|
.. _message-headers:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Tests for the anymail/utils.py module
|
# Tests for the anymail/utils.py module
|
||||||
# (not to be confused with utilities for testing found in in tests/utils.py)
|
# (not to be confused with utilities for testing found in in tests/utils.py)
|
||||||
import base64
|
import base64
|
||||||
|
from email.mime.image import MIMEImage
|
||||||
from unittest import skipIf
|
from unittest import skipIf
|
||||||
|
|
||||||
import six
|
import six
|
||||||
@@ -21,6 +22,7 @@ except ImportError:
|
|||||||
from anymail.exceptions import AnymailInvalidAddress, _LazyError
|
from anymail.exceptions import AnymailInvalidAddress, _LazyError
|
||||||
from anymail.utils import (
|
from anymail.utils import (
|
||||||
parse_address_list, parse_single_address, EmailAddress,
|
parse_address_list, parse_single_address, EmailAddress,
|
||||||
|
Attachment,
|
||||||
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
|
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
|
||||||
update_deep,
|
update_deep,
|
||||||
get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst)
|
get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst)
|
||||||
@@ -161,6 +163,51 @@ class ParseAddressListTests(SimpleTestCase):
|
|||||||
parse_single_address(" ")
|
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):
|
class LazyCoercionTests(SimpleTestCase):
|
||||||
"""Test utils.is_lazy and force_non_lazy*"""
|
"""Test utils.is_lazy and force_non_lazy*"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user