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
|
### 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`).
|
- 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.
|
- `reply_all=true` includes original recipients; otherwise it replies to the sender/Reply-To.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import email
|
import email
|
||||||
|
import html
|
||||||
from email.header import decode_header
|
from email.header import decode_header
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
@@ -464,9 +465,11 @@ class EmailService:
|
|||||||
cc = context["cc"]
|
cc = context["cc"]
|
||||||
in_reply_to = context["in_reply_to"]
|
in_reply_to = context["in_reply_to"]
|
||||||
references = context["references"]
|
references = context["references"]
|
||||||
|
original = context["original"]
|
||||||
else:
|
else:
|
||||||
in_reply_to = None
|
in_reply_to = None
|
||||||
references = None
|
references = None
|
||||||
|
original = None
|
||||||
|
|
||||||
if not to:
|
if not to:
|
||||||
return OperationResult(success=False, message="'to' is required for drafts")
|
return OperationResult(success=False, message="'to' is required for drafts")
|
||||||
@@ -475,6 +478,9 @@ class EmailService:
|
|||||||
if body is None:
|
if body is None:
|
||||||
return OperationResult(success=False, message="'body' is required for drafts")
|
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(
|
msg = self._build_draft_message(
|
||||||
to=to,
|
to=to,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
@@ -554,9 +560,11 @@ class EmailService:
|
|||||||
resolved_cc = context["cc"]
|
resolved_cc = context["cc"]
|
||||||
in_reply_to = context["in_reply_to"]
|
in_reply_to = context["in_reply_to"]
|
||||||
references = context["references"]
|
references = context["references"]
|
||||||
|
original = context["original"]
|
||||||
else:
|
else:
|
||||||
in_reply_to = None
|
in_reply_to = None
|
||||||
references = None
|
references = None
|
||||||
|
original = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = self._get_imap_client()
|
client = self._get_imap_client()
|
||||||
@@ -565,6 +573,9 @@ class EmailService:
|
|||||||
client.delete_messages([uid])
|
client.delete_messages([uid])
|
||||||
client.expunge()
|
client.expunge()
|
||||||
|
|
||||||
|
if original:
|
||||||
|
resolved_body, resolved_html = self._build_reply_bodies(original, resolved_body)
|
||||||
|
|
||||||
msg = self._build_draft_message(
|
msg = self._build_draft_message(
|
||||||
to=resolved_to,
|
to=resolved_to,
|
||||||
subject=resolved_subject,
|
subject=resolved_subject,
|
||||||
@@ -975,6 +986,7 @@ class EmailService:
|
|||||||
references.append(in_reply_to)
|
references.append(in_reply_to)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"original": original,
|
||||||
"to": to,
|
"to": to,
|
||||||
"cc": reply_cc or None,
|
"cc": reply_cc or None,
|
||||||
"subject": subject,
|
"subject": subject,
|
||||||
@@ -982,6 +994,44 @@ class EmailService:
|
|||||||
"references": references or None,
|
"references": references or None,
|
||||||
}, 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(
|
def _resolve_sender(
|
||||||
self, sender_email: Optional[str], sender_name: Optional[str]
|
self, sender_email: Optional[str], sender_name: Optional[str]
|
||||||
) -> tuple[Optional[str], str]:
|
) -> tuple[Optional[str], str]:
|
||||||
|
|||||||
Reference in New Issue
Block a user