Email MCP Server
An MCP server that exposes IMAP/SMTP email accounts to AI assistants as 52 tools for reading, searching, sending, organizing, scheduling, and analyzing mail across multiple accounts.
Accounts & folders — list configured accounts, list mailboxes with unread counts, create/rename/delete folders
Read mail — list, search (keyword, sender, recipient, size, attachments, dates), fetch one or many emails (max 20), reconstruct threads, check read/flag/label status, find which real folder holds a virtual-folder message
Attachments — download by filename (base64, ≤5 MB)
Send mail — send, reply (with/without reply-all), forward, save drafts, send drafts
Labels — provider-aware (ProtonMail folders, Gmail labels, IMAP keywords): list, add, remove, create, delete
Organize — move, delete (trash or permanent), mark read/unread/flag/unflag, bulk actions on up to 100 emails
Scheduling — queue future emails, list pending/sent/failed, cancel
Templates — list templates and apply one via preview, draft, or send with variable substitution
Contacts & analytics — extract frequent contacts, email volume/top-sender/daily-trend stats, connection health and quota
Calendar & reminders — extract ICS events from mail, analyze an email for scheduling, add to local calendar, list calendars/events, create and list Reminders (macOS-centric)
Watcher & alerts — IMAP IDLE watcher status, AI triage presets, hooks config, diagnose/test desktop notifications, configure alert routing at runtime
Provides comprehensive email capabilities for Gmail accounts via IMAP/SMTP, including reading, sending, managing labels, scheduling emails, and experimental OAuth2 support.
Enables email management for GMX accounts via IMAP/SMTP, with features like reading, sending, and organizing emails.
Enables email management for iCloud accounts via IMAP/SMTP, with features like reading, sending, and organizing emails.
Enables email management for ProtonMail accounts via IMAP/SMTP, with features like reading, sending, and organizing emails.
Enables email management for Zoho accounts via IMAP/SMTP, with features like reading, sending, and organizing emails.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Email MCP Server summarize my unread emails from today"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Email MCP Server
An MCP (Model Context Protocol) server providing comprehensive email capabilities via IMAP and SMTP.
Enables AI assistants to read, search, send, manage, schedule, and analyze emails across multiple accounts. Exposes 52 tools, 7 prompts, and 6 resources over the MCP protocol with OAuth2 support (experimental), email scheduling, calendar extraction, analytics, provider-aware label management, real-time IMAP IDLE watcher with AI-powered triage, instant verification-code catching with clipboard copy, customizable presets and static rules, and a guided setup wizard.
Built on the MCP TypeScript SDK v2 (spec revision 2026-07-28), it serves over stdio for local clients or Streamable HTTP for networked access — existing stdio configurations keep working unchanged.
Highlights
Feature | email-mcp | Typical MCP email |
Multi-account | ✅ | ❌ |
Send / reply / forward | ✅ | ✅ |
Drafts & templates | ✅ | ❌ |
Labels & bulk ops | ✅ provider-aware | ❌ |
Schedule future emails | ✅ | ❌ |
Real-time IMAP IDLE watcher | ✅ | ❌ |
Verification codes → clipboard | ✅ concealed + auto-clear | ❌ |
AI triage with presets | ✅ | ❌ |
Desktop & webhook alerts | ✅ | ❌ |
Calendar (ICS) extraction | ✅ | ❌ |
Email analytics | ✅ | ❌ |
OAuth2 (Gmail / M365) | ✅ experimental | ❌ |
Guided setup wizard | ✅ auto-detect | ❌ |
Streamable HTTP transport | ✅ | ❌ |
Related MCP server: imap-mcp
Table of Contents
Security
All connections use TLS/STARTTLS encryption
Passwords are never logged; audit trail records operations without credentials
Token-bucket rate limiter prevents abuse (one bucket per account, sharing a global
rate_limit)OAuth2 XOAUTH2 authentication for Gmail and Microsoft 365 (experimental)
Attachment downloads capped at 5 MB with base64 encoding
Background
Most MCP email implementations provide only basic read/send. This server aims to be a full-featured email client for AI assistants, covering the entire lifecycle: reading, composing, managing, scheduling, and analyzing email — all from a single MCP server.
Key design decisions:
XDG-compliant config — TOML at
~/.config/email-mcp/config.tomlMulti-account — Operate across multiple IMAP/SMTP accounts simultaneously
Layered services — Business logic is decoupled from MCP wiring for testability
Provider auto-detection — Gmail, Outlook, Yahoo, iCloud, Fastmail, ProtonMail, Zoho, GMX
Install
Requires Node.js ≥ 24.
# Run directly (no install needed)
npx @codefuturist/email-mcp setup
# or
pnpm dlx @codefuturist/email-mcp setup
# Or install globally
npm install -g @codefuturist/email-mcp
# or
pnpm add -g @codefuturist/email-mcpnpm install -g fails with EACCES when npm's global prefix is a root-owned
directory (common on a system Node install) — the npx/pnpm dlx form above
avoids this entirely by not installing anything globally. If you do want a
permanent global install, fix npm's prefix instead of reaching for sudo
(npm config set prefix ~/.npm-global and add that to PATH, or use a Node
version manager like nvm/fnm/volta) — running npm install -g as root
routinely leaves root-owned files mixed into a user-owned node_modules tree,
which causes harder-to-diagnose permission errors on the next global install.
Homebrew (native binary)
The prebuilt, dependency-free binary — no Node.js at runtime — from the codefuturist tap:
brew install codefuturist/tap/email-mcpPublished as a cask on every stable release; works on macOS (arm64 + x64) and on Linuxbrew ≥ 4.3 (binary-only casks).
Native binary (Bun)
A dependency-free single binary (minified + bytecode-precompiled, ~32 ms cold start, no Node.js at runtime) can be compiled with Bun:
bun run build:binary # native binary → build/email-mcp
cp build/email-mcp ~/.local/bin/
bun run build:binaries # release matrix: darwin-arm64/x64, linux-x64/arm64,
# windows-x64 → build/release/*.{tar.gz,zip} + sha256 checksumsPrebuilt binaries for every release are built by CI with GoReleaser Pro
(.goreleaser.yaml) for all common platforms — Linux glibc + musl, macOS, and
Windows, each x64 + arm64 where Bun supports it — provenance-attested (verify with
gh attestation verify <asset> --repo codefuturist/email-mcp) and shipped with
GitHub releases —
installable via mise: mise use -g ubi:codefuturist/email-mcp.
The same binary can also be built locally from the published npm package (no
checkout needed; ≥ 0.5.1, which carries the imapflow TLS fix at runtime —
older versions need the repo's pnpm patch). Note that a plain bun add -g
shim executes dist/main.js under node via its shebang, so the compile step
is what buys the ~11 ms cold start:
bun add -g @codefuturist/email-mcp
bun build --compile --minify --bytecode \
"${BUN_INSTALL:-$HOME/.bun}/install/global/node_modules/@codefuturist/email-mcp/dist/main.js" \
--outfile ~/.local/bin/email-mcpAn installed binary keeps itself current: email-mcp update checks the latest
release and — after a confirmation, a sha256 check against the release's
checksums file, and (when the gh CLI is present) a provenance-attestation
check — atomically replaces itself. --check only reports (exit code 1 =
update available), --yes skips the prompt for scripts. npm- and mise-managed
installs are recognized and pointed at npm install -g …@latest /
mise upgrade instead of being overwritten.
server install from the binary points the launchd login item at it. Requires the
bundled imapflow patch (patches/) — imapflow passes servername: false to
tls.connect for IP hosts, which Bun's stricter node:tls rejects.
Docker
No Node.js required — just Docker.
# Latest stable release
docker pull ghcr.io/codefuturist/email-mcp:latest
# Pin to an exact version (immutable)
docker pull ghcr.io/codefuturist/email-mcp:0.2.3
# Auto-update patches within a minor version
docker pull ghcr.io/codefuturist/email-mcp:0.2
# Track a major version (won't cross breaking-change boundary)
docker pull ghcr.io/codefuturist/email-mcp:0
# Pin to an exact git commit (immutable, CI traceability)
docker pull ghcr.io/codefuturist/email-mcp:sha-abc1234
# Or build from source
docker build -t ghcr.io/codefuturist/email-mcp .Tag convention: Tags follow bare semver (no
vprefix), matching Docker ecosystem standards (e.g.node:24,nginx:1.25). Thelatesttag is only updated on stable releases, never pre-releases.
Note: By default, the server uses stdio transport. Config must be created on the host first (via
npx @codefuturist/email-mcp setupor manually) and mounted into the container.
Usage
Setup
# Add an email account interactively (recommended)
# On macOS and Linux this offers to store the password in the OS keychain,
# so config.toml only carries a `keychain:<account>` reference.
email-mcp account add
# Or use the legacy alias
email-mcp setup
# Or create a template config manually
email-mcp config initThe setup wizard auto-detects server settings, tests connections, saves config, and outputs the MCP client config snippet.
Test Connections
email-mcp test # all accounts
email-mcp test personal # specific accountConfigure Your MCP Client
The snippets below use the default stdio transport, best for local desktop clients. For networked or remote access over Streamable HTTP, see Streamable HTTP (networked) below.
Recommended — use the guided installer (auto-detects Claude Desktop, Cursor, Windsurf; for VS Code use the manual snippet below):
email-mcp installOr add manually using the snippets below.
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"email": {
"command": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"]
}
}
}Option 1 — Extensions gallery (easiest):
Open the Extensions view (⇧⌘X / Ctrl+Shift+X)
Search
@mcp email-mcpClick Install (user-wide) or right-click → Install in Workspace
Option 2 — Workspace config (.vscode/mcp.json, committed to source control):
{
"servers": {
"email": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"]
}
}
}Option 3 — User config (settings.json, applies to all workspaces):
Open the Command Palette → Preferences: Open User Settings (JSON) and add:
{
"mcp": {
"servers": {
"email": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"]
}
}
}
}Edit ~/.cursor/mcp.json:
{
"mcpServers": {
"email": {
"command": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"]
}
}
}Edit ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"email": {
"command": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"]
}
}
}Edit ~/.config/zed/settings.json:
{
"context_servers": {
"email": {
"command": {
"path": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"]
}
}
}
}Add to ~/.vibe/config.toml:
[[mcp_servers]]
name = "email-mcp"
transport = "stdio"
command = "npx"
args = ["-y", "@codefuturist/email-mcp", "stdio"]To pass credentials directly instead of using a config file, use the env field:
[[mcp_servers]]
name = "email-mcp"
transport = "stdio"
command = "npx"
args = ["-y", "@codefuturist/email-mcp", "stdio"]
env = { "EMAIL_ACCOUNTS" = "<your-accounts-json>" }MCP tools are exposed as email-mcp_<tool_name> (e.g. email-mcp_list_emails). Restart Vibe after editing the config.
Run the server in a container — mount your config directory read-only:
docker run --rm -i \
-v ~/.config/email-mcp:/home/node/.config/email-mcp:ro \
-v email-mcp-state:/home/node/.local/state/email-mcp \
-v email-mcp-cache:/home/node/.cache/email-mcp \
ghcr.io/codefuturist/email-mcpThe config mount can stay read-only, but state and cache must be persisted.
The image declares both as volumes, so Docker keeps them in anonymous volumes
if you omit the flags — naming them, as above, is what makes them survive a
docker run --rm. State holds the scheduled-email queue: without it, a restart
drops every email scheduled but not yet sent. Cache holds the local mirror,
which is regenerable, so losing that only costs a re-sync.
For MCP client configuration (e.g. Claude Desktop):
{
"mcpServers": {
"email": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "~/.config/email-mcp:/home/node/.config/email-mcp:ro",
"-v", "email-mcp-state:/home/node/.local/state/email-mcp",
"-v", "email-mcp-cache:/home/node/.cache/email-mcp",
"ghcr.io/codefuturist/email-mcp"
]
}
}
}{
"mcpServers": {
"email": {
"command": "npx",
"args": ["-y", "@codefuturist/email-mcp", "stdio"],
"env": {
"MCP_EMAIL_ADDRESS": "you@gmail.com",
"MCP_EMAIL_PASSWORD": "your-app-password",
"MCP_EMAIL_IMAP_HOST": "imap.gmail.com",
"MCP_EMAIL_SMTP_HOST": "smtp.gmail.com"
}
}
}
}Always-on server (detached)
The stdio transport lives only as long as an MCP client keeps it open — which also
means the IMAP watcher (and verification catching) stop with it. email-mcp server
runs the Streamable HTTP server as a background service instead, iMCP-style:
email-mcp server start # detached on http://127.0.0.1:8080/mcp
email-mcp server start --port 3199 # any http flags pass through (--host, --token, …)
email-mcp server start --attach # foreground instead (Ctrl-C stops)
email-mcp server status # pid, address, /healthz, uptime
email-mcp server logs -n 50 # tail the daemon log
email-mcp server restart # reuses the previous flags
email-mcp server stop # graceful SIGTERM (add --force for SIGKILL)The binding lives in the config file, so a plain server start is deterministic:
[settings.server]
host = "127.0.0.1"
port = 8080
path = "/mcp"
token = "" # bearer token; empty = no auth (loopback only)
allowed_hosts = [] # Host-header allowlist for reverse proxiesPrecedence: CLI flags → EMAIL_MCP_HTTP_* env → [settings.server] → defaults —
for email-mcp http, server start, and the launchd login item alike. Edit it
interactively with config edit server (the token renders masked). --insecure
is deliberately flag-only and cannot be persisted.
Start at login (macOS): email-mcp server install writes a launchd login item
(com.email-mcp.server, RunAtLoad + KeepAlive) running email-mcp http with no
flags — the config file is its single source of truth across reboots and crashes.
server status recognizes the launchd mode; server uninstall removes it.
State lives in $XDG_STATE_HOME/email-mcp/daemon.json, logs in server.log next to
it (rotated at 5 MB). stop only ever signals a process whose command line is
verifiably this server (PID-reuse safe). Point HTTP-capable MCP clients at
http://127.0.0.1:8080/mcp; binding non-loopback hosts still requires a token.
Streamable HTTP (networked)
By default the server speaks MCP over stdio, ideal for local desktop clients (see the snippets above). For networked or remote access, run it as a Streamable HTTP server instead:
email-mcp http --port 8080This serves MCP at http://127.0.0.1:8080/mcp, with a health probe at GET /healthz.
Flags & environment variables
Flag | Environment variable | Default | Description |
|
|
| Address to bind |
|
|
| Port to listen on |
|
|
| HTTP path serving MCP |
|
| — | Require |
|
| loopback names + bind host | Comma-separated |
| — |
| Allow binding a non-loopback host without a token |
Security
Token auth — When a token is set, every request must send
Authorization: Bearer <token>.Non-loopback bind guard — Binding a non-loopback host (e.g.
0.0.0.0) without a token is refused unless--insecureis passed. Use--insecureonly when TLS and authentication are terminated by an upstream reverse proxy.DNS-rebinding protection — The
Hostheader is validated against an allowlist (loopback names plus the bind host by default). SetEMAIL_MCP_HTTP_ALLOWED_HOSTSto your public domain, or*to disable the check when a proxy already enforces it.
Client configuration
For clients that support the Streamable HTTP transport:
{
"mcpServers": {
"email": {
"type": "streamable-http",
"url": "http://127.0.0.1:8080/mcp"
}
}
}When a token is configured, add an Authorization: Bearer <token> header if your client supports custom headers.
Docker
docker compose --profile http upRequires EMAIL_MCP_HTTP_TOKEN to be set; the image EXPOSEs port 8080.
CLI Commands
email-mcp [command]
Commands:
stdio Run as MCP server over stdio (default)
http Run as MCP server over Streamable HTTP (networked)
server start [--attach] Start an always-on HTTP server (detached by default)
server stop [--force] Stop the detached server
server status Show pid, address, health, uptime (exit 1 if stopped)
server restart Stop and start again (reuses previous flags)
server logs [-n N] Show the last N daemon log lines
server install Install as macOS login item (launchd, survives reboots)
server uninstall Remove the login item and stop the server
account list List all configured accounts
account add Add a new email account interactively
account edit [name] Edit an existing account
account delete [name] Remove an account
setup Alias for 'account add'
test Test connections for all or a specific account
open <id> [--print] Open an email in the native mail client via its message: link
(macOS; --print or other platforms print the URL instead)
install Register email-mcp with MCP clients interactively
install status Show registration status for detected clients
install remove Unregister email-mcp from MCP clients
config show Show config (passwords masked)
config edit [section] Interactive settings editor (general, server, send, attachments,
scheduler, watcher, verification, cache, hooks, alerts)
config validate Check syntax, schema, typo'd keys, and cross-setting consistency
completion zsh|bash|fish Print shell completion script (accounts and sections complete dynamically)
config path Print config file path
config init Create template config
scheduler check Process pending scheduled emails
scheduler list Show all scheduled emails
scheduler install Install OS-level scheduler (launchd/crontab)
scheduler uninstall Remove OS-level scheduler
scheduler status Show scheduler installation status
cache status [--json] Show cache size, contents, coverage and freshness
cache clear [--account N] Delete cached mail/attachments and free the disk space
(--json supported by status, clear and verify)
cache verify Check the cache for corruption and reclaim orphaned files
update [--check|--yes] Self-update the binary to the latest GitHub release
help Show helpShell completion
# zsh (one line in ~/.zshrc)
source <(email-mcp completion zsh)
# bash
source <(email-mcp completion bash)
# fish
email-mcp completion fish > ~/.config/fish/completions/email-mcp.fishCompletions cover every command, subcommand, and flag — and complete account
names from your config (email-mcp test <TAB>, account edit <TAB>) and
settings sections (config edit <TAB>) dynamically.
Checking logs
The background daemon logs to $XDG_STATE_HOME/email-mcp/server.log. View it
with a tail/follow/filter viewer — no need to find the file:
email-mcp logs # last 40 lines (colored by level)
email-mcp logs -n 200 # more lines
email-mcp logs -f # live follow (Ctrl-C to stop)
email-mcp logs --level warn # only WARN and ERROR (continuation lines kept)
email-mcp logs --grep imap # only lines matching a substring
email-mcp logs -f --level error # watch for errors as they happenemail-mcp server logs is an alias with the same options. Color auto-disables
when piped or under NO_COLOR; on a systemd host, journalctl --user -u email-mcp is also available.
Reading and sending mail from the CLI
Beyond running as an MCP server, email-mcp is a usable mail client in its own
right — no MCP client and no coding required. Commands default to your first
configured account (pass --account <name> to pick another), print clean,
colored tables (color auto-disables when piped or under NO_COLOR), and tab
complete (see Shell completion).
email-mcp list # recent INBOX messages (● unread, ★ flagged, 📎 attachment)
email-mcp list --unread --limit 50 # filters
email-mcp list --mailbox Archive --search invoice
email-mcp read 42 # full message (headers, body, attachment names + sizes)
email-mcp read 42 --mark-read
email-mcp search "from bank" # IMAP search
email-mcp mailboxes # folders with unread/total counts
email-mcp flag 42 # flag / --off to unflag
email-mcp attach 42 report.pdf ~/Downloads # save an attachment
email-mcp delete 42 --yes # delete (asks to confirm without --yes)
email-mcp reply 42 --body "on it" --all
email-mcp send --to a@b.c,d@e.f --subject "Hi" --body "..." --attach ./file.pdfEvery command accepts --help; the read commands (list, read, search,
mailboxes) accept --json for machine-readable output you can pipe to jq:
email-mcp list --json | jq '.items[] | {id, subject, from: .from.address}'
email-mcp read 42 --json | jq .attachmentsAttachment sizes on send/reply are checked against the configured limit before anything leaves your machine (see Storing account secrets for keeping credentials out of plaintext).
Configuration
Located at $XDG_CONFIG_HOME/email-mcp/config.toml (default: ~/.config/email-mcp/config.toml). Validate it with email-mcp config validate (TOML syntax, schema, typo'd keys with did-you-mean, cross-setting consistency; exit 1 on errors — CI-friendly) and edit it interactively with email-mcp config edit [section] — every save validates first and backs up the previous file to config.toml.bak. Note: programmatic saves rewrite the file without TOML comments (the backup keeps them).
[settings]
rate_limit = 10 # max emails per minute per account
[settings.send]
max_attachment_mb = 25 # global cap on the base64-encoded attachment total
save_to_sent = false # file a copy of each sent message in the IMAP Sent folder
[settings.attachments]
auto_download = true # save attachments locally when calendar events are created from emails
[settings.attachment_download]
# dir = "/data/attachments" # unset (default) disables download_attachment's saveToDownloadDir
allowed_extensions = ["pdf", "png", "jpg", "jpeg", "webp", "tiff", "txt", "md", "doc", "docx", "odt", "xls", "xlsx", "csv"]
max_bytes = 26214400 # 25 MB
[[accounts]]
name = "personal"
email = "you@gmail.com"
full_name = "Your Name"
password = "keychain:personal" # or "env:VAR", or a plaintext app password
# max_attachment_mb = 25 # optional per-account override (beats global + provider limits)
# sent_mailbox = "INBOX.Sent Messages" # optional, see "Sent-folder copies" below
# save_to_sent = true # optional per-account override of the Gmail-skip guess
[accounts.imap]
host = "imap.gmail.com"
port = 993
tls = true
[accounts.smtp]
host = "smtp.gmail.com"
port = 465
tls = true
starttls = false
verify_ssl = true
[accounts.smtp.pool]
enabled = true
max_connections = 1
max_messages = 100Storing account secrets
The password field (and OAuth2 client_secret / refresh_token) accepts an
indirection so credentials need not sit in the file as plaintext:
Value | Resolved from |
| OS keychain — macOS Keychain ( |
| macOS Keychain with an explicit service name |
| the environment variable |
any other string | used literally (plaintext — backward compatible) |
The config file itself is always written 0600 (owner-only), and so is its
.bak.
macOS Keychain (multi-account): store each account's secret once —
email-mcp account set-secret personal # store a NEW secret (prompts; nothing echoed or in argv)
email-mcp account migrate-secret personal # move an EXISTING plaintext password into the keychainset-secret prompts for the value and stores it; migrate-secret reads the
account's current plaintext password from config.toml, stores it in the
keychain, verifies it, then rewrites the line to password = "keychain:personal".
Either way you end up with password = "keychain:personal". Each account is a separate Keychain
item under the shared email-mcp service, so multiple accounts coexist.
Secrets are read back at startup over the security CLI; only the service and
account names are ever passed as arguments — the secret travels on stdout only.
At rest the secret lives in your login keychain, encrypted and unlocked by
your macOS login (Touch ID at the login level). A running email-mcp daemon
reads it once at startup; it does not prompt for Touch ID on every IMAP
reconnect — per-access biometric prompts are deliberately not used for an
unattended background service. (True per-access Touch ID would require the
data-protection keychain and a native helper.)
OAuth2 (experimental)
Note: OAuth2 support is experimental. Token refresh and provider-specific flows may require additional testing in your environment.
[[accounts]]
name = "work"
email = "you@company.com"
full_name = "Your Name"
[accounts.oauth2]
provider = "google" # or "microsoft"
client_id = "your-client-id"
client_secret = "your-client-secret"
refresh_token = "your-refresh-token"
[accounts.imap]
host = "imap.gmail.com"
port = 993
tls = true
[accounts.smtp]
host = "smtp.gmail.com"
port = 465
tls = true
[accounts.smtp.pool]
enabled = true
max_connections = 1
max_messages = 100Environment Variables
For single-account setups (overrides config file):
Variable | Default | Description |
| required | Email address |
| required | Password or app password |
| required | IMAP server hostname |
| required | SMTP server hostname |
|
| Account name |
| — | Display name |
| Login username | |
|
| IMAP port |
|
| IMAP TLS |
|
| SMTP port |
|
| SMTP TLS |
|
| SMTP STARTTLS |
|
| Verify SSL certificates |
|
| Enable SMTP transport pooling |
|
| Max pooled SMTP connections |
|
| Max messages per pooled connection |
|
| Max sends per minute |
|
|
|
|
|
|
|
| Days of sent-email history to keep; |
| — | Directory |
| (built-in list — see below) | Comma-separated extension allowlist for saved attachments |
|
| Max attachment size |
Local Cache
Reads are served from a local SQLite mirror of your mail (plus a
content-addressed store for attachments) under
${XDG_CACHE_HOME:-~/.cache}/email-mcp/. It makes listing, searching, and
re-reading mail fast, and it keeps mail readable when the mail server is
unreachable. It is on by default.
The cache is never authoritative. Every row can be re-fetched from IMAP, so deleting it costs a resync and never loses mail — you can remove the directory at any time, as the XDG cache convention allows.
Seeing and reclaiming what it holds
email-mcp cache status # size vs budget, per-mailbox coverage, last sync
email-mcp cache status --json # same, for scripts
email-mcp cache clear # free the space (asks first; mail is re-fetched)
email-mcp cache clear --account work --yes
email-mcp cache verify # integrity check + reclaim orphaned filescache status reports the index size and attachment bytes separately, how many
messages have full text cached, and how long ago each mailbox was reconciled.
check_health exposes the same numbers to an AI client, plus the read hit ratio.
How freshness is decided
Correctness comes first: a stale answer to "any new mail?" is a wrong answer.
Before serving a mailbox from the cache, the server either trusts a reconcile
from the last 10 seconds, or spends one STATUS round trip comparing the
server's UIDVALIDITY/UIDNEXT/HIGHESTMODSEQ/MESSAGES against what the
mirror recorded — the IMAP equivalent of a conditional request. If anything
moved, it resynchronises before answering (bounded; past the bound you get the
cached answer marked stale). If the server is unreachable you get the cached
answer marked offline.
Message bodies and attachments are immutable for a given UIDVALIDITY/UID, so
once stored they are served with no revalidation at all. Writes made through
this server (flagging, moving, deleting) resynchronise the affected mailboxes
immediately, which matters on servers without CONDSTORE where a flag change
is otherwise invisible.
Settings
Setting | Default | What it does |
|
|
|
|
| Synced in the background. Others mirror lazily once read |
|
| How far back the first sync of a mailbox reaches |
|
| Bodies kept per mailbox. |
|
| Budget for index + attachments combined |
|
| Seconds between background reconciles |
Past the budget, eviction runs cheapest-to-rebuild-first: attachment files
(least recently read first), then message bodies, then envelopes older than
window_days. Envelopes are kept as long as possible, because they are what
listings and search need.
Coverage, not just freshness. The cache only answers a listing when it
holds every message the server reports for that mailbox. Set window_days to
something other than 0 and the mirror becomes a slice — it then cannot report
a truthful total or know whether more pages exist, so reads fall back to the
live path until it catches up. That is also why envelope eviction clears the
mailbox's sync watermarks: the next sync rebuilds coverage instead of leaving a
permanent, invisible hole.
Data at rest
Cached mail — including message bodies and attachment content — is stored
unencrypted, in a directory and files readable only by your user
(0700/0600). This matches what desktop mail clients do; the recommended
protection is full-disk encryption (FileVault, LUKS). If you would rather not
have bodies and attachments on disk at all, set body_messages = 0, which
keeps the cache metadata-only.
Gmail note: do not add [Gmail]/All Mail to mailboxes. Gmail presents the
same message in every label folder, so mirroring All Mail alongside INBOX
stores the same mail twice.
Email Scheduling
The scheduler enables future email delivery with a layered architecture:
MCP auto-check — Processes the queue on server startup and every 60 seconds while the MCP server is running
CLI —
email-mcp scheduler checkfor manual or cron-based processingOS-level daemon —
email-mcp scheduler installsets up launchd (macOS) or crontab (Linux) to run every minute, independently of the MCP server
Important — the daemon must be installed for reliable delivery. Without it, scheduled emails only fire while an AI client is actively connected. Your machine also needs to be running at the scheduled time; if it's asleep or off, the daemon will process overdue emails on next wake/startup. Failed sends are retried up to 3 times before being marked
failed.
Setting up the daemon
# Install (macOS launchd / Linux crontab — runs every minute)
email-mcp scheduler install
# Verify it's running
email-mcp scheduler status
# View pending / sent / failed scheduled emails
email-mcp scheduler list
# Trigger a manual check immediately
email-mcp scheduler check
# Remove the daemon
email-mcp scheduler uninstallScheduled emails are stored as JSON files in ~/.local/state/email-mcp/scheduled/ with status-based locking. Each entry tracks attempts (max 3) and the last error, so you can inspect failures with scheduler list.
Real-time Watcher & AI Hooks
The IMAP IDLE watcher monitors configured mailboxes in real-time using persistent IDLE connections (separate from tool connections). When new emails arrive:
Static rules — Pattern-match on from/to/subject → apply labels, flag, or mark read instantly (no AI)
AI triage — Remaining emails are analyzed via MCP sampling with a customizable preset prompt
Notify mode — Falls back to logging if AI triage is disabled
Configure in config.toml:
[settings.watcher]
enabled = true
folders = ["INBOX"]
idle_timeout = 1740 # 29 minutes (IMAP spec max is 30)
[settings.hooks]
on_new_email = "triage" # "triage" | "notify" | "none"
preset = "inbox-zero" # "inbox-zero" | "gtd" | "priority-focus" | "notification-only" | "custom"
auto_label = true # apply AI-suggested labels
auto_flag = true # flag urgent emails
batch_delay = 5 # seconds to batch before triage
# User context — appended to preset's AI prompt
custom_instructions = """
I'm a software engineer. Emails from @mycompany.com are always high priority.
Newsletters I read: TL;DR, Hacker Newsletter.
"""
# Static rules — run BEFORE AI, skip AI if matched
[[settings.hooks.rules]]
name = "GitHub Notifications"
match = { from = "*@github.com" }
actions = { labels = ["Dev"], mark_read = true }
[[settings.hooks.rules]]
name = "Newsletter Archive"
match = { from = "*@substack.com|*@buttondown.email" }
actions = { labels = ["Newsletter"] }
[[settings.hooks.rules]]
name = "VIP Contacts"
match = { from = "ceo@company.com|cto@company.com" }
actions = { flag = true, labels = ["VIP"] }Presets
Preset | Focus | Suggested Labels |
| Aggressive categorization + archiving | Newsletter, Notification, Updates, Finance, Social, Promo |
| Getting Things Done contexts | @Action, @Waiting, @Reference, @Someday, @Delegated |
| Simple priority classification (default) | (none — just priority + flag) |
| No AI triage, just log | (none) |
| User defines full system prompt | User-defined |
Static Rules
Static rules use glob-style patterns (*@github.com) with | as OR separator (*@github.com|*@gitlab.com). All conditions within a match are AND'd. First matching rule wins.
Available actions: labels (string array), flag (boolean), mark_read (boolean), alert (boolean — forces desktop notification).
Alerts
Urgency-based multi-channel notification routing — grab attention for important emails even when you're not looking at the chat. All channels are opt-in and disabled by default.
Priority | Desktop | Sound | MCP Log Level | Webhook |
| ✅ Banner | 🔊 Alert |
| ✅ |
| ✅ Banner | 🔇 Silent |
| ✅ |
| ❌ | ❌ |
| ❌ |
| ❌ | ❌ |
| ❌ |
[settings.hooks.alerts]
desktop = true # OS-level notifications (macOS/Linux/Windows)
sound = true # play sound for urgent emails
urgency_threshold = "high" # minimum priority to trigger desktop alert
webhook_url = "https://ntfy.sh/my-email-alerts" # optional: Slack, Discord, ntfy.sh, etc.
webhook_events = ["urgent", "high"]Supported platforms: macOS (Notification Center via osascript), Linux (notify-send), Windows (PowerShell toast). Zero npm dependencies — uses native OS commands.
Notification setup by platform:
Desktop notifications use osascript (built-in). The terminal app running the MCP server needs notification permission:
Open System Settings → Notifications & Focus
Find your terminal app (Terminal, iTerm2, VS Code, Cursor, etc.)
Enable Allow Notifications and choose Banners or Alerts
Ensure Focus / Do Not Disturb is not blocking notifications
Use check_notification_setup to diagnose and test_notification to verify.
Requires notify-send from libnotify. For sound alerts, paplay is also needed:
# Ubuntu / Debian
sudo apt install libnotify-bin pulseaudio-utils
# Fedora
sudo dnf install libnotify pulseaudio-utils
# Arch
sudo pacman -S libnotifyDesktop notifications require a running display server (X11/Wayland) — they will not work in headless/SSH sessions.
Uses PowerShell toast notifications (built-in):
Open Settings → System → Notifications
Ensure Notifications is turned on
Set Focus Assist to allow notifications
If using Windows Terminal, ensure its notifications are enabled
AI-configurable: The AI can check, test, and configure notifications at runtime:
check_notification_setup— diagnose platform support and show setup instructionstest_notification— send a test notification to verify everything worksconfigure_alerts— enable/disable desktop, sound, threshold, webhook (with optional persist to config file)
Webhook payload:
{
"event": "email.urgent",
"account": "work",
"sender": { "name": "John CEO", "address": "ceo@company.com" },
"subject": "Q4 Review Due Today",
"priority": "urgent",
"labels": ["VIP"],
"rule": "VIP Contacts",
"timestamp": "2026-02-18T11:30:00Z"
}Static rules can force desktop notifications with alert = true, regardless of urgency threshold:
[[settings.hooks.rules]]
name = "VIP Contacts"
match = { from = "ceo@company.com" }
actions = { flag = true, alert = true, labels = ["VIP"] }Features:
Auto-reconnect — Exponential backoff (1s → 60s) on connection failures
Batching — Groups arrivals within a configurable delay to reduce AI calls
Rate limiting — Max 10 sampling calls per minute
Graceful degradation — Falls back to notify mode if client doesn't support sampling
Resource subscriptions — Pushes
notifications/resources/updatedfor unread counts
Verification codes (instant OTP catch)
When a mail carrying a verification code (OTP/2FA) or sign-in magic link arrives, email-mcp copies it to your clipboard within ~1–2 seconds and shows a notification — “✅ Code 482913 from GitHub — Copied to clipboard, clears in 60s”. Press ⌘V / Ctrl+V, done.
Detection is scoring-based (English + German), not a bare digit regex: keyword proximity
(verification, Bestätigungscode, expires, …) minus negative context (order numbers,
invoices, tracking, Zoom passcodes, phone numbers, prices, dates). A miss beats a wrong
code on your clipboard.
[settings.watcher]
enabled = true # REQUIRED for ambient catching (IMAP IDLE push)
[settings.verification]
enabled = true # on by default
auto_copy = true # copy caught codes/links to the clipboard
confirm_copy = false # ask (native dialog) before touching the clipboard
notify = true # desktop notification on catch
copy_links = true # also catch magic links (only when no code found)
link_action = "open" # "open" = offer to open links in the browser, "copy" = clipboard
clear_after_seconds = 60 # conditional auto-clear (0 = never)
max_age_minutes = 10 # ignore stale messages (IDLE replays)
accounts = [] # limit to specific accounts (empty = all)
sender_allowlist = [] # e.g. ["*@github.com", "*@google.com"]
sender_denylist = []Ask-first mode: with confirm_copy = true, a native macOS dialog ("Code 482913 from
GitHub — Copy to clipboard?") appears before anything touches your clipboard. Sign-in
links get a three-button dialog by default (link_action = "open"): Open launches
the link in your default browser, Copy puts it on the clipboard, Cancel does
nothing — opening is always confirmed, since auto-launching a browser from mail content
would be a phishing hazard. Set link_action = "copy" for the silent clipboard behavior.
Dialogs are macOS-only (same mechanism as the calendar confirmation); elsewhere these
modes degrade to notification-only.
Clipboard hygiene (macOS): codes are written via a JXA osascript with the
org.nspasteboard.ConcealedType marker — the same convention 1Password/Bitwarden use — so
well-behaved clipboard managers (Maccy, Paste, …) never record them. The auto-clear is
conditional: the clipboard is only cleared if it still holds the caught value, so anything
you copied in the meantime is never stomped. Linux uses wl-copy/xclip, Windows uses
PowerShell Set-Clipboard (no concealment convention exists there). Zero npm dependencies.
For agents: get_verification_code fetches the newest code on demand — during an
automated signup, call it with wait_seconds = 30 and it returns the code seconds after
the mail lands (event-driven with the watcher, 5 s polling without). If nothing arrives in
time, it returns found: false with a hint — just call it again.
Security notes: the clipboard is shared OS state — any app can read it while the code
is there (that window is bounded by clear_after_seconds). The code itself is never
written to server logs or the audit log; it does appear in get_verification_code results,
which is the point of that tool. Kill switches: enabled = false (feature off) or
auto_copy = false (notification only). Environment overrides: MCP_EMAIL_VERIFICATION_*
(_ENABLED, _AUTO_COPY, _CONFIRM_COPY, _NOTIFY, _COPY_LINKS, _LINK_ACTION,
_CLEAR_AFTER_SECONDS, _MAX_AGE_MINUTES, _ACCOUNTS, _SENDER_ALLOWLIST,
_SENDER_DENYLIST).
Use check_clipboard_setup to diagnose the pipeline (add test_write = true for a
harmless copy → read-back → clear round-trip).
API
Tools (52)
Structured output:
list_emails,search_emails,get_email_status,list_mailboxes, andget_verification_codealso return machine-readable results (outputSchema+structuredContent) alongside the human-readable text, for clients that consume typed tool output.
Email bodies: HTML and other formats
Nearly all real mail is HTML — in one sampled mailbox, 165 of 169 messages were
single-part text/html — so the read tools convert it rather than handing back
markup.
get_email, get_emails, and get_thread take a format:
| Body you get |
| HTML converted to readable text: links become |
| As |
| The raw body part, HTML included |
Bodies are capped by default — 10 000 characters for get_email, 5 000 per
message for get_emails and get_thread — and a hint reports how much was
left out. Pass maxLength: 0 for no cap. Each side of the body (text, HTML) is
read up to 512 KB, shared across its parts.
Other formats behave as follows:
multipart/alternative— both halves are fetched, sobodyTextandbodyHtmlare both available. A plain part too small to hold the same content as its HTML sibling is treated as a "view this in an HTML reader" placeholder and skipped.multipart/mixedwith several text parts — concatenated in order, so a note split either side of an inline image (Apple Mail) or a per-message digest section is not truncated to its first part.text/plain; format=flowed(RFC 3676) — soft line wraps are undone, so paragraphs read as paragraphs rather than hard-wrapped at 72 columns;DelSpand space-stuffing are honoured.Inline (
cid:) images — listed as attachments withpartId,contentId, andinline, and downloadable viadownload_attachment'spartId. To embed one when sending, pass{path, cid, contentType}inattachmentsand reference it as<img src="cid:…">.Bounces (
multipart/report) — the machine-readablemessage/delivery-statuspart is surfaced separately from the body.Calendar invites — handled by
extract_calendar/add_to_calendar, honouring the part's declared charset.Encrypted mail (S/MIME, PGP/MIME) — not decrypted; the body says so plainly instead of appearing empty.
Attached messages (
message/rfc822) — never mistaken for the body, but used as the body when the message carries no other text (forward-as-attachment, digests).
Attachment download directory. download_attachment normally returns the
file base64-encoded (≤5 MB). Set [settings.attachment_download] dir (or
MCP_EMAIL_ATTACHMENT_DOWNLOAD_DIR) to a directory and pass
saveToDownloadDir: true to instead write the file there and get back its
path — useful for larger files, or when the caller just wants the attachment
on disk rather than inlined into the conversation. The option is unavailable
(the call fails) until dir is set — there is no default location. Only
allowed_extensions are written (default: common document/image types; see
the config example above); max_bytes caps the size, independent of the
5 MB base64 cap. Pass saveAs to choose the filename written there (a plain
basename — no path separators); otherwise the attachment's own filename is
sanitized and used. Running in Docker, point dir at a mounted volume shared
with whatever reads the files back, e.g. in docker-compose.yml:
services:
email-mcp:
environment:
MCP_EMAIL_ATTACHMENT_DOWNLOAD_DIR: /data/attachments
volumes:
- attachments:/data/attachments
other-service:
volumes:
- attachments:/data/attachments:ro
volumes:
attachments:Sent-folder copies. SMTP delivers mail; it does not file it. [settings.send] save_to_sent defaults to false; turn it on for an IMAP host that does not
capture sends itself. Gmail does capture sends server-side, so an account
whose IMAP server advertises the X-GM-EXT-1 capability is skipped
automatically even with the setting on — filing a copy there would give you
two of everything. Set the account's own save_to_sent to force the decision
either way (true files anyway, false never files, regardless of the
global setting or the Gmail guess).
The copy carries the same Message-ID the server accepted, is flagged
\Seen, and lands in the folder the server marks \Sent (or a folder named
Sent, created if absent). On a mailbox that carries several sent-shaped
folders — a leftover Sent beside the one actually in use — that guess can
land on an empty one; set the account's sent_mailbox to the full path
(as list_mailboxes reports it) to settle it. The copy omits Bcc, which the
MIME composer strips unconditionally.
A failure to file never fails the send — the mail has already left, and
saying otherwise would invite a duplicate send — but send_draft keeps the
draft in that case rather than deleting the only remaining copy of the
message. send_email, reply_email, forward_email and send_draft all
report the outcome in their result's sentCopy field: {kind: "filed", path},
{kind: "skipped"} (setting off, or Gmail's own capture), or
{kind: "failed", error}.
Outbound HTML (html: true) is sent as multipart/alternative with a
generated plain-text alternative. Forwards quote the original as converted
text; pass html: true to forward_email to also carry the original HTML.
Read (14)
Tool | Description |
| List all configured email accounts |
| List folders with unread counts and special-use flags |
| Paginated email listing with date, sender, subject, and flag filters; marks bulk mail; optional body preview |
| Read full email content with attachment metadata and bulk-mail classification (HTML converted to readable text by default) |
| Fetch full content of multiple emails in a single call (max 20); same bulk markers |
| Get read/flag/label state of an email without fetching the body |
| Search by keyword across subject, sender, and body; same bulk markers and optional preview as |
| Download an email attachment by filename or MIME part id, base64-encoded or (if configured) saved to the server's attachment download directory |
| Discover the real folder(s) an email resides in (resolves virtual folders) |
| Extract unique contacts from recent email headers |
| Reconstruct a conversation thread via References/In-Reply-To; same bulk markers |
| List available email templates |
| Email analytics — volume, top senders, daily trends |
| Connection health, latency, quota, and IMAP capabilities |
Bulk-mail classification
list_emails, search_emails, get_email, get_emails and get_thread mark
list and machine-generated mail, so an inbox can be triaged without reading
bodies:
Marker | Meaning | Derived from |
📰 | Mailing list or subscription |
|
🤖 | Machine-generated notification |
|
(none) | Ordinary person-to-person mail | no bulk headers present |
A newsletter also reports its unsubscribe URI when the sender offers one, and
flags 1-click when RFC 8058 one-click unsubscribe is supported. The headers
are requested as part of the listing fetch, so classifying a whole page costs no
extra round trip.
Classification is header-derived and therefore deterministic — no model call and no heuristics on subject text. A sender that omits the headers is reported as personal mail rather than guessed at. These are sender-asserted headers, not verified ones — treat the marker as a signal to weigh, not a fact to defer to.
Body previews
list_emails and search_emails accept preview: true to include roughly 200
characters of each message body. It is off by default: it fetches 1500 extra
bytes per message and lengthens the tool output, which is a cost the caller
should choose rather than inherit.
The preview is decoded from one body part rather than from message source —
IMAP section 1, or 1.1 when section 1 is itself a multipart. Because the fetch
stops at a byte count rather than at an encoding boundary, decoding tolerates
input cut mid-stream: base64 is truncated to a whole group, a half-written
quoted-printable escape is dropped, and the replacement character left by a
severed UTF-8 sequence is trimmed. Charsets beyond UTF-8 are handled — real mail
still ships iso-8859-1.
Markup is stripped whether or not the sender declared it, since bulk senders
routinely put HTML and stylesheets in a part labelled text/plain. Entities and
zero-width preheader padding are decoded, and a preview that reduces to markup
residue is omitted: no preview beats a misleading one.
Write (9)
Tool | Description |
| Send a new email (plain text or HTML, CC/BCC) |
| Reply with proper threading (In-Reply-To, References) |
| Forward with original content quoted |
| Save an email draft to the Drafts folder |
| Send an existing draft and remove from Drafts |
| Apply a template with variable substitution |
| Schedule an email for future delivery |
| List scheduled emails by status |
| Cancel a pending scheduled email |
Manage (7)
Tool | Description |
| Move email between folders |
| Move to Trash or permanently delete |
| Mark as read/unread, flag/unflag |
| Batch operation on up to 100 emails |
| Create a new mailbox folder |
| Rename an existing mailbox folder |
| Permanently delete a mailbox and contents |
Labels (5)
Tool | Description |
| Discover available labels (auto-detects provider strategy) |
| Add a label to an email (ProtonMail folders, Gmail X-GM-LABELS, or IMAP keywords) |
| Remove a label from an email |
| Create a new label |
| Delete a label |
Watcher & Alerts (6)
Tool | Description |
| Show IMAP IDLE connections, folders being monitored, and last-seen UIDs |
| List available AI triage presets with descriptions and suggested labels |
| Show current hooks configuration — preset, rules, and custom instructions |
| Update alert/notification settings at runtime |
| Diagnose desktop notification support and provide setup instructions |
| Send a test notification to verify OS permissions are configured |
Verification (3)
Tool | Description |
| Find (or wait up to 45 s for) the newest OTP/2FA code or sign-in magic link, with optional clipboard copy |
| Diagnose clipboard integration (platform tools, concealed-write support) with optional round-trip test |
| Update verification-catching settings at runtime (incl. starting/stopping the catcher), optional persist to config file |
Calendar & Reminders (8)
Tool | Description |
| Extract ICS/iCalendar events from an email |
| Analyze an email to detect events and reminder-worthy content |
| Add an email event to the local calendar (macOS Calendar.app; on Linux hands the .ics to xdg-open); event notes link back to the source email |
| Create a reminder in macOS Reminders.app from an email; reminder notes link back to the source email |
| List all available local calendars (macOS Calendar.app; elsewhere a single placeholder) |
| Check whether the local calendar is accessible (macOS probes for real; elsewhere always reports accessible) |
| List events from the local calendar in a date range (macOS only; returns empty elsewhere) |
| List reminders from Reminders.app (macOS only) |
Prompts (7)
Prompt | Description |
| Categorize and prioritize unread emails with suggested actions |
| Summarize an email conversation thread |
| Draft a context-aware reply to an email |
| Compose a new email from provided context and instructions |
| Extract actionable tasks from email threads |
| Summarize upcoming calendar events from emails |
| Suggest emails to archive, delete, or unsubscribe from |
Resources (6)
Resource | URI | Description |
Accounts |
| List of configured accounts |
Mailboxes |
| Folder tree for an account |
Unread |
| Unread email summary |
Templates |
| One email template by name; the resource lists the available names |
Stats |
| Email statistics snapshot |
Scheduled |
| Pending scheduled emails |
Provider Auto-Detection
Provider | Domains |
Gmail | gmail.com, googlemail.com |
Outlook / Hotmail | outlook.com, hotmail.com, live.com, outlook.de, outlook.co.uk |
Yahoo Mail | yahoo.com, yahoo.co.uk, yahoo.de, ymail.com |
iCloud | icloud.com, me.com, mac.com |
Fastmail | fastmail.com, fastmail.fm |
ProtonMail Bridge | proton.me, protonmail.com, protonmail.ch, pm.me |
Zoho Mail | zoho.com, zohomail.com |
GMX | gmx.com, gmx.de, gmx.net, gmx.at, gmx.ch |
Architecture
src/
├── main.ts — Entry point and subcommand routing
├── server.ts — MCP server factory
├── logging.ts — MCP protocol logging bridge
├── cli/ — Interactive CLI commands
│ ├── account-commands.ts — Account CRUD (list, add, edit, delete)
│ ├── setup.ts — Legacy setup alias → account add
│ ├── test.ts — Connection tester
│ ├── config-commands.ts — Config management (show, edit, path, init)
│ ├── install-commands.ts — MCP client registration (install, status, remove)
│ ├── cache-commands.ts — Local-mirror CLI (status, clear, verify)
│ └── scheduler.ts — Scheduler CLI
├── config/ — Configuration layer
│ ├── xdg.ts — XDG Base Directory paths
│ ├── schema.ts — Zod validation schemas
│ ├── providers.ts — Provider auto-detection + OAuth2 endpoints (experimental)
│ └── loader.ts — Config loader (TOML + env vars)
├── cache/ — Local SQLite mirror
│ ├── schema.ts — Mirror schema + migrations
│ ├── store.ts — Read/write API over the mirror
│ └── sync-engine.ts — Background reconcile + body backfill
├── connections/
│ └── manager.ts — Lazy persistent IMAP/SMTP with OAuth2 (experimental)
├── services/ — Business logic
│ ├── imap.service.ts — IMAP operations
│ ├── label-strategy.ts — Provider-aware label strategy (ProtonMail/Gmail/IMAP keywords)
│ ├── smtp.service.ts — SMTP operations
│ ├── template.service.ts — Email template engine
│ ├── oauth.service.ts — OAuth2 token management (experimental)
│ ├── calendar.service.ts — ICS/iCalendar parsing
│ ├── scheduler.service.ts — Email scheduling queue
│ ├── watcher.service.ts — IMAP IDLE real-time watcher with auto-reconnect
│ ├── hooks.service.ts — AI triage via MCP sampling + static rules + auto-labeling/flagging
│ ├── notifier.service.ts — Multi-channel notification dispatcher (desktop/sound/webhook)
│ ├── clipboard.service.ts — Concealed clipboard writes for caught codes (zero deps)
│ ├── dialog.service.ts — Native confirm dialogs + browser hand-off (macOS)
│ ├── verification-catcher.service.ts — Instant OTP/magic-link catch on new mail
│ ├── presets.ts — Built-in hook presets (inbox-zero, gtd, priority-focus, etc.)
│ └── event-bus.ts — Typed EventEmitter for internal email events
├── tools/ — MCP tool definitions (52)
├── prompts/ — MCP prompt definitions (7)
├── resources/ — MCP resource definitions (6)
├── safety/ — Audit trail and rate limiter
└── types/ — Shared TypeScript typesRoadmap
Ideas raised but deliberately not built yet, kept here so they aren't lost or quietly re-proposed:
AgentMail as an alternative provider. AgentMail offers API-key-only, programmatic
@agentmail.toinboxes — no IMAP/SMTP host/port/TLS/credential setup, useful for spinning up throwaway agent-owned addresses. It would be additive (a second provider alongside real IMAP/SMTP accounts, not a replacement), but is a product/architecture decision — a new vendor dependency and tool surface — rather than a bug fix or gap-fill, so it's parked here rather than auto-built. Revisit if there's real demand for agent-provisioned inboxes rather than access to existing mailboxes.
Maintainers
Release prerequisites
release.yml checks these before publishing anything, so a missing one is a
fast red run rather than a half-published release:
Secret | Needed for |
| GoReleaser Pro license — |
| PAT with |
| npm publish |
| Docker Hub push (GHCR uses |
The Homebrew cask ships the unsigned Bun binary, so its postflight clears
the macOS quarantine bit — macOS only, since Linux has no xattr(1) and the
cask DSL's system_command raises on a non-zero exit. brew uninstall --zap
trashes the config, state, cache and data directories plus the two launch
agents.
Contributing
PRs accepted. Please conform to the standard-readme specification when editing this README.
# Development workflow
pnpm install
pnpm typecheck # type check
pnpm check # lint and format
pnpm build # build
pnpm start # runTesting
pnpm test # unit tests
pnpm test:integration # against a throwaway GreenMail server (Docker required)
pnpm smoke # most MCP tools against a real configured account (see caveats below)The integration suite runs each tool against a throwaway GreenMail server
(spun up via Testcontainers). Attachment coverage
(email-attachments.integration.test.ts) drives the real MCP tool layer and
locks in the size-awareness contract: every send reports each file's size and
the raw≈base64 total, the cap is enforced on the encoded total (what the
wire carries), an over-cap send fails closed (nothing is delivered), and
forwards carry the original attachments unless includeAttachments: false.
The effective cap is resolved per account: an explicit per-account
max_attachment_mb wins outright; otherwise the stricter of the global
[settings.send] cap and the provider's documented limit applies (Gmail,
Yahoo, and Proton 25 MB; Outlook.com, iCloud, and GMX 20 MB; Fastmail 50 MB).
pnpm smoke drives all tools over stdio the way a client does, which reaches
what GreenMail cannot: provider quirks, real-world MIME, and the macOS
Calendar bridges. It needs a working account, so it is not part of CI. Writes
stay inside a scratch folder and a draft it creates itself; it does not touch
Calendar, Reminders, notifications, or reply_email, which would send mail to
a real correspondent. Pass E2E_ACCOUNT=<name> to choose an account, otherwise
it uses the first one configured.
License
Available Tools
52 toolsadd_labelAdd labelAIdempotent
Add a label to an email. For ProtonMail, this copies the email into the corresponding Labels/ folder. For Gmail and standard IMAP, this sets a keyword flag on the message.
Inputs: label (required), account (required), emailId (required), mailbox (required).
add_to_calendarAdd to calendarA
Add an email event to the local calendar (macOS Calendar.app / Linux via xdg-open). Automatically extracts event data from the email: ICS attachments, meeting URL (Zoom/Teams/Meet), conference dial-in / ID / passcode, attendees, and email body excerpt. All relevant email attachments (PDFs, docs, etc.) are saved locally and linked in the event notes. A native confirmation dialog is shown on macOS before the event is written. Returns one of: added | cancelled | timed_out | no_display.
Inputs: account (required), email_id (required), confirm, mailbox, +3 more.
analyze_email_for_schedulingAnalyze email for schedulingARead-only
Analyze an email to detect calendar events and/or reminder-worthy content. Returns structured analysis so the AI can decide whether to call add_to_calendar, create_reminder, both, or neither. Use this as the first step before creating any scheduling resource.
Inputs: account (required), email_id (required), mailbox.
apply_templateApply templateA
Apply an email template with variable substitution. Use action "preview" to see the result, "draft" to save as draft, or "send" to send immediately.
Inputs: account (required), template (required), variables (required), to, +1 more.
bulk_actionBulk actionADestructive
Batch operation on multiple emails by UID list. Supports mark_read, mark_unread, flag, unflag, move, and delete. Max 100 IDs per call. Returns success/failure counts.
Inputs: ids (required), action (required), account (required), mailbox, +1 more.
cancel_scheduledCancel scheduledADestructive
Cancel a scheduled email. Removes it from the queue and deletes the associated draft.
Inputs: schedule_id (required).
check_calendar_permissionsCheck calendar permissionsARead-only
Check whether the local calendar is accessible. On macOS, verifies Calendar.app access (requires Privacy & Security → Calendars permission). Returns granted status and step-by-step setup instructions if access is denied.
Inputs: no named parameters.
check_clipboard_setupCheck clipboard setupARead-only
Diagnose the clipboard integration used for caught verification codes: platform tools, concealed-write support, current [settings.verification] values, and the watcher prerequisite. With test_write=true it performs a harmless clipboard round-trip (writes a test string, reads it back, clears it).
Inputs: test_write.
check_healthCheck healthARead-only
Check connection health, quota, and capabilities for email accounts, plus local cache size, coverage and hit ratio. Useful for diagnosing issues.
Inputs: account.
check_notification_setupCheck notification setupARead-only
Diagnose desktop notification support on this platform. Checks if required OS tools are available and provides setup instructions to enable notification permissions (macOS, Linux, Windows).
Inputs: no named parameters.
configure_alertsConfigure alertsA
Update alert/notification settings at runtime. Changes take effect immediately. Use save=true to persist changes to the config file. Omit any field to leave it unchanged.
Inputs: save, sound, desktop, webhook_url, +2 more.
configure_verificationConfigure verification catchingA
Update [settings.verification] at runtime — changes take effect immediately, including starting/stopping the ambient catcher via enabled. Use save=true to persist to config.toml (previous file is backed up). Omit every field to see the current configuration. Registered in read-only mode too: this writes config, never the mailbox.
Inputs: save, notify, enabled, accounts, +8 more.
create_labelCreate labelA
Create a new label. For ProtonMail, creates a folder under Labels/. For standard IMAP keywords, labels are auto-created on first use — this is a no-op.
Inputs: name (required), account (required).
create_mailboxCreate mailboxAIdempotent
Create a new mailbox (folder). Use '/' as separator for nested folders (e.g., 'Work/Projects'). Use list_mailboxes to see existing folders.
Inputs: path (required), account (required).
create_reminderCreate reminderA
Create a reminder in macOS Reminders.app from an email. Shows a native confirmation dialog before adding. The reminder notes include a message: deep link that reopens the source email in Mail.app. Use for action items, deadlines, and follow-up tasks extracted from emails. Use analyze_email_for_scheduling first to let the AI decide if a reminder is appropriate.
Inputs: account (required), email_id (required), notes, title, +5 more.
delete_emailDelete emailADestructive
Delete an email. By default moves to Trash. Set permanent=true for permanent deletion (⚠️ irreversible). The mailbox must be a real folder. Use find_email_folder first if the email was found in a virtual folder.
Inputs: account (required), emailId (required), mailbox, permanent.
delete_labelDelete labelADestructive
Delete a label. For ProtonMail, deletes the label folder. For standard IMAP keywords, labels cannot be deleted server-wide — use remove_label on individual emails.
Inputs: name (required), account (required).
delete_mailboxDelete mailboxADestructive
⚠️ DESTRUCTIVE: Permanently delete a mailbox and ALL its contents. This cannot be undone. Use list_mailboxes to verify the folder path.
Inputs: path (required), account (required).
download_attachmentDownload attachmentARead-only
Download an email attachment by filename or part id. First use get_email to see available attachments with their filenames and partIds (partId is unambiguous — use it for duplicate filenames and inline cid images). Returns base64-encoded content for files ≤5MB. saveToDownloadDir is unavailable — no attachment download directory is configured on this server.
Inputs: id (required), account (required), partId, saveAs, +3 more.
extract_calendarExtract calendarBRead-only
Extract calendar events (ICS/iCalendar) from an email. Returns structured event data including time, location, attendees, and status.
Inputs: account (required), email_id (required), mailbox.
extract_contactsExtract contactsARead-only
Extract unique contacts from recent email headers. Returns contacts sorted by frequency (most frequent first). Useful for finding frequent correspondents or building an address book.
Inputs: account (required), limit, mailbox.
find_email_folderFind email folderARead-only
Find a real mailbox folder an email belongs to. Required before move_email or delete_email when the email was found in a virtual folder (e.g., "All Mail", "Starred"). Returns the first real folder path found, to use as sourceMailbox; on a label-based server a message may also sit in other folders.
Inputs: account (required), emailId (required), sourceMailbox.
forward_emailForward emailA
Forward an email to new recipients with optional additional message. The original is converted to readable text and "> "-quoted below your note; its attachments are re-attached unless includeAttachments is false. Set html=true to also send an HTML part carrying the original HTML in a blockquote.
Inputs: to (required), account (required), emailId (required), cc, +4 more.
get_emailGet emailARead-only
Get the full content of a specific email by ID. Does NOT mark the email as seen (uses IMAP BODY.PEEK — non-destructive). format: text (default) = HTML converted to readable text with markdown; stripped = text minus quoted replies and signatures; full = raw body part (HTML mail returns raw HTML). Body is capped at 10000 chars by default — pass maxLength (0 = unlimited) to change. Set markRead=true only when you want to explicitly mark the email as read. A Bulk: line appears for list or machine-generated mail (📰 newsletter / 🤖 automated), derived from RFC headers, with the unsubscribe URI when the sender offers one.
Inputs: account (required), emailId (required), format, mailbox, +2 more.
get_emailsGet multiple emailsARead-only
Fetch the full content of multiple emails in a single call (max 20). More efficient than calling get_email repeatedly when triaging or summarising several emails. Does NOT mark emails as seen. Defaults to format="text" (HTML stripped) for compact, AI-friendly output. Each message carries the same 📰 newsletter / 🤖 automated marker as get_email.
Inputs: ids (required), account (required), format, mailbox, +1 more.
get_email_statsGet email statsARead-only
Get email statistics and analytics for a mailbox. Shows volume, top senders, daily trends, and read/flagged counts.
Inputs: account (required), period, mailbox.
get_email_statusGet email statusARead-only
Get the current read/flag/label state of an email without fetching its body. Much cheaper than get_email when you only need to check whether an email is unread, flagged, or which labels it has. Also useful to confirm the result of a mark_email call. Does NOT mark the email as seen.
Inputs: account (required), emailId (required), mailbox. Returns: id, date, from, seen, +5 more
get_hooks_configGet hooks configARead-only
Get the current AI hooks configuration including preset, rules, and custom instructions.
Inputs: no named parameters.
get_threadGet threadARead-only
Reconstruct a full email conversation thread by following References and In-Reply-To headers. Returns all related messages. Does NOT mark emails as seen. format: text (default) = HTML converted to readable text with markdown; stripped = text minus quoted replies; full = raw body part. Each body is capped at 5000 chars by default — pass maxLength (0 = unlimited) to change. Use newestFirst=true to show the most recent message in full and older messages as header-only summaries. Use get_email first to obtain the message_id. Each message carries a Bulk line when it is list or machine-generated mail.
Inputs: account (required), message_id (required), format, mailbox, +2 more.
get_verification_codeGet verification codeA
Find the newest verification code (OTP/2FA) or sign-in magic link in recent mail, using false-positive-resistant scoring (EN/DE). Use wait_seconds (max 45) to wait for a mail still in flight; if nothing arrives, simply call again. Works without the watcher via polling — with [settings.watcher] enabled, hits land within ~1 second. Set copy_to_clipboard=true to also place the value on the clipboard (concealed from clipboard managers, auto-cleared). The code/link is returned in the result by design.
Inputs: account, mailbox, wait_seconds, lookback_minutes, +1 more. Returns: code, from, hint, kind, +11 more
get_watcher_statusGet watcher statusBRead-only
Get the status of IMAP IDLE watcher connections and recent activity.
Inputs: no named parameters.
list_accountsList accountsARead-only
List all configured email accounts. Call this first to discover available account names for use with other tools.
Inputs: no named parameters.
list_calendarsList calendarsARead-only
List all available local calendars (macOS Calendar.app / Linux default). Use the returned calendar names with add_to_calendar to target a specific calendar.
Inputs: no named parameters.
list_emailsList emailsARead-only
List emails in a mailbox with optional filters. Returns paginated results with metadata (read/unread 🔵, flagged ⭐, replied ↩️, attachments 📎, labels 🏷️). List or machine-generated mail is marked 📰 newsletter / 🤖 automated from its RFC headers, with the unsubscribe URI when the sender offers one — personal mail carries no marker. Use get_email to fetch full body content. ProtonMail note: labels are represented as IMAP folders — use list_labels to discover them, then list_emails with mailbox="Labels/X" to find labeled emails.
Inputs: account (required), from, page, seen, +9 more. Returns: page, cache, count, total, +5 more
list_eventsList eventsARead-only
List local calendar events with optional filters. Search by title, date range, or calendar name. Use to check for existing events before creating new ones, or to verify a recently added event. Returns event id, title, start/end time, location, and calendar name.
Inputs: to, from, limit, title, +1 more.
list_labelsList labelsARead-only
List available labels for an email account. Auto-detects the label system: ProtonMail folder-labels, Gmail X-GM-LABELS, or IMAP keywords. ProtonMail note: labels are represented as IMAP folders under the Labels/ prefix. Use list_emails with mailbox="Labels/" to find emails tagged with a ProtonMail label.
Inputs: account (required).
list_mailboxesList mailboxesARead-only
List all mailbox folders for an account with unread counts and special-use flags. Use list_accounts first to get the account name.
Inputs: account (required). Returns: count, account, mailboxes
list_presetsList presetsARead-only
List all available AI triage presets with their descriptions and suggested labels.
Inputs: no named parameters.
list_remindersList remindersARead-only
List reminders from macOS Reminders.app with optional filters. Search by title or list name. By default only shows incomplete reminders. Use to check for existing reminders before creating new ones, or to verify a recently added reminder. Returns reminder id, title, due date, completion status, priority, and list name.
Inputs: limit, title, list_name, include_completed.
list_scheduledList scheduledBRead-only
List scheduled emails. Shows pending, sent, or all scheduled emails.
Inputs: status, account.
list_templatesList templatesARead-only
List all available email templates. Templates are TOML files in ~/.config/email-mcp/templates/ with {{variable}} placeholders for subject and body.
Inputs: no named parameters.
mark_emailMark emailAIdempotent
Change email flags — mark as read/unread, flag/unflag. Idempotent: marking an already-read email as read is a no-op.
Inputs: id (required), action (required), account (required), mailbox.
move_emailMove emailAIdempotent
Move an email to a different mailbox folder. The sourceMailbox must be a real folder, not a virtual one like "All Mail". Use find_email_folder first if the email was discovered in a virtual folder.
Inputs: account (required), emailId (required), sourceMailbox (required), destinationMailbox (required).
remove_labelRemove labelAIdempotent
Remove a label from an email. For ProtonMail, this removes the email from the label folder. For Gmail and standard IMAP, this removes a keyword flag.
Inputs: label (required), account (required), emailId (required), mailbox (required).
rename_mailboxRename mailboxA
Rename an existing mailbox (folder). Use list_mailboxes to see current folder paths.
Inputs: path (required), account (required), new_path (required).
reply_emailReply emailA
Reply to an email with proper threading (In-Reply-To & References headers). Use get_email first to read the original. The original message is quoted below the reply by default (Thunderbird/Outlook style) — set quoteOriginal=false for the bare body only.
Inputs: body (required), account (required), emailId (required), html, +4 more.
save_draftSave draftA
Save an email draft to the Drafts folder. Compose over time, then use send_draft to send it. Use list_emails with the Drafts mailbox to see saved drafts.
Inputs: body (required), account (required), subject (required), cc, +5 more.
schedule_emailSchedule emailA
Schedule an email to be sent at a specific time in the future. The email is queued locally and sent automatically when the time arrives.
Inputs: to (required), body (required), account (required), send_at (required), +6 more.
search_emailsSearch emailsARead-only
Search emails by keyword across subject, sender, and body — and, where the local mirror is enabled, attachment filenames (searching "report" finds an email carrying Q3-report.pdf). Omit query (or pass an empty string) to use it as a pure filter — e.g. find all emails from a specific sender without a keyword. Supports additional filters for sender, recipient, date range (since/before — INTERNALDATE; sent_since/sent_before — Date: header), attachments, size, and reply status. At least one of query/from/to/since/before/sent_since/sent_before/has_attachment/larger_than/smaller_than/answered must be set; otherwise the call errors (use list_emails to browse without filters). Results carry the same 📰 newsletter / 🤖 automated markers as list_emails.
Inputs: account (required), to, from, page, +12 more. Returns: page, cache, count, total, +5 more
send_draftSend draftADestructive
Send an existing draft email and remove it from Drafts. The draft is fetched, sent via SMTP, then deleted. Use list_emails with the Drafts mailbox to find draft IDs.
Inputs: id (required), account (required), mailbox.
send_emailSend emailB
Send a new email. Supports plain text or HTML body, CC, and BCC.
Inputs: to (required), body (required), account (required), subject (required), +4 more.
test_notificationTest notificationA
Send a test desktop notification to verify that OS permissions are correctly configured. Use check_notification_setup first to diagnose any issues.
Inputs: sound.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 52 tools
Many tools target the same resource with overlapping actions—e.g., list_emails vs search_emails, get_email vs get_emails vs get_email_status, and mark_email vs bulk_action—so an agent could easily misselect despite detailed descriptions. Calendar/reminder tools (extract_calendar, analyze_email_for_scheduling, add_to_calendar) also have blurry boundaries. Some overlap exists but descriptions help.
Tool names consistently use snake_case and mostly follow a verb_noun pattern (list_emails, send_email, create_label, delete_mailbox). A few deviations like bulk_action (noun phrase) and list_scheduled (missing noun) are minor. The convention is predictable overall.
52 tools is far beyond the 3–15 typical for a well-scoped server and exceeds the 50+ threshold for extreme mismatch. Even accounting for the broad email+calendar+reminders scope, the surface is heavily overloaded with many niche configuration and diagnostic tools.
Email lifecycle is thoroughly covered: list/get/search/send/reply/forward/move/delete/mark/label/draft/schedule/templates. However, calendar and reminder operations lack update/delete tools, and account management (add/remove) is absent, leaving some dead ends.
Maintenance
Related MCP Connectors
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
Email inboxes for AI agents: send, receive, reply, search, and manage threaded email over MCP.
Hosted email MCP for AI agents with inboxes, send/receive, memory, recovery, and credits.
An MCP server that provides email capabilities, hosted on Alpic platform
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAn MCP server that enables AI models to read, search, and send emails via IMAP and SMTP protocols. It supports various providers like Gmail and Outlook, allowing for tasks such as retrieving unread messages, searching by sender, and managing mailbox folders.-
- AlicenseBqualityCmaintenanceAn MCP server that exposes IMAP operations as tools for AI assistants, enabling email management including listing mailboxes, reading, searching, moving, flagging emails, and creating drafts.721 npmMIT
- AlicenseAqualityDmaintenanceAn MCP server providing comprehensive email capabilities via IMAP and SMTP, enabling AI assistants to read, search, send, manage, schedule, and analyze emails across multiple accounts.49905 npmLGPL 3.0
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to read, search, send, manage, schedule, and analyze emails across multiple IMAP/SMTP accounts via MCP, with features like OAuth2, calendar extraction, analytics, and real-time email watching.222 npmLGPL 3.0