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:
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user