your-mail-mcp
About
Read-only, self-hosted MCP access to IMAP email, mirrored to a local maildir and indexed by notmuch.
Details
- Author
- wildsurfer
- Categories
- Communication, Automation, Productivity
Jump to
Setup
Install your-mail-mcp in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/wildsurfer/your-mail-mcp
Follow the installation instructions in the repository README, then restart your MCP client.
Your mail already holds the answers: booking references, gate codes, invoices, warranty periods, promises people made in writing. This server lets your AI assistant find them.
- "Find the booking reference for the June ferry."
- "What was the wifi password the hotel sent last summer?"
- "What did the accountant answer about VAT, and when?"
- "Collect everything between me and the builder about the roof, in order, and summarize who promised what."
- "What arrived this morning, across all my accounts, that actually needs me?"
- Search that understands questions.Full-text search over your entire history, every account in one index, phrased the way you think instead of the way search syntax works.
- Triage from your phone.A morning summary of what came in overnight, with junk already filtered out, from wherever you are.
- Mail as context for other work.Pull the client's requirements out of the thread and into your coding or writing session, instead of retyping them.
- Agents you can leave running.The server can only read. A malicious email that reaches your assistant gets read and nothing more, because sending, deleting and moving do not exist here. That makes scheduled digests and always-on agents a calm thing to run.
Setup is two files anddocker compose up -d— seeRunning it.
A self-hosted MCP server that gives an MCP client (Claude, or any other client that speaks streamable HTTP MCP with OAuth) read access to your mail. It mirrors one or more IMAP accounts into a local maildir withmbsync, indexes them withnotmuch, and answers tool calls from that index.
Mail only ever moves left to right in that picture. The one arrow the server makes back toward a provider is a single IMAPLISTat startup, to find out what that server calls its junk and trash folders; it never selects a mailbox and never fetches a message. The diagram source isdocs/diagrams/how-it-works.html.
The read-only property is built into the architecture.
The mirror is pull-only. The generated mbsync configuration for every account carriesSync Pull,Create Near,Remove None,Expunge None— nothing in that configuration can push a change back to the server, delete a message, or expunge one.
The only IMAP operation anywhere in the Go code isLIST, issued once per account at startup to find each account's junk and trash folders (seeProvider notesandTroubleshooting). That connection logs in, lists mailboxes, and logs out. It never selects a mailbox and never fetches a message.
There is no send, no delete, no move, and no tag. Attachments are listed inshowandthreadand served read-only by theattachmenttool, one part at a time, capped at 5MB. Bigger parts are served raw atGET /attachment/{id}/{part}, authenticated by a bearer token or by the short-lived signed link the tool returns when it refuses an oversized part. Nothing in the process holds write access to any account.
search,ids,filesandcounttake a notmuch query (from:,to:,subject:,tag:,folder:,date:2026-01-01..2026-06-30, combined with and/or/not), an optionalaccountto scope to one account, and can include junk/trash withinclude_excluded.
Three ways to run this. They differ in one thing: who can reach the server. Start at case 1 and move up only when you need to. None of them is hardened beyond the defaults — that isHardening, further down, and it is deliberately separate so you can get the thing working first.
The server ships as a container image atghcr.io/wildsurfer/your-mail-mcp, built and published by CI for amd64 and arm64. Nothing needs compiling, and every case starts the same way — two files in an empty directory:
mkdir your-mail && cd your-mail curl -fsSLO https://raw.githubusercontent.com/wildsurfer/your-mail-mcp/main/compose.yaml curl -fsSL https://raw.githubusercontent.com/wildsurfer/your-mail-mcp/main/accounts.example.json -o accounts.json
Editaccounts.jsonwith your accounts (seeThe accounts file), then put the secrets it references in a.envfile next tocompose.yaml:
# .env OAUTH_PASSPHRASE=pick-a-long-one-you-can-type-on-a-smartphone WORK_PASS=your-gmail-app-password PERSONAL_PASS=your-icloud-app-specific-password
OAUTH_PASSPHRASEis the only credential between the internet and your mail in cases 2 and 3. Treat it accordingly.
These two files hold your mail passwords. If you ever put this directory under version control or into a backup that leaves the machine, treat them accordingly.
Case 1 — on your machine, for your machine only
The server binds to loopback. Nothing outside your machine can reach it, so there is no TLS to arrange and no hostname to own. Your CLI tools can use it. Your smartphone cannot.
docker compose up -d docker compose logs -f # watch the first sync
The first sync populates the maildir and takes a while on a large mailbox. It is slower than it could be on purpose, one IMAP command at a time, because providers throttle. There is no separate initialization step.
claude mcp add --transport http your-mail http://127.0.0.1:8080/mcp
Then run/mcpinside Claude Code, pickyour-mail, and authenticate. A browser opens the consent page, which asks for one thing: yourOAUTH_PASSPHRASE. Until you do this,claude mcp listshowsNeeds authentication.
codex mcp add your-mail --url http://127.0.0.1:8080/mcp codex mcp login your-mail
codex mcp listshows the auth status. If the tools still do not appear in a session after a successful login, that is a known Codex bug where the OAuth credentials are obtained and then never used (openai/codex#20009). Use the bridge below until it is fixed.
mcp-remotedoes the OAuth dance itself and re-exposes the server over stdio, which every MCP client supports:
# ~/.codex/config.toml [mcp_servers.your-mail] command = "npx" args = ["-y", "mcp-remote", "http://127.0.0.1:8080/mcp"]
It opens the same consent page on first run and caches the tokens.
Case 2 — on your machine, reachable from anywhere
Same server, plus something that gives it a public HTTPS address. Your mail stays on your machine, and nothing listens on your home network, because the tunnel dials out. You need this for the smartphone and desktop apps: a custom connector is fetched by the vendor's servers, so it cannot reach a private address.
One command, same on macOS and Linux, and you get an HTTPS hostname without owning a domain.
--bgkeeps it running across reboots. It prints the public URL, which looks likehttps://your-machine.your-tailnet.ts.net. That is the hostname to use:
# .env PUBLIC_URL=https://your-machine.your-tailnet.ts.net
Funnel needs HTTPS certificates and the Funnel node attribute enabled for your tailnet; the CLI offers to add the policy line the first time, and the rest is in your admin console.tailscale funnel statusshows what is exposed, andtailscale funnel --https=443 offtakes it down.
With Cloudflare (you own a domain, and it is on Cloudflare)
Use this if you want a hostname on your own domain rather than a.ts.netone.mail.example.combelow isyourdomain, already added to your Cloudflare account — Cloudflare does not hand you a hostname for a named tunnel.
cloudflared tunnel login cloudflared tunnel create your-mail
createprints the tunnel's UUID and the credentials file it just wrote:
Tunnel credentials written to /Users/you/.cloudflared/f9e2…-… .json Created tunnel your-mail with id f9e2…-…
Use that exact path below;cloudflared tunnel listprints the UUID again if you lose it. Route the hostname, then write~/.cloudflared/config.yml:
cloudflared tunnel route dns your-mail mail.example.com
tunnel: your-mail credentials-file: /Users/you/.cloudflared/f9e2….json # the path create printed url: http://localhost:8080
To keep it running: on Linux,sudo cloudflared service install. On macOS, install it through Homebrew and usebrew services start cloudflared, because thesudoinstall path looks for its certificate under the root user's home and will not find the onecloudflared tunnel loginwrote to yours.
Then setPUBLIC_URL=https://mail.example.comin.envanddocker compose up -d.
PUBLIC_URLhas to match what you type into the client exactly. The server publishesPUBLIC_URL + /mcpas theresourcein its OAuth metadata, and a mismatch there is the most common reason a connector refuses to add.
One thing to know before you start on a smartphone:neither Claude nor ChatGPT lets you add a connector from the smartphone app.You add it once on the web (or in Claude's desktop app), and it then shows up on your smartphone. Trying to do the setup on the smartphone itself will waste your time.
Claude — add on web or desktop, then use on your smartphone
- Onclaude.aior in Claude Desktop, go toSettings → Connectors, and click+next to Connectors, orAdd custom connector.
- Give it a name and the URL<PUBLIC_URL>/mcp. Leave the advanced OAuth fields empty: this server registers clients dynamically.
- Claude opens the consent page. Enter yourOAUTH_PASSPHRASE.
- Open the Claude app on your smartphone. The connector is already there, and the tools are available in a chat. Turn it on for a conversation from the tools or connectors menu in the composer.
ChatGPT — add on web, then use on your smartphone
Custom MCP connectors live behind developer mode, which needs a Pro, Plus, Business, Enterprise or Education account and is only available on the web.
- In ChatGPT on the web, openSettings → Security and loginand turn onDeveloper mode. On Business and Enterprise workspaces an admin may have to allow it first.
- Add a connector for a remote MCP server and give it the URL<PUBLIC_URL>/mcp, with OAuth as the authentication. ChatGPT supports dynamic client registration, so there is nothing to paste.
- Approve the consent page with yourOAUTH_PASSPHRASE.
- Open ChatGPT on your smartphone and enable the connector in a chat.
These menus move. If the names above do not match what you see, look for developer mode in settings, then for the place that adds a connector by URL.
ChatGPT disables some MCP write actions on mobile. That has no effect here, because this server has no write actions at all.
claude mcp add --transport http your-mail https://your-host/mcp
codex mcp add your-mail --url https://your-host/mcp codex mcp login your-mail
Case 3 — on a VPS, reachable from anywhere
Pick this when you want the mirror to stay up whether or not your machine is on. It costs a few dollars a month and one real trade-off: a full plaintext copy of your mail moves onto a rented disk, with the app passwords in the same environment. ReadSecuritybefore you choose it.
The install is case 1 plus a tunnel, on someone else's computer. No ports to open, no DNS to configure, no certificates to manage.
# 1. Docker curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER && newgrp docker # 2. The two files, and your accounts mkdir your-mail && cd your-mail curl -fsSLO https://raw.githubusercontent.com/wildsurfer/your-mail-mcp/main/compose.yaml curl -fsSL https://raw.githubusercontent.com/wildsurfer/your-mail-mcp/main/accounts.example.json -o accounts.json $EDITOR accounts.json # your accounts $EDITOR .env # OAUTH_PASSPHRASE and the account passwords # 3. A public address, exactly as in case 2 curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up tailscale funnel --bg 8080 # prints your https://….ts.net hostname # 4. Put that hostname in .env, then start echo "PUBLIC_URL=https://your-machine.your-tailnet.ts.net" >> .env docker compose up -d docker compose logs -f
PUBLIC_URLcomes last because you do not know the hostname until step 3 prints it.
Connecting a client is identical to case 2.
restart: unless-stoppedincompose.yamlbrings the containers back after a reboot. Check on it with thefolderstool, which reports each account's last sync and its last error, or withdocker compose logs --tail=50.
Now go and readHardening. A VPS you can SSH into with a password, holding a copy of your mail, is worse than not running this at all.
None of this is needed to make the server work, which is why it is not in the install steps. It is ordered by how much it buys you. Case 1 needs none of it.
Pick a real passphrase.OAUTH_PASSPHRASEis the whole door. A wrong guess costs the attacker one second, and guesses are serialised so running them in parallel does not help, but neither of those saves a short passphrase. Use a long one you can still type on a smartphone.
Lock down SSH(case 3). A rented box with password login and a copy of your mail on it is the worst combination in this document. As root, before anything else:
adduser mail && usermod -aG sudo mail rsync --archive --chown=mail:mail ~/.ssh /home/mail sed -i 's/^#\?PermitRootLogin./PermitRootLogin no/; s/^#\?PasswordAuthentication./PasswordAuthentication no/' /etc/ssh/sshd_config systemctl restart ssh
Then do the install asmail, not as root.
Close the ports you are not using(case 3). With a tunnel you need no inbound ports at all, so:
sudo ufw allow OpenSSH && sudo ufw --force enable
Restrict who can reach the connector.If the only thing that talks to your server is a custom connector in a Claude app, that traffic arrives from Anthropic's published egress range,160.79.104.0/21, and you can refuse everything else at the tunnel or firewall. Do not do this if you also use Claude Code or Codex from a laptop, since those connect from wherever you are.
Back up the volumes, or accept a re-sync.compose.yamlkeeps the maildir and the index in named volumes. Nothing in them is unique — it is all still on your mail server — but re-downloading a large mailbox takes a while and annoys providers that throttle.
Know what the passphrase does not protect.It gates the MCP surface. It does not encrypt anything at rest. SeeSecurity.
If you would rather terminate TLS yourself on a domain you own, point anArecord at the box and put Caddy in front. Addcompose.override.yaml:
services: caddy: image: caddy:2 restart: unless-stopped ports: ["80:80", "443:443"] volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data volumes: caddy_data:
# Caddyfile mail.example.com { reverse_proxy your-mail-mcp:8080 }
Open both ports — 80 is not optional, Caddy uses it for the certificate challenge and the HTTPS redirect:
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.


