Skip to content

Commit

Permalink
Mailjet: Prevent empty attachment filename
Browse files Browse the repository at this point in the history
Mailjet requires all attachments/inlines have a non-empty Filename field.
Substitute `"attachment"` for missing filenames.

Fixes #407.
  • Loading branch information
medmunds committed Dec 10, 2024
1 parent 4584844 commit 026dcc4
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 7 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ Breaking changes
(Postal's signature verification uses the "cryptography" package, which is no
longer reliably installable with Python 3.8.)

Fixes
~~~~~

* **Mailjet:** Avoid a Mailjet API error when sending an inline image without a
filename. (Anymail now substitutes ``"attachment"`` for the missing filename.)
(Thanks to `@chickahoona`_ for reporting the issue.)


v12.0
-----
Expand Down Expand Up @@ -1737,6 +1744,7 @@ Features
.. _@b0d0nne11: https://github.com/b0d0nne11
.. _@calvin: https://github.com/calvin
.. _@carrerasrodrigo: https://github.com/carrerasrodrigo
.. _@chickahoona: https://github.com/chickahoona
.. _@chrisgrande: https://github.com/chrisgrande
.. _@cjsoftuk: https://github.com/cjsoftuk
.. _@costela: https://github.com/costela
Expand Down
3 changes: 2 additions & 1 deletion anymail/backends/mailjet.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ def set_html_body(self, body):
def add_attachment(self, attachment):
att = {
"ContentType": attachment.mimetype,
"Filename": attachment.name or "",
# Mailjet requires a non-empty Filename.
"Filename": attachment.name or "attachment",
"Base64Content": attachment.b64content,
}
if attachment.inline:
Expand Down
9 changes: 9 additions & 0 deletions docs/esps/mailjet.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ Limitations and quirks
:ref:`esp-send-status`, because Mailjet's other (statistics, event tracking)
APIs don't yet support MessageUUID.

**Attachments require filenames**
Mailjet requires that all attachments and inline images have filenames. If you
don't supply a filename, Anymail will use ``"attachment"`` as the filename.

.. versionchanged:: 13.0

Earlier Anymail versions would default to an empty string, resulting in
a Mailjet API error.

**Older limitations**

.. versionchanged:: 6.0
Expand Down
14 changes: 10 additions & 4 deletions tests/test_mailjet_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
AnymailSerializationError,
AnymailUnsupportedFeature,
)
from anymail.message import attach_inline_image_file
from anymail.message import attach_inline_image, attach_inline_image_file

from .mock_requests_backend import (
RequestsBackendMockAPITestCase,
Expand Down Expand Up @@ -266,7 +266,7 @@ def test_attachments(self):
self.assertNotIn("ContentID", attachments[1])

self.assertEqual(attachments[2]["ContentType"], "application/pdf")
self.assertEqual(attachments[2]["Filename"], "") # none
self.assertEqual(attachments[2]["Filename"], "attachment")
self.assertEqual(decode_att(attachments[2]["Base64Content"]), pdf_content)
self.assertNotIn("ContentID", attachments[2])

Expand Down Expand Up @@ -297,6 +297,7 @@ def test_embedded_images(self):
image_data = sample_image_content(image_filename)

cid = attach_inline_image_file(self.message, image_path) # Read from a png file
cid2 = attach_inline_image(self.message, image_data)
html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
)
Expand All @@ -307,11 +308,16 @@ def test_embedded_images(self):
self.assertEqual(data["Globals"]["HTMLPart"], html_content)

attachments = data["Globals"]["InlinedAttachments"]
self.assertEqual(len(attachments), 1)
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0]["Filename"], image_filename)
self.assertEqual(attachments[0]["ContentID"], cid)
self.assertEqual(attachments[0]["ContentType"], "image/png")
self.assertEqual(decode_att(attachments[0]["Base64Content"]), image_data)
# Mailjet requires a filename for all attachments, so make sure it's not empty:
self.assertEqual(attachments[1]["Filename"], "attachment")
self.assertEqual(attachments[1]["ContentID"], cid2)
self.assertEqual(attachments[1]["ContentType"], "image/png")
self.assertEqual(decode_att(attachments[1]["Base64Content"]), image_data)

self.assertNotIn("Attachments", data["Globals"])

Expand Down Expand Up @@ -340,7 +346,7 @@ def test_attached_images(self):
"Base64Content": image_data_b64,
},
{
"Filename": "", # the unnamed one
"Filename": "attachment", # the unnamed one
"ContentType": "image/png",
"Base64Content": image_data_b64,
},
Expand Down
4 changes: 2 additions & 2 deletions tests/test_mailjet_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from anymail.exceptions import AnymailAPIError
from anymail.message import AnymailMessage

from .utils import AnymailTestMixin, sample_image_path
from .utils import AnymailTestMixin, sample_image_content

ANYMAIL_TEST_MAILJET_API_KEY = os.getenv("ANYMAIL_TEST_MAILJET_API_KEY")
ANYMAIL_TEST_MAILJET_SECRET_KEY = os.getenv("ANYMAIL_TEST_MAILJET_SECRET_KEY")
Expand Down Expand Up @@ -91,7 +91,7 @@ def test_all_options(self):
)
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
cid = message.attach_inline_image_file(sample_image_path())
cid = message.attach_inline_image(sample_image_content())
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
Expand Down

0 comments on commit 026dcc4

Please sign in to comment.