Mail MCP agent

Secure IMAP/SMTP/EWS/Graph API MCP server in Rust — Microsoft 365, Hotmail, Gmail, Zoho, OAuth2, multi-account

by tecnologicachile·MIT license·★ 98 Stars on the repo·GitHub ↗

Files of Mail MCP

tecnologicachile/main1 file
README.md
Show the full text789 lines

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.4.16

  • imap_list_mailboxes cap is now configurable and visible by @tdabasinskas in #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 in #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 in #31, closing #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 in #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 in #29 — the follow-up to their #19. The from_email → user fallback now lives in one place (SmtpAccountConfig::effective_from()), and MAIL_SMTP_<ID>_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 in #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 in #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 in #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 in #26, reported in #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 diagnosed and fixed the same root cause concurrently in #24.
  • Fixed: message reads returned empty on iCloud by @tdabasinskas in #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 in #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_<ID>_FROM_EMAIL — sender address override by @arwack in #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 in #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_<ID>_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 </body_text><parameter name="body_html"> 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: <body_text>, </body_text>, <body_html>, </body_html>, <function_calls>, </function_calls>, <invoke name=, </invoke>, and <parameter name="body_*">. Generic technical content that happens to mention <parameter> for an XML schema or <invoke> 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 (<p>, <strong>, <a href>...) 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 <body_text>...</body_html> 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 <invoke>...</invoke> 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 <t:InternetMessageHeaders>), 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 (<soap:Body> vs <t:Body>), 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_<ID>_USER and MAIL_EWS_<ID>_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:
- <[email protected]>

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)

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.):

