Mailjet: Prevent empty attachment filename

Mailjet requires all attachments/inlines have a non-empty Filename field.
Substitute `"attachment"` for missing filenames.

Fixes #407.
This commit is contained in:
Mike Edmunds
2024-12-10 12:01:17 -08:00
parent 45848440b3
commit c7f7428b7a
5 changed files with 31 additions and 7 deletions

View File

@@ -37,6 +37,13 @@ Breaking changes
(Postal's signature verification uses the "cryptography" package, which is no (Postal's signature verification uses the "cryptography" package, which is no
longer reliably installable with Python 3.8.) 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 v12.0
----- -----
@@ -1737,6 +1744,7 @@ Features
.. _@b0d0nne11: https://github.com/b0d0nne11 .. _@b0d0nne11: https://github.com/b0d0nne11
.. _@calvin: https://github.com/calvin .. _@calvin: https://github.com/calvin
.. _@carrerasrodrigo: https://github.com/carrerasrodrigo .. _@carrerasrodrigo: https://github.com/carrerasrodrigo
.. _@chickahoona: https://github.com/chickahoona
.. _@chrisgrande: https://github.com/chrisgrande .. _@chrisgrande: https://github.com/chrisgrande
.. _@cjsoftuk: https://github.com/cjsoftuk .. _@cjsoftuk: https://github.com/cjsoftuk
.. _@costela: https://github.com/costela .. _@costela: https://github.com/costela

View File

@@ -195,7 +195,8 @@ class MailjetPayload(RequestsPayload):
def add_attachment(self, attachment): def add_attachment(self, attachment):
att = { att = {
"ContentType": attachment.mimetype, "ContentType": attachment.mimetype,
"Filename": attachment.name or "", # Mailjet requires a non-empty Filename.
"Filename": attachment.name or "attachment",
"Base64Content": attachment.b64content, "Base64Content": attachment.b64content,
} }
if attachment.inline: if attachment.inline:

View File

@@ -144,6 +144,15 @@ Limitations and quirks
:ref:`esp-send-status`, because Mailjet's other (statistics, event tracking) :ref:`esp-send-status`, because Mailjet's other (statistics, event tracking)
APIs don't yet support MessageUUID. 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** **Older limitations**
.. versionchanged:: 6.0 .. versionchanged:: 6.0

View File

@@ -13,7 +13,7 @@ from anymail.exceptions import (
AnymailSerializationError, AnymailSerializationError,
AnymailUnsupportedFeature, 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 ( from .mock_requests_backend import (
RequestsBackendMockAPITestCase, RequestsBackendMockAPITestCase,
@@ -266,7 +266,7 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
self.assertNotIn("ContentID", attachments[1]) self.assertNotIn("ContentID", attachments[1])
self.assertEqual(attachments[2]["ContentType"], "application/pdf") 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.assertEqual(decode_att(attachments[2]["Base64Content"]), pdf_content)
self.assertNotIn("ContentID", attachments[2]) self.assertNotIn("ContentID", attachments[2])
@@ -297,6 +297,7 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
image_data = sample_image_content(image_filename) image_data = sample_image_content(image_filename)
cid = attach_inline_image_file(self.message, image_path) # Read from a png file cid = attach_inline_image_file(self.message, image_path) # Read from a png file
cid2 = attach_inline_image(self.message, image_data)
html_content = ( html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid '<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
) )
@@ -307,11 +308,16 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
self.assertEqual(data["Globals"]["HTMLPart"], html_content) self.assertEqual(data["Globals"]["HTMLPart"], html_content)
attachments = data["Globals"]["InlinedAttachments"] 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]["Filename"], image_filename)
self.assertEqual(attachments[0]["ContentID"], cid) self.assertEqual(attachments[0]["ContentID"], cid)
self.assertEqual(attachments[0]["ContentType"], "image/png") self.assertEqual(attachments[0]["ContentType"], "image/png")
self.assertEqual(decode_att(attachments[0]["Base64Content"]), image_data) 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"]) self.assertNotIn("Attachments", data["Globals"])
@@ -340,7 +346,7 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
"Base64Content": image_data_b64, "Base64Content": image_data_b64,
}, },
{ {
"Filename": "", # the unnamed one "Filename": "attachment", # the unnamed one
"ContentType": "image/png", "ContentType": "image/png",
"Base64Content": image_data_b64, "Base64Content": image_data_b64,
}, },

View File

@@ -7,7 +7,7 @@ from django.test import SimpleTestCase, override_settings, tag
from anymail.exceptions import AnymailAPIError from anymail.exceptions import AnymailAPIError
from anymail.message import AnymailMessage 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_API_KEY = os.getenv("ANYMAIL_TEST_MAILJET_API_KEY")
ANYMAIL_TEST_MAILJET_SECRET_KEY = os.getenv("ANYMAIL_TEST_MAILJET_SECRET_KEY") ANYMAIL_TEST_MAILJET_SECRET_KEY = os.getenv("ANYMAIL_TEST_MAILJET_SECRET_KEY")
@@ -91,7 +91,7 @@ class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
) )
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain") message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv") 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( message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>" "<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid, "and image: <img src='cid:%s'></div>" % cid,