Add reply_email tool and sender override
All checks were successful
Build And Test / publish (push) Successful in 1m0s
All checks were successful
Build And Test / publish (push) Successful in 1m0s
This commit is contained in:
@@ -139,7 +139,7 @@ Add your MCP endpoint at https://poke.com/settings/connections.
|
||||
|
||||
| Category | Tools |
|
||||
| --- | --- |
|
||||
| Email | `list_mailboxes`, `list_emails`, `list_drafts`, `read_email`, `search_emails`, `move_email`, `delete_email`, `delete_draft`, `save_draft`, `edit_draft`, `send_email`, `set_email_flags`, `unsubscribe_email` |
|
||||
| Email | `list_mailboxes`, `list_emails`, `list_drafts`, `read_email`, `search_emails`, `move_email`, `delete_email`, `delete_draft`, `save_draft`, `edit_draft`, `send_email`, `reply_email`, `set_email_flags`, `unsubscribe_email` |
|
||||
| Calendar | `list_calendars`, `list_events`, `get_event`, `create_event`, `update_event`, `delete_event` |
|
||||
| Contacts | `list_addressbooks`, `list_contacts`, `get_contact`, `create_contact`, `update_contact`, `delete_contact` |
|
||||
| System | `get_server_info` |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -210,7 +210,7 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
||||
)
|
||||
return result.model_dump()
|
||||
|
||||
@mcp.tool(description="Send a new email via SMTP. Supports plain text and HTML content, CC, BCC, and reply-to.")
|
||||
@mcp.tool(description="Send a new email via SMTP. Supports plain text and HTML content, CC, BCC, reply-to, and custom sender.")
|
||||
async def send_email(
|
||||
to: list[str],
|
||||
subject: str,
|
||||
@@ -219,6 +219,8 @@ def register_email_tools(mcp: FastMCP, service: 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,
|
||||
) -> dict:
|
||||
"""
|
||||
Send a new email.
|
||||
@@ -231,8 +233,62 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
||||
bcc: List of BCC recipients (optional)
|
||||
reply_to: Reply-to address (optional)
|
||||
html_body: HTML version of the email body (optional)
|
||||
sender_email: Sender email address (optional, defaults to SMTP_FROM_EMAIL)
|
||||
sender_name: Sender display name (optional, defaults to SMTP_FROM_NAME)
|
||||
"""
|
||||
result = await service.send_email(to, subject, body, cc, bcc, reply_to, html_body)
|
||||
result = await service.send_email(
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
cc,
|
||||
bcc,
|
||||
reply_to,
|
||||
html_body,
|
||||
sender_email,
|
||||
sender_name,
|
||||
)
|
||||
return result.model_dump()
|
||||
|
||||
@mcp.tool(description="Reply to an existing email by ID, with optional reply-all behavior.")
|
||||
async def reply_email(
|
||||
email_id: str,
|
||||
body: str,
|
||||
mailbox: str = "INBOX",
|
||||
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,
|
||||
) -> dict:
|
||||
"""
|
||||
Reply to an existing email.
|
||||
|
||||
Args:
|
||||
email_id: The unique ID of the email to reply to
|
||||
body: Plain text email body
|
||||
mailbox: The mailbox containing the email (default: INBOX)
|
||||
reply_all: Whether to include original recipients (default: False)
|
||||
cc: List of CC recipients (optional)
|
||||
bcc: List of BCC recipients (optional)
|
||||
reply_to: Reply-to address for the reply (optional)
|
||||
html_body: HTML version of the email body (optional)
|
||||
sender_email: Sender email address (optional, defaults to SMTP_FROM_EMAIL)
|
||||
sender_name: Sender display name (optional, defaults to SMTP_FROM_NAME)
|
||||
"""
|
||||
result = await service.reply_email(
|
||||
mailbox,
|
||||
email_id,
|
||||
body,
|
||||
reply_all,
|
||||
cc,
|
||||
bcc,
|
||||
reply_to,
|
||||
html_body,
|
||||
sender_email,
|
||||
sender_name,
|
||||
)
|
||||
return result.model_dump()
|
||||
|
||||
@mcp.tool(description="Set or remove IMAP flags on an email. Standard flags: \\Seen, \\Answered, \\Flagged, \\Deleted, \\Draft. Custom keywords are also supported.")
|
||||
|
||||
Reference in New Issue
Block a user