diff --git a/README.md b/README.md
index faa34a7..0d97a21 100644
--- a/README.md
+++ b/README.md
@@ -158,7 +158,7 @@ Emails are sent only from drafts. Create or edit a draft with `save_draft`/`edit
### Replying to an email
-Use `in_reply_to_email_id` on `save_draft` or `edit_draft` to create a reply without a separate tool. Then send it with `send_draft`.
+Use `in_reply_to_email_id` on `save_draft` or `edit_draft` to create a reply without a separate tool. The draft includes reply headers and a quoted original message so webmail clients can preserve threading on send. Then send it with `send_draft`.
- Provide `in_reply_to_email_id` (and optionally `in_reply_to_mailbox`, default `INBOX`).
- `reply_all=true` includes original recipients; otherwise it replies to the sender/Reply-To.
diff --git a/src/services/email_service.py b/src/services/email_service.py
index 7bf9cd2..f7d2ae0 100644
--- a/src/services/email_service.py
+++ b/src/services/email_service.py
@@ -1,4 +1,5 @@
import email
+import html
from email.header import decode_header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
@@ -464,9 +465,11 @@ class EmailService:
cc = context["cc"]
in_reply_to = context["in_reply_to"]
references = context["references"]
+ original = context["original"]
else:
in_reply_to = None
references = None
+ original = None
if not to:
return OperationResult(success=False, message="'to' is required for drafts")
@@ -475,6 +478,9 @@ class EmailService:
if body is None:
return OperationResult(success=False, message="'body' is required for drafts")
+ if original:
+ body, html_body = self._build_reply_bodies(original, body)
+
msg = self._build_draft_message(
to=to,
subject=subject,
@@ -554,9 +560,11 @@ class EmailService:
resolved_cc = context["cc"]
in_reply_to = context["in_reply_to"]
references = context["references"]
+ original = context["original"]
else:
in_reply_to = None
references = None
+ original = None
try:
client = self._get_imap_client()
@@ -565,6 +573,9 @@ class EmailService:
client.delete_messages([uid])
client.expunge()
+ if original:
+ resolved_body, resolved_html = self._build_reply_bodies(original, resolved_body)
+
msg = self._build_draft_message(
to=resolved_to,
subject=resolved_subject,
@@ -975,6 +986,7 @@ class EmailService:
references.append(in_reply_to)
return {
+ "original": original,
"to": to,
"cc": reply_cc or None,
"subject": subject,
@@ -982,6 +994,44 @@ class EmailService:
"references": references or None,
}, None
+ def _build_reply_bodies(self, original: Email, body_text: str) -> tuple[str, Optional[str]]:
+ intro = self._format_reply_intro(original)
+ quoted_text = original.body_text or ""
+ text = body_text or ""
+ if intro:
+ text = f"{text}\n\n{intro}\n\n{quoted_text}".rstrip()
+
+ html_body = None
+ quoted_html = original.body_html
+ if not quoted_html and quoted_text:
+ quoted_html = html.escape(quoted_text).replace("\n", "
")
+ if quoted_html:
+ cite = original.headers.get("Message-ID") or original.in_reply_to or ""
+ cite_attr = f' cite="{html.escape(cite)}"' if cite else ""
+ html_intro = html.escape(intro).replace("\n", "
")
+ html_body = (
+ f"
" + f"{quoted_html}" + f"