diff --git a/README.md b/README.md index abe3df4..ce1b900 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com). ## Features -- **11 MCP tools**: search, read, send, archive, move, mark, list/create/rename/delete folders, server info +- **12 MCP tools**: search, read, send, draft, archive, move, mark, list/create/rename/delete folders, server info +- **Send toggle**: Disable `send_email` globally or per account — agents use `create_draft` instead - **IMAP IDLE watcher**: Real-time monitoring of new emails, forwarded to Poke automatically - **Multi-account support**: Configure multiple email accounts in a single config file - **Bearer token auth**: Secure the server with `MCP_API_KEY` so only you can use it @@ -96,7 +97,8 @@ docker run -d \ |------|-------------| | `search_emails` | Search by from, to, subject, date range | | `read_email` | Read full email content by UID | -| `send_email` | Send email with text/HTML, CC/BCC, reply threading | +| `send_email` | Send email with text/HTML, CC/BCC, reply threading (can be disabled) | +| `create_draft` | Save email as draft for review before sending | | `archive_email` | Move email to Archive folder | | `move_email` | Move email between folders | | `mark_email` | Set read/unread/flagged/unflagged | diff --git a/config.example.yml b/config.example.yml index 9b9deca..c5a1c7d 100644 --- a/config.example.yml +++ b/config.example.yml @@ -7,6 +7,10 @@ poke_api_key: "your-api-key-here" # MCP_API_KEY is set via environment variable (not in this file) # It secures the MCP server so only you can use it +# Global send toggle — set to false to disable send_email for all accounts +# Agents can still use create_draft. Override per account with allow_send. +allow_send: true + accounts: # Minimal — SMTP falls back to IMAP host/credentials - id: personal diff --git a/src/server.py b/src/server.py index d3f1e12..f7afe90 100755 --- a/src/server.py +++ b/src/server.py @@ -85,6 +85,7 @@ def parse_accounts(config: dict) -> list[dict]: } ] + global_allow_send = config.get("allow_send", True) required = ("imap_host", "imap_username", "imap_password") for i, acc in enumerate(accounts): acc.setdefault("id", f"account-{i}") @@ -95,6 +96,7 @@ def parse_accounts(config: dict) -> list[dict]: acc.setdefault("smtp_port", 587) acc.setdefault("smtp_username", acc.get("imap_username")) acc.setdefault("smtp_password", acc.get("imap_password")) + acc.setdefault("allow_send", global_allow_send) for field in required: if field not in acc: raise RuntimeError( @@ -229,6 +231,18 @@ def detect_archive_folder(client: IMAPClient) -> str: return "Archive" +def detect_drafts_folder(client: IMAPClient) -> str: + import imapclient as imc + + result = client.find_special_folder(imc.DRAFTS) + if result: + return result + for name in ("Drafts", "[Gmail]/Drafts", "INBOX.Drafts"): + if client.folder_exists(name): + return name + return "Drafts" + + # --------------------------------------------------------------------------- # Poke webhook # --------------------------------------------------------------------------- @@ -568,6 +582,11 @@ async def send_email( accounts = ctx.lifespan_context["accounts"] acc = resolve_account(accounts, account_id) + if not acc.get("allow_send", True): + return { + "error": f"Sending is disabled for account '{acc['id']}'. Use create_draft instead." + } + def _send(): msg = MIMEMultipart("alternative") if html else MIMEText(body) if html: @@ -627,6 +646,49 @@ async def send_email( return await asyncio.to_thread(_send) +@mcp.tool( + description="Save an email as a draft for review before sending. The draft appears in the account's Drafts folder." +) +async def create_draft( + ctx: Context, + to: str, + subject: str, + body: str, + account_id: Optional[str] = None, + cc: Optional[str] = None, + bcc: Optional[str] = None, + html: Optional[str] = None, +) -> dict: + accounts = ctx.lifespan_context["accounts"] + acc = resolve_account(accounts, account_id) + + def _draft(): + msg = MIMEMultipart("alternative") if html else MIMEText(body) + if html: + msg.attach(MIMEText(body, "plain")) + msg.attach(MIMEText(html, "html")) + + msg["From"] = acc["smtp_username"] + msg["To"] = to + msg["Subject"] = subject + if cc: + msg["Cc"] = cc + if bcc: + msg["Bcc"] = bcc + + client = get_imap_client(acc) + try: + drafts_folder = detect_drafts_folder(client) + if not client.folder_exists(drafts_folder): + client.create_folder(drafts_folder) + client.append(drafts_folder, msg.as_bytes(), flags=[b"\\Draft", b"\\Seen"]) + return {"success": True, "folder": drafts_folder} + finally: + client.logout() + + return await asyncio.to_thread(_draft) + + @mcp.tool( description="Archive an email by moving it to the Archive folder instead of deleting." )