diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 37729812c1..5546450f61 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) @@ -571,11 +582,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 = [] @@ -590,7 +612,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..27e061d0b0 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -137,6 +137,43 @@ 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. + 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" + + html_content = '
Logo
' + + inline_images = [ + { + "filename": "files/nonexistent_test_image.png", + "filecontent": test_image_content, + } + ] + + # use QueueBuilder to send the email (sendmail uses this 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, + ) + + mail = builder.prepare_email_content() + email_string = mail.as_string() + + self.assertIn("cid:", email_string) + self.assertNotIn('embed="files/nonexistent_test_image.png"', email_string) + def test_inline_styling(self): html = """

Hi John