Giving Claude My Inbox

AI MCP Cloudflare Workers Architecture Security

The Copy-Paste Assistant

Claude is genuinely useful for the busywork that comes with researching what’s next in a career. Summarizing a long thread, figuring out which of three proposed times actually works, roughing out a reply that I am going to rewrite in my own words anyway. But every one of those tasks started the same way: me, copying an email out of Mail.app, pasting it into a chat window, and then copying the answer back out.

That is not an assistant. That is a very smart clipboard.

So I built iCloud MCP: a single Cloudflare Worker that speaks IMAP, CalDAV, and CardDAV to iCloud directly and exposes 23 MCP tools for mail, calendar, and contacts. Claude reads the real inbox, searches the real calendar, and leaves real work behind: a rough draft waiting in my Drafts folder for me to make my own, an event with a signed confirmation behind it. My credentials never leave the server. Nothing gets pasted anywhere.

The protocol work was the fun part. The safety work turned out to be the actual project, and it changed how I think about giving a model access to anything that matters.

First Problem: There Is No IMAP Client for Workers

Cloudflare Workers can open raw TCP sockets and make HTTPS requests from the same runtime. As far as I can tell it is the only edge platform where that combination exists, and it is exactly what talking to iCloud requires: IMAP is a TCP protocol, CalDAV and CardDAV ride on HTTP.

What Workers does not have is an IMAP client. Every library on npm (imapflow, node-imap, the emailjs family) reaches for Node’s net and tls modules at the source level and dies in the Workers runtime. There was no published confirmation that IMAP against iCloud from a Worker was even possible.

So the very first deliverable of the project was a proof: open a socket to imap.mail.me.com:993, complete TLS, log in with an app-specific password, and read a mailbox. Everything else waited until that worked.

Then came the part nobody writes tutorials for: implementing LOGIN, EXAMINE, FETCH, SEARCH, and APPEND by hand, with a parser that survives real-world IMAP. Literals with embedded CRLF that corrupt naive line-splitting. UID versus sequence numbers, which look interchangeable right up until they delete the wrong message. Non-synchronizing literals that make a client wait forever for a continuation prompt that never comes. This is thirty-year-old protocol territory, and the sharp edges are all still sharp.

Second Problem: The Model Is Not the Only Thing You Cannot Trust

Here is the mental shift the project forced. When you give an AI your inbox, you are managing two untrusted parties at once, and they meet each other inside your tool responses.

The first is the model. It hallucinates identifiers, retries things that should not be retried, and takes actions with confidence it has not earned. The second is your email itself. Email is stranger-authored content that arrives without your consent. A stranger’s meeting invite is a live prompt-injection vector, and it shows up in a list response before the model has decided to look at anything.

Neither of these is fixable with a system prompt. Both had to be handled in code.

What That Looks Like in Practice

No sending, ever. There is no SMTP code in the repository. Claude roughs out a starting point; it lands in my Drafts folder; I rewrite it in my own voice and hit send. Nothing leaves this account that I did not write the final version of. That human step is also the single control standing between injected email content and a message going out under my name, and removing it would be a safety regression dressed up as a feature.

Writes are preview-then-commit. Updating or deleting a calendar event writes nothing on the first call. The tool returns a preview of the exact change plus a signed confirmation token: HMAC, single-use, five-minute expiry, bound to a hash of the change itself. The commit call then carries an ETag precondition so even a race with another client fails safely. The design requirement, stated plainly in the docs: a hallucinated UID must not be able to delete a real event.

Hostile content gets fenced. Every tool response wraps third-party content in a marker with a random per-response nonce, so injected text cannot forge its own closing fence. The preamble tells the model that subjects, senders, bodies, and filenames are data to report, never instructions to follow. And one rule with no exceptions: no write tool may take a message or event id as the source of an attendee list. Injected content never gets to choose who receives an email.

Reading never mutates. Every mailbox opens read-only and every fetch uses IMAP’s peeking form, so browsing a fifty-message inbox page cannot silently mark fifty messages read across my devices.

Credentials are unloggable. IMAP’s login command carries the password inline in the command stream. There is no named field a log redactor could scrub, so the rule has to be blunt: zero logging under src/, credentials consumed by write-only functions that return nothing, and error handling that never reads a message string.

Rules That Enforce Themselves

Every rule above has a failure mode: someone (including a future AI session working on the codebase) forgets it. Code review is a detective control. I wanted preventive ones.

The most dangerous API in the runtime is the opportunistic TLS upgrade, which is unreliable enough on Workers that the tracking bug has been open for months. The project’s answer was structural. The one function allowed to open a socket takes no parameters. No host, no port, no transport flag. The unsafe paths are not rejected in review; they are impossible to express in a call.

Behind that sits a 1,157-line source scanner that runs in the test suite and again in a pre-commit hook, so a skipped test run cannot disable a ban. Its rules count in both directions: the socket choke point appearing twice is a violation, and so is it appearing zero times, because a required module that got deleted or renamed fails silently in a way a duplicate never does. The scanner even tests itself against known-violating fixtures, on the theory that a rule which matches nothing is indistinguishable from a rule that was never written.

The numbers, for scale: roughly 33,000 lines of source, 53,000 lines of tests running inside the real Workers runtime rather than a Node mock, built in fourteen days with a spec-driven workflow doing the planning and Claude writing a large share of the code.

The Takeaway

The instinct when wiring an AI into personal data is to write better prompts. Prompts are suggestions. Every protection in this project that I actually trust is one the model cannot talk its way past: an API with no unsafe parameters, a scanner that fails the commit, a send capability that simply does not exist.

Claude preps. I write and send. A machine that could read my mail and act on its own initiative would be more impressive in a demo. This one I actually connected to my real account.

github.com/russellkmoore/icloud-mcp

Share: LinkedIn X