mail-mcp v0.1 — Rust MCP server for Sulkta email

Phase A: mail_send + mail_inbox_list + mail_inbox_read. Replaces
scripts/kayos_mail.py with a typed MCP server. Outbound guarantees Date,
Message-ID (own-domain), User-Agent, MIME-Version, multipart/alternative
for HTML+text, multipart/mixed for attachments, In-Reply-To +
References for threading.

Single account in v0.1 (default_account from config). Phase B adds
multi-account + threading + search; Phase C adds mark + attachments +
reply helper.

Stack: rmcp 0.1 (matches aldabra), lettre 0.11 + tokio-rustls, async-imap
0.10, mail-parser 0.9. Stderr-only logging (stdout is the MCP transport).

Smoke verified 2026-05-21: send -> land -> read kayos@sulkta.com round
trip, DKIM-Signature + Authentication-Results pass at the rspamd relay.
This commit is contained in:
Kayos 2026-05-21 06:50:25 -07:00
commit 2240bf745e
11 changed files with 3552 additions and 0 deletions

73
README.md Normal file
View file

@ -0,0 +1,73 @@
# 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). Uses `BODY.PEEK` so 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 hostname
- `From``name <addr>`
- `MIME-Version: 1.0`
- `User-Agent: mail-mcp/<version>`
- `In-Reply-To` + `References` when threading args present
- `Content-Type` correct for the body shape (text-only / alternative / mixed)
DKIM-Signature is applied by the relay (rspamd on Rackham), not the client.
## Build
```bash
cargo build --release
```
Binary lands at `target/release/mail-mcp`.
## Config
```bash
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:
1. Looked up from the env var named in `password_env`
2. Falling back to `password_file` (shell-format: `KEY=VALUE` per line)
3. 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)
```json
{
"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.*]`, plus `mail_thread` and `mail_search`.
- **Phase C** (~150 LOC): `mail_mark` (read/unread/flag/trash/archive), `mail_attachment_get`, `mail_reply` helper.
Full locked spec: `kayos/openclaw-workspace``memory/spec-mail-mcp.md`.