Skip to content

Commit 390818c

Browse files
Jamie Kirkpatrickclaude
andcommitted
fix: handle HTML emails and add Date header in forward_email
- Extract html_body from emails alongside plain text body - Forward HTML-only emails as HTML with proper formatting - Use justhtml to convert HTML to clean text for multipart emails - Add RFC 5322 Date header to forwarded emails - Add tests for HTML, plain text, multipart, and Date header Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d6a400f commit 390818c

File tree

4 files changed

+330
-28
lines changed

4 files changed

+330
-28
lines changed

mcp_email_server/emails/classic.py

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
from email.mime.text import MIMEText
99
from email.parser import BytesParser
1010
from email.policy import default
11+
from html import escape
1112
from pathlib import Path
1213
from typing import Any
1314

1415
import aioimaplib
1516
import aiosmtplib
17+
import justhtml
1618

1719
from mcp_email_server.config import EmailServer, EmailSettings
1820
from mcp_email_server.emails import EmailHandler
@@ -835,8 +837,9 @@ async def get_email_for_forward(self, email_id: str, mailbox: str = "INBOX") ->
835837
to_header = email_message.get("To", "")
836838
cc_header = email_message.get("Cc", "")
837839

838-
# Extract body and attachments
840+
# Extract body (plain text and HTML) and attachments
839841
body = ""
842+
html_body = ""
840843
attachment_parts: list[MIMEApplication] = []
841844

842845
if email_message.is_multipart():
@@ -867,14 +870,31 @@ async def get_email_for_forward(self, email_id: str, mailbox: str = "INBOX") ->
867870
body += body_part.decode(charset)
868871
except UnicodeDecodeError:
869872
body += body_part.decode("utf-8", errors="replace")
873+
elif content_type == "text/html":
874+
html_part = part.get_payload(decode=True)
875+
if html_part:
876+
charset = part.get_content_charset("utf-8")
877+
try:
878+
html_body += html_part.decode(charset)
879+
except UnicodeDecodeError:
880+
html_body += html_part.decode("utf-8", errors="replace")
870881
else:
871882
payload = email_message.get_payload(decode=True)
872883
if payload:
873884
charset = email_message.get_content_charset("utf-8")
885+
content_type = email_message.get_content_type()
874886
try:
875-
body = payload.decode(charset)
887+
decoded = payload.decode(charset)
876888
except UnicodeDecodeError:
877-
body = payload.decode("utf-8", errors="replace")
889+
decoded = payload.decode("utf-8", errors="replace")
890+
891+
if content_type == "text/html":
892+
html_body = decoded
893+
else:
894+
body = decoded
895+
896+
# Use HTML only when there's no plain text alternative
897+
is_html = bool(html_body and not body)
878898

879899
return {
880900
"email_id": email_id,
@@ -884,6 +904,8 @@ async def get_email_for_forward(self, email_id: str, mailbox: str = "INBOX") ->
884904
"cc": cc_header,
885905
"date": date_str,
886906
"body": body,
907+
"html_body": html_body,
908+
"is_html": is_html,
887909
"attachment_parts": attachment_parts,
888910
}
889911

