Add create_draft tool and allow_send toggle per account

- create_draft: saves email as draft in IMAP Drafts folder
- allow_send: global config toggle (default true), overridable per account
- When sending disabled, send_email returns error suggesting create_draft
This commit is contained in:
Kacper Kwapisz
2026-03-23 00:13:35 +01:00
parent 6e9f1ca617
commit 9c84ea1a91
3 changed files with 70 additions and 2 deletions
+4 -2
View File
@@ -4,7 +4,8 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com).
## Features ## 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 - **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 - **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 - **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 | | `search_emails` | Search by from, to, subject, date range |
| `read_email` | Read full email content by UID | | `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 | | `archive_email` | Move email to Archive folder |
| `move_email` | Move email between folders | | `move_email` | Move email between folders |
| `mark_email` | Set read/unread/flagged/unflagged | | `mark_email` | Set read/unread/flagged/unflagged |
+4
View File
@@ -7,6 +7,10 @@ poke_api_key: "your-api-key-here"
# MCP_API_KEY is set via environment variable (not in this file) # MCP_API_KEY is set via environment variable (not in this file)
# It secures the MCP server so only you can use it # 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: accounts:
# Minimal — SMTP falls back to IMAP host/credentials # Minimal — SMTP falls back to IMAP host/credentials
- id: personal - id: personal
+62
View File
@@ -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") required = ("imap_host", "imap_username", "imap_password")
for i, acc in enumerate(accounts): for i, acc in enumerate(accounts):
acc.setdefault("id", f"account-{i}") 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_port", 587)
acc.setdefault("smtp_username", acc.get("imap_username")) acc.setdefault("smtp_username", acc.get("imap_username"))
acc.setdefault("smtp_password", acc.get("imap_password")) acc.setdefault("smtp_password", acc.get("imap_password"))
acc.setdefault("allow_send", global_allow_send)
for field in required: for field in required:
if field not in acc: if field not in acc:
raise RuntimeError( raise RuntimeError(
@@ -229,6 +231,18 @@ def detect_archive_folder(client: IMAPClient) -> str:
return "Archive" 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 # Poke webhook
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -568,6 +582,11 @@ async def send_email(
accounts = ctx.lifespan_context["accounts"] accounts = ctx.lifespan_context["accounts"]
acc = resolve_account(accounts, account_id) 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(): def _send():
msg = MIMEMultipart("alternative") if html else MIMEText(body) msg = MIMEMultipart("alternative") if html else MIMEText(body)
if html: if html:
@@ -627,6 +646,49 @@ async def send_email(
return await asyncio.to_thread(_send) 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( @mcp.tool(
description="Archive an email by moving it to the Archive folder instead of deleting." description="Archive an email by moving it to the Archive folder instead of deleting."
) )