{
  "mcpServers": {
    "mail": {
      "command": "./target/release/mail-mcp",
      "env": {
        "MAIL_IMAP_DEFAULT_HOST": "imap.gmail.com",
        "MAIL_IMAP_DEFAULT_USER": "[email protected]",
        "MAIL_IMAP_DEFAULT_PASS": "your-app-password",
        "MAIL_SMTP_DEFAULT_HOST": "smtp.gmail.com",
        "MAIL_SMTP_DEFAULT_PORT": "587",
        "MAIL_SMTP_DEFAULT_USER": "[email protected]",
        "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:

{
  "env": {
    "MAIL_IMAP_DEFAULT_HOST": "outlook.office365.com",
    "MAIL_IMAP_DEFAULT_USER": "[email protected]",
    "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": "<your-token>"
  }
}

Get your token in 1 minute with device code flow. See Account Setup Guide.

31 MCP Tools

Read (9 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
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
Attachments

Send files with any send tool. Two modes:

// 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.

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:

# Gmail
MAIL_IMAP_GMAIL_HOST=imap.gmail.com
[email protected]
MAIL_IMAP_GMAIL_PASS=app-password

# Apple iCloud (App-Specific Password from appleid.apple.com)
MAIL_IMAP_ICLOUD_HOST=imap.mail.me.com
[email protected]
MAIL_IMAP_ICLOUD_PASS=app-specific-password
MAIL_SMTP_ICLOUD_HOST=smtp.mail.me.com
[email protected]
MAIL_SMTP_ICLOUD_PASS=app-specific-password
MAIL_SMTP_ICLOUD_SECURE=starttls

# Microsoft 365
MAIL_IMAP_WORK_HOST=outlook.office365.com
[email protected]
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
[email protected]
MAIL_IMAP_DEFAULT_PASS=password
MAIL_SMTP_DEFAULT_HOST=smtp.zoho.com
[email protected]
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
  • 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_<ID>_HOST Yes — IMAP server
MAIL_IMAP_<ID>_PORT No 993 IMAP port
MAIL_IMAP_<ID>_USER Yes — Username
MAIL_IMAP_<ID>_PASS Yes* — Password (*optional with OAuth2)
MAIL_IMAP_<ID>_SECURE No true Use TLS
SMTP (per account)
Variable Required Default Description
MAIL_SMTP_<ID>_HOST Yes — SMTP server
MAIL_SMTP_<ID>_PORT No 587 SMTP port
MAIL_SMTP_<ID>_USER Yes — Username
MAIL_SMTP_<ID>_PASS No — Password (optional with OAuth2)
MAIL_SMTP_<ID>_SECURE No starttls starttls, tls, or plain
MAIL_SMTP_<ID>_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_<ID>_PROVIDER Yes — google or microsoft
MAIL_OAUTH2_<ID>_CLIENT_ID Yes — OAuth2 client ID
MAIL_OAUTH2_<ID>_CLIENT_SECRET Yes — Client secret (none for public clients)
MAIL_OAUTH2_<ID>_REFRESH_TOKEN Yes — Refresh token
Graph API OAuth2 (per account)
Variable Required Default Description
MAIL_GRAPH_<ID>_PROVIDER Yes — microsoft
MAIL_GRAPH_<ID>_CLIENT_ID Yes — OAuth2 client ID
MAIL_GRAPH_<ID>_CLIENT_SECRET Yes — Client secret (none for public clients)
MAIL_GRAPH_<ID>_REFRESH_TOKEN Yes — Refresh token (Mail.Send scope)
EWS — Exchange Web Services (per account, simplest for Microsoft)
Variable Required Default Description
MAIL_EWS_<ID>_USER Yes — Email address
MAIL_EWS_<ID>_REFRESH_TOKEN Yes — OAuth2 refresh token (EWS scope)
MAIL_EWS_<ID>_CLIENT_ID No d3590ed6... (Microsoft Office) OAuth2 client ID
MAIL_EWS_<ID>_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_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_MCP_TRANSPORT stdio stdio, or http to serve MCP streamable HTTP (see 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

  • IMAP read operations (search, fetch, parse)
  • IMAP write operations (copy, move, delete, flags)
  • IMAP bulk operations (up to 500 per call)
  • Cursor-based pagination with TTL
  • SMTP send, reply, forward
  • Microsoft Graph API (sendMail)
  • OAuth2 XOAUTH2 (Google + Microsoft)
  • Separate Graph API tokens for enterprise
  • Multi-account via environment variables
  • PDF text extraction from attachments
  • HTML sanitization (ammonia)
  • Provider setup documentation with direct links
  • Attachment sending (SMTP/Graph)
  • Reply with original attachments
  • CDATA sanitization (Zoho bug fix)
  • Email confirmation protocol (preview before send)
  • Token-optimized instructions (75% reduction)
  • On-demand setup guide tool
  • EWS (Exchange Web Services) — single token for read + send on Microsoft
  • EWS with Microsoft Office Client ID (works on restricted tenants)
  • Graph API threading — createReply flow for proper conversation threading
  • HTML formatting guidance — LLM prefers multipart (text + HTML) for human emails
  • Sent folder archiving preserves full MIME — byte-identical copy of what the recipient received (v0.4.1)
  • Localized Sent folder detection — Spanish / Portuguese / French / German / Italian / Dutch / Polish (v0.4.1)
  • EWS feature parity with SMTP/Graph — BCC, threading headers, recipient validation (v0.4.1)
  • EWS XML parser via quick-xml — correct entity/CDATA/namespace handling (v0.4.1)
  • 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 Step-by-step per provider, OAuth2, App Passwords, Azure Client ID
Tool Contract Complete tool definitions and schemas
Message ID Format Stable message identifier format
Cursor Pagination Pagination behavior and expiration
Security Security features and best practices
Advanced Configuration Timeouts and performance tuning

Development

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. 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:
    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 for good first issues.

If mail-mcp is useful to you, a ⭐ on the repo helps others discover it.

License

MIT License — see LICENSE for details.

1<p align="center">
2 <h1 align="center">mail-mcp</h1>
3 <p align="center">
4 <strong>Production-ready email MCP server for AI agents</strong><br>
5 IMAP + SMTP + EWS + Microsoft Graph API — built in Rust
6 </p>
7 <p align="center">
8 <a href="https://github.com/tecnologicachile/mail-mcp/releases"><img src="https://img.shields.io/github/v/release/tecnologicachile/mail-mcp?label=release" alt="Release"></a>
9 <a href="LICENSE"><img src="https://img.shields.io/github/license/tecnologicachile/mail-mcp" alt="License"></a>
10 <a href="https://github.com/tecnologicachile/mail-mcp/stargazers"><img src="https://img.shields.io/github/stars/tecnologicachile/mail-mcp?style=social" alt="Stars"></a>
11 </p>
12</p>
13 
14---
15 
16Most 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.
17 
18## What's New in v0.4.16
19 
20- **`imap_list_mailboxes` cap is now configurable and visible** by
21 [@tdabasinskas](https://github.com/tdabasinskas) in
22 [#33](https://github.com/tecnologicachile/mail-mcp/pull/33). The tool used to
23 silently drop everything past 200 mailboxes — on accounts with more folders
24 (e.g. 217 on iCloud), folders like `Sent Messages` simply never appeared.
25 The cap is now set with `MAIL_IMAP_MAX_MAILBOXES` (default `200`, clamped to
26 `1..=10000`), and the response carries two additive fields, `total` and
27 `truncated`, so clients can always tell a capped list from a complete one.
28 Nothing changes unless the variable is set.
29 
30## What's New in v0.4.15
31 
32- **New: opt-in streamable HTTP transport** by
33 [@tdabasinskas](https://github.com/tdabasinskas) in
34 [#32](https://github.com/tecnologicachile/mail-mcp/pull/32). Set
35 `MAIL_MCP_TRANSPORT=http` to serve MCP streamable HTTP (stateless) instead
36 of stdio — useful for running the server on another machine behind an MCP
37 gateway. Binds to `127.0.0.1:8000` at `/mcp` by default, configurable via
38 `MAIL_MCP_HTTP_HOST` / `MAIL_MCP_HTTP_PORT` / `MAIL_MCP_HTTP_PATH`, with
39 graceful shutdown on SIGINT/SIGTERM. **The endpoint has no built-in
40 authentication** — keep it on loopback or behind an authenticating gateway;
41 see `docs/advanced-configuration.md#remote-http-transport`. stdio remains
42 the default: nothing changes unless the variable is set.
43 
44## What's New in v0.4.14
45 
46Community release — both changes came from external contributors. Thank you!
47 
48- **New: `MAIL_ATTACHMENT_UPLOAD_DIR` — confine outbound `file_path`
49 attachments** by [@mjones-PL](https://github.com/mjones-PL) in
50 [#31](https://github.com/tecnologicachile/mail-mcp/pull/31), closing
51 [#11](https://github.com/tecnologicachile/mail-mcp/issues/11). Without a
52 restriction, send tools would read any file the server process can access —
53 a prompt-injected model could exfiltrate SSH keys or `.env` files as
54 attachments. Set this variable to a directory and every `file_path` is
55 canonicalized (`..` and symlinks resolved) and must land inside it; missing
56 and out-of-scope files return the same error so the check cannot probe for
57 file existence. Opt-in: when unset, behavior is unchanged. See
58 `docs/security.md#outbound-attachment-scope`.
59- **Fixed: `FROM_EMAIL` validation no longer rejects dotless internal hosts**
60 by [@arwack](https://github.com/arwack) in
61 [#30](https://github.com/tecnologicachile/mail-mcp/pull/30). Addresses the
62 v0.4.13 upgrade caveat: `noreply@localhost` / `alerts@intranet` style
63 addresses on corporate internal relays are accepted again, while the real
64 typo checks (multiple `@`, whitespace, empty local part) remain. If you held
65 the v0.4.13 upgrade because of this, v0.4.14 is safe.
66 
67## What's New in v0.4.13
68 
69- **`effective_from()` helper + `FROM_EMAIL` startup validation** by
70 [@arwack](https://github.com/arwack) in
71 [#29](https://github.com/tecnologicachile/mail-mcp/pull/29) — the follow-up
72 to their #19. The `from_email` → `user` fallback now lives in one place
73 (`SmtpAccountConfig::effective_from()`), and `MAIL_SMTP_<ID>_FROM_EMAIL` is
74 validated when the server starts instead of failing on the first send.
75- **Behavior change — read before upgrading:** a malformed `FROM_EMAIL`
76 (multiple `@`, whitespace, empty local part, or a domain without a dot) now
77 prevents the server from starting, for **all** accounts. Note that dotless
78 domains such as `user@localhost` or `alerts@intranet` are currently rejected
79 too; if you use an internal relay address like that, hold the upgrade — a
80 follow-up relaxing the dot rule is under discussion in #29.
81- Hardened APPEND wire-format tests by
82 [@tordable](https://github.com/tordable) in
83 [#28](https://github.com/tecnologicachile/mail-mcp/pull/28): mailbox
84 quoting, announced literal length, and byte-for-byte payload are now
85 asserted for every append test.
86 
87## What's New in v0.4.12
88 
89Community release — both changes came from external contributors. Thank you!
90 
91- **iCloud mailbox aliases + reads no longer mark messages as read** by
92 [@felipefdl](https://github.com/felipefdl) in
93 [#16](https://github.com/tecnologicachile/mail-mcp/pull/16). Short mailbox
94 names now resolve to each provider's real folder (`Sent` → `Sent Messages`
95 on iCloud / `[Gmail]/Sent Mail` on Gmail, `Trash` → `Deleted Messages`, and
96 so on, multi-language) in search, copy, and move. Raw message fetches now use
97 `BODY.PEEK[]`, so reading a message through the MCP no longer sets `\Seen`
98 as a side effect — with a `BODY[]` fallback for servers that reject `PEEK`
99 (the deprecated `RFC822` item, removed in #23, stays out). Validated against
100 a real iCloud mailbox by the author; includes alias-resolution tests and
101 iCloud setup docs.
102- **Optimized multi-stage Dockerfile + docker-compose** by
103 [@monssefbaakka](https://github.com/monssefbaakka) in
104 [#5](https://github.com/tecnologicachile/mail-mcp/pull/5). cargo-chef layer
105 caching, TARGETARCH-aware musl cross-builds (amd64/arm64), and a `scratch`
106 runtime image — 16.9 MB, down from 25.5 MB — verified to respond to MCP
107 initialize/tools-list over stdio. The toolchain pin was bumped to
108 Rust 1.90 (the codebase's let-chains require >= 1.88).
109 
110## What's New in v0.4.11
111 
112Community bugfix release — both fixes came from external contributors. Thank you!
113 
114- **Fixed: save-to-Sent silently failed on strict IMAP servers (iCloud and
115 others)** by [@dominikknafelj](https://github.com/dominikknafelj) in
116 [#26](https://github.com/tecnologicachile/mail-mcp/pull/26), reported in
117 [#25](https://github.com/tecnologicachile/mail-mcp/issues/25). The `\Seen`
118 flag introduced in v0.4.10 was sent without the RFC 3501 parenthesized
119 flag-list syntax (`APPEND "Sent" \Seen …` instead of `APPEND "Sent" (\Seen) …`),
120 because `async-imap` interpolates the flags argument verbatim. Strict servers
121 rejected the APPEND and the sent copy was lost — while the tool still reported
122 `status: ok`. Flags are now normalized before hitting the wire, and
123 `smtp_send_message` / `smtp_reply_message` / `smtp_forward_message` responses
124 include a new `saved_to_sent` field (`true`/`false`, or `null` when saving is
125 disabled) so callers can detect archival failures.
126 [@tordable](https://github.com/tordable) diagnosed and fixed the same root
127 cause concurrently in [#24](https://github.com/tecnologicachile/mail-mcp/pull/24).
128- **Fixed: message reads returned empty on iCloud** by
129 [@tdabasinskas](https://github.com/tdabasinskas) in
130 [#23](https://github.com/tecnologicachile/mail-mcp/pull/23). Raw message
131 fetches used the deprecated `RFC822` item, which iCloud accepts but leaves
132 unpopulated. Fetches now use the IMAP4rev1 `BODY[]` item — same `\Seen`
133 semantics, works everywhere — with a mock-server regression test pinning the
134 wire format.
135 
136## What's New in v0.4.10
137 
138Community release — all three changes came from external contributors. Thank you!
139 
140- **NetEase IMAP compatibility (126.com / 163.com / yeah.net)** by
141 [@pep-27](https://github.com/pep-27) in
142 [#21](https://github.com/tecnologicachile/mail-mcp/pull/21). NetEase servers
143 reject mailbox access from clients that don't identify themselves. mail-mcp
144 now sends the RFC 2971 `ID` command after authentication whenever the server
145 advertises the `ID` capability. Includes mock-server regression tests and
146 NetEase setup docs in `docs/account-setup.md`.
147- **`MAIL_SMTP_<ID>_FROM_EMAIL` — sender address override** by
148 [@arwack](https://github.com/arwack) in
149 [#19](https://github.com/tecnologicachile/mail-mcp/pull/19). For shared/group
150 mailboxes where SMTP authenticates with a personal account but the From
151 address should be the group address. Applies to send, reply (including
152 reply-all self-address detection) and forward; falls back to `_USER` when
153 unset.
154- **Sent-mail copies are now marked `\Seen`** by
155 [@ray-of-darkness](https://github.com/ray-of-darkness) in
156 [#9](https://github.com/tecnologicachile/mail-mcp/pull/9). Copies the MCP
157 appends to the Sent folder after SMTP send no longer show up as unread.
158 
159## What's New in v0.4.9
160 
161- **New tool `imap_get_attachment` — download a single attachment to disk.**
162 Until now the only ways to reach attachment bytes were `imap_get_message`
163 (which returns attachment *metadata* and optional extracted PDF *text*, never
164 the binary) and `imap_get_message_raw` (capped at 1 MB and base64-encoded
165 into the response). A 7 MB email with X-ray images could not be retrieved at
166 all — over the cap, and dumping it into the response would blow up the model's
167 context anyway.
168- **How it works:** call `imap_get_attachment` with the `message_id` plus a
169 selector — either `part_id` (the value `imap_get_message` reports for each
170 attachment) or `filename`. The server fetches the full message (no size cap on
171 the server side), extracts and decodes just that one part, and **writes it to
172 disk**, returning `{ file_path, filename, content_type, part_id, size_bytes }`.
173 The binary never enters the response, so context stays small. The saved path
174 feeds straight into a local reader (e.g. an image-description tool or a PDF
175 reader).
176- **Where files land:** `output_dir` argument if given, else the
177 `MAIL_ATTACHMENT_DOWNLOAD_DIR` environment variable, else the system temp dir.
178 Filenames are sanitized (basename only, control characters stripped) to
179 prevent path traversal, and prefixed with the message UID and part id to avoid
180 collisions.
181- **Optional inline base64:** set `include_base64: true` to also get the bytes
182 in the response, but only when the attachment is at most `max_inline_bytes`
183 (default 256 KiB). Off by default.
184 
185## What's New in v0.4.8
186 
187- **`SAVE_SENT` is now per-account with a provider-aware default.**
188 Previously, saving a copy of outgoing mail to the Sent folder via IMAP
189 APPEND was controlled by a single global flag, `MAIL_SMTP_SAVE_SENT`. The
190 problem: providers that **already save** sent mail server-side (Gmail,
191 Zoho) ended up with **two identical copies** in Sent, while a generic SMTP
192 server or Office 365 (which do **not** auto-save on SMTP submission) lost
193 the copy entirely when the flag was `false`.
194- **Provider-aware default** (when nothing is configured):
195 - **Gmail** (`smtp.gmail.com`): saves server-side and deduplicates by
196 Message-ID → the MCP does **not** append (`false`).
197 - **Zoho** (`smtp.zoho.com`): saves server-side but does **not**
198 deduplicate → the MCP does **not** append (`false`), avoiding the
199 duplicate.
200 - **Office 365 / generic SMTP**: do **not** auto-save on SMTP submission →
201 the MCP **does** append (`true`), or the sent copy would be lost.
202- **Per-account override**: `MAIL_SMTP_<ID>_SAVE_SENT=true|false` takes
203 priority over everything. The global `MAIL_SMTP_SAVE_SENT` still works as a
204 coarse override (wins over the provider default, loses to the per-account
205 override).
206- Precedence: **per-account** → **global** → **provider-aware default**.
207 
208| Provider | Auto-saves server-side | MCP default |
209|---|---|---|
210| Gmail | Yes (with dedupe) | `false` |
211| Zoho | Yes (no dedupe) | `false` |
212| Office 365 (SMTP) | No | `true` |
213| Generic SMTP / relays | No | `true` |
214 
215## What's New in v0.4.7
216 
217- **Critical fix — `graph_send_message` silently dropped attachments on
218 threaded replies.** When called with `in_reply_to` + `attachments`, the
219 `createReply → PATCH → send` flow included the attachments in the PATCH
220 against `/me/messages/{id}`. Microsoft Graph treats `Message.attachments`
221 as a navigation property and **silently discards the field** on PATCH
222 (2xx response, no error), so the message went out as single-part
223 `text/html` with no file. The MCP returned `status: ok` and the caller
224 assumed success. Invisible data loss.
225- **The fix:** in `send_via_reply()`, attachments are now uploaded one by one
226 to `POST /me/messages/{draft_id}/attachments` between the PATCH and the
227 send. Files < 3 MB go inline (JSON with base64 `contentBytes`); files
228 ≥ 3 MB use `createUploadSession` with 4 MB chunked PUTs. The `attachments`
229 field was removed from the `PatchDraftRequest` struct so the regression
230 cannot be reintroduced by a type-correct edit.
231- **No change to flows that already worked.** `send_via_sendmail` (new
232 messages without `in_reply_to`) uses `POST /me/sendMail` with `attachments`
233 inline in the JSON — Graph DOES accept the field on that endpoint and never
234 dropped it. That path is untouched.
235- **Regression test added:** `patch_draft_request_never_serializes_attachments`
236 fails if anyone re-adds the field to the struct.
237- **Reference:** `BUG_GRAPH_ATTACHMENTS.md` at the repo root documents the
238 full reproduction, root cause, and the empirical evidence behind the fix.
239 
240## What's New in v0.4.6
241 
242- **Server-side enforcement of HARD RULE #1.** Three releases of prompt-only
243 hardening (v0.4.3 → v0.4.4 → v0.4.5) still left LLMs occasionally leaking
244 literal `</body_text><parameter name="body_html">` markup into the
245 recipient's inbox. v0.4.6 adds a real validator that **rejects** the tool
246 call before any SMTP / Graph / EWS attempt if `body_text` or `body_html`
247 contains tool-call wrapper syntax. The check is wired into all 5 send
248 paths (`smtp_send_message`, `smtp_reply_message`, `smtp_forward_message`,
249 `graph_send_message`, `ews_send_message`).
250- The forbidden markers are case-insensitive and tightly scoped — only
251 the pseudo-tags that have no legitimate use in human correspondence:
252 `<body_text>`, `</body_text>`, `<body_html>`, `</body_html>`,
253 `<function_calls>`, `</function_calls>`, `<invoke name=`, `</invoke>`,
254 and `<parameter name="body_*">`. Generic technical content that happens
255 to mention `<parameter>` for an XML schema or `<invoke>` in a code
256 example still passes.
257- **HARD RULE #1 wording updated** to announce the server-side rejection,
258 so the LLM knows it's a hard contract — not a suggestion it can ignore.
259- No breaking changes for clean callers: well-behaved messages send
260 exactly as before.
261 
262## What's New in v0.4.5
263 
264- **`serverInfo` now reports `name="mail-mcp"` + the crate `version`** (the
265 framework previously returned its own `rmcp 0.16.0`, which never changes
266 between releases). Useful for verifying the active version with `/mcp`, and
267 so any client-side cache keyed by (server, version) invalidates on each bump.
268- **MCP instructions reorganized**: the 3 critical anti-concatenation rules
269 (which in v0.4.3 and v0.4.4 sat at the end of the block and could be lost
270 to truncation / diluted attention) now appear as **HARD RULE #1, #2, #3 at
271 the TOP**, right after the title. Consolidated into 3 short paragraphs
272 (previously 3 long sections, ~1500 characters combined).
273- **No functional changes to the server.** Same SMTP/IMAP/EWS/Graph, same
274 tool set, same behavior. Only the text exposed to the client changed.
275 
276### Important for these rules to take effect
277 
278Clients that resume a session with `claude --continue` (or `/resume`) do
279**NOT** refresh the MCP `system_prompt` — they keep the one from that
280session's first handshake. If your session predates v0.4.5, the rules won't
281reach your context even if the on-disk binary is updated. To receive them,
282start a NEW session in the project (not `--continue`).
283 
284## What's New in v0.4.4
285 
286- **Preview hygiene rule** in MCP `instructions`: when the LLM shows the
287 user the email preview before sending, it should render ONE clean
288 version of the body (markdown-style bullets, bold, links as text + URL)
289 and state that the message will go multipart — but it must NOT dump
290 the raw HTML source (`<p>`, `<strong>`, `<a href>`...) into the
291 preview. Two reasons:
292 1. The human reviewer wants to read the message, not audit markup —
293 showing the HTML is noise.
294 2. Exhibiting both the plain-text string AND the HTML string side by
295 side in the preview is exactly the context that has historically
296 led LLMs to concatenate them in the eventual tool call (the bug
297 v0.4.3 documented). Hiding the HTML source from the preview
298 removes the temptation.
299 
300 Complements the **PREVIEW DOES NOT EQUAL TOOL CALL** rule introduced
301 in v0.4.3.
302 
303## What's New in v0.4.3
304 
305- **Server-side guidance against malformed tool calls.** The MCP
306 `instructions` block now explicitly tells the calling LLM that
307 `body_text` and `body_html` are TWO SEPARATE JSON fields and must
308 NEVER be concatenated. Previous wording ("send BOTH body_text AND
309 body_html") was ambiguous and some LLMs interpreted it as "concatenate
310 both with `<body_text>...</body_html>` pseudo-tags inside a single
311 `body_text` string". When that happens, the recipient sees garbled
312 duplicated content, AND any later Claude session that opens the saved
313 copy via this MCP gets a Usage Policy block (the leaked
314 `<invoke>...</invoke>` looks like a prompt-injection attempt to safety
315 filters). The new instruction shows a CORRECT vs WRONG example and
316 bans pseudo-tags / tool-call wrapper syntax inside email fields.
317 
318## What's New in v0.4.2
319 
320- **Release pipeline fixed**: the `publish-npm` job in the CI release
321 workflow has been disabled. It was inherited from the upstream fork and
322 tried to publish to `@bradsjm/mail-imap-mcp-rs`, a scope this org does
323 not own — every release was 404-ing on that step. See "Releasing" below
324 for the full explanation and how to re-enable npm publishing if needed.
325- **Auto-trigger releases on tag push**: `.github/workflows/release.yml`
326 now fires on `push: tags: ['v*']`, so tagging `vX.Y.Z` and pushing is
327 all it takes to cut a release. `workflow_dispatch` is retained as a
328 manual escape hatch.
329- **Cleanup**: removed the dangling `init-npm-placeholder.yml` workflow
330 (also referenced the fork's npm scope).
331- **docs**: README gains a "Releasing" section documenting the new flow
332 and the npm decision.
333 
334## What's New in v0.4.1
335 
336- **Fix**: `save_to_sent_folder` now archives the exact RFC822 bytes that were
337 sent (via `lettre.formatted()`), instead of a hand-rolled text-only stub.
338 The Sent-folder copy keeps the HTML body, the multipart/alternative
339 structure, and the RFC 2047-encoded subject — no more `???` where accents
340 used to be, and HTML is no longer silently dropped.
341- **Improved**: localized Sent-folder detection — `Enviado[s]`, `Elementos
342 enviados`, `Enviadas`, `Itens enviados`, `Envoyés`, `Éléments envoyés`,
343 `Gesendet`, `Posta inviata`, `Verzonden`, `Wysłane`, plus nested variants.
344 Previously only English names were recognized, so Zoho/localized IMAP
345 accounts fell through to a non-existent `"Sent"` folder.
346- **Improved**: `smtp_forward_message` accepts `body_html` (was hardcoded to
347 plain-text only).
348- **Improved**: EWS send gains `bcc`, `in_reply_to`, `references` (via
349 `<t:InternetMessageHeaders>`), plus full recipient + subject-length
350 validation — now at parity with the SMTP and Graph send paths.
351- **Improved**: Graph API threading fallbacks now log. `WARN` when the
352 message-lookup HTTP call fails (rate limit, 5xx, permissions) so operators
353 see threading degraded due to a real error; `DEBUG` when the original
354 message is legitimately not found.
355- **Refactor**: EWS XML parsing migrated from substring matching to
356 `quick-xml`. Fixes a latent namespace-collision bug (`<soap:Body>` vs
357 `<t:Body>`), correctly decodes XML entities and CDATA, and handles
358 attribute values containing `=` (common in base64-like EWS item IDs).
359- **Cleanup**: zero warnings on `cargo build --release`.
360- **Tests**: 64 (up from 47).
361 
362## Why This Project
363 
364| | mail-mcp | Typical email MCP |
365|---|:---:|:---:|
366| IMAP read/write | 18 tools | 3-5 tools |
367| SMTP send/reply/forward | Yes | No or broken |
368| Microsoft Graph API | Yes | No |
369| EWS (Exchange Web Services) | Yes | No |
370| OAuth2 (XOAUTH2) | Native | No |
371| Multi-account | Yes | Single account |
372| Microsoft 365 + Hotmail | Both work | Usually neither |
373| Language | Rust (fast, safe) | TypeScript/Python |
374| Tests | 64 unit + integration | Mocks only |
375| Warnings in release build | 0 | Varies |
376 
377## Feature Matrix
378 
379| Provider | IMAP | SMTP | Graph API | EWS | OAuth2 | Multi-account |
380|----------|:----:|:----:|:---------:|:---:|:------:|:-------------:|
381| Microsoft 365 (enterprise) | Yes | Admin-dependent | Yes | **Yes** | Yes | Yes |
382| Hotmail / Outlook.com | Yes | Blocked by MS | Yes | **Yes** | Yes | Yes |
383| Gmail | Yes | Yes | — | — | Yes | Yes |
384| Apple iCloud | Yes | Yes | — | — | — | Yes |
385| Zoho | Yes | Yes | — | — | — | Yes |
386| Fastmail | Yes | Yes | — | — | — | Yes |
387| Any IMAP/SMTP server | Yes | Yes | — | — | — | Yes |
388 
389> **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.
390 
391## Quickstart — Let Claude Code do it
392 
393Copy and paste this prompt into Claude Code and it will install, compile, and configure everything for you:
394 
395```
396Install and configure the mail-mcp MCP server from https://github.com/tecnologicachile/mail-mcp
397 
3981. Clone the repo, build with cargo build --release
3992. Add the MCP server to .claude.json with the binary path
4003. For Microsoft accounts: use EWS (simplest) — run device code flow with
401 client_id d3590ed6-52b3-4102-aeff-aad2292ab01c and scope
402 https://outlook.office365.com/EWS.AccessAsUser.All offline_access
403 Then configure MAIL_EWS_<ID>_USER and MAIL_EWS_<ID>_REFRESH_TOKEN
4044. For Gmail: configure MAIL_IMAP + MAIL_SMTP with App Password from
405 https://myaccount.google.com/apppasswords
4065. For Zoho: configure MAIL_IMAP + MAIL_SMTP with standard password
4076. Enable write/send: MAIL_IMAP_WRITE_ENABLED=true, MAIL_SMTP_WRITE_ENABLED=true
408 
409My email accounts to configure:
410- <[email protected]>
411```
412 
413Replace the last line with your email(s). Claude Code will guide you through each step including the OAuth2 device code flow for Microsoft accounts.
414 
415## Manual Setup (2 minutes)
416 
417```bash
418git clone https://github.com/tecnologicachile/mail-mcp.git
419cd mail-mcp
420cargo build --release
421```
422 
423Add to your MCP client config (Claude Code, Cursor, etc.):
424 
425```json
426{
427 "mcpServers": {
428 "mail": {
429 "command": "./target/release/mail-mcp",
430 "env": {
431 "MAIL_IMAP_DEFAULT_HOST": "imap.gmail.com",
432 "MAIL_IMAP_DEFAULT_USER": "[email protected]",
433 "MAIL_IMAP_DEFAULT_PASS": "your-app-password",
434 "MAIL_SMTP_DEFAULT_HOST": "smtp.gmail.com",
435 "MAIL_SMTP_DEFAULT_PORT": "587",
436 "MAIL_SMTP_DEFAULT_USER": "[email protected]",
437 "MAIL_SMTP_DEFAULT_PASS": "your-app-password",
438 "MAIL_SMTP_DEFAULT_SECURE": "starttls",
439 "MAIL_IMAP_WRITE_ENABLED": "true",
440 "MAIL_SMTP_WRITE_ENABLED": "true"
441 }
442 }
443 }
444}
445```
446 
447That's it. Your AI agent can now read, search, send, reply, and manage emails.
448 
449### Microsoft Account? Use Graph API
450 
451Microsoft blocks SMTP on personal accounts. Use Graph API instead:
452 
453```json
454{
455 "env": {
456 "MAIL_IMAP_DEFAULT_HOST": "outlook.office365.com",
457 "MAIL_IMAP_DEFAULT_USER": "[email protected]",
458 "MAIL_IMAP_DEFAULT_PASS": "your-app-password",
459 "MAIL_OAUTH2_DEFAULT_PROVIDER": "microsoft",
460 "MAIL_OAUTH2_DEFAULT_CLIENT_ID": "9e5f94bc-e8a4-4e73-b8be-63364c29d753",
461 "MAIL_OAUTH2_DEFAULT_CLIENT_SECRET": "none",
462 "MAIL_OAUTH2_DEFAULT_REFRESH_TOKEN": "<your-token>"
463 }
464}
465```
466 
467Get your token in 1 minute with device code flow. See [Account Setup Guide](docs/account-setup.md).
468 
469## 31 MCP Tools
470 
471### Read (9 tools)
472 
473| Tool | What it does |
474|------|-------------|
475| `list_all_accounts` | **List all accounts with capabilities** (IMAP, SMTP, Graph, EWS) |
476| `imap_list_accounts` | List IMAP accounts |
477| `imap_verify_account` | Test connectivity and auth |
478| `imap_list_mailboxes` | List folders |
479| `imap_mailbox_status` | Message counts |
480| `imap_search_messages` | Search with cursor pagination |
481| `imap_get_message` | Parsed message (text, HTML, attachments) |
482| `imap_get_message_raw` | RFC822 source |
483| `imap_get_attachment` | Download one attachment to disk (bypasses the raw size cap) |
484 
485### Write (11 tools)
486 
487| Tool | What it does |
488|------|-------------|
489| `imap_update_message_flags` | Add/remove flags |
490| `imap_copy_message` | Copy (cross-account supported) |
491| `imap_move_message` | Move to folder |
492| `imap_delete_message` | Delete with confirmation |
493| `imap_create_mailbox` | Create folder |
494| `imap_delete_mailbox` | Delete folder |
495| `imap_rename_mailbox` | Rename folder |
496| `imap_append_message` | Append raw message |
497| `imap_bulk_move` | Move up to 500 at once |
498| `imap_bulk_delete` | Delete up to 500 at once |
499| `imap_bulk_update_flags` | Flag up to 500 at once |
500 
501### Send (5 tools)
502 
503| Tool | What it does |
504|------|-------------|
505| `smtp_send_message` | Send email (text/HTML, CC/BCC) |
506| `smtp_reply_message` | Reply with threading headers |
507| `smtp_forward_message` | Forward with original inline |
508| `smtp_verify_account` | Test SMTP connectivity |
509| `graph_send_message` | Send via Microsoft Graph API (with reply threading) |
510 
511### EWS — Exchange Web Services (3 tools)
512 
513| Tool | What it does |
514|------|-------------|
515| `ews_search_messages` | Search emails via EWS (inbox, sent, drafts, etc.) |
516| `ews_get_message` | Get full email content via EWS |
517| `ews_send_message` | Send email via EWS |
518 
519### Attachments
520 
521Send files with any send tool. Two modes:
522 
523```json
524// Large files — MCP reads from disk (recommended)
525"attachments": [{"file_path": "/path/to/report.pdf"}]
526 
527// Small files — inline base64
528"attachments": [{"filename": "note.txt", "content_type": "text/plain", "content_base64": "SGVsbG8="}]
529```
530 
531Filename and MIME type are auto-detected from the file path. Reply with `include_original_attachments: true` to forward original attachments.
532 
533**Downloading** an attachment from a received message: use `imap_get_attachment`
534with the `message_id` and a `part_id` (from `imap_get_message`) or `filename`.
535It writes the decoded file to disk and returns the path — no size cap, and the
536binary stays out of the response. Set the default download directory with
537`MAIL_ATTACHMENT_DOWNLOAD_DIR` (falls back to the system temp dir), or pass
538`output_dir` per call.
539 
540To limit which local files send tools may attach via `file_path`, set
541`MAIL_ATTACHMENT_UPLOAD_DIR`; paths outside it (including via `..` or
542symlinks) are rejected. See [docs/security.md](docs/security.md#outbound-attachment-scope).
543 
544### Bulk Operations (2 tools)
545 
546| Tool | What it does |
547|------|-------------|
548| `imap_search_and_move` | Search + move matches |
549| `imap_search_and_delete` | Search + delete matches |
550 
551### Setup Helper (1 tool)
552 
553| Tool | What it does |
554|------|-------------|
555| `get_setup_guide` | Provider-specific setup instructions (Microsoft OAuth2, Gmail/iCloud App Passwords, Zoho, etc.) |
556 
557## Multi-Account
558 
559Configure as many accounts as you need:
560 
561```bash
562# Gmail
563MAIL_IMAP_GMAIL_HOST=imap.gmail.com
564[email protected]
565MAIL_IMAP_GMAIL_PASS=app-password
566 
567# Apple iCloud (App-Specific Password from appleid.apple.com)
568MAIL_IMAP_ICLOUD_HOST=imap.mail.me.com
569[email protected]
570MAIL_IMAP_ICLOUD_PASS=app-specific-password
571MAIL_SMTP_ICLOUD_HOST=smtp.mail.me.com
572[email protected]
573MAIL_SMTP_ICLOUD_PASS=app-specific-password
574MAIL_SMTP_ICLOUD_SECURE=starttls
575 
576# Microsoft 365
577MAIL_IMAP_WORK_HOST=outlook.office365.com
578[email protected]
579MAIL_OAUTH2_WORK_PROVIDER=microsoft
580MAIL_OAUTH2_WORK_CLIENT_ID=your-client-id
581MAIL_OAUTH2_WORK_CLIENT_SECRET=none
582MAIL_OAUTH2_WORK_REFRESH_TOKEN=your-token
583 
584# Zoho
585MAIL_IMAP_DEFAULT_HOST=imap.zoho.com
586[email protected]
587MAIL_IMAP_DEFAULT_PASS=password
588MAIL_SMTP_DEFAULT_HOST=smtp.zoho.com
589[email protected]
590MAIL_SMTP_DEFAULT_PASS=password
591MAIL_SMTP_DEFAULT_SECURE=starttls
592```
593 
594Use `account_id` in tool calls: `"account_id": "gmail"`, `"account_id": "icloud"`, `"account_id": "work"`, `"account_id": "default"`.
595 
596## Security
597 
598- **TLS enforced** on all connections (except localhost proxies)
599- **Passwords in SecretString** — never logged or returned in responses
600- **Write operations gated** — require explicit `MAIL_IMAP_WRITE_ENABLED=true`
601- **Send operations gated** — require explicit `MAIL_SMTP_WRITE_ENABLED=true`
602- **Delete confirmation** — requires `confirm: true`
603- **HTML sanitized** with ammonia (prevents XSS)
604- **Bounded outputs** — body text, HTML, attachments truncated to configurable limits
605- **OAuth2 tokens cached** with 10-minute refresh margin
606- **No secrets in responses** — credentials never exposed via MCP tools
607- **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
608 
609## Configuration Reference
610 
611<details>
612<summary>Full environment variable reference</summary>
613 
614### IMAP (per account)
615 
616| Variable | Required | Default | Description |
617|----------|----------|---------|-------------|
618| `MAIL_IMAP_<ID>_HOST` | Yes | — | IMAP server |
619| `MAIL_IMAP_<ID>_PORT` | No | 993 | IMAP port |
620| `MAIL_IMAP_<ID>_USER` | Yes | — | Username |
621| `MAIL_IMAP_<ID>_PASS` | Yes* | — | Password (*optional with OAuth2) |
622| `MAIL_IMAP_<ID>_SECURE` | No | true | Use TLS |
623 
624### SMTP (per account)
625 
626| Variable | Required | Default | Description |
627|----------|----------|---------|-------------|
628| `MAIL_SMTP_<ID>_HOST` | Yes | — | SMTP server |
629| `MAIL_SMTP_<ID>_PORT` | No | 587 | SMTP port |
630| `MAIL_SMTP_<ID>_USER` | Yes | — | Username |
631| `MAIL_SMTP_<ID>_PASS` | No | — | Password (optional with OAuth2) |
632| `MAIL_SMTP_<ID>_SECURE` | No | starttls | `starttls`, `tls`, or `plain` |
633| `MAIL_SMTP_<ID>_FROM_EMAIL` | No | = `_USER` | Sender address when it differs from the SMTP auth username (e.g. shared/group mailboxes) |
634 
635### OAuth2 (per account)
636 
637| Variable | Required | Default | Description |
638|----------|----------|---------|-------------|
639| `MAIL_OAUTH2_<ID>_PROVIDER` | Yes | — | `google` or `microsoft` |
640| `MAIL_OAUTH2_<ID>_CLIENT_ID` | Yes | — | OAuth2 client ID |
641| `MAIL_OAUTH2_<ID>_CLIENT_SECRET` | Yes | — | Client secret (`none` for public clients) |
642| `MAIL_OAUTH2_<ID>_REFRESH_TOKEN` | Yes | — | Refresh token |
643 
644### Graph API OAuth2 (per account)
645 
646| Variable | Required | Default | Description |
647|----------|----------|---------|-------------|
648| `MAIL_GRAPH_<ID>_PROVIDER` | Yes | — | `microsoft` |
649| `MAIL_GRAPH_<ID>_CLIENT_ID` | Yes | — | OAuth2 client ID |
650| `MAIL_GRAPH_<ID>_CLIENT_SECRET` | Yes | — | Client secret (`none` for public clients) |
651| `MAIL_GRAPH_<ID>_REFRESH_TOKEN` | Yes | — | Refresh token (Mail.Send scope) |
652 
653### EWS — Exchange Web Services (per account, simplest for Microsoft)
654 
655| Variable | Required | Default | Description |
656|----------|----------|---------|-------------|
657| `MAIL_EWS_<ID>_USER` | Yes | — | Email address |
658| `MAIL_EWS_<ID>_REFRESH_TOKEN` | Yes | — | OAuth2 refresh token (EWS scope) |
659| `MAIL_EWS_<ID>_CLIENT_ID` | No | `d3590ed6...` (Microsoft Office) | OAuth2 client ID |
660| `MAIL_EWS_<ID>_CLIENT_SECRET` | No | `none` | Client secret |
661 
662> **Tip:** EWS only needs 2 variables (USER + REFRESH_TOKEN). Client ID defaults to Microsoft Office which has all permissions pre-approved.
663 
664### Global Settings
665 
666| Variable | Default | Description |
667|----------|---------|-------------|
668| `MAIL_IMAP_WRITE_ENABLED` | false | Enable IMAP write operations |
669| `MAIL_SMTP_WRITE_ENABLED` | false | Enable SMTP/Graph send operations |
670| `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) |
671| `MAIL_SMTP_CONNECT_TIMEOUT_MS` | 30000 | SMTP TCP/TLS/auth timeout (connect phase) |
672| `MAIL_SMTP_SEND_TIMEOUT_MS` | 300000 | SMTP DATA transmission timeout (5 min — accommodates large attachments) |
673| `MAIL_SMTP_TIMEOUT_MS` | _(deprecated)_ | Legacy single timeout. Honored as fallback for `MAIL_SMTP_SEND_TIMEOUT_MS`. Prefer the split vars above. |
674| `MAIL_IMAP_CONNECT_TIMEOUT_MS` | 30000 | TCP connection timeout |
675| `MAIL_IMAP_GREETING_TIMEOUT_MS` | 15000 | TLS/greeting timeout |
676| `MAIL_IMAP_SOCKET_TIMEOUT_MS` | 300000 | Socket I/O timeout |
677| `MAIL_IMAP_MAX_MAILBOXES` | 200 | Max mailboxes `imap_list_mailboxes` returns (1–10000); the response reports `total` and `truncated` |
678| `MAIL_MCP_TRANSPORT` | stdio | `stdio`, or `http` to serve MCP streamable HTTP (see [Remote HTTP transport](docs/advanced-configuration.md#remote-http-transport)) |
679| `MAIL_MCP_HTTP_HOST` | 127.0.0.1 | HTTP bind address (IP literal) |
680| `MAIL_MCP_HTTP_PORT` | 8000 | HTTP bind port |
681| `MAIL_MCP_HTTP_PATH` | /mcp | HTTP endpoint path |
682 
683</details>
684 
685## Roadmap
686 
687- [x] IMAP read operations (search, fetch, parse)
688- [x] IMAP write operations (copy, move, delete, flags)
689- [x] IMAP bulk operations (up to 500 per call)
690- [x] Cursor-based pagination with TTL
691- [x] SMTP send, reply, forward
692- [x] Microsoft Graph API (sendMail)
693- [x] OAuth2 XOAUTH2 (Google + Microsoft)
694- [x] Separate Graph API tokens for enterprise
695- [x] Multi-account via environment variables
696- [x] PDF text extraction from attachments
697- [x] HTML sanitization (ammonia)
698- [x] Provider setup documentation with direct links
699- [x] Attachment sending (SMTP/Graph)
700- [x] Reply with original attachments
701- [x] CDATA sanitization (Zoho bug fix)
702- [x] Email confirmation protocol (preview before send)
703- [x] Token-optimized instructions (75% reduction)
704- [x] On-demand setup guide tool
705- [x] **EWS (Exchange Web Services)** — single token for read + send on Microsoft
706- [x] EWS with Microsoft Office Client ID (works on restricted tenants)
707- [x] **Graph API threading** — `createReply` flow for proper conversation threading
708- [x] **HTML formatting guidance** — LLM prefers multipart (text + HTML) for human emails
709- [x] **Sent folder archiving preserves full MIME** — byte-identical copy of what the recipient received (v0.4.1)
710- [x] **Localized Sent folder detection** — Spanish / Portuguese / French / German / Italian / Dutch / Polish (v0.4.1)
711- [x] **EWS feature parity with SMTP/Graph** — BCC, threading headers, recipient validation (v0.4.1)
712- [x] **EWS XML parser via `quick-xml`** — correct entity/CDATA/namespace handling (v0.4.1)
713 
714### Next — Local cache with instant search
715- [ ] **SQLite + FTS5 local email cache** — instant searches (<10ms vs 3-10s)
716- [ ] **Incremental sync** — UIDVALIDITY + last UID delta sync
717- [ ] **Connection pooling** — persistent IMAP sessions per account
718- [ ] **Cross-account search** — search all accounts at once
719- [ ] **Email statistics** — counts, top senders, activity by date
720 
721### Future
722- [ ] Docker image
723- [ ] npm/npx distribution
724- [ ] Draft management
725- [ ] Contact search
726- [ ] IMAP IDLE (real-time notifications)
727- [ ] Hosted documentation site
728 
729## Documentation
730 
731| Guide | Description |
732|-------|-------------|
733| [Account Setup](docs/account-setup.md) | Step-by-step per provider, OAuth2, App Passwords, Azure Client ID |
734| [Tool Contract](docs/tool-contract.md) | Complete tool definitions and schemas |
735| [Message ID Format](docs/message-id-format.md) | Stable message identifier format |
736| [Cursor Pagination](docs/cursor-pagination.md) | Pagination behavior and expiration |
737| [Security](docs/security.md) | Security features and best practices |
738| [Advanced Configuration](docs/advanced-configuration.md) | Timeouts and performance tuning |
739 
740## Development
741 
742```bash
743cargo test # 64 unit + integration tests
744cargo fmt -- --check # formatting
745cargo clippy --all-targets -- -D warnings # linting
746```
747 
748See `AGENTS.md` for contributor guidelines.
749 
750## Releasing
751 
752Releases are automated via [`cargo-dist`](https://github.com/axodotdev/cargo-dist). To ship a new version:
753 
7541. Bump `version = "X.Y.Z"` in `Cargo.toml` (the release workflow enforces
755 that this matches the pushed tag).
7562. Commit the bump + any release notes to `main`.
7573. Tag and push:
758 ```bash
759 git tag vX.Y.Z
760 git push origin main --tags
761 ```
7624. The `push: tags: ['v*']` trigger in `.github/workflows/release.yml`
763 compiles binaries for Linux / macOS (Intel + Apple Silicon) / Windows,
764 generates installer scripts (`.sh`, `.ps1`), creates the GitHub Release,
765 and attaches all artifacts with SHA256 checksums.
7665. If anything fails you can re-run the workflow manually from the Actions
767 tab (the `workflow_dispatch` trigger is preserved as an escape hatch).
768 
769**npm publishing is intentionally disabled.** The upstream fork was
770configured to publish as `@bradsjm/mail-imap-mcp-rs`, a scope this
771organization does not own, which caused every release to 404 on `npm
772publish`. The npm tarball is still generated and attached to each GitHub
773Release so users can install via `npm install ./mail-mcp-npm-package.tar.gz`
774manually. To enable npm registry publishing for this fork: create an npm
775org (e.g. `@tecnologicachile`), configure Trusted Publishing on
776npmjs.com pointing at this repo, set `publish-jobs = ["npm"]` in
777`dist-workspace.toml`, and run `dist generate --allow-dirty` to restore
778the `publish-npm` job in `release.yml`.
779 
780## Contributing
781 
782Contributions welcome! Check out the [issues](https://github.com/tecnologicachile/mail-mcp/issues) for good first issues.
783 
784If mail-mcp is useful to you, a ⭐ on the repo helps others discover it.
785 
786## License
787 
788MIT License — see [LICENSE](LICENSE) for details.
789 

Discussion

Alternatives