mail-mcp

mcp
Security Audit
Pass
Health Pass
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 98 GitHub stars
Code Pass
  • Code scan — Scanned 11 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

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

README.md

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; thread_message_id finds a message and its replies
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)

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

Reviews (0)

No results found