universal-email-mcp
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., "@universal-email-mcpfind the latest invoice email in all my mailboxes and summarize it"
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.
universal-email-mcp
Status: under development — not usable yet. Version 0.0.1 only reserves the package name. Follow the design plan.
A vendor-neutral Model Context Protocol server that connects AI assistants (Claude, ChatGPT, any MCP client) to ordinary mailboxes over IMAP, POP3 and SMTP — for everyone whose mail is not on Gmail: web hosters, self-hosted servers, Exchange on-prem, and the like.
Planned highlights:
Several accounts per user — search across all IMAP/POP3 mailboxes at once, send from the right identity automatically, per-account permissions (e.g. read-only).
Smart retrieval — time windows, fuzzy matching of people, subjects and folders, contact lookup from your mail history.
Two modes — a local single-user server (stdio) and a multi-user remote server with built-in OAuth 2.1 and a self-service portal.
Safe by design — mail is treated as untrusted input; the server checks recipients (new or look-alike addresses) before anything is sent; no permanent deletion.
Operable — encrypted credentials, pseudonymous audit logging, admin policy; reference deployment on Google Cloud Run (
docs/deploy-gcp.md).
Try the probe
The first working piece is a read-only diagnostic that logs in to an IMAP server and shows what it supports (no message content is displayed). From a checkout:
UEM_PASSWORD=... uv run universal-email-mcp probe --server mail.example.com --user alice@example.com--server takes a preset name (e.g. united-domains) or a host name; --starttls
and --port select other connection modes, --account NAME uses an account from
the local config file (see docs/config.example.toml).
Related MCP server: MCP Email
Local mode (stdio)
universal-email-mcp local runs the MCP server over stdio for one user, with the
accounts from the config file (docs/config.example.toml;
default ~/.config/universal-email-mcp/config.toml, or --config PATH / UEM_CONFIG).
Passwords come from environment variables (password_env) or the OS keyring —
never from the file. Six read tools are always there:
Tool | What it does |
| accounts, permissions, server features, quota, identities, limits, plus a cheap overview (unread in INBOX, Drafts/Junk counts, number of folders) and the folder map |
| top level first with subfolder counts ( |
| time window ( |
| headers, text body (paged, fenced as untrusted), attachments; |
| one attachment by the id |
| recent correspondents; |
Accounts with the organize and/or delete permission (default: read only)
add tools that change mail. A tool no account allows — or any tool under
[policy] read_only = true — is not offered at all, and each call is checked
again against the permissions of the account each message belongs to:
Tool | Permission | What it does |
| organize | read/unread and flagged, by message id (batch, capped) |
| organize | into another folder (exact folder name, unique leaf name or role; a typo or ambiguous name changes nothing and returns the candidates); moved messages get new ids. |
| organize | a folder (nested levels, |
| delete | moves to Trash (recoverable); mail already in Trash stays; there is no permanent deletion |
| save_draft | drafts | writes a draft into the account's Drafts folder: new, reply (reply_to_id, reply_all), forward (forward_id, attaches the original's files only), or replaces an earlier one (draft_id); never sends |
| send_message | an identity with send = true | sends a saved draft (draft_id) or a new message (the save_draft arguments; it is saved as a draft first). Irreversible, so: every recipient is classified (internal / written to before / new / look-alike of an address you know), the policy decides, and normally you are asked to confirm in your client; see below |
save_draft picks the sender among your configured identities (from names one;
else the identity the original was addressed to, else the default; a made-up
address is refused), adds the signature and the quoted original, validates every
address and refuses line breaks in headers. A reply goes to the original's
Reply-To (else From), like in a mail client, and warns when that points to another
domain. The result previews the draft and warns about recipients you never wrote
to. An identity needs store_account (or account) for its drafts; no SMTP server
is needed for drafts. Plain text only.
Sending is the riskiest tool and therefore the most guarded (design plan section 8):
The message always exists as a draft first; a declined, impossible or failed send leaves it in Drafts (the result and error hints give its id).
SMTP goes through the same SSRF-safe connector as IMAP: the host is resolved once, every address checked, TLS >= 1.2 verified against the host name. Implicit TLS (465) or STARTTLS (587) - a server without STARTTLS is refused before any login; there is no plain-text mode. The envelope sender is the identity address, a
Bccheader is never transmitted, and if the server refuses any recipient nothing is sent. A broken connection after the body went out is reported asSEND_OUTCOME_UNKNOWN(never retried).Confirmation (
[policy] send):confirm(default) asks the user through MCP elicitation, showing sender, To/Cc/Bcc with their class and warnings, subject, attachments and the start of the text;confirm-externalasks unless every recipient is internal;onasks only for look-alikes;draftnever sends;offremoves the tool. A look-alike recipient is always put to the user. A client that cannot elicit (or a user who declines) means: nothing is sent, the mail stays a draft and must be sent from the mail client. (Remote mode can instead park it for approval in the portal, see below.)Recipient classes:
internal= your own identity addresses and[policy] internal_domains;known= you wrote to it (Sent, To/Cc, 2 years);new;lookalike= a typo or confusable (digits for letters, Cyrillic or Greek letters,xn--homographs, mixed scripts, another top-level domain) of an address or domain you know - including a known address that has a near-twin (you once mistypedoliver.grnat@and it went out).Hard limits:
allowed_recipient_domains,max_recipients,max_sends_per_hour/max_sends_per_dayper SMTP account,limits.max_send_bytesand the server's SIZE.Afterwards: a copy in Sent (identity
save_sent), the draft removed (UID-scoped),\Answeredon exactly the replied-to message (its account needsorganizeordrafts), and -file_replies = "both"is the default - the copy of a reply also goes into the user folder the original is filed in (sent/thread_folderare options).The confirmation shows the whole new text (up to 3000 characters / 80 lines; what is cut is announced with its size), who the original of a reply or forward is (sender, date, subject, first lines; a warning if the subject does not match), and up to 20 attachments with sizes.
One audit line per attempt on stderr (JSON; counts per class, size bucket, outcome - never addresses, subjects or text; see
docs/audit.md).
The archive scheme of an account (archive_scheme = auto | flat | yearly |
monthly, see docs/config.example.toml) is detected from the archive folder's
subfolders; an empty archive is flat. Each message goes to the folder of its
Date header when plausible (not before 1990, not later than the arrival date + 1 day), else its arrival date (INTERNALDATE).
Changes are reported per message. They use UIDs checked against the folder's
UIDVALIDITY, UID MOVE (or UID COPY + UID EXPUNGE with UIDPLUS, otherwise
refused), and never a plain EXPUNGE. Try them on the sandbox mailbox
(scripts/dev_mailbox.py, see below) before pointing them at a real one.
query works the same everywhere: with * or ? it is a case-insensitive,
umlaut-folded wildcard pattern that starts at a word start (hub* finds "Anna
Huber", *gmbh; inside a word: *ub*). For folders a pattern without /
matches the folder's own name at any level, one with / its path, where *
also crosses levels (clients/m*, */2025). Anything else is matched fuzzily
(typos, Müller/Mueller, name order). Lists are bounded; each result's footer says how
to narrow it or fetch the next page (cursor).
Attachment downloads. Files too big for a tool result (and every attachment
get_message lists) come with a download link, http://127.0.0.1:<port>/a/<token>,
served by a small listener on the loopback interface for as long as the server runs.
The token is signed with a key that is random per run and expires after
[downloads] link_ttl (default 24 h), so a link stops working when the server
stops — ask for a fresh one. The file is streamed from the mailbox in chunks and
decoded on the fly (never held in memory whole, never marks mail as read); it is
served as a download with nosniff and a sandboxing CSP, and requests with any
other Host header are refused. Set [downloads] enabled = false to switch it off
or port = … for a fixed port.
POP3 accounts (kind = "pop3", read-only) work in the read tools, in one search
together with IMAP accounts, but POP3 is a much smaller protocol:
One folder,
INBOX. There is no read/unread or flagged state: messages show "unknown", never "unread", and theunread/flaggedcriteria are ignored (the result says so).Ids are built from the server's
UIDL, so they stay valid across sessions (a server withoutUIDLorTOPis refused). Headers come fromTOP n 0, newest first, and are cached per account by UIDL; a later call only reads what is new.find_messageswith header criteria, time windows orquerylooks at the newestlimits.max_headers_scannedmessages (the notes say so; a call has a time budget for loading headers and continues on the next call). Without criteria all messages are listed. POP3 has no search:bodyand the fuzzy "also in the body" match only see the headers. Dates come from the firstReceivedheader (POP3 has no arrival date), elseDate.Whole messages are read with
RETR, capped bylimits.max_message_bytes: a larger message is read partially (TOP) and marked truncated. Attachments are numbered by this server's own MIME parse andget_attachmentreads the message again; there are no download links for POP3 mail.Nothing is ever changed or deleted on the server: no
DELE, noRSET, the session ends withQUIT, and write tools refuse POP3 ids ("POP3 accounts are read-only"). Fresh mail appears after a new connection (a listing older than 20 s reconnects).TLS is mandatory: implicit TLS (995) or
STLS(110), verified, TLS 1.2 or newer.
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"email": {
"command": "uvx",
"args": ["universal-email-mcp", "local"],
"env": { "UEM_WORK_PASSWORD": "…" }
}
}
}Claude Code:
claude mcp add email -e UEM_WORK_PASSWORD=… -- uvx universal-email-mcp localAdd "--config", "/path/to/config.toml" to the arguments for a non-default config
file. Until the first functional release is on PyPI, run it from a checkout
instead: "command": "uv", "args": ["run", "--directory", "/path/to/universal-email-mcp", "universal-email-mcp", "local"].
What the assistant knows at the start: the server instructions include a folder map per account, read when the server starts (all accounts in parallel, 3 seconds at most; an account that is slow or down shows "not read at startup" and the server starts anyway):
Work: INBOX, Drafts, Sent, Archive ▸ 8 (yearly: 2019 … 2026), Trash,
Clients ▸ 87 (e.g. Huber, Müller, Schmidt …), Projects ▸ 12, PersonalSpecial folders come first (labelled with their role where the name differs, e.g.
Gesendet (sent)), then the other top-level folders alphabetically; ▸ N is the
number of direct subfolders, with a few example names or, for the archive, its
scheme. The map is capped (30 folders, about 1500 characters per account), shows
only your own folders (never other users' or shared ones) and treats every name as
untrusted data. account_info returns the current map, so a long-running session
can refresh it.
Results come as Markdown tables (mail text escaped, links defanged) plus structured JSON; message bodies are fenced as untrusted content, so the assistant can tell mail from instructions.
Remote mode (preview)
universal-email-mcp serve runs the server over HTTP (Streamable HTTP, stateless:
protocol 2026-07-28 and the legacy transport) with /health and /ready.
OAuth mode (the default): the server is its own OAuth 2.1 authorization server
(authorization code + PKCE S256, refresh-token rotation, revocation, Client ID Metadata
Documents with Dynamic Client Registration as fallback). Users sign in with their mailbox
login, see a consent page and grant the connecting AI client what it may do; /mcp needs
the resulting access token. A self-service portal at /portal lets each user add
their mail accounts (IMAP or POP3, connection tested before it is saved), set permissions,
manage sender identities, see the connected AI clients and disconnect or reduce them, and read
their own Activity feed (what the clients did, in plain words)
(docs/portal.md).
/mcp serves each user's own accounts from the store with exactly the tools the client's
grant allows (read, organize, delete, drafts, and send_message when the grant, the
sender identity and the operator's policy all allow it). A send asks the user through MCP
elicitation (the continuation state is sealed, bound to user and grant, and cannot be forged
or replayed); a client that cannot ask gets the operator's SEND_FALLBACK: keep a draft, or
park the message under Pending approvals in the portal, where the user reads exactly what
would go out and approves it with a password check. Messages in tool results link to a message viewer in the portal
(PUBLIC_URL/m/<id>: text, HTML in a sandbox with no remote content unless clicked, conversation,
raw headers, .eml, attachment downloads streamed from the mail server; the portal session
authorises, no tokens in the links). How it works and how to run it: docs/oauth.md;
all variables: docs/operator-env.md.
export PUBLIC_URL=http://127.0.0.1:8080 STORE_BACKEND=memory # development store
export LOGIN_DOMAINS=company.example=mail.company.example # who may sign in, and where
uv run universal-email-mcp serve --host 127.0.0.1
claude mcp add --transport http email http://127.0.0.1:8080/mcpDev mode (temporary, with the mail tools): with UEM_DEV_TOKEN (or --insecure-local,
loopback only, no token) /mcp takes one static bearer token and serves the accounts of a
local config file:
export UEM_DEV_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
export ALLOWED_HOSTS=localhost,127.0.0.1
uv run universal-email-mcp serve --config config.local.toml --host 127.0.0.1 --port 8080
claude mcp add --transport http email http://localhost:8080/mcp --header "Authorization: Bearer $UEM_DEV_TOKEN"Reading the audit log: universal-email-mcp audit summarises audit lines (files, stdin or a
gcloud logging read --format=json export): per tool, failed sign-ins, sends, one user or client
(--user alice@example.org recomputes the pseudonym from PSEUDONYM_KEY). See
docs/audit.md.
Remote mode: stored data
Remote mode keeps users, accounts, sessions and tokens in a store (memory or Firestore, secrets encrypted with a rotatable key ring, no mail content): see docs/stored-data.md.
Documentation
For | Document |
Operators: local and remote setup, policy, keys, backups, monitoring, incidents, troubleshooting | |
End users: connecting a client, the portal, sending and approvals, safety tips | |
Data protection: GDPR notes and a fill-in DPIA | |
Reference (remote mode) |
|
Design and roadmap |
Development
To try the server without a real mailbox, start the sandbox: a throw-away local Dovecot (podman or docker) with a few weeks of realistic German/English mail and hostile samples (prompt injection, crafted headers, broken encodings):
uv run scripts/dev_mailbox.py up # prints the probe and `claude mcp add` commands
uv run scripts/dev_mailbox.py downContributor guidelines and checks: AGENTS.md.
License
Apache License 2.0 — see LICENSE and NOTICE. Copyright 2026 ARJOMA FlexCo.
This server cannot be deployed
Maintenance
Related MCP Connectors
Email inboxes for AI agents: send, receive, search, drafts with owner approval, and webhooks.
Email inboxes and calendars for AI agents: send, receive, search, draft and schedule.
Email inboxes and calendars for AI agents: send, receive, search, draft and schedule.
- WorkwaeOAuthcom.workwae
Email, calendar and contacts for AI agents: Gmail, Microsoft 365 and IMAP accounts behind one API.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to read, search, and manage email via IMAP, and send emails with attachments via SMTP, using natural language commands.386 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to read, send, organize, and search emails across multiple IMAP/SMTP mailboxes.3,017 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to search, read, send, move, mark, and delete email across multiple IMAP/SMTP accounts, with secure AES-256-encrypted credential storage and connection pooling.800 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to securely manage multiple email accounts via IMAP and SMTP, including searching, reading, sending, replying, forwarding, organizing folders, marking, and bulk deleting messages.800 npmMIT