Quote original message in reply drafts
All checks were successful
Build And Test / publish (push) Successful in 49s
All checks were successful
Build And Test / publish (push) Successful in 49s
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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", "<br/>")
|
||||
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", "<br/>")
|
||||
html_body = (
|
||||
f"<div>{html.escape(body_text).replace('\\n', '<br/>')}</div>"
|
||||
f"<br/><br/>{html_intro}<br/><br/>"
|
||||
f"<blockquote type=\"cite\"{cite_attr}>"
|
||||
f"<div dir=\"ltr\">{quoted_html}</div>"
|
||||
f"</blockquote><br/><br/><br/>"
|
||||
)
|
||||
|
||||
return text, html_body
|
||||
|
||||
def _format_reply_intro(self, original: Email) -> str:
|
||||
date_str = ""
|
||||
if original.date:
|
||||
try:
|
||||
date_str = original.date.strftime("%A, %B %d, %Y %H:%M %Z").strip()
|
||||
except Exception:
|
||||
date_str = str(original.date)
|
||||
from_name = original.from_address.name or original.from_address.email
|
||||
from_email = original.from_address.email
|
||||
if date_str:
|
||||
return f"On {date_str}, {from_name} <{from_email}> wrote:"
|
||||
return f"On {from_name} <{from_email}> wrote:"
|
||||
|
||||
def _resolve_sender(
|
||||
self, sender_email: Optional[str], sender_name: Optional[str]
|
||||
) -> tuple[Optional[str], str]:
|
||||
|
||||
Reference in New Issue
Block a user