your-mail-mcp
CLAUDE.md20 starsChanged 14 days agoMITView on GitHub
A self-hosted MCP server that gives an MCP client read access to one or more IMAP accounts. Mail is mirrored to a local maildir by mbsync, indexed by notmuch, and served over streamable HTTP behind an in-process OAuth 2.0 authorization server. This file is the briefing for anyone changing the code. README.md is for operators running it. The reasoning behind the design is in docs/specs/2026-08-19-your-mail-mcp-design.md, and what was verified against real tools is in docs/reviews/. v1 is implemented: eleven read…
In this repositoryOne other file it gives its agents.
- emailSkill
The file
We split this CLAUDE.md into the rules it gives an agent
4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 69
Build, not adopt: Every existing email MCP server is a live-IMAP server with a send path, which is the opposite of both choices above.
- Distinctive: line 72
notmuch over a maildir, not live IMAP: Search over IMAP means round-tripping SEARCH against a slow, rate-limited provider, or maintaining a …
- Distinctive: line 73
SEARCH against a slow, rate-limited provider, or maintaining a local index anyway.
- Distinctive: line 123
Commit messages describe the change in plain terms.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
your-mail-mcp
A self-hosted MCP server that gives an MCP client read access to one or more IMAP accounts. Mail is mirrored to a local maildir by mbsync, indexed by notmuch, and served over streamable HTTP behind an in-process OAuth 2.0 authorization server.
This file is the briefing for anyone changing the code. README.md is for
operators running it. The reasoning behind the design is in
docs/specs/2026-08-19-your-mail-mcp-design.md, and what was verified
against real tools is in docs/reviews/.
Status
v1 is implemented: eleven read tools, multi-account sync, SPECIAL-USE junk discovery, OAuth with dynamic client registration, and a container image.
Running against one real iCloud account and two real Gmail accounts. That is one operator's mailbox, so see "Before trusting it" below for what is still unproven.
Invariants
These are the reasons this project exists in this shape. Do not weaken one without changing the spec first.
- Nothing in the process may write to a mail account. mbsync is configured
Sync Pull/Create Near/Remove Near, withExpunge Noneby default andExpunge Near— which deletes local files only — for an account that opts intoexpunge_local.Remove Nearalso acts on the local side only: when a folder disappears from the server, mbsync removes the local folder only if it is empty, so its mail stays. The file is generated by the program rather than mounted, so the directives cannot be edited into something that pushes. There is no send, delete, move or tag path against any account. The one local deletion in Go code is the account's own-recentbootstrap store, removed once the full mirror covers it. - The only IMAP operation in the server's Go code is
LIST, used once per account to find the junk and trash folders. It never selects a mailbox and never fetches a message, and the client object does not escape the function that opens it. The live test uses more of IMAP, against its own test server only, to stage what a user does in webmail. - Every byte of mail text reaching the model passes through
render().page()inmcp.gois the only place that constructs text content, andrenderneutralises the marker sentinel so mail cannot forge the untrusted-content wrapper. The one deliberate exception is image attachment parts: text cannot mark pixels, so theattachmenttool returns images as typed MCP content whose schema names them attacker-authored, capped at 5MB. Textual parts — text/*, JSON, XML, message/rfc822 — pass throughrender; every other binary is served only as a short-lived signed link toGET /attachment/{id}/{part}(or, with no listener, a file under the index fordocker cp) (bearer token also accepted): those bytes go to a shell or a browser, never into model context, so no cap and norenderapply there. - Junk and trash are excluded from search by default, discovered per account through RFC 6154 SPECIAL-USE so it works whatever the folders are named and in whatever language.
- Exactly two dependencies:
modelcontextprotocol/go-sdkandemersion/go-imap/v2. Everything else is the standard library, tests included.
Design decisions
- v1 is read-only; that is scope, not the product's final identity. v2 intends draft composition behind a send gate built as a type — a constructor that returns a refusing implementation when sending is off — per the spec.
- One process, two attach points. The container runs
serve: sync, index, a Unix socket for local clients, and HTTP only whenPUBLIC_URLis set.stdiobridges stdin/stdout to that socket, or runs the daemon in-process when no socket exists. Spec section 7. - Build, not adopt. Every existing email MCP server is a live-IMAP server with
a send path, which is the opposite of both choices above. See
docs/research/email-mcp-landscape.md. - notmuch over a maildir, not live IMAP. Search over IMAP means round-tripping SEARCH against a slow, rate-limited provider, or maintaining a local index anyway. notmuch already is that index, with threading and stable message ids.
- Go, for a small dependency tree in a long-running service that holds mail credentials and parses untrusted input, and for a static binary in a small image.
- notmuch is executed, never linked through cgo. Keeps the build static and decouples the binary from the installed notmuch version.
- OAuth 2.0 with Dynamic Client Registration, implemented in-process. Claude's connector documentation makes DCR the supported path and rules out a machine-to-machine grant, so consent is always required.
- The container is the unit of deployment. Where it runs is a runtime decision the code knows nothing about.
Layout
| Path | What |
main.go | environment, accounts file, wiring, tickers, HTTP handler, bearer check |
mcp.go | the eleven tools, render() chokepoint, query building, exclusions |
notmuch.go | executing notmuch, query validation, account scoping |
sync.go | generated configs, mbsync, sync mutex, guards, SPECIAL-USE discovery |
oauth.go | authorization-server endpoints and the client/token store |
bridge.go | stdio bridge to the daemon socket |
.github/workflows/ci.yaml | vet, unit and live tests on every PR; multi-arch image to GHCR on main and tags; release binaries on tags |
docs/specs/ | the design spec, which is the binding authority |
docs/reviews/ | what was verified against the real toolchain, and what was not |
docs/research/ | the survey and provider research the design rests on |
Conventions
- Tests use
testingfrom the standard library. No framework, no assertion library, no mocking library. go vet ./...,go build ./...,go test ./...andgo test -race ./...all clean before a commit.go test -tags live -run TestLive -timeout 15mruns the full stack — the shipped container against a real IMAP server, every tool through the real OAuth flow. It needs Docker, isolates itself under the compose projectymm-live, and tears everything down including volumes. Run it before any change to the sync path, the generated configs, or the Dockerfile: the class of defect it catches (container toolchain skew, the real IMAP conversation) is invisible to the rest of the suite.INBOXis the only folder name that may be hardcoded; RFC 3501 requires it.- Pipeline depth is pinned to 1 and
SubFolderstoVerbatim.AuthMechsis left unset unless an account setsauth_mechs. Those are settled; see the spec for why. - Dependencies stay on the latest stable release everywhere: the Go toolchain,
the two modules, and the image's base and packages. When no stable release
exists, the newest available is used and named as the exception — today that
is
go-imap/v2, which has never shipped a stable v2. When bumping the base image, run the live test: the container's tool versions are exactly what it exists to catch. - Commit messages describe the change in plain terms. No AI or agent attribution anywhere in the repository.
Before trusting it
Sync and serving now run against real iCloud and Gmail accounts, so the IMAP
LIST and the mirror are exercised in the field. The gap between that and
"safe against any real mailbox" is these four:
- Whether SPECIAL-USE discovery picks the right junk and trash folders per
provider. It runs against real Gmail and iCloud now, but nobody has read
back the
foldersoutput and confirmed the discovered names against what the provider actually calls those mailboxes. The live test's server does not advertise SPECIAL-USE, so the test suite cannot answer this. - A real connector handshake. The OAuth flow has only been exercised through
httptestand over localhost. - The README quick start, followed literally on a clean machine.
- Whether
MaxMessages 1000on therecentchannel behaves as the mbsync manual describes against a real provider. The live test cannot reach that boundary. The first real run found one thing the manual does not stress: withoutExpireUnread yes, mbsync skips a mailbox holding more unread messages than the cap. That is set now; the cap itself is still unproven.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

