mail-mcp

Production-ready email MCP server for AI agents
IMAP + SMTP + EWS + Microsoft Graph API — built in Rust

Release License Stars

--- Most email MCP servers only do IMAP reads. This one does **everything**: read, search, send, reply, forward, bulk operations, Microsoft Graph API, and Exchange Web Services — with real OAuth2, multi-account, and multi-provider support. Written in Rust for speed and safety. ## What's New in v0.5.0 - **New: draft tools** (`draft_create`, `draft_update`, `draft_send`, `draft_delete`, `draft_list`) closing [#3](https://github.com/tecnologicachile/mail-mcp/issues/3) and [#17](https://github.com/tecnologicachile/mail-mcp/issues/17). An agent can save a message in the account's own Drafts folder, revise it, and send it later, so a person can review it in their mail client first. The same five tools work on every backend: IMAP + SMTP, Exchange Web Services and Microsoft Graph, picked per account (EWS config, else Graph config, else IMAP). `list_all_accounts` reports the backend as `drafts_with`. - **Separate gate: `MAIL_DRAFTS_ENABLED`** (default `false`). It is independent of `MAIL_IMAP_WRITE_ENABLED`: every operation on an existing `draft_id` first checks that the target really is a draft (IMAP Drafts folder, EWS `IsDraft`, Graph `isDraft`), so enabling drafts never grants move or delete on other mail. `draft_send` also requires `MAIL_SMTP_WRITE_ENABLED`. - **Graph accounts need the `Mail.ReadWrite` scope** for drafts; see [account setup](docs/account-setup.md). The EWS attachment flow (`CreateItem` with `SaveOnly`, then `CreateAttachment`) follows the approach proposed by [@verdelyi](https://github.com/verdelyi) in [#14](https://github.com/tecnologicachile/mail-mcp/pull/14). - **Fix: SMTP messages now carry a Message-ID on the sender's domain** ([#7](https://github.com/tecnologicachile/mail-mcp/issues/7)). Before, lettre sent no Message-ID of its own, so the id returned by the send tools was empty and the Sent copy had none. - Verified live on IMAP (Zoho) and Microsoft Graph; the EWS draft path is covered by unit tests. ## What's New in v0.4.17 - **New: `imap_search_all_mailboxes`** by [@tdabasinskas](https://github.com/tdabasinskas) in [#35](https://github.com/tecnologicachile/mail-mcp/pull/35). Runs one search in every selectable mailbox of an account and returns the newest matches, each labelled with its mailbox. Built for providers without an "All Mail" folder (iCloud), where one conversation is spread over INBOX, Sent Messages and project folders. The work is spread over `MAIL_IMAP_SEARCH_CONCURRENCY` connections (default `4`, clamped to `1..=8`); a mailbox that fails is reported and does not abort the search. Scope it with `include_mailboxes` / `exclude_mailboxes`. - **Thread search by Message-ID** by [@tdabasinskas](https://github.com/tdabasinskas) in [#34](https://github.com/tecnologicachile/mail-mcp/pull/34). `imap_search_messages` takes a new `thread_message_id` criterion that matches a message and every message whose `In-Reply-To` or `References` carries it, so an agent can pull a whole conversation in one search. ## What's New in v0.4.16 - **`imap_list_mailboxes` cap is now configurable and visible** by [@tdabasinskas](https://github.com/tdabasinskas) in [#33](https://github.com/tecnologicachile/mail-mcp/pull/33). The tool used to silently drop everything past 200 mailboxes — on accounts with more folders (e.g. 217 on iCloud), folders like `Sent Messages` simply never appeared. The cap is now set with `MAIL_IMAP_MAX_MAILBOXES` (default `200`, clamped to `1..=10000`), and the response carries two additive fields, `total` and `truncated`, so clients can always tell a capped list from a complete one. Nothing changes unless the variable is set. ## What's New in v0.4.15 - **New: opt-in streamable HTTP transport** by [@tdabasinskas](https://github.com/tdabasinskas) in [#32](https://github.com/tecnologicachile/mail-mcp/pull/32). Set `MAIL_MCP_TRANSPORT=http` to serve MCP streamable HTTP (stateless) instead of stdio — useful for running the server on another machine behind an MCP gateway. Binds to `127.0.0.1:8000` at `/mcp` by default, configurable via `MAIL_MCP_HTTP_HOST` / `MAIL_MCP_HTTP_PORT` / `MAIL_MCP_HTTP_PATH`, with graceful shutdown on SIGINT/SIGTERM. **The endpoint has no built-in authentication** — keep it on loopback or behind an authenticating gateway; see `docs/advanced-configuration.md#remote-http-transport`. stdio remains the default: nothing changes unless the variable is set. ## What's New in v0.4.14 Community release — both changes came from external contributors. Thank you! - **New: `MAIL_ATTACHMENT_UPLOAD_DIR` — confine outbound `file_path` attachments** by [@mjones-PL](https://github.com/mjones-PL) in [#31](https://github.com/tecnologicachile/mail-mcp/pull/31), closing [#11](https://github.com/tecnologicachile/mail-mcp/issues/11). Without a restriction, send tools would read any file the server process can access — a prompt-injected model could exfiltrate SSH keys or `.env` files as attachments. Set this variable to a directory and every `file_path` is canonicalized (`..` and symlinks resolved) and must land inside it; missing and out-of-scope files return the same error so the check cannot probe for file existence. Opt-in: when unset, behavior is unchanged. See `docs/security.md#outbound-attachment-scope`. - **Fixed: `FROM_EMAIL` validation no longer rejects dotless internal hosts** by [@arwack](https://github.com/arwack) in [#30](https://github.com/tecnologicachile/mail-mcp/pull/30). Addresses the v0.4.13 upgrade caveat: `noreply@localhost` / `alerts@intranet` style addresses on corporate internal relays are accepted again, while the real typo checks (multiple `@`, whitespace, empty local part) remain. If you held the v0.4.13 upgrade because of this, v0.4.14 is safe. ## What's New in v0.4.13 - **`effective_from()` helper + `FROM_EMAIL` startup validation** by [@arwack](https://github.com/arwack) in [#29](https://github.com/tecnologicachile/mail-mcp/pull/29) — the follow-up to their #19. The `from_email` → `user` fallback now lives in one place (`SmtpAccountConfig::effective_from()`), and `MAIL_SMTP__FROM_EMAIL` is validated when the server starts instead of failing on the first send. - **Behavior change — read before upgrading:** a malformed `FROM_EMAIL` (multiple `@`, whitespace, empty local part, or a domain without a dot) now prevents the server from starting, for **all** accounts. Note that dotless domains such as `user@localhost` or `alerts@intranet` are currently rejected too; if you use an internal relay address like that, hold the upgrade — a follow-up relaxing the dot rule is under discussion in #29. - Hardened APPEND wire-format tests by [@tordable](https://github.com/tordable) in [#28](https://github.com/tecnologicachile/mail-mcp/pull/28): mailbox quoting, announced literal length, and byte-for-byte payload are now asserted for every append test. ## What's New in v0.4.12 Community release — both changes came from external contributors. Thank you! - **iCloud mailbox aliases + reads no longer mark messages as read** by [@felipefdl](https://github.com/felipefdl) in [#16](https://github.com/tecnologicachile/mail-mcp/pull/16). Short mailbox names now resolve to each provider's real folder (`Sent` → `Sent Messages` on iCloud / `[Gmail]/Sent Mail` on Gmail, `Trash` → `Deleted Messages`, and so on, multi-language) in search, copy, and move. Raw message fetches now use `BODY.PEEK[]`, so reading a message through the MCP no longer sets `\Seen` as a side effect — with a `BODY[]` fallback for servers that reject `PEEK` (the deprecated `RFC822` item, removed in #23, stays out). Validated against a real iCloud mailbox by the author; includes alias-resolution tests and iCloud setup docs. - **Optimized multi-stage Dockerfile + docker-compose** by [@monssefbaakka](https://github.com/monssefbaakka) in [#5](https://github.com/tecnologicachile/mail-mcp/pull/5). cargo-chef layer caching, TARGETARCH-aware musl cross-builds (amd64/arm64), and a `scratch` runtime image — 16.9 MB, down from 25.5 MB — verified to respond to MCP initialize/tools-list over stdio. The toolchain pin was bumped to Rust 1.90 (the codebase's let-chains require >= 1.88). ## What's New in v0.4.11 Community bugfix release — both fixes came from external contributors. Thank you! - **Fixed: save-to-Sent silently failed on strict IMAP servers (iCloud and others)** by [@dominikknafelj](https://github.com/dominikknafelj) in [#26](https://github.com/tecnologicachile/mail-mcp/pull/26), reported in [#25](https://github.com/tecnologicachile/mail-mcp/issues/25). The `\Seen` flag introduced in v0.4.10 was sent without the RFC 3501 parenthesized flag-list syntax (`APPEND "Sent" \Seen …` instead of `APPEND "Sent" (\Seen) …`), because `async-imap` interpolates the flags argument verbatim. Strict servers rejected the APPEND and the sent copy was lost — while the tool still reported `status: ok`. Flags are now normalized before hitting the wire, and `smtp_send_message` / `smtp_reply_message` / `smtp_forward_message` responses include a new `saved_to_sent` field (`true`/`false`, or `null` when saving is disabled) so callers can detect archival failures. [@tordable](https://github.com/tordable) diagnosed and fixed the same root cause concurrently in [#24](https://github.com/tecnologicachile/mail-mcp/pull/24). - **Fixed: message reads returned empty on iCloud** by [@tdabasinskas](https://github.com/tdabasinskas) in [#23](https://github.com/tecnologicachile/mail-mcp/pull/23). Raw message fetches used the deprecated `RFC822` item, which iCloud accepts but leaves unpopulated. Fetches now use the IMAP4rev1 `BODY[]` item — same `\Seen` semantics, works everywhere — with a mock-server regression test pinning the wire format. ## What's New in v0.4.10 Community release — all three changes came from external contributors. Thank you! - **NetEase IMAP compatibility (126.com / 163.com / yeah.net)** by [@pep-27](https://github.com/pep-27) in [#21](https://github.com/tecnologicachile/mail-mcp/pull/21). NetEase servers reject mailbox access from clients that don't identify themselves. mail-mcp now sends the RFC 2971 `ID` command after authentication whenever the server advertises the `ID` capability. Includes mock-server regression tests and NetEase setup docs in `docs/account-setup.md`. - **`MAIL_SMTP__FROM_EMAIL` — sender address override** by [@arwack](https://github.com/arwack) in [#19](https://github.com/tecnologicachile/mail-mcp/pull/19). For shared/group mailboxes where SMTP authenticates with a personal account but the From address should be the group address. Applies to send, reply (including reply-all self-address detection) and forward; falls back to `_USER` when unset. - **Sent-mail copies are now marked `\Seen`** by [@ray-of-darkness](https://github.com/ray-of-darkness) in [#9](https://github.com/tecnologicachile/mail-mcp/pull/9). Copies the MCP appends to the Sent folder after SMTP send no longer show up as unread. ## What's New in v0.4.9 - **New tool `imap_get_attachment` — download a single attachment to disk.** Until now the only ways to reach attachment bytes were `imap_get_message` (which returns attachment *metadata* and optional extracted PDF *text*, never the binary) and `imap_get_message_raw` (capped at 1 MB and base64-encoded into the response). A 7 MB email with X-ray images could not be retrieved at all — over the cap, and dumping it into the response would blow up the model's context anyway. - **How it works:** call `imap_get_attachment` with the `message_id` plus a selector — either `part_id` (the value `imap_get_message` reports for each attachment) or `filename`. The server fetches the full message (no size cap on the server side), extracts and decodes just that one part, and **writes it to disk**, returning `{ file_path, filename, content_type, part_id, size_bytes }`. The binary never enters the response, so context stays small. The saved path feeds straight into a local reader (e.g. an image-description tool or a PDF reader). - **Where files land:** `output_dir` argument if given, else the `MAIL_ATTACHMENT_DOWNLOAD_DIR` environment variable, else the system temp dir. Filenames are sanitized (basename only, control characters stripped) to prevent path traversal, and prefixed with the message UID and part id to avoid collisions. - **Optional inline base64:** set `include_base64: true` to also get the bytes in the response, but only when the attachment is at most `max_inline_bytes` (default 256 KiB). Off by default. ## What's New in v0.4.8 - **`SAVE_SENT` is now per-account with a provider-aware default.** Previously, saving a copy of outgoing mail to the Sent folder via IMAP APPEND was controlled by a single global flag, `MAIL_SMTP_SAVE_SENT`. The problem: providers that **already save** sent mail server-side (Gmail, Zoho) ended up with **two identical copies** in Sent, while a generic SMTP server or Office 365 (which do **not** auto-save on SMTP submission) lost the copy entirely when the flag was `false`. - **Provider-aware default** (when nothing is configured): - **Gmail** (`smtp.gmail.com`): saves server-side and deduplicates by Message-ID → the MCP does **not** append (`false`). - **Zoho** (`smtp.zoho.com`): saves server-side but does **not** deduplicate → the MCP does **not** append (`false`), avoiding the duplicate. - **Office 365 / generic SMTP**: do **not** auto-save on SMTP submission → the MCP **does** append (`true`), or the sent copy would be lost. - **Per-account override**: `MAIL_SMTP__SAVE_SENT=true|false` takes priority over everything. The global `MAIL_SMTP_SAVE_SENT` still works as a coarse override (wins over the provider default, loses to the per-account override). - Precedence: **per-account** → **global** → **provider-aware default**. | Provider | Auto-saves server-side | MCP default | |---|---|---| | Gmail | Yes (with dedupe) | `false` | | Zoho | Yes (no dedupe) | `false` | | Office 365 (SMTP) | No | `true` | | Generic SMTP / relays | No | `true` | ## What's New in v0.4.7 - **Critical fix — `graph_send_message` silently dropped attachments on threaded replies.** When called with `in_reply_to` + `attachments`, the `createReply → PATCH → send` flow included the attachments in the PATCH against `/me/messages/{id}`. Microsoft Graph treats `Message.attachments` as a navigation property and **silently discards the field** on PATCH (2xx response, no error), so the message went out as single-part `text/html` with no file. The MCP returned `status: ok` and the caller assumed success. Invisible data loss. - **The fix:** in `send_via_reply()`, attachments are now uploaded one by one to `POST /me/messages/{draft_id}/attachments` between the PATCH and the send. Files < 3 MB go inline (JSON with base64 `contentBytes`); files ≥ 3 MB use `createUploadSession` with 4 MB chunked PUTs. The `attachments` field was removed from the `PatchDraftRequest` struct so the regression cannot be reintroduced by a type-correct edit. - **No change to flows that already worked.** `send_via_sendmail` (new messages without `in_reply_to`) uses `POST /me/sendMail` with `attachments` inline in the JSON — Graph DOES accept the field on that endpoint and never dropped it. That path is untouched. - **Regression test added:** `patch_draft_request_never_serializes_attachments` fails if anyone re-adds the field to the struct. - **Reference:** `BUG_GRAPH_ATTACHMENTS.md` at the repo root documents the full reproduction, root cause, and the empirical evidence behind the fix. ## What's New in v0.4.6 - **Server-side enforcement of HARD RULE #1.** Three releases of prompt-only hardening (v0.4.3 → v0.4.4 → v0.4.5) still left LLMs occasionally leaking literal `` markup into the recipient's inbox. v0.4.6 adds a real validator that **rejects** the tool call before any SMTP / Graph / EWS attempt if `body_text` or `body_html` contains tool-call wrapper syntax. The check is wired into all 5 send paths (`smtp_send_message`, `smtp_reply_message`, `smtp_forward_message`, `graph_send_message`, `ews_send_message`). - The forbidden markers are case-insensitive and tightly scoped — only the pseudo-tags that have no legitimate use in human correspondence: ``, ``, ``, ``, ``, ``, ``, and ``. Generic technical content that happens to mention `` for an XML schema or `` in a code example still passes. - **HARD RULE #1 wording updated** to announce the server-side rejection, so the LLM knows it's a hard contract — not a suggestion it can ignore. - No breaking changes for clean callers: well-behaved messages send exactly as before. ## What's New in v0.4.5 - **`serverInfo` now reports `name="mail-mcp"` + the crate `version`** (the framework previously returned its own `rmcp 0.16.0`, which never changes between releases). Useful for verifying the active version with `/mcp`, and so any client-side cache keyed by (server, version) invalidates on each bump. - **MCP instructions reorganized**: the 3 critical anti-concatenation rules (which in v0.4.3 and v0.4.4 sat at the end of the block and could be lost to truncation / diluted attention) now appear as **HARD RULE #1, #2, #3 at the TOP**, right after the title. Consolidated into 3 short paragraphs (previously 3 long sections, ~1500 characters combined). - **No functional changes to the server.** Same SMTP/IMAP/EWS/Graph, same tool set, same behavior. Only the text exposed to the client changed. ### Important for these rules to take effect Clients that resume a session with `claude --continue` (or `/resume`) do **NOT** refresh the MCP `system_prompt` — they keep the one from that session's first handshake. If your session predates v0.4.5, the rules won't reach your context even if the on-disk binary is updated. To receive them, start a NEW session in the project (not `--continue`). ## What's New in v0.4.4 - **Preview hygiene rule** in MCP `instructions`: when the LLM shows the user the email preview before sending, it should render ONE clean version of the body (markdown-style bullets, bold, links as text + URL) and state that the message will go multipart — but it must NOT dump the raw HTML source (`

