from typing import Optional from fastmcp import FastMCP from services.email_service import EmailService from tools.logging_utils import log_tool_call def register_email_tools(mcp: FastMCP, service: EmailService): """Register all email-related MCP tools.""" @mcp.tool(description="List all mailboxes/folders in the email account. Returns name, path, message count, and unread count for each mailbox.") @log_tool_call def list_mailboxes() -> list[dict]: """List all IMAP mailboxes/folders.""" mailboxes = service.list_mailboxes() return [m.model_dump() for m in mailboxes] @mcp.tool(description="List emails in a mailbox with pagination. Returns email summaries including subject, from, date, and read status.") @log_tool_call def list_emails( mailbox: str = "INBOX", limit: int = 50, offset: int = 0, include_body: bool = False, ) -> dict: """ List emails in a mailbox. Args: mailbox: The mailbox/folder to list (default: INBOX) limit: Maximum number of emails to return (default: 50) offset: Number of emails to skip for pagination (default: 0) include_body: Whether to include email body snippets (default: False) """ result = service.list_emails(mailbox, limit, offset, include_body) return result.model_dump() @mcp.tool(description="List draft emails in the Drafts mailbox with pagination.") @log_tool_call def list_drafts( mailbox: Optional[str] = None, limit: int = 50, offset: int = 0, include_body: bool = False, ) -> dict: """ List draft emails. Args: mailbox: Drafts mailbox/folder override (default: auto-detect) limit: Maximum number of drafts to return (default: 50) offset: Number of drafts to skip for pagination (default: 0) include_body: Whether to include body snippets (default: False) """ result = service.list_drafts(mailbox, limit, offset, include_body) return result.model_dump() @mcp.tool(description="Read a specific email by ID with full body content and attachment information.") @log_tool_call def read_email( mailbox: str, email_id: str, format: str = "text", ) -> Optional[dict]: """ Read a specific email. Args: mailbox: The mailbox containing the email email_id: The unique ID of the email format: Body format to return - 'text', 'html', or 'both' (default: text) """ result = service.read_email(mailbox, email_id, format) return result.model_dump() if result else None @mcp.tool(description="Search emails in a mailbox using various criteria like subject, sender, or body content.") @log_tool_call def search_emails( query: str, mailbox: str = "INBOX", search_in: Optional[list[str]] = None, date_from: Optional[str] = None, date_to: Optional[str] = None, limit: int = 50, ) -> dict: """ Search for emails matching criteria. Args: query: Search term to look for mailbox: Mailbox to search in (default: INBOX) search_in: Fields to search - any of ['subject', 'from', 'body'] (default: all) date_from: Only emails after this date (format: DD-Mon-YYYY, e.g., 01-Jan-2024) date_to: Only emails before this date (format: DD-Mon-YYYY) limit: Maximum results to return (default: 50) """ if search_in is None: search_in = ["subject", "from", "body"] result = service.search_emails(query, mailbox, search_in, date_from, date_to, limit) return result.model_dump() @mcp.tool(description="Move an email from one mailbox/folder to another.") @log_tool_call def move_email( email_id: str, source_mailbox: str, destination_mailbox: str, ) -> dict: """ Move an email to a different folder. Args: email_id: The unique ID of the email to move source_mailbox: The current mailbox containing the email destination_mailbox: The target mailbox to move the email to """ result = service.move_email(email_id, source_mailbox, destination_mailbox) return result.model_dump() @mcp.tool(description="Delete an email, either moving it to trash or permanently deleting it.") @log_tool_call def delete_email( email_id: str, mailbox: str, permanent: bool = False, ) -> dict: """ Delete an email. Args: email_id: The unique ID of the email to delete mailbox: The mailbox containing the email permanent: If True, permanently delete; if False, move to Trash (default: False) """ result = service.delete_email(email_id, mailbox, permanent) return result.model_dump() @mcp.tool(description="Delete a drafted email by ID, optionally permanently.") @log_tool_call def delete_draft( email_id: str, mailbox: Optional[str] = None, permanent: bool = False, ) -> dict: """ Delete a draft email. Args: email_id: The unique ID of the draft mailbox: Drafts mailbox/folder override (default: auto-detect) permanent: If True, permanently delete; if False, move to Trash (default: False) """ result = service.delete_draft(email_id, mailbox, permanent) return result.model_dump() @mcp.tool(description="Save a new draft email to the Drafts mailbox. Supports reply threading via in_reply_to_email_id.") @log_tool_call def save_draft( to: Optional[list[str]] = None, subject: Optional[str] = None, body: Optional[str] = None, cc: Optional[list[str]] = None, bcc: Optional[list[str]] = None, mailbox: Optional[str] = None, in_reply_to_email_id: Optional[str] = None, in_reply_to_mailbox: Optional[str] = None, reply_all: bool = False, ) -> dict: """ Save a new email draft. Args: to: List of recipient email addresses (required unless in_reply_to_email_id is set) subject: Email subject line (required unless in_reply_to_email_id is set) body: Plain text email body (required unless in_reply_to_email_id is set) cc: List of CC recipients (optional) bcc: List of BCC recipients (optional) mailbox: Drafts mailbox/folder override (default: auto-detect) in_reply_to_email_id: Email UID to reply to (optional, derives recipients/subject and sets threading headers) in_reply_to_mailbox: Mailbox containing the in_reply_to_email_id (default: INBOX) reply_all: Whether to include original recipients when replying (default: False) """ result = service.save_draft( to=to, subject=subject, body=body, cc=cc, bcc=bcc, html_body=None, mailbox=mailbox, in_reply_to_email_id=in_reply_to_email_id, in_reply_to_mailbox=in_reply_to_mailbox, reply_all=reply_all, ) return result.model_dump() @mcp.tool(description="Edit an existing draft email. Only provided fields will be modified. Supports reply threading via in_reply_to_email_id.") @log_tool_call def edit_draft( email_id: str, mailbox: Optional[str] = None, to: Optional[list[str]] = None, subject: Optional[str] = None, body: Optional[str] = None, cc: Optional[list[str]] = None, bcc: Optional[list[str]] = None, in_reply_to_email_id: Optional[str] = None, in_reply_to_mailbox: Optional[str] = None, reply_all: bool = False, ) -> dict: """ Update an existing draft email. Args: email_id: The unique ID of the draft mailbox: Drafts mailbox/folder override (default: auto-detect) to: List of recipient email addresses subject: Email subject line body: Plain text email body cc: List of CC recipients (optional) bcc: List of BCC recipients (optional) in_reply_to_email_id: Email UID to reply to (optional, derives recipients/subject and sets threading headers) in_reply_to_mailbox: Mailbox containing the in_reply_to_email_id (default: INBOX) reply_all: Whether to include original recipients when replying (default: False) """ result = service.update_draft( email_id=email_id, mailbox=mailbox, to=to, subject=subject, body=body, cc=cc, bcc=bcc, html_body=None, in_reply_to_email_id=in_reply_to_email_id, in_reply_to_mailbox=in_reply_to_mailbox, reply_all=reply_all, ) return result.model_dump() @mcp.tool(description="Send an existing draft by ID. Only drafts can be sent.") @log_tool_call async def send_draft( email_id: str, mailbox: Optional[str] = None, ) -> dict: """ Send a draft email. Args: email_id: The unique ID of the draft to send mailbox: Drafts mailbox/folder override (default: auto-detect) """ result = await service.send_draft(email_id, mailbox) 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.") @log_tool_call def set_email_flags( email_id: str, mailbox: str, add_flags: Optional[list[str]] = None, remove_flags: Optional[list[str]] = None, ) -> dict: """ Set or remove flags on an email. Args: email_id: The unique ID of the email mailbox: The mailbox containing the email add_flags: Flags to add (e.g., ["\\Flagged", "important"]) remove_flags: Flags to remove (e.g., ["\\Seen"]) """ result = service.set_flags(email_id, mailbox, add_flags, remove_flags) return result.model_dump() @mcp.tool(description="Unsubscribe from a mailing list. Parses List-Unsubscribe headers and attempts automatic unsubscribe via HTTP or provides mailto instructions.") @log_tool_call async def unsubscribe_maillist( email_id: str, mailbox: str = "INBOX", ) -> dict: """ Unsubscribe from a mailing list based on email headers. Args: email_id: The unique ID of the email containing unsubscribe info mailbox: The mailbox containing the email (default: INBOX) Returns: Result with unsubscribe status. For HTTP unsubscribe, it attempts automatic unsubscription. For mailto, it returns the email details to send. """ result = await service.unsubscribe(mailbox, email_id) return result.model_dump()