Add reply_email tool and sender override
All checks were successful
Build And Test / publish (push) Successful in 1m0s

This commit is contained in:
2026-01-01 12:16:06 -08:00
parent 0459fb1d4c
commit 71c55f7289
3 changed files with 171 additions and 6 deletions

View File

@@ -543,19 +543,26 @@ class EmailService:
bcc: Optional[list[str]] = None,
reply_to: Optional[str] = None,
html_body: Optional[str] = None,
sender_email: Optional[str] = None,
sender_name: Optional[str] = None,
in_reply_to: Optional[str] = None,
references: Optional[list[str]] = None,
) -> OperationResult:
try:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = formataddr(
(self.settings.smtp_from_name or "", self.settings.smtp_from_email)
)
resolved_name, resolved_email = self._resolve_sender(sender_email, sender_name)
msg["From"] = formataddr((resolved_name or "", resolved_email))
msg["To"] = ", ".join(to)
if cc:
msg["Cc"] = ", ".join(cc)
if reply_to:
msg["Reply-To"] = reply_to
if in_reply_to:
msg["In-Reply-To"] = in_reply_to
if references:
msg["References"] = " ".join(references)
# Add plain text body
msg.attach(MIMEText(body, "plain", "utf-8"))
@@ -589,6 +596,82 @@ class EmailService:
except Exception as e:
return OperationResult(success=False, message=str(e))
async def reply_email(
self,
mailbox: str,
email_id: str,
body: str,
reply_all: bool = False,
cc: Optional[list[str]] = None,
bcc: Optional[list[str]] = None,
reply_to: Optional[str] = None,
html_body: Optional[str] = None,
sender_email: Optional[str] = None,
sender_name: Optional[str] = None,
) -> OperationResult:
original = self.read_email(mailbox, email_id, format="both")
if not original:
return OperationResult(
success=False,
message=f"Email {email_id} not found in {mailbox}",
id=email_id,
)
resolved_name, resolved_email = self._resolve_sender(sender_email, sender_name)
reply_to_header = original.headers.get("Reply-To")
reply_to_email = None
if reply_to_header:
_, reply_to_email = parseaddr(reply_to_header)
if not reply_to_email:
reply_to_email = original.from_address.email
to = [reply_to_email] if reply_to_email else []
reply_cc: list[str] = []
if reply_all:
for addr in original.to_addresses + original.cc_addresses:
if addr.email and addr.email not in to:
reply_cc.append(addr.email)
if cc:
reply_cc.extend(cc)
to = self._dedupe_emails(to, resolved_email)
reply_cc = self._dedupe_emails(reply_cc, resolved_email)
if not to and reply_cc:
to = [reply_cc.pop(0)]
if not to:
return OperationResult(
success=False,
message="No valid recipients found for reply",
id=email_id,
)
subject = original.subject or "(No Subject)"
if not subject.lower().startswith("re:"):
subject = f"Re: {subject}"
in_reply_to = original.headers.get("Message-ID") or original.in_reply_to
references = list(original.references)
if in_reply_to and in_reply_to not in references:
references.append(in_reply_to)
return await self.send_email(
to=to,
subject=subject,
body=body,
cc=reply_cc or None,
bcc=bcc,
reply_to=reply_to,
html_body=html_body,
sender_email=resolved_email,
sender_name=resolved_name,
in_reply_to=in_reply_to,
references=references or None,
)
def _parse_envelope_addresses(self, addresses) -> list[EmailAddress]:
if not addresses:
return []
@@ -744,6 +827,32 @@ class EmailService:
msg.attach(MIMEText(html_body, "html", "utf-8"))
return msg
def _resolve_sender(
self, sender_email: Optional[str], sender_name: Optional[str]
) -> tuple[Optional[str], str]:
if sender_email:
return sender_name, sender_email
if sender_name:
name, email_addr = parseaddr(sender_name)
if email_addr:
return name or None, email_addr
return self.settings.smtp_from_name, self.settings.smtp_from_email
def _dedupe_emails(self, emails: list[str], self_email: Optional[str]) -> list[str]:
seen = set()
cleaned = []
for addr in emails:
if not addr:
continue
addr_lower = addr.lower()
if self_email and addr_lower == self_email.lower():
continue
if addr_lower in seen:
continue
seen.add(addr_lower)
cleaned.append(addr)
return cleaned
def set_flags(
self,
email_id: str,