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