From 026dcc49e4dd90467fba9f1986f2e32209c3e1bb Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Tue, 10 Dec 2024 12:01:17 -0800 Subject: [PATCH] Mailjet: Prevent empty attachment filename Mailjet requires all attachments/inlines have a non-empty Filename field. Substitute `"attachment"` for missing filenames. Fixes #407. --- CHANGELOG.rst | 8 ++++++++ anymail/backends/mailjet.py | 3 ++- docs/esps/mailjet.rst | 9 +++++++++ tests/test_mailjet_backend.py | 14 ++++++++++---- tests/test_mailjet_integration.py | 4 ++-- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5bd950f1..218f3502 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ----- @@ -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 diff --git a/anymail/backends/mailjet.py b/anymail/backends/mailjet.py index 1c17bee4..eb424bd4 100644 --- a/anymail/backends/mailjet.py +++ b/anymail/backends/mailjet.py @@ -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: diff --git a/docs/esps/mailjet.rst b/docs/esps/mailjet.rst index 78b253f1..68f0fc7c 100644 --- a/docs/esps/mailjet.rst +++ b/docs/esps/mailjet.rst @@ -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 diff --git a/tests/test_mailjet_backend.py b/tests/test_mailjet_backend.py index dec6c575..14c1a7e7 100644 --- a/tests/test_mailjet_backend.py +++ b/tests/test_mailjet_backend.py @@ -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, @@ -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]) @@ -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 = ( '

This has an inline image.

' % cid ) @@ -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"]) @@ -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, }, diff --git a/tests/test_mailjet_integration.py b/tests/test_mailjet_integration.py index d3f183fa..96a40600 100644 --- a/tests/test_mailjet_integration.py +++ b/tests/test_mailjet_integration.py @@ -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") @@ -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( "

HTML: with link" "and image: " % cid,