Merge pull request #37246 from prathameshkurunkar7/37186-inline_images-parameter-in-frappesendmail-is-ignored

fix(sendmail): respect inline_images parameter in sendmail
This commit is contained in:
s-aga-r 2026-04-01 18:10:30 +05:30 committed by GitHub
commit b6c9cebc27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 3 deletions

View file

@ -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 <img embed="assets/frappe/images/filename.jpg" ...> with
<img src="cid:content_id" ...> 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

View file

@ -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:
<img src="cid:content_id" ...> -> Correct behavior
If the image is not embedded with cid: -> Incorrect behavior
"""
test_image_content = b"FAKE_PNG_BINARY_CONTENT_FOR_TESTING"
html_content = '<div><img embed="files/nonexistent_test_image.png" alt="Logo"></div>'
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 = """
<h3>Hi John</h3>