Add tool call logging and reply fields
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:
@@ -433,17 +433,49 @@ class EmailService:
|
||||
|
||||
def save_draft(
|
||||
self,
|
||||
to: list[str],
|
||||
subject: str,
|
||||
body: str,
|
||||
to: Optional[list[str]] = None,
|
||||
subject: Optional[str] = None,
|
||||
body: Optional[str] = None,
|
||||
cc: Optional[list[str]] = None,
|
||||
bcc: Optional[list[str]] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
html_body: Optional[str] = None,
|
||||
mailbox: Optional[str] = None,
|
||||
reply_to_email_id: Optional[str] = None,
|
||||
reply_mailbox: Optional[str] = None,
|
||||
reply_all: bool = False,
|
||||
) -> OperationResult:
|
||||
try:
|
||||
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
||||
if reply_to_email_id:
|
||||
context, error = self._get_reply_context(
|
||||
reply_mailbox or "INBOX",
|
||||
reply_to_email_id,
|
||||
reply_all,
|
||||
cc,
|
||||
self.settings.smtp_from_email,
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
if not to:
|
||||
to = context["to"]
|
||||
if subject is None:
|
||||
subject = context["subject"]
|
||||
if cc is None:
|
||||
cc = context["cc"]
|
||||
in_reply_to = context["in_reply_to"]
|
||||
references = context["references"]
|
||||
else:
|
||||
in_reply_to = None
|
||||
references = None
|
||||
|
||||
if not to:
|
||||
return OperationResult(success=False, message="'to' is required for drafts")
|
||||
if subject is None:
|
||||
return OperationResult(success=False, message="'subject' is required for drafts")
|
||||
if body is None:
|
||||
return OperationResult(success=False, message="'body' is required for drafts")
|
||||
|
||||
msg = self._build_draft_message(
|
||||
to=to,
|
||||
subject=subject,
|
||||
@@ -452,6 +484,8 @@ class EmailService:
|
||||
bcc=bcc,
|
||||
reply_to=reply_to,
|
||||
html_body=html_body,
|
||||
in_reply_to=in_reply_to,
|
||||
references=references,
|
||||
)
|
||||
client = self._get_imap_client()
|
||||
append_result = client.append(
|
||||
@@ -481,6 +515,9 @@ class EmailService:
|
||||
bcc: Optional[list[str]] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
html_body: Optional[str] = None,
|
||||
reply_to_email_id: Optional[str] = None,
|
||||
reply_mailbox: Optional[str] = None,
|
||||
reply_all: bool = False,
|
||||
) -> OperationResult:
|
||||
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
||||
try:
|
||||
@@ -502,6 +539,28 @@ class EmailService:
|
||||
resolved_body = body if body is not None else (existing.body_text or "")
|
||||
resolved_html = html_body if html_body is not None else existing.body_html
|
||||
|
||||
if reply_to_email_id:
|
||||
context, error = self._get_reply_context(
|
||||
reply_mailbox or "INBOX",
|
||||
reply_to_email_id,
|
||||
reply_all,
|
||||
cc,
|
||||
self.settings.smtp_from_email,
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
if to is None:
|
||||
resolved_to = context["to"]
|
||||
if subject is None:
|
||||
resolved_subject = context["subject"]
|
||||
if cc is None:
|
||||
resolved_cc = context["cc"]
|
||||
in_reply_to = context["in_reply_to"]
|
||||
references = context["references"]
|
||||
else:
|
||||
in_reply_to = None
|
||||
references = None
|
||||
|
||||
try:
|
||||
client = self._get_imap_client()
|
||||
client.select_folder(draft_mailbox)
|
||||
@@ -517,6 +576,8 @@ class EmailService:
|
||||
bcc=resolved_bcc,
|
||||
reply_to=reply_to,
|
||||
html_body=resolved_html,
|
||||
in_reply_to=in_reply_to,
|
||||
references=references,
|
||||
)
|
||||
append_result = client.append(
|
||||
draft_mailbox,
|
||||
@@ -536,9 +597,9 @@ class EmailService:
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
to: list[str],
|
||||
subject: str,
|
||||
body: str,
|
||||
to: Optional[list[str]] = None,
|
||||
subject: Optional[str] = None,
|
||||
body: Optional[str] = None,
|
||||
cc: Optional[list[str]] = None,
|
||||
bcc: Optional[list[str]] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
@@ -547,8 +608,38 @@ class EmailService:
|
||||
sender_name: Optional[str] = None,
|
||||
in_reply_to: Optional[str] = None,
|
||||
references: Optional[list[str]] = None,
|
||||
reply_to_email_id: Optional[str] = None,
|
||||
reply_mailbox: Optional[str] = None,
|
||||
reply_all: bool = False,
|
||||
) -> OperationResult:
|
||||
try:
|
||||
if reply_to_email_id:
|
||||
resolved_name, resolved_email = self._resolve_sender(sender_email, sender_name)
|
||||
context, error = self._get_reply_context(
|
||||
reply_mailbox or "INBOX",
|
||||
reply_to_email_id,
|
||||
reply_all,
|
||||
cc,
|
||||
resolved_email,
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
if not to:
|
||||
to = context["to"]
|
||||
if subject is None:
|
||||
subject = context["subject"]
|
||||
if cc is None:
|
||||
cc = context["cc"]
|
||||
in_reply_to = context["in_reply_to"]
|
||||
references = context["references"]
|
||||
|
||||
if not to:
|
||||
return OperationResult(success=False, message="'to' is required to send email")
|
||||
if subject is None:
|
||||
return OperationResult(success=False, message="'subject' is required to send email")
|
||||
if body is None:
|
||||
return OperationResult(success=False, message="'body' is required to send email")
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
resolved_name, resolved_email = self._resolve_sender(sender_email, sender_name)
|
||||
@@ -609,67 +700,17 @@ class EmailService:
|
||||
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,
|
||||
sender_email=sender_email,
|
||||
sender_name=sender_name,
|
||||
reply_to_email_id=email_id,
|
||||
reply_mailbox=mailbox,
|
||||
reply_all=reply_all,
|
||||
cc=cc,
|
||||
)
|
||||
|
||||
def _parse_envelope_addresses(self, addresses) -> list[EmailAddress]:
|
||||
@@ -805,6 +846,8 @@ class EmailService:
|
||||
bcc: Optional[list[str]] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
html_body: Optional[str] = None,
|
||||
in_reply_to: Optional[str] = None,
|
||||
references: Optional[list[str]] = None,
|
||||
) -> MIMEMultipart:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
@@ -821,12 +864,80 @@ class EmailService:
|
||||
msg["Bcc"] = ", ".join(bcc)
|
||||
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)
|
||||
|
||||
msg.attach(MIMEText(body or "", "plain", "utf-8"))
|
||||
if html_body:
|
||||
msg.attach(MIMEText(html_body, "html", "utf-8"))
|
||||
return msg
|
||||
|
||||
def _get_reply_context(
|
||||
self,
|
||||
mailbox: str,
|
||||
email_id: str,
|
||||
reply_all: bool,
|
||||
cc: Optional[list[str]],
|
||||
sender_email: Optional[str],
|
||||
) -> tuple[dict, Optional[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,
|
||||
)
|
||||
|
||||
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, sender_email)
|
||||
reply_cc = self._dedupe_emails(reply_cc, sender_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 {
|
||||
"to": to,
|
||||
"cc": reply_cc or None,
|
||||
"subject": subject,
|
||||
"in_reply_to": in_reply_to,
|
||||
"references": references or None,
|
||||
}, None
|
||||
|
||||
def _resolve_sender(
|
||||
self, sender_email: Optional[str], sender_name: Optional[str]
|
||||
) -> tuple[Optional[str], str]:
|
||||
|
||||
Reference in New Issue
Block a user