diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f354ff0..a3d5e2e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,16 @@ Breaking changes * **SendGrid:** Remove the legacy SendGrid *v2* EmailBackend (Anymail has defaulted to SendGrid's newer v3 API since Anymail v0.8.) +Fixes +~~~~~ + +* Change `attach_inline_image()` default domain for *Content-ID* to "inline" (rather + than Python's `make_msgid()` default local hostname). This avoids problems with ESPs + that don't distinguish *Content-ID* from attachment filename, where a local hostname + ending in ".com" could cause Gmail to block messages sent with inline attachments. + (Mailgun, Mailjet, Mandrill and SparkPost have APIs affected by this. + See `#112`_ for more details.) + Other ~~~~~ @@ -745,6 +755,7 @@ Features .. _#108: https://github.com/anymail/issues/108 .. _#110: https://github.com/anymail/issues/110 .. _#111: https://github.com/anymail/issues/111 +.. _#112: https://github.com/anymail/issues/112 .. _@calvin: https://github.com/calvin .. _@joshkersey: https://github.com/joshkersey diff --git a/anymail/message.py b/anymail/message.py index 044e6a9..1684816 100644 --- a/anymail/message.py +++ b/anymail/message.py @@ -59,6 +59,10 @@ def attach_inline_image_file(message, path, subtype=None, idstring="img", domain def attach_inline_image(message, content, filename=None, subtype=None, idstring="img", domain=None): """Add inline image to an EmailMessage, and return its content id""" + if domain is None: + # Avoid defaulting to hostname that might end in '.com', because some ESPs + # use Content-ID as filename, and Gmail blocks filenames ending in '.com'. + domain = 'inline' # valid domain for a msgid; will never be a real TLD content_id = make_msgid(idstring, domain) # Content ID per RFC 2045 section 7 (with <...>) image = MIMEImage(content, subtype) image.add_header('Content-Disposition', 'inline', filename=filename) diff --git a/docs/sending/anymail_additions.rst b/docs/sending/anymail_additions.rst index 6cf5c09..05724c2 100644 --- a/docs/sending/anymail_additions.rst +++ b/docs/sending/anymail_additions.rst @@ -398,9 +398,15 @@ classes.) `idstring` and `domain` are optional, and are passed to Python's :func:`~email.utils.make_msgid` to generate the :mailheader:`Content-ID`. Generally the defaults should be fine. - (But be aware the default `domain` can leak your server's local hostname - in the resulting email.) + .. versionchanged:: 4.0 + + If you don't supply a `domain`, Anymail will use the simple string "inline" + rather than :func:`~email.utils.make_msgid`'s default local hostname. This + avoids a problem with ESPs that confuse :mailheader:`Content-ID` and attachment + filename: if your local server's hostname ends in ".com", Gmail could block + messages with inline attachments generated by earlier Anymail versions and sent + through these ESPs. .. function:: attach_inline_image(message, content, filename=None, subtype=None, idstring="img", domain=None) diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 0000000..b82313c --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,31 @@ +from django.core.mail import EmailMultiAlternatives +from django.test import SimpleTestCase +from mock import patch + +from anymail.message import attach_inline_image + +from .utils import AnymailTestMixin, sample_image_content + + +class InlineImageTests(AnymailTestMixin, SimpleTestCase): + def setUp(self): + self.message = EmailMultiAlternatives() + super(InlineImageTests, self).setUp() + + @patch("email.utils.socket.getfqdn") + def test_default_domain(self, mock_getfqdn): + """The default Content-ID domain should *not* use local hostname""" + # (This avoids problems with ESPs that re-use Content-ID as attachment + # filename: if the local hostname ends in ".com", you can end up with + # an inline attachment filename that causes Gmail to reject the message.) + mock_getfqdn.return_value = "server.example.com" + cid = attach_inline_image(self.message, sample_image_content()) + self.assertRegex(cid, r"[\w.]+@inline", + "Content-ID should be a valid Message-ID, " + "but _not_ @server.example.com") + + def test_domain_override(self): + cid = attach_inline_image(self.message, sample_image_content(), + domain="example.org") + self.assertRegex(cid, r"[\w.]+@example\.org", + "Content-ID should be a valid Message-ID @example.org")