seitime-frappe/frappe/email/test_email_body.py
2026-02-19 15:04:26 +05:30

296 lines
9.2 KiB
Python

# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import base64
import os
import frappe
from frappe import safe_decode
from frappe.core.doctype.communication.communication import Communication
from frappe.email.doctype.email_queue.email_queue import QueueBuilder, SendMailContext
from frappe.email.email_body import (
get_email,
get_header,
inline_style_in_html,
replace_filename_with_cid,
)
from frappe.email.receive import Email, InboundMail
from frappe.tests import IntegrationTestCase
class TestEmailBody(IntegrationTestCase):
def setUp(self):
email_html = """
<div>
<h3>Hey John Doe!</h3>
<p>This is embedded image you asked for</p>
<img embed="assets/frappe/images/frappe-favicon.svg" />
</div>
"""
email_text = """
Hey John Doe!
This is the text version of this email
"""
img_path = os.path.abspath("assets/frappe/images/frappe-favicon.svg")
with open(img_path, "rb") as f:
img_content = f.read()
img_base64 = base64.b64encode(img_content).decode()
# email body keeps 76 characters on one line
self.img_base64 = fixed_column_width(img_base64, 76)
self.email_string = (
get_email(
recipients=["test@example.com"],
sender="me@example.com",
subject="Test Subject",
content=email_html,
text_content=email_text,
)
.as_string()
.replace("\r\n", "\n")
)
def test_prepare_message_returns_already_encoded_string(self):
uni_chr1 = chr(40960)
uni_chr2 = chr(1972)
QueueBuilder(
recipients=["test@example.com"],
sender="me@example.com",
subject="Test Subject",
message=f"<h1>{uni_chr1}abcd{uni_chr2}</h1>",
text_content="whatever",
).process()
queue_doc = frappe.get_last_doc("Email Queue")
mail_ctx = SendMailContext(queue_doc=queue_doc)
result = mail_ctx.build_message(recipient_email="test@test.com")
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)
def test_prepare_message_returns_cr_lf(self):
QueueBuilder(
recipients=["test@example.com"],
sender="me@example.com",
subject="Test Subject",
message="<h1>\n this is a test of newlines\n" + "</h1>",
text_content="whatever",
).process()
queue_doc = frappe.get_last_doc("Email Queue")
mail_ctx = SendMailContext(queue_doc=queue_doc)
result = safe_decode(mail_ctx.build_message(recipient_email="test@test.com"))
self.assertTrue(result.count("\n") == result.count("\r"))
def test_image(self):
img_signature = """
Content-Type: image/svg+xml
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="frappe-favicon.svg"
"""
self.assertTrue(img_signature in self.email_string)
self.assertTrue(self.img_base64 in self.email_string)
def test_text_content(self):
text_content = """
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
Hey John Doe!
This is the text version of this email
"""
self.assertTrue(text_content in self.email_string)
def test_email_content(self):
html_head = """
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns=3D"http://www.w3.org/1999/xhtml">
"""
html = """<h3>Hey John Doe!</h3>"""
self.assertTrue(html_head in self.email_string)
self.assertTrue(html in self.email_string)
def test_replace_filename_with_cid(self):
original_message = """
<div>
<img embed="assets/frappe/images/frappe-favicon.svg" alt="test" />
<img embed="notexists.jpg" />
</div>
"""
message, inline_images = replace_filename_with_cid(original_message)
processed_message = """
<div>
<img src="cid:{}" alt="test" />
<img />
</div>
""".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>
<p>This is a test email</p>
"""
transformed_html = """
<h3>Hi John</h3>
<p style="margin:1em 0 !important">This is a test email</p>
"""
self.assertTrue(transformed_html in inline_style_in_html(html))
def test_email_header(self):
email_html = """
<h3>Hey John Doe!</h3>
<p>This is embedded image you asked for</p>
"""
email_string = get_email(
recipients=["test@example.com"],
sender="me@example.com",
subject="Test Subject\u2028, with line break, \nand Line feed \rand carriage return.",
content=email_html,
header=["Email Title", "green"],
).as_string()
# REDESIGN: Add style for indicators in email
self.assertIn("indicator", email_string)
self.assertIn("indicator-green", email_string)
self.assertTrue("<span>Email Title</span>" in email_string)
self.assertIn(
"Subject: Test Subject, with line break, and Line feed and carriage return.", email_string
)
def test_get_email_header(self):
html = get_header(["This is test", "orange"])
self.assertTrue('<span class="indicator indicator-orange"></span>' in html)
self.assertTrue("<span>This is test</span>" in html)
html = get_header(["This is another test"])
self.assertTrue("<span>This is another test</span>" in html)
html = get_header("This is string")
self.assertTrue("<span>This is string</span>" in html)
def test_8bit_utf_8_decoding(self):
text_content_bytes = b"\xed\x95\x9c\xea\xb8\x80\xe1\xa5\xa1\xe2\x95\xa5\xe0\xba\xaa\xe0\xa4\x8f"
text_content = text_content_bytes.decode("utf-8")
content_bytes = (
b"""MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
From: test1_@erpnext.com
Reply-To: test2_@erpnext.com
"""
+ text_content_bytes
)
mail = Email(content_bytes)
self.assertEqual(mail.text_content, text_content)
def test_poorly_encoded_messages(self):
mail = Email.decode_email(
"=?iso-2022-jp?B?VEFLQVlBTUEgS2FvcnUgWxskQnxiOzMbKEIgGyRCNzAbKEJd?=\n\t<user@example.com>"
)
self.assertIn("user@example.com", mail)
def test_poorly_encoded_messages2(self):
mail = Email.decode_email(" =?UTF-8?B?X\xe0\xe0Y?= <xy@example.com>")
self.assertIn("xy@example.com", mail)
def test_quotes_in_email_sender(self):
content_bytes = rb"""MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
To: "\"fail@example.com\" via ABC" <success@example.com>
From: "\"fail@example.com\" via DEF" <success@example.com>
Reply-To: "\"fail@example.com\" via GHI" <success@example.com>
CC: "\"fail@example.com\" via JKL" <success@example.com>
"""
mail = Email(content_bytes)
self.assertEqual(mail.from_email, "success@example.com")
self.assertEqual(mail.from_real_name, "failexamplecom via DEF")
# https://github.com/frappe/frappe/pull/3371
# self.assertEqual(mail.from_real_name, '"fail@example.com" via DEF')
email_account = frappe._dict({"email_id": "receive@example.com"})
mail = InboundMail(content_bytes, email_account)
communication: Communication = mail.process() # type: ignore
self.assertEqual(communication.sender_full_name, "failexamplecom via DEF")
def test_quotes_in_email_recipients(self):
content_bytes = rb"""MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
From: "=?utf-8?Q?=F0=9F=98=83?="
=?utf-8?Q?=3Ctest=40ex?= =?utf-8?Q?ample=2Eco?= =?utf-8?Q?m=3E?=
To: =?iso-8859-1?Q?X=E9Y=40example=2Ecom?= <xy@example.com>, "fail@example.com" <success@example.com>
"""
# https://ldu2.github.io/rfc2047/
email_account = frappe._dict({"email_id": "receive@example.com"})
mail = InboundMail(content_bytes, email_account)
communication: Communication = mail.process() # type: ignore
self.assertEqual(communication.sender_mailid, "test@example.com")
# self.assertEqual(communication.sender_full_name, "😃")
# # TODO: Fix get_name_from_email_string to accept non-ASCII chars
self.assertEqual(
communication.recipients,
'XéY@example.com <xy@example.com>, "fail@example.com" <success@example.com>',
)
frappe.db.rollback()
def fixed_column_width(string, chunk_size):
parts = [string[0 + i : chunk_size + i] for i in range(0, len(string), chunk_size)]
return "\n".join(parts)