mailmux
A self-hosted MCP server that unifies multiple IMAP/SMTP mailboxes into one agentic inbox, enabling agents to list, search, read, and send email through MCP tools.
README
mailmux
Free, self-hosted multi-mailbox agentic inbox.
Connect any IMAP/SMTP mail. One unified inbox in the browser. One MCP surface for every agent.
No paid SaaS required for core receive + send. MIT licensed.
Quick start
cd Projects/mailmux # or your clone path
npm install
./scripts/start.sh --fixture
# equivalent: npm run dev -- --fixture
Open http://127.0.0.1:8787 — on localhost the UI auto-loads the bearer token.
Fixture mode seeds two demo mailboxes (personal, work) so you can try the UI and MCP without real credentials.
Real mail
npm run dev
In the UI: Connect mailbox → pick a preset (Gmail / Fastmail / Outlook / iCloud) or enter IMAP/SMTP hosts → use an app password where required.
Production-ish start
npm run build
npm start
# or: node dist/cli.js serve
Using the hosted interface
mailmux has one web interface: the Next.js app in apps/web, built as a static export. You can serve it from your own process or host it elsewhere. Either way it talks to the mailmux server on your machine, and it never sends your mail or your token anywhere else.
Local (recommended — works in every browser)
npm run build
npm start
npm run build compiles the server, builds apps/web, and copies the export to web-next/. Open the URL it prints, normally http://127.0.0.1:8787. Same origin as the API, so nothing else is needed.
To run it on its own during development:
cd apps/web && npm install && npm run dev # http://localhost:3000
cd apps/web && npm run build && npm run serve # the real static export
Hosted (deployed somewhere else)
The page runs entirely in your browser and fetches mail directly from your machine.
1. Deploy apps/web. On Vercel and equivalents, set the project's Root Directory to apps/web and leave the build and output commands on auto. That setting is what keeps the CLI's better-sqlite3 out of the front-end install; it lives in the dashboard and cannot be expressed in a file. Do not add a workspaces key to the root package.json.
No environment variable is required. One optional, non-secret variable exists:
| Name | Value | Purpose |
|---|---|---|
NEXT_PUBLIC_DEFAULT_API_BASE |
http://127.0.0.1:8787 |
The pre-filled Server URL on a browser with nothing in localStorage. Public by definition — it is inlined into the bundle at build time. Omit it and the same default comes from apps/web/src/lib/constants.ts. |
2. Allow the origin. On your machine, not on the host:
MAILMUX_ALLOWED_ORIGINS=https://your-deployment.example.com mailmux serve
See the rules and the cost of doing this under Browser origins below.
3. Copy the token. mailmux serve prints it on first run; it is also in bearer.token inside your data directory (~/.mailmux by default).
4. Point the page at your server. Open the deployed page, click Set up mailmux, and enter the Server URL and the token. Both are stored in your browser's localStorage and are sent only to the server URL you entered.
5. Allow local network access (Chrome, Edge, Brave). Since Chromium 142 the browser asks permission before a website may reach 127.0.0.1. Allow it when prompted; if you dismissed the prompt, re-enable it under Site settings → Apps on device.
Safari, and the mixed-content limit
A page served over https cannot reach an http address. For 127.0.0.1 and localhost Chromium and Firefox make an exception; WebKit does not, and there is no workaround (WebKit bug 171934, still open). This is also why MAILMUX_ALLOWED_ORIGINS drops http:// entries: the configuration that would avoid the block is the one that makes the allowlist spoofable.
If your server is not on loopback, put it behind https or reach it over a tunnel. Otherwise use the local build — run mailmux serve and open http://127.0.0.1:8787 directly. It is the same interface.
What the host can see
Nothing. The deployed page has no server-side code: no API routes, no server actions, no proxy, no middleware. Your token, your mail credentials and every message body travel only between your browser and your own machine.
Desktop app
A window instead of a terminal, for people who do not want either. apps/desktop is an Electron shell: it starts the same server inside its own process, binds 127.0.0.1, and uses the same ~/.mailmux data directory, master key and bearer token. An account connected in the desktop app is the same account your agents reach over MCP.
npm run build # repository root: server + UI export
cd apps/desktop
npm install # its own package.json and lockfile
npm run dev # opens the window
npm install rebuilds better-sqlite3 against Electron's ABI (electron-builder install-app-deps). If Electron's own binary is missing afterwards — this repo blocks install scripts unless allowScripts names them — run node node_modules/electron/install.js once.
Installers:
npm run dist:mac # mac: signed dmg in apps/desktop/release/
npm run dist # other platforms — nsis or AppImage
On mac, dist:mac signs the app and the dmg with a Developer ID certificate pinned by hash in scripts/sign-mac.sh (electron-builder's by-name signing is ambiguous when the keychain holds two same-named certificates). Notarization is a separate, credential-holding step; the commands are at the top of that script. npm run dist builds for the platform it runs on. Both scripts re-copy the compiled server and the UI export out of the repository root first, so run the root npm run build again after any server change. The port follows MAILMUX_PORT (default 8787); if something already holds it — mailmux serve in a terminal, or a second copy of the app — the window does not open and the app says so.
Agent MCP (any client)
HTTP MCP (Cursor / remote-capable clients)
{
"mcpServers": {
"mailmux": {
"url": "http://127.0.0.1:8787/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}
Token lives in ~/.mailmux/bearer.token (or MAILMUX_TOKEN).
stdio MCP (Claude Desktop / Claude Code)
npm run mcp
# or: npx tsx src/cli.ts mcp
{
"mcpServers": {
"mailmux": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/mailmux/src/cli.ts", "mcp"],
"env": {
"MAILMUX_DATA_DIR": "/Users/you/.mailmux"
}
}
}
}
Tools
| Tool | Purpose |
|---|---|
accounts_list |
Connected aliases |
messages_list |
Inbox list (account: alias or all) |
messages_search |
Free-text search |
message_get |
Full body |
message_send |
Send (confirm in your agent) |
chat_await_message |
Wait for the user's next message in the mailmux window |
chat_say |
Answer them there |
chat_activity |
Post a one-line "here is what I am doing" |
chat_history |
Re-read the conversation |
Accounts are connected once in the web UI (or API). Agents reuse the same store — no per-agent OAuth.
Talking to your agent inside mailmux
The Agent view is the app's first screen, and mailmux runs no model behind it.
The agent is whichever MCP client you already use — Claude Code, Codex, Cursor,
Claude Desktop — and the four chat_* tools above are how it holds the
conversation in the mailmux window instead of in its own terminal. There is no
per-client integration: a long-polling tool call is the one capability every MCP
client has.
Connect the client as above, then say this to it once, in its own window:
You are my mailmux inbox agent. Use the mailmux MCP tools.
Loop: call chat_await_message, do the work, post the answer with chat_say, then
call chat_await_message again. Keep going until I tell you to stop.
Everything I read appears in the mailmux window, so every answer must go through
chat_say — do not answer here. A chat_await_message that returns no message is
normal; call it again. Use chat_activity for anything slow. Draft rather than
send unless I ask you to send.
The kickoff is not optional and cannot be automated away: MCP is client-driven, so nothing on the mailmux side can make an agent start listening. Anything you type before one does is queued and delivered when it arrives.
Notes on what the UI claims. "Listening" means an agent is parked in an open
chat_await_message — a request that is open right now, not an inference. It
never says "connected", because a stateless POST /mcp cannot tell a configured
client from one that was never started. Each message goes to exactly one agent,
so do not point two at the same server. The conversation is stored in
~/.mailmux/mailmux.db, encrypted with the same master key as the account
passwords, because an agent summarising an inbox puts mail content in those rows.
Install options
| Method | Command |
|---|---|
| Dev | npm install && npm run dev |
| Fixture demo | npm run dev -- --fixture |
| Built | npm run build && npm start |
| Init data dir | npx tsx src/cli.ts init |
Env
| Variable | Default | Meaning |
|---|---|---|
MAILMUX_DATA_DIR |
~/.mailmux |
SQLite + keys |
MAILMUX_HOST |
127.0.0.1 |
Bind address — see below |
MAILMUX_PORT |
8787 |
Port |
MAILMUX_TOKEN |
auto file | API/MCP bearer |
MAILMUX_MASTER_KEY |
auto file | AES key for passwords — see below |
MAILMUX_FIXTURE |
off | Demo provider |
MAILMUX_ALLOWED_ORIGINS |
empty | Extra browser origins allowed to call the API — see below |
Bind address (MAILMUX_HOST)
The default binds to loopback, so only your own machine can reach the server. Change it and the server answers on the network, where the bearer token is the only thing between a stranger and your mail.
One behaviour changes on a non-loopback bind: /api/local-bootstrap, which hands out the bearer token in plaintext, answers 404 and hands out nothing. Its Host and Origin checks are browser guards, and a remote client picks both headers itself. Paste the token in by hand instead; it is in ~/.mailmux/bearer.token.
Master key (MAILMUX_MASTER_KEY)
This key encrypts your stored mail passwords. Leave it unset and mailmux generates a random one in ~/.mailmux/master.key.
Set it to 64 hex characters — a full random 32-byte key:
openssl rand -hex 32
Any other value is treated as a passphrase and stretched with scrypt (N=2¹⁷, r=8 — 128 MB per attempt, about 0.2s once at startup). The salt is random per install and stored in ~/.mailmux/master.salt, so no precomputed table applies and the same passphrase on two machines produces two different keys. A passphrase still holds far less entropy than a random key, so prefer the hex form.
Back up master.salt with your data directory. Lose it and a passphrase no longer derives the key that encrypted your stored mail passwords.
Upgrading: passphrases used to be hashed once with SHA-256. The scrypt change means a passphrase set before this version derives a different key, and stored mail passwords no longer decrypt. Re-enter each account's password once, or keep the old key by setting MAILMUX_MASTER_KEY to the hex of sha256(<your passphrase>).
Browser origins (MAILMUX_ALLOWED_ORIGINS)
By default mailmux accepts browser requests only from your own machine. A page on any other origin gets 403 {"error":"forbidden origin"}. Leave the variable unset and nothing changes.
Set it when you want a web interface hosted somewhere else — a deployment of apps/web, for example — to talk to your local server. The page still runs entirely in your browser and still fetches mail directly from your machine; the variable only tells your server which page origins it will answer. See Using the hosted interface for the full walkthrough.
MAILMUX_ALLOWED_ORIGINS=https://your-deployment.example.com mailmux serve
Rules:
- Comma-separated, exact origins.
https://a.example.com,https://b.example.com. - Only
https://entries are kept. A plaintext origin is trivially spoofed on a hostile network, sohttp://entries are dropped. - Path, query and case are stripped:
https://A.App/xbecomeshttps://a.app. A port must match exactly —https://a.appdoes not allowhttps://a.app:8443. *is ignored on purpose. mailmux holds your mail credentials; an any-origin allowlist would let any page you visit probe your server.- Loopback (
127.0.0.1,localhost,::1) always passes, so the self-hosted UI needs no configuration. - Requests with no
Originheader (curl, MCP clients) are unaffected.
What enabling this costs you. The origin check is the last defence-in-depth layer in front of a service that holds decrypted IMAP passwords. Adding an origin means:
- Anyone who can serve a page at that exact hostname can reach your server if they also have your bearer token. On shared hosting platforms that includes preview deployments and anyone with deploy access. Prefer a custom domain you control over a platform-assigned hostname.
- It is the only remaining barrier against a DNS-rebinding page reaching your loopback service, so the list should stay as short as you can make it.
- The token is still required on every request.
Access-Control-Allow-Credentialsis never sent — mailmux authenticates by header, never by cookie — so no page can ride ambient credentials. /api/local-bootstrap, which hands out the bearer token in plaintext, is not widened by this variable. It stays loopback-only. A remote page must have its token pasted in by a human.
Architecture
See docs/ARCHITECTURE.md.
One Node process:
/— web UI (theapps/webexport, served fromweb-next/)/api/*— REST (same mail core)/mcp— JSON-RPC MCPmailmux mcp— stdio MCP
IMAP via ImapFlow, SMTP via Nodemailer, secrets AES-256-GCM, state SQLite.
Tests
npm test
Tests call shipped MailService, crypto, HTTP app, and MCP handlers with an in-memory FixtureProvider — no live mail accounts required.
Security notes
- Default bind is localhost.
- Browser requests are loopback-only unless
MAILMUX_ALLOWED_ORIGINSnames another origin. Default is closed. - Passwords encrypted at rest; master key in
~/.mailmux/master.key(mode 0600). A passphrase inMAILMUX_MASTER_KEYis stretched with scrypt against~/.mailmux/master.salt. - Prefer app passwords over primary account passwords.
- Keep
message_sendbehind agent confirmation.
License
MIT — free to use, modify, and redistribute.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。