From a252e7e2650e861e555483764858b02ec2d70528 Mon Sep 17 00:00:00 2001 From: prathameshkurunkar7 Date: Thu, 19 Feb 2026 14:53:32 +0530 Subject: [PATCH 1/2] fix(sendmail): respect inline_images parameter in sendmail --- frappe/email/email_body.py | 32 +++++++++++++++++++++++++++++--- frappe/email/test_email_body.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 5dc3054843..4ef17acef6 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -210,7 +210,18 @@ class EMail: if has_inline_images: # process inline images - message, _inline_images = replace_filename_with_cid(message) + provided_images = {} + if inline_images: + for img in inline_images: + if img.get("filename") and img.get("filecontent"): + # index by full path and basename for flexible matching + provided_images[img["filename"]] = img["filecontent"] + basename = img["filename"].rsplit("/", 1)[-1] + if basename not in provided_images: + provided_images[basename] = img["filecontent"] + + # process inline images while preferring provided_images over disk reads + message, _inline_images = replace_filename_with_cid(message, provided_images) # prepare parts msg_related = MIMEMultipart("related", policy=policy.SMTP) @@ -552,11 +563,22 @@ def get_footer(email_account, footer=None): return footer -def replace_filename_with_cid(message): +def replace_filename_with_cid(message, provided_images=None): """Replaces with and return the modified message and a list of inline_images with {filename, filecontent, content_id} + + Args: + message: The HTML message to process + provided_images: A dictionary of images to use instead of reading from disk + Example: + { + "assets/frappe/images/filename.jpg": filecontent, + "filename.jpg": filecontent, + } """ + if provided_images is None: + provided_images = {} inline_images = [] @@ -571,7 +593,11 @@ def replace_filename_with_cid(message): img_path_escaped = frappe.utils.html_utils.unescape_html(img_path) filename = img_path_escaped.rsplit("/")[-1] - filecontent = get_filecontent_from_path(img_path_escaped) + # check if the image is provided in the provided_images(by checking full path and basename) + filecontent = provided_images.get(img_path_escaped) or provided_images.get(filename) + if not filecontent: + filecontent = get_filecontent_from_path(img_path_escaped) + if not filecontent: message = re.sub(f"""embed=['"]{re.escape(img_path)}['"]""", "", message) continue diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 2152c50917..bd10cc1632 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -137,6 +137,39 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> """.format(inline_images[0].get("content_id")) self.assertEqual(message, processed_message) + def test_sendmail_inline_images_parameter_respected(self): + """Test that inline_images parameter works through sendmail.""" + + test_image_content = b"FAKE_PNG_BINARY_CONTENT_FOR_TESTING" + + html_content = '
Logo
' + + inline_images = [ + { + "filename": "files/nonexistent_test_image.png", + "filecontent": test_image_content, + } + ] + + # Use QueueBuilder directly (what sendmail uses internally) + from frappe.email.doctype.email_queue.email_queue import QueueBuilder + + builder = QueueBuilder( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Inline Images", + message=html_content, + inline_images=inline_images, + ) + + # Get the email content that would be sent + mail = builder.prepare_email_content() + email_string = mail.as_string() + + # Assertions + self.assertIn("cid:", email_string) + self.assertNotIn('embed="files/nonexistent_test_image.png"', email_string) + def test_inline_styling(self): html = """

Hi John

From 90615ea4df94c4c9bc0b9b4ae97b70545cdbf33e Mon Sep 17 00:00:00 2001 From: prathameshkurunkar7 Date: Thu, 19 Feb 2026 15:04:26 +0530 Subject: [PATCH 2/2] docs(test_email_body): clarify test docs --- frappe/email/test_email_body.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index bd10cc1632..27e061d0b0 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -138,7 +138,13 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> self.assertEqual(message, processed_message) def test_sendmail_inline_images_parameter_respected(self): - """Test that inline_images parameter works through sendmail.""" + """ + Test that inline_images parameter works through sendmail. + Earlier this was ignored and the image was read from disk instead of using the provided content. + The way to check this is essentially checking if the image is embedded with cid: + -> Correct behavior + If the image is not embedded with cid: -> Incorrect behavior + """ test_image_content = b"FAKE_PNG_BINARY_CONTENT_FOR_TESTING" @@ -151,7 +157,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> } ] - # Use QueueBuilder directly (what sendmail uses internally) + # use QueueBuilder to send the email (sendmail uses this internally) from frappe.email.doctype.email_queue.email_queue import QueueBuilder builder = QueueBuilder( @@ -162,11 +168,9 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> inline_images=inline_images, ) - # Get the email content that would be sent mail = builder.prepare_email_content() email_string = mail.as_string() - # Assertions self.assertIn("cid:", email_string) self.assertNotIn('embed="files/nonexistent_test_image.png"', email_string)