Files
dav-imap-mcp/src/tools/email_tools.py
Yigit Colakoglu bd8e1412e4
All checks were successful
Build And Test / publish (push) Successful in 47s
Use configured contacts address book
2026-01-01 15:55:00 -08:00

297 lines
11 KiB
Python

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()