`, ``, ``...) into the preview. Two reasons: 1. The human reviewer wants to read the message, not audit markup — showing the HTML is noise. 2. Exhibiting both the plain-text string AND the HTML string side by side in the preview is exactly the context that has historically led LLMs to concatenate them in the eventual tool call (the bug v0.4.3 documented). Hiding the HTML source from the preview removes the temptation. Complements the **PREVIEW DOES NOT EQUAL TOOL CALL** rule introduced in v0.4.3. ## What's New in v0.4.3 - **Server-side guidance against malformed tool calls.** The MCP `instructions` block now explicitly tells the calling LLM that `body_text` and `body_html` are TWO SEPARATE JSON fields and must NEVER be concatenated. Previous wording ("send BOTH body_text AND body_html") was ambiguous and some LLMs interpreted it as "concatenate both with `...` pseudo-tags inside a single `body_text` string". When that happens, the recipient sees garbled duplicated content, AND any later Claude session that opens the saved copy via this MCP gets a Usage Policy block (the leaked `...` looks like a prompt-injection attempt to safety filters). The new instruction shows a CORRECT vs WRONG example and bans pseudo-tags / tool-call wrapper syntax inside email fields. ## What's New in v0.4.2 - **Release pipeline fixed**: the `publish-npm` job in the CI release workflow has been disabled. It was inherited from the upstream fork and tried to publish to `@bradsjm/mail-imap-mcp-rs`, a scope this org does not own — every release was 404-ing on that step. See "Releasing" below for the full explanation and how to re-enable npm publishing if needed. - **Auto-trigger releases on tag push**: `.github/workflows/release.yml` now fires on `push: tags: ['v*']`, so tagging `vX.Y.Z` and pushing is all it takes to cut a release. `workflow_dispatch` is retained as a manual escape hatch. - **Cleanup**: removed the dangling `init-npm-placeholder.yml` workflow (also referenced the fork's npm scope). - **docs**: README gains a "Releasing" section documenting the new flow and the npm decision. ## What's New in v0.4.1 - **Fix**: `save_to_sent_folder` now archives the exact RFC822 bytes that were sent (via `lettre.formatted()`), instead of a hand-rolled text-only stub. The Sent-folder copy keeps the HTML body, the multipart/alternative structure, and the RFC 2047-encoded subject — no more `???` where accents used to be, and HTML is no longer silently dropped. - **Improved**: localized Sent-folder detection — `Enviado[s]`, `Elementos enviados`, `Enviadas`, `Itens enviados`, `Envoyés`, `Éléments envoyés`, `Gesendet`, `Posta inviata`, `Verzonden`, `Wysłane`, plus nested variants. Previously only English names were recognized, so Zoho/localized IMAP accounts fell through to a non-existent `"Sent"` folder. - **Improved**: `smtp_forward_message` accepts `body_html` (was hardcoded to plain-text only). - **Improved**: EWS send gains `bcc`, `in_reply_to`, `references` (via ``), plus full recipient + subject-length validation — now at parity with the SMTP and Graph send paths. - **Improved**: Graph API threading fallbacks now log. `WARN` when the message-lookup HTTP call fails (rate limit, 5xx, permissions) so operators see threading degraded due to a real error; `DEBUG` when the original message is legitimately not found. - **Refactor**: EWS XML parsing migrated from substring matching to `quick-xml`. Fixes a latent namespace-collision bug (`` vs ``), correctly decodes XML entities and CDATA, and handles attribute values containing `=` (common in base64-like EWS item IDs). - **Cleanup**: zero warnings on `cargo build --release`. - **Tests**: 64 (up from 47). ## Why This Project | | mail-mcp | Typical email MCP | |---|:---:|:---:| | IMAP read/write | 18 tools | 3-5 tools | | SMTP send/reply/forward | Yes | No or broken | | Microsoft Graph API | Yes | No | | EWS (Exchange Web Services) | Yes | No | | OAuth2 (XOAUTH2) | Native | No | | Multi-account | Yes | Single account | | Microsoft 365 + Hotmail | Both work | Usually neither | | Language | Rust (fast, safe) | TypeScript/Python | | Tests | 64 unit + integration | Mocks only | | Warnings in release build | 0 | Varies | ## Feature Matrix | Provider | IMAP | SMTP | Graph API | EWS | OAuth2 | Multi-account | |----------|:----:|:----:|:---------:|:---:|:------:|:-------------:| | Microsoft 365 (enterprise) | Yes | Admin-dependent | Yes | **Yes** | Yes | Yes | | Hotmail / Outlook.com | Yes | Blocked by MS | Yes | **Yes** | Yes | Yes | | Gmail | Yes | Yes | — | — | Yes | Yes | | Apple iCloud | Yes | Yes | — | — | — | Yes | | Zoho | Yes | Yes | — | — | — | Yes | | Fastmail | Yes | Yes | — | — | — | Yes | | Any IMAP/SMTP server | Yes | Yes | — | — | — | Yes | > **EWS is the simplest way to add Microsoft accounts** — single OAuth2 token for both reading and sending. Works even on tenants that block Graph API and IMAP. ## Quickstart — Let Claude Code do it Copy and paste this prompt into Claude Code and it will install, compile, and configure everything for you: ``` Install and configure the mail-mcp MCP server from https://github.com/tecnologicachile/mail-mcp 1. Clone the repo, build with cargo build --release 2. Add the MCP server to .claude.json with the binary path 3. For Microsoft accounts: use EWS (simplest) — run device code flow with client_id d3590ed6-52b3-4102-aeff-aad2292ab01c and scope https://outlook.office365.com/EWS.AccessAsUser.All offline_access Then configure MAIL_EWS__USER and MAIL_EWS__REFRESH_TOKEN 4. For Gmail: configure MAIL_IMAP + MAIL_SMTP with App Password from https://myaccount.google.com/apppasswords 5. For Zoho: configure MAIL_IMAP + MAIL_SMTP with standard password 6. Enable write/send: MAIL_IMAP_WRITE_ENABLED=true, MAIL_SMTP_WRITE_ENABLED=true My email accounts to configure: - ``` Replace the last line with your email(s). Claude Code will guide you through each step including the OAuth2 device code flow for Microsoft accounts. ## Manual Setup (2 minutes) Install a prebuilt binary: ```bash # macOS and Linux (Homebrew) brew install tecnologicachile/tap/mail-mcp # macOS and Linux (shell installer) curl --proto '=https' --tlsv1.2 -LsSf https://github.com/tecnologicachile/mail-mcp/releases/latest/download/mail-mcp-installer.sh | sh ``` ```powershell # Windows powershell -ExecutionPolicy Bypass -c "irm https://github.com/tecnologicachile/mail-mcp/releases/latest/download/mail-mcp-installer.ps1 | iex" ``` Or build from source: ```bash git clone https://github.com/tecnologicachile/mail-mcp.git cd mail-mcp cargo build --release ``` Add to your MCP client config (Claude Code, Cursor, etc.). With a prebuilt binary, use `"command": "mail-mcp"`: ```json { "mcpServers": { "mail": { "command": "./target/release/mail-mcp", "env": { "MAIL_IMAP_DEFAULT_HOST": "imap.gmail.com", "MAIL_IMAP_DEFAULT_USER": "you@gmail.com", "MAIL_IMAP_DEFAULT_PASS": "your-app-password", "MAIL_SMTP_DEFAULT_HOST": "smtp.gmail.com", "MAIL_SMTP_DEFAULT_PORT": "587", "MAIL_SMTP_DEFAULT_USER": "you@gmail.com", "MAIL_SMTP_DEFAULT_PASS": "your-app-password", "MAIL_SMTP_DEFAULT_SECURE": "starttls", "MAIL_IMAP_WRITE_ENABLED": "true", "MAIL_SMTP_WRITE_ENABLED": "true" } } } } ``` That's it. Your AI agent can now read, search, send, reply, and manage emails. ### Microsoft Account? Use Graph API Microsoft blocks SMTP on personal accounts. Use Graph API instead: ```json { "env": { "MAIL_IMAP_DEFAULT_HOST": "outlook.office365.com", "MAIL_IMAP_DEFAULT_USER": "you@hotmail.com", "MAIL_IMAP_DEFAULT_PASS": "your-app-password", "MAIL_OAUTH2_DEFAULT_PROVIDER": "microsoft", "MAIL_OAUTH2_DEFAULT_CLIENT_ID": "9e5f94bc-e8a4-4e73-b8be-63364c29d753", "MAIL_OAUTH2_DEFAULT_CLIENT_SECRET": "none", "MAIL_OAUTH2_DEFAULT_REFRESH_TOKEN": "" } } ``` Get your token in 1 minute with device code flow. See [Account Setup Guide](docs/account-setup.md). ## 37 MCP Tools ### Read (10 tools) | Tool | What it does | |------|-------------| | `list_all_accounts` | **List all accounts with capabilities** (IMAP, SMTP, Graph, EWS) | | `imap_list_accounts` | List IMAP accounts | | `imap_verify_account` | Test connectivity and auth | | `imap_list_mailboxes` | List folders | | `imap_mailbox_status` | Message counts | | `imap_search_messages` | Search with cursor pagination; `thread_message_id` finds a message and its replies | | `imap_search_all_mailboxes` | Same criteria in every mailbox at once (e.g. a whole conversation spread over INBOX, Sent and project folders), newest first, labelled with the mailbox | | `imap_get_message` | Parsed message (text, HTML, attachments) | | `imap_get_message_raw` | RFC822 source | | `imap_get_attachment` | Download one attachment to disk (bypasses the raw size cap) | ### Write (11 tools) | Tool | What it does | |------|-------------| | `imap_update_message_flags` | Add/remove flags | | `imap_copy_message` | Copy (cross-account supported) | | `imap_move_message` | Move to folder | | `imap_delete_message` | Delete with confirmation | | `imap_create_mailbox` | Create folder | | `imap_delete_mailbox` | Delete folder | | `imap_rename_mailbox` | Rename folder | | `imap_append_message` | Append raw message | | `imap_bulk_move` | Move up to 500 at once | | `imap_bulk_delete` | Delete up to 500 at once | | `imap_bulk_update_flags` | Flag up to 500 at once | ### Send (5 tools) | Tool | What it does | |------|-------------| | `smtp_send_message` | Send email (text/HTML, CC/BCC) | | `smtp_reply_message` | Reply with threading headers | | `smtp_forward_message` | Forward with original inline | | `smtp_verify_account` | Test SMTP connectivity | | `graph_send_message` | Send via Microsoft Graph API (with reply threading) | ### EWS — Exchange Web Services (3 tools) | Tool | What it does | |------|-------------| | `ews_search_messages` | Search emails via EWS (inbox, sent, drafts, etc.) | | `ews_get_message` | Get full email content via EWS | | `ews_send_message` | Send email via EWS | ### Drafts (5 tools) The agent prepares the email, a person reviews it in their mail client (or asks the agent to send it). One set of tools for every account: drafts go to the IMAP Drafts folder, or through EWS or Microsoft Graph when the account is configured for them (`list_all_accounts` shows `drafts_with`). | Tool | What it does | |------|-------------| | `draft_create` | Save a new draft (CC/BCC, HTML, attachments, threading) | | `draft_update` | Replace a draft's content; returns a new `draft_id` | | `draft_send` | Send a draft and remove it from Drafts | | `draft_delete` | Delete a draft (only drafts; needs `confirm: true`) | | `draft_list` | List drafts, newest first | Enable with `MAIL_DRAFTS_ENABLED=true`. It is independent of `MAIL_IMAP_WRITE_ENABLED`: with only drafts enabled, the agent can write drafts but cannot move, flag or delete other mail, and every draft tool refuses ids that are not drafts. `draft_send` additionally needs `MAIL_SMTP_WRITE_ENABLED=true`. Graph accounts need the `Mail.ReadWrite` scope. ### Attachments Send files with any send tool. Two modes: ```json // Large files — MCP reads from disk (recommended) "attachments": [{"file_path": "/path/to/report.pdf"}] // Small files — inline base64 "attachments": [{"filename": "note.txt", "content_type": "text/plain", "content_base64": "SGVsbG8="}] ``` Filename and MIME type are auto-detected from the file path. Reply with `include_original_attachments: true` to forward original attachments. **Downloading** an attachment from a received message: use `imap_get_attachment` with the `message_id` and a `part_id` (from `imap_get_message`) or `filename`. It writes the decoded file to disk and returns the path — no size cap, and the binary stays out of the response. Set the default download directory with `MAIL_ATTACHMENT_DOWNLOAD_DIR` (falls back to the system temp dir), or pass `output_dir` per call. To limit which local files send tools may attach via `file_path`, set `MAIL_ATTACHMENT_UPLOAD_DIR`; paths outside it (including via `..` or symlinks) are rejected. See [docs/security.md](docs/security.md#outbound-attachment-scope). ### Bulk Operations (2 tools) | Tool | What it does | |------|-------------| | `imap_search_and_move` | Search + move matches | | `imap_search_and_delete` | Search + delete matches | ### Setup Helper (1 tool) | Tool | What it does | |------|-------------| | `get_setup_guide` | Provider-specific setup instructions (Microsoft OAuth2, Gmail/iCloud App Passwords, Zoho, etc.) | ## Multi-Account Configure as many accounts as you need: ```bash # Gmail MAIL_IMAP_GMAIL_HOST=imap.gmail.com MAIL_IMAP_GMAIL_USER=me@gmail.com MAIL_IMAP_GMAIL_PASS=app-password # Apple iCloud (App-Specific Password from appleid.apple.com) MAIL_IMAP_ICLOUD_HOST=imap.mail.me.com MAIL_IMAP_ICLOUD_USER=you@icloud.com MAIL_IMAP_ICLOUD_PASS=app-specific-password MAIL_SMTP_ICLOUD_HOST=smtp.mail.me.com MAIL_SMTP_ICLOUD_USER=you@icloud.com MAIL_SMTP_ICLOUD_PASS=app-specific-password MAIL_SMTP_ICLOUD_SECURE=starttls # Microsoft 365 MAIL_IMAP_WORK_HOST=outlook.office365.com MAIL_IMAP_WORK_USER=me@company.com MAIL_OAUTH2_WORK_PROVIDER=microsoft MAIL_OAUTH2_WORK_CLIENT_ID=your-client-id MAIL_OAUTH2_WORK_CLIENT_SECRET=none MAIL_OAUTH2_WORK_REFRESH_TOKEN=your-token # Zoho MAIL_IMAP_DEFAULT_HOST=imap.zoho.com MAIL_IMAP_DEFAULT_USER=info@mydomain.com MAIL_IMAP_DEFAULT_PASS=password MAIL_SMTP_DEFAULT_HOST=smtp.zoho.com MAIL_SMTP_DEFAULT_USER=info@mydomain.com MAIL_SMTP_DEFAULT_PASS=password MAIL_SMTP_DEFAULT_SECURE=starttls ``` Use `account_id` in tool calls: `"account_id": "gmail"`, `"account_id": "icloud"`, `"account_id": "work"`, `"account_id": "default"`. ## Security - **TLS enforced** on all connections (except localhost proxies) - **Passwords in SecretString** — never logged or returned in responses - **Write operations gated** — require explicit `MAIL_IMAP_WRITE_ENABLED=true` - **Send operations gated** — require explicit `MAIL_SMTP_WRITE_ENABLED=true` - **Drafts gated separately** — `MAIL_DRAFTS_ENABLED=true` allows writing drafts without any other write access - **Delete confirmation** — requires `confirm: true` - **HTML sanitized** with ammonia (prevents XSS) - **Bounded outputs** — body text, HTML, attachments truncated to configurable limits - **OAuth2 tokens cached** with 10-minute refresh margin - **No secrets in responses** — credentials never exposed via MCP tools - **HTTP transport is opt-in and unauthenticated** — stdio by default; `MAIL_MCP_TRANSPORT=http` binds to loopback unless told otherwise, and belongs behind an authenticating gateway ## Configuration Reference

