mail-mcp
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.
Secure IMAP/SMTP/EWS/Graph API MCP server in Rust — Microsoft 365, Hotmail, Gmail, Zoho, OAuth2, multi-account
mail-mcp
Production-ready email MCP server for AI agents
IMAP + SMTP + EWS + Microsoft Graph API — built in Rust
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_mailboxescap 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 likeSent Messagessimply never appeared.
The cap is now set withMAIL_IMAP_MAX_MAILBOXES(default200, clamped to1..=10000), and the response carries two additive fields,totalandtruncated, 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. SetMAIL_MCP_TRANSPORT=httpto serve MCP streamable HTTP (stateless) instead
of stdio — useful for running the server on another machine behind an MCP
gateway. Binds to127.0.0.1:8000at/mcpby default, configurable viaMAIL_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;
seedocs/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 outboundfile_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.envfiles as
attachments. Set this variable to a directory and everyfile_pathis
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. Seedocs/security.md#outbound-attachment-scope. - Fixed:
FROM_EMAILvalidation no longer rejects dotless internal hosts
by @arwack in
#30. Addresses the
v0.4.13 upgrade caveat:noreply@localhost/alerts@intranetstyle
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_EMAILstartup validation by
@arwack in
#29 — the follow-up
to their #19. Thefrom_email→userfallback now lives in one place
(SmtpAccountConfig::effective_from()), andMAIL_SMTP_<ID>_FROM_EMAILis
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 asuser@localhostoralerts@intranetare 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 Mailon Gmail,Trash→Deleted Messages, and
so on, multi-language) in search, copy, and move. Raw message fetches now useBODY.PEEK[], so reading a message through the MCP no longer sets\Seen
as a side effect — with aBODY[]fallback for servers that rejectPEEK
(the deprecatedRFC822item, 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 ascratch
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 ofAPPEND "Sent" (\Seen) …),
becauseasync-imapinterpolates the flags argument verbatim. Strict servers
rejected the APPEND and the sent copy was lost — while the tool still reportedstatus: ok. Flags are now normalized before hitting the wire, andsmtp_send_message/smtp_reply_message/smtp_forward_messageresponses
include a newsaved_to_sentfield (true/false, ornullwhen 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 deprecatedRFC822item, which iCloud accepts but leaves
unpopulated. Fetches now use the IMAP4rev1BODY[]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 2971IDcommand after authentication whenever the server
advertises theIDcapability. Includes mock-server regression tests and
NetEase setup docs indocs/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_USERwhen
unset.- Sent-mail copies are now marked
\Seenby
@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 wereimap_get_message
(which returns attachment metadata and optional extracted PDF text, never
the binary) andimap_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_attachmentwith themessage_idplus a
selector — eitherpart_id(the valueimap_get_messagereports for each
attachment) orfilename. 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_dirargument if given, else theMAIL_ATTACHMENT_DOWNLOAD_DIRenvironment 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: trueto also get the bytes
in the response, but only when the attachment is at mostmax_inline_bytes
(default 256 KiB). Off by default.
What's New in v0.4.8
SAVE_SENTis 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 wasfalse.- 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.
- Gmail (
- Per-account override:
MAIL_SMTP_<ID>_SAVE_SENT=true|falsetakes
priority over everything. The globalMAIL_SMTP_SAVE_SENTstill 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_messagesilently dropped attachments on
threaded replies. When called within_reply_to+attachments, thecreateReply → PATCH → sendflow included the attachments in the PATCH
against/me/messages/{id}. Microsoft Graph treatsMessage.attachments
as a navigation property and silently discards the field on PATCH
(2xx response, no error), so the message went out as single-parttext/htmlwith no file. The MCP returnedstatus: okand the caller
assumed success. Invisible data loss. - The fix: in
send_via_reply(), attachments are now uploaded one by one
toPOST /me/messages/{draft_id}/attachmentsbetween the PATCH and the
send. Files < 3 MB go inline (JSON with base64contentBytes); files
≥ 3 MB usecreateUploadSessionwith 4 MB chunked PUTs. Theattachments
field was removed from thePatchDraftRequeststruct so the regression
cannot be reintroduced by a type-correct edit. - No change to flows that already worked.
send_via_sendmail(new
messages withoutin_reply_to) usesPOST /me/sendMailwithattachments
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.mdat 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 ifbody_textorbody_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
serverInfonow reportsname="mail-mcp"+ the crateversion(the
framework previously returned its ownrmcp 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:- The human reviewer wants to read the message, not audit markup —
showing the HTML is noise. - 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.- The human reviewer wants to read the message, not audit markup —
What's New in v0.4.3
- Server-side guidance against malformed tool calls. The MCP
instructionsblock now explicitly tells the calling LLM thatbody_textandbody_htmlare 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 singlebody_textstring". 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-npmjob 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 onpush: tags: ['v*'], so taggingvX.Y.Zand pushing is
all it takes to cut a release.workflow_dispatchis retained as a
manual escape hatch. - Cleanup: removed the dangling
init-npm-placeholder.ymlworkflow
(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_foldernow archives the exact RFC822 bytes that were
sent (vialettre.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_messageacceptsbody_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.
WARNwhen the
message-lookup HTTP call fails (rate limit, 5xx, permissions) so operators
see threading degraded due to a real error;DEBUGwhen 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 withMAIL_ATTACHMENT_DOWNLOAD_DIR (falls back to the system temp dir), or passoutput_dir per call.
To limit which local files send tools may attach via file_path, setMAIL_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=httpbinds to loopback unless told otherwise, and belongs behind an authenticating gateway
Configuration Reference
Full environment variable referenceIMAP (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 —
createReplyflow 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:
- Bump
version = "X.Y.Z"inCargo.toml(the release workflow enforces
that this matches the pushed tag). - Commit the bump + any release notes to
main. - Tag and push:
git tag vX.Y.Z git push origin main --tags - 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. - If anything fails you can re-run the workflow manually from the Actions
tab (theworkflow_dispatchtrigger 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"] indist-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)
Sign in to leave a review.
Leave a reviewNo results found