88from email .mime .text import MIMEText
99from email .parser import BytesParser
1010from email .policy import default
11+ from html import escape
1112from pathlib import Path
1213from typing import Any
1314
1415import aioimaplib
1516import aiosmtplib
17+ import justhtml
1618
1719from mcp_email_server .config import EmailServer , EmailSettings
1820from 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 (
0 commit comments