agentleFS
wildsurferwildsurfer/your-mail-mcp/CLAUDE.md

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.

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
  1. Very specific:

    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.

    line 69
  2. Distinctive:

    notmuch over a maildir, not live IMAP: Search over IMAP means round-tripping SEARCH against a slow, rate-limited provider, or maintaining a …

    line 72
  3. Distinctive:

    SEARCH against a slow, rate-limited provider, or maintaining a local index anyway.

    line 73
  4. Distinctive:

    Commit messages describe the change in plain terms.

    line 123

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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, with Expunge None by default and Expunge Near — which deletes local files only — for an account that opts into expunge_local. Remove Near also 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 -recent bootstrap 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() in mcp.go is the only place that constructs text content, and render neutralises the marker sentinel so mail cannot forge the untrusted-content wrapper. The one deliberate exception is image attachment parts: text cannot mark pixels, so the attachment tool returns images as typed MCP content whose schema names them attacker-authored, capped at 5MB. Textual parts — text/*, JSON, XML, message/rfc822 — pass through render; every other binary is served only as a short-lived signed link to GET /attachment/{id}/{part} (or, with no listener, a file under the index for docker cp) (bearer token also accepted): those bytes go to a shell or a browser, never into model context, so no cap and no render apply 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-sdk and emersion/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 when PUBLIC_URL is set. stdio bridges 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

PathWhat
main.goenvironment, accounts file, wiring, tickers, HTTP handler, bearer check
mcp.gothe eleven tools, render() chokepoint, query building, exclusions
notmuch.goexecuting notmuch, query validation, account scoping
sync.gogenerated configs, mbsync, sync mutex, guards, SPECIAL-USE discovery
oauth.goauthorization-server endpoints and the client/token store
bridge.gostdio bridge to the daemon socket
.github/workflows/ci.yamlvet, 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 testing from the standard library. No framework, no assertion library, no mocking library.
  • go vet ./..., go build ./..., go test ./... and go test -race ./... all clean before a commit.
  • go test -tags live -run TestLive -timeout 15m runs 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 project ymm-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.
  • INBOX is the only folder name that may be hardcoded; RFC 3501 requires it.
  • Pipeline depth is pinned to 1 and SubFolders to Verbatim. AuthMechs is left unset unless an account sets auth_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:

  1. 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 folders output 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.
  2. A real connector handshake. The OAuth flow has only been exercised through httptest and over localhost.
  3. The README quick start, followed literally on a clean machine.
  4. Whether MaxMessages 1000 on the recent channel 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: without ExpireUnread yes, mbsync skips a mailbox holding more unread messages than the cap. That is set now; the cap itself is still unproven.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.