Full environment variable reference ### IMAP (per account) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `MAIL_IMAP__HOST` | Yes | — | IMAP server | | `MAIL_IMAP__PORT` | No | 993 | IMAP port | | `MAIL_IMAP__USER` | Yes | — | Username | | `MAIL_IMAP__PASS` | Yes* | — | Password (*optional with OAuth2) | | `MAIL_IMAP__SECURE` | No | true | Use TLS | ### SMTP (per account) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `MAIL_SMTP__HOST` | Yes | — | SMTP server | | `MAIL_SMTP__PORT` | No | 587 | SMTP port | | `MAIL_SMTP__USER` | Yes | — | Username | | `MAIL_SMTP__PASS` | No | — | Password (optional with OAuth2) | | `MAIL_SMTP__SECURE` | No | starttls | `starttls`, `tls`, or `plain` | | `MAIL_SMTP__FROM_EMAIL` | No | = `_USER` | Sender address when it differs from the SMTP auth username (e.g. shared/group mailboxes) | ### OAuth2 (per account) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `MAIL_OAUTH2__PROVIDER` | Yes | — | `google` or `microsoft` | | `MAIL_OAUTH2__CLIENT_ID` | Yes | — | OAuth2 client ID | | `MAIL_OAUTH2__CLIENT_SECRET` | Yes | — | Client secret (`none` for public clients) | | `MAIL_OAUTH2__REFRESH_TOKEN` | Yes | — | Refresh token | ### Graph API OAuth2 (per account) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `MAIL_GRAPH__PROVIDER` | Yes | — | `microsoft` | | `MAIL_GRAPH__CLIENT_ID` | Yes | — | OAuth2 client ID | | `MAIL_GRAPH__CLIENT_SECRET` | Yes | — | Client secret (`none` for public clients) | | `MAIL_GRAPH__REFRESH_TOKEN` | Yes | — | Refresh token (Mail.Send scope) | ### EWS — Exchange Web Services (per account, simplest for Microsoft) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `MAIL_EWS__USER` | Yes | — | Email address | | `MAIL_EWS__REFRESH_TOKEN` | Yes | — | OAuth2 refresh token (EWS scope) | | `MAIL_EWS__CLIENT_ID` | No | `d3590ed6...` (Microsoft Office) | OAuth2 client ID | | `MAIL_EWS__CLIENT_SECRET` | No | `none` | Client secret | > **Tip:** EWS only needs 2 variables (USER + REFRESH_TOKEN). Client ID defaults to Microsoft Office which has all permissions pre-approved. ### Global Settings | Variable | Default | Description | |----------|---------|-------------| | `MAIL_IMAP_WRITE_ENABLED` | false | Enable IMAP write operations | | `MAIL_SMTP_WRITE_ENABLED` | false | Enable SMTP/Graph send operations | | `MAIL_DRAFTS_ENABLED` | false | Enable the draft tools (`draft_send` also needs `MAIL_SMTP_WRITE_ENABLED`) | | `MAIL_SMTP_SAVE_SENT` | false | Save sent emails to IMAP Sent folder (enable if your provider doesn't auto-save on send — e.g. Gmail does, Zoho doesn't always) | | `MAIL_SMTP_CONNECT_TIMEOUT_MS` | 30000 | SMTP TCP/TLS/auth timeout (connect phase) | | `MAIL_SMTP_SEND_TIMEOUT_MS` | 300000 | SMTP DATA transmission timeout (5 min — accommodates large attachments) | | `MAIL_SMTP_TIMEOUT_MS` | _(deprecated)_ | Legacy single timeout. Honored as fallback for `MAIL_SMTP_SEND_TIMEOUT_MS`. Prefer the split vars above. | | `MAIL_IMAP_CONNECT_TIMEOUT_MS` | 30000 | TCP connection timeout | | `MAIL_IMAP_GREETING_TIMEOUT_MS` | 15000 | TLS/greeting timeout | | `MAIL_IMAP_SOCKET_TIMEOUT_MS` | 300000 | Socket I/O timeout | | `MAIL_IMAP_MAX_MAILBOXES` | 200 | Max mailboxes `imap_list_mailboxes` returns (1–10000); the response reports `total` and `truncated` | | `MAIL_IMAP_SEARCH_CONCURRENCY` | 4 | Parallel IMAP connections for `imap_search_all_mailboxes` (1–8) | | `MAIL_MCP_TRANSPORT` | stdio | `stdio`, or `http` to serve MCP streamable HTTP (see [Remote HTTP transport](docs/advanced-configuration.md#remote-http-transport)) | | `MAIL_MCP_HTTP_HOST` | 127.0.0.1 | HTTP bind address (IP literal) | | `MAIL_MCP_HTTP_PORT` | 8000 | HTTP bind port | | `MAIL_MCP_HTTP_PATH` | /mcp | HTTP endpoint path |
## Roadmap - [x] IMAP read operations (search, fetch, parse) - [x] IMAP write operations (copy, move, delete, flags) - [x] IMAP bulk operations (up to 500 per call) - [x] Cursor-based pagination with TTL - [x] SMTP send, reply, forward - [x] Microsoft Graph API (sendMail) - [x] OAuth2 XOAUTH2 (Google + Microsoft) - [x] Separate Graph API tokens for enterprise - [x] Multi-account via environment variables - [x] PDF text extraction from attachments - [x] HTML sanitization (ammonia) - [x] Provider setup documentation with direct links - [x] Attachment sending (SMTP/Graph) - [x] Reply with original attachments - [x] CDATA sanitization (Zoho bug fix) - [x] Email confirmation protocol (preview before send) - [x] Token-optimized instructions (75% reduction) - [x] On-demand setup guide tool - [x] **EWS (Exchange Web Services)** — single token for read + send on Microsoft - [x] EWS with Microsoft Office Client ID (works on restricted tenants) - [x] **Graph API threading** — `createReply` flow for proper conversation threading - [x] **HTML formatting guidance** — LLM prefers multipart (text + HTML) for human emails - [x] **Sent folder archiving preserves full MIME** — byte-identical copy of what the recipient received (v0.4.1) - [x] **Localized Sent folder detection** — Spanish / Portuguese / French / German / Italian / Dutch / Polish (v0.4.1) - [x] **EWS feature parity with SMTP/Graph** — BCC, threading headers, recipient validation (v0.4.1) - [x] **EWS XML parser via `quick-xml`** — correct entity/CDATA/namespace handling (v0.4.1) ### Next — Local cache with instant search - [ ] **SQLite + FTS5 local email cache** — instant searches (<10ms vs 3-10s) - [ ] **Incremental sync** — UIDVALIDITY + last UID delta sync - [ ] **Connection pooling** — persistent IMAP sessions per account - [ ] **Cross-account search** — search all accounts at once - [ ] **Email statistics** — counts, top senders, activity by date ### Future - [ ] Docker image - [ ] npm/npx distribution - [ ] Draft management - [ ] Contact search - [ ] IMAP IDLE (real-time notifications) - [ ] Hosted documentation site ## Documentation | Guide | Description | |-------|-------------| | [Account Setup](docs/account-setup.md) | Step-by-step per provider, OAuth2, App Passwords, Azure Client ID | | [Tool Contract](docs/tool-contract.md) | Complete tool definitions and schemas | | [Message ID Format](docs/message-id-format.md) | Stable message identifier format | | [Cursor Pagination](docs/cursor-pagination.md) | Pagination behavior and expiration | | [Security](docs/security.md) | Security features and best practices | | [Advanced Configuration](docs/advanced-configuration.md) | Timeouts and performance tuning | ## Development ```bash cargo test # 64 unit + integration tests cargo fmt -- --check # formatting cargo clippy --all-targets -- -D warnings # linting ``` See `AGENTS.md` for contributor guidelines. ## Releasing Releases are automated via [`cargo-dist`](https://github.com/axodotdev/cargo-dist). To ship a new version: 1. Bump `version = "X.Y.Z"` in `Cargo.toml` (the release workflow enforces that this matches the pushed tag). 2. Commit the bump + any release notes to `main`. 3. Tag and push: ```bash git tag vX.Y.Z git push origin main --tags ``` 4. The `push: tags: ['v*']` trigger in `.github/workflows/release.yml` compiles binaries for Linux / macOS (Intel + Apple Silicon) / Windows, generates installer scripts (`.sh`, `.ps1`), creates the GitHub Release, and attaches all artifacts with SHA256 checksums. 5. If anything fails you can re-run the workflow manually from the Actions tab (the `workflow_dispatch` trigger is preserved as an escape hatch). **npm publishing is intentionally disabled.** The upstream fork was configured to publish as `@bradsjm/mail-imap-mcp-rs`, a scope this organization does not own, which caused every release to 404 on `npm publish`. The npm tarball is still generated and attached to each GitHub Release so users can install via `npm install ./mail-mcp-npm-package.tar.gz` manually. To enable npm registry publishing for this fork: create an npm org (e.g. `@tecnologicachile`), configure Trusted Publishing on npmjs.com pointing at this repo, set `publish-jobs = ["npm"]` in `dist-workspace.toml`, and run `dist generate --allow-dirty` to restore the `publish-npm` job in `release.yml`. ## Contributing Contributions welcome! Check out the [issues](https://github.com/tecnologicachile/mail-mcp/issues) for good first issues. If mail-mcp is useful to you, a ⭐ on the repo helps others discover it. ## License MIT License — see [LICENSE](LICENSE) for details.