Three new tools complete the planned Phase C scope:
- mail_mark { uid, action, folder? }: action is one of read, unread,
flagged, unflagged, trash, archive. read/unread toggle \Seen via UID
STORE +/-FLAGS.SILENT (idempotent, no fetch round-trip). flagged/
unflagged the same for \Flagged. trash is a MOVE to Trash. archive
errors out with a clear pointer to mail_move because Sulkta's Dovecot
doesn't ship a canonical Archive folder.
- mail_attachment_get { uid, attachment_index, folder? }: fetches the
full RFC822 (within the existing 20 MB raw_eml cap), parses with
mail-parser, returns the N-th attachment as base64. The index matches
mail_inbox_read's attachments[] ordering. Returns {filename,
mime_type, size, content_base64}. SAFETY note in the tool description
warns the LLM not to execute / render / open attachment bytes
blindly.
- mail_reply { uid, body, body_html?, attachments?, reply_all?,
to_override? }: fetches the original to pull From / Subject /
Message-Id / References, then sends with proper In-Reply-To +
References + 'Re: ' subject prefix (skipped if already prefixed).
reply_all=true echoes the original Cc. to_override replaces To.
Threading headers still set against the original regardless of
to_override.
Smoke verified 2026-05-21:
- Send kayos->kayos with a 54-byte text attachment
- mail_inbox_read shows attachments=[('smoke.txt', 54)]
- mail_attachment_get returns the exact bytes (b'Hello from mail-mcp
Phase C smoke!\r\nLine 2.\r\nLine 3.\r\n', 54 bytes)
- mail_mark unread -> flags=[] (\Seen cleared)
- mail_mark flagged -> flags=['\\Flagged']
- mail_reply -> message lands as 'Re: mail-mcp phase-C smoke' with
In-Reply-To = parent Message-Id and References = parent Message-Id
ServerHandler instructions updated to enumerate all 10 tools + the new
attachment-safety note. Tools live on the wire: mail_send,
mail_inbox_list, mail_inbox_read, mail_folder_list, mail_search,
mail_thread, mail_move, mail_mark, mail_attachment_get, mail_reply.
Test count 27 -> 33: 2 for MarkAction::parse (alias coverage + unknown
rejection), 4 for tools::extract_addr (display-name strip + bare-addr
passthrough + garbage tolerance + ToField unwrap).
|
||
|---|---|---|
| crates/mail-mcp | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.example.toml | ||
| README.md | ||
mail-mcp
Rust MCP server for Sulkta-hosted email. SMTP send + IMAP read with RFC-correct headers, multipart/alternative when HTML is included, multipart/mixed for attachments, threading via In-Reply-To/References.
Replaces the scripts/kayos_mail.py CLI path that lived in kayos/openclaw-workspace since 2026-04-23.
Why a server, not a CLI
kayos_mail.py shipped without Date or Message-ID headers until a 2026-05-18 patch — exactly the kind of header-discipline regression a typed Rust server prevents at compile time. The "no spam bin" framing is mostly upstream of any client (Rackham postfix + rspamd DKIM-sign at the relay; mail-tester scored 10/10 and port25 SpamAssassin −7.31 on 2026-05-20), but a correct client doesn't trip filters with bad MIME structure, broken threading, or missing headers.
Tools (v0.1)
mail_send— send mail. Args:account?,to,cc[]?,bcc[]?,subject,body,body_html?,attachments[]?,in_reply_to?,references[]?. Returns{message_id, sent_at}.mail_inbox_list— list folder messages newest-first. Args:account?,since?(YYYY-MM-DD),unread_only?,limit?(default 50, max 500),folder?(default INBOX). UsesBODY.PEEKso it does not toggle\Seen.mail_inbox_read— fetch one message by UID. Args:account?,uid,folder?,format?(text|html|raw_eml). Attachment payloads are not inlined — only filename/mime_type/size metadata.
Headers we guarantee on outbound
Date— UTC, RFC 5322 (lettre auto)Message-ID—<UUIDv4@<from_addr_domain>>— own-domain, never the container hostnameFrom—name <addr>MIME-Version: 1.0User-Agent: mail-mcp/<version>In-Reply-To+Referenceswhen threading args presentContent-Typecorrect for the body shape (text-only / alternative / mixed)
DKIM-Signature is applied by the relay (rspamd on Rackham), not the client.
Build
cargo build --release
Binary lands at target/release/mail-mcp.
Config
mkdir -p ~/.config/mail-mcp
cp config.example.toml ~/.config/mail-mcp/config.toml
chmod 600 ~/.config/mail-mcp/config.toml
Edit accounts as needed. Passwords are NEVER inline:
- Looked up from the env var named in
password_env - Falling back to
password_file(shell-format:KEY=VALUEper line) - Hard-failing with a vault-pointer hint if neither resolves
Vault canonical: bw.sulkta.com → kayos@sulkta.com — IMAP/SMTP.
MCP wiring (Claude Code / kayos-house)
{
"mcpServers": {
"mail-mcp": {
"command": "/usr/local/bin/mail-mcp",
"args": []
}
}
}
Logging is stderr-only — stdout is the JSON-RPC transport.
Future phases
- Phase B (~200 LOC): multi-account routing across all configured
[accounts.*], plusmail_threadandmail_search. - Phase C (~150 LOC):
mail_mark(read/unread/flag/trash/archive),mail_attachment_get,mail_replyhelper.
Full locked spec: kayos/openclaw-workspace → memory/spec-mail-mcp.md.