@@ -1071,38 +1093,75 @@ async def forward_email(
10711093
else:
10721094
forward_subject = original_subject
10731095

1074-
# Build the quoted original message
1075-
quoted_body = "\n\n---------- Forwarded message ----------\n"
1076-
quoted_body += f"From: {original['from']}\n"
1077-
quoted_body += f"Date: {original['date']}\n"
1078-
quoted_body += f"Subject: {original['subject']}\n"
1079-
if original["to"]:
1080-
quoted_body += f"To: {original['to']}\n"
1081-
if original["cc"]:
1082-
quoted_body += f"Cc: {original['cc']}\n"
1083-
quoted_body += "\n"
1084-
quoted_body += original["body"]
1085-
1086-
# Combine with additional message if provided
1087-
if additional_message:
1088-
forward_body = additional_message + quoted_body
1089-
else:
1090-
forward_body = quoted_body
1096+
# Determine if original is HTML
1097+
is_html = original.get("is_html", False)
1098+
html_body = original.get("html_body", "")
1099+
plain_body = original.get("body", "")
1100+
1101+
# For plain text forwarding, prefer converting HTML to clean text
1102+
# This avoids issues where plain_body contains embedded HTML tags
1103+
if not is_html and html_body:
1104+
plain_body = justhtml.JustHTML(html_body).to_text()
1105+
elif is_html and not plain_body and html_body:
1106+
# HTML-only email, need plain text fallback
1107+
plain_body = justhtml.JustHTML(html_body).to_text()
10911108

10921109
# Build attachment file paths (we need to temporarily save them for send_email)
10931110
# For simplicity, we'll send without file attachments and include the attachment parts directly
10941111
attachment_parts = original.get("attachment_parts", []) if include_attachments else []
10951112

10961113
try:
1097-
# Build the message manually with attachments
1098-
if attachment_parts:
1099-
msg = MIMEMultipart()
1100-
text_part = MIMEText(forward_body, "plain", "utf-8")
1101-
msg.attach(text_part)
1102-
for attachment in attachment_parts:
1103-
msg.attach(attachment)
1114+
if is_html and html_body:
1115+
# Build HTML forward
1116+
quoted_html = "<br><br>---------- Forwarded message ----------<br>"
1117+
quoted_html += f"From: {escape(original['from'])}<br>"
1118+
quoted_html += f"Date: {escape(original['date'])}<br>"
1119+
quoted_html += f"Subject: {escape(original['subject'])}<br>"
1120+
if original["to"]:
1121+
quoted_html += f"To: {escape(original['to'])}<br>"
1122+
if original["cc"]:
1123+
quoted_html += f"Cc: {escape(original['cc'])}<br>"
1124+
quoted_html += f"<br>{html_body}"
1125+
1126+
if additional_message:
1127+
forward_html = f"<p>{escape(additional_message)}</p>{quoted_html}"
1128+
else:
1129+
forward_html = quoted_html
1130+
1131+
if attachment_parts:
1132+
msg = MIMEMultipart()
1133+
html_part = MIMEText(forward_html, "html", "utf-8")
1134+
msg.attach(html_part)
1135+
for attachment in attachment_parts:
1136+
msg.attach(attachment)
1137+
else:
1138+
msg = MIMEText(forward_html, "html", "utf-8")
11041139
else:
1105-
msg = MIMEText(forward_body, "plain", "utf-8")
1140+
# Build plain text forward
1141+
quoted_body = "\n\n---------- Forwarded message ----------\n"
1142+
quoted_body += f"From: {original['from']}\n"
1143+
quoted_body += f"Date: {original['date']}\n"
1144+
quoted_body += f"Subject: {original['subject']}\n"
1145+
if original["to"]:
1146+
quoted_body += f"To: {original['to']}\n"
1147+
if original["cc"]:
1148+
quoted_body += f"Cc: {original['cc']}\n"
1149+
quoted_body += "\n"
1150+
quoted_body += plain_body
1151+
1152+
if additional_message:
1153+
forward_body = additional_message + quoted_body
1154+
else:
1155+
forward_body = quoted_body
1156+
1157+
if attachment_parts:
1158+
msg = MIMEMultipart()
1159+
text_part = MIMEText(forward_body, "plain", "utf-8")
1160+
msg.attach(text_part)
1161+
for attachment in attachment_parts:
1162+
msg.attach(attachment)
1163+
else:
1164+
msg = MIMEText(forward_body, "plain", "utf-8")
11061165

11071166
# Set headers
11081167
if any(ord(c) > 127 for c in forward_subject):
@@ -1116,6 +1175,7 @@ async def forward_email(
11161175
msg["From"] = sender
11171176

11181177
msg["To"] = ", ".join(recipients)
1178+
msg["Date"] = email.utils.formatdate(localtime=True)
11191179

11201180
# Send via SMTP using outgoing server
11211181
async with aiosmtplib.SMTP(

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies = [
2222
"aiosmtplib>=4.0.0",
2323
"gradio>=6.0.1",
2424
"jinja2>=3.1.5",
25+
"justhtml>=0.35.0",
2526
"loguru>=0.7.3",
2627
"mcp[cli]>=1.3.0",
2728
"pydantic>=2.11.0",

0 commit comments

Comments
 (0)