Abhishek ChaudharyAbhishek Chaudhary

Claude Code IMAP Email: The Agent Drafts, You Hit Send

Give Claude Code IMAP email with himalaya and one skill: it reads threads cheaply, saves replies to Drafts in-thread, and sends only on your word.

Abhishek Chaudhary12 min read

Claude Code can read and answer a real IMAP mailbox from the terminal through a stateless mail CLI called himalaya, wrapped in a small skill that reads threads cheaply, saves every reply to the mailbox's own Drafts folder in-thread, and sends nothing until a human says "send". I built this in September 2026 for the support inbox of one of my production SaaS ventures and tested it against the live mailbox. Below: the setup, the six commands, the approval contract, and the six gotchas the live mailbox surfaced.

TL;DR

  • Use himalaya v2 as the IMAP and SMTP layer. It is stateless, prints JSON, and reads the password from a command, so the secret never sits in a config file or the model's context.
  • Wrap it in a skill, not a local MCP server. A skill costs nothing until it runs; each stdio MCP server cost 150 to 220 MB per session on my dev VM.
  • Read cheap: one line per message, quoted history and signatures stripped. A 10-message Outlook thread went from 125,228 characters to 13,513.
  • Approval is the mailbox itself. "Save" appends a threaded draft to Drafts; "send" sends that one draft and files the Sent copy.
  • Build headers at 998 columns, reply to the latest message only, and route SMTP through a tunnel if you send from home.
  • The skill is MIT at ph33nx/claude-code-email-skill.

Why a skill beats an email MCP server here

The obvious move in 2026 is to install one of the open-source email MCP servers. I tried that path on paper and dropped it for three reasons.

Memory. A local MCP server on the stdio transport is launched by the client as a subprocess, one per session. On my dev VM each one measured 150 to 220 MB resident for the whole life of the session, and ten parallel agents means ten copies. A Claude Code skill is a folder with a SKILL.md; per the Claude Code skills docs, only its description sits in context and the body loads when the skill is used. Zero resident memory, about 40 tokens of description.

Credentials. The stdio email servers I evaluated take the IMAP and SMTP password as a plain environment variable in the client config. That file gets copied, synced and pasted into bug reports.

The Sent folder. SMTP delivers a message; it does not file a copy anywhere. The client has to APPEND it to Sent itself, and two open-source email MCP servers shipped without doing that correctly: email-mcp issue 54 is still open, and mail-mcp issue 25 reported "ok" while the copy silently failed. A support reply that never lands in Sent is a reply your team cannot see.

I already run my site's admin as a remote MCP server, so this is not an anti-MCP position. Remote HTTP servers cost no local memory. The rule I follow is transport-shaped: HTTP when a server exists, a skill over a CLI when it does not, stdio last.

Set up himalaya v2 with one password file

Himalaya is a Rust email CLI, dual-licensed MIT or Apache-2.0. Its README is explicit that it is stateless: no event loop, and each invocation opens a fresh TCP, TLS and SASL session. That is exactly the shape an agent wants. Every call is independent, every call can emit --json, and nothing stays running between turns.

Two steps per mailbox. First, the password goes into a file only you can read, typed in a terminal and never in chat:

mkdir -p ~/.config/mail && ( umask 077; read -rs P && printf %s "$P" > ~/.config/mail/work.pass )

Use an application password if your provider offers one, so it can be revoked without touching the main login. Second, one account block in ~/.config/himalaya/config.toml (mode 0600):

[accounts.work]
default = true
email = "support@example.com"
display-name = "Example Support"
mailbox.alias.inbox = "INBOX"
mailbox.alias.sent = "Sent"
mailbox.alias.drafts = "Drafts"
imap.server = "imaps://imap.example.com:993"
imap.sasl.plain.username = "support@example.com"
imap.sasl.plain.password.command = ["sh", "-c", "printf %s \"$(cat \"$HOME/.config/mail/work.pass\")\""]
smtp.server = "smtps://smtp.example.com:465"
smtp.sasl.plain.username = "support@example.com"
smtp.sasl.plain.password.command = ["sh", "-c", "printf %s \"$(cat \"$HOME/.config/mail/work.pass\")\""]

himalaya -a work account check must print imap: OK and smtp: OK. The $( ) strips a trailing newline from the password file, which otherwise fails authentication with no useful error.

The wiki is v1. This is the trap every himalaya agent guide I found falls into. Version 2.0.0 shipped on 2026-07-26 and changed the config to [accounts.<name>] blocks with imap.* and smtp.* sub-keys, renamed save to add, and removed template, edit and delete (MIGRATION.md). The project wiki and most skill directories still show flat v1 keys like imap-host. Trust himalaya <command> --help and the config.sample.toml at the release tag. Everything in this post was verified against v2.1.0.

Six commands an agent actually needs

Himalaya's surface is large. An agent answering support mail needs six verbs, so the skill wraps exactly those in one stdlib Python script (plus the markdown package for rich-text drafts):

CommandWhat it returns
newInbox messages since the last run, one line each. First run: unread. --unanswered hides handled mail
searchServer-side search across Inbox and Sent together, merged newest first. from:, subject:, body:, an address
threadOne conversation, oldest first, across Inbox and Sent, trimmed to what each message said
readOne message, trimmed the same way
draftA markdown reply appended to Drafts, threaded, reply-all. Prints the draft id and the To, Cc and Subject it set
sendSends one draft, files the Sent copy, removes the draft, flags the original as answered

Every command is compact by default and widens on request: -v for full dates and headers, --cap N for longer bodies, --full for the raw text.

Reading is where the tokens go. A raw email thread is mostly the same text quoted over and over, plus signatures, legal footers and "Sent from my iPhone". The thread command drops > lines, On ... wrote: headers, Outlook's From: ... Sent: blocks, signatures after -- , HTML and mobile footers, lists attachments by name and size, and caps each message at 1,000 characters. On a real 10-message Outlook conversation that took the agent's input from 125,228 characters to 13,513, and a 6-message thread from 76,686 to 6,705.

Two cheap signals do the triage. The IMAP \Answered flag shows as an A in every listing, so unanswered mail is visible without opening a single thread. And a per-account ignore file, one sender address per line, hides payment receipts, DMARC reports and CI notices from new. List exact automated addresses, never a whole domain: a vendor's domain also carries the humans you need to hear from.

Drafts on approval: the save and send contract

The approval layer is not a web app or a queue. It is the mailbox's own Drafts folder, which every mail client on every device already shows.

  1. The agent reads the thread and writes the reply once, into a markdown file I review in the editor. It never reprints the reply in chat; revisions are small edits to that file.
  2. On my "save", draft renders the markdown to a multipart plain-text and HTML message, sets the threading headers, and appends it to Drafts with the \Draft flag. It prints the To, Cc and Subject so both of us check the recipients before anything else happens. I can open it on my phone and it sits in the right conversation.
  3. On my "send" for that exact message, send delivers it over SMTP with himalaya's --save sent so the Sent copy is filed exactly once, removes the draft, and flags the original \Answered. Never batch-sent, never sent on "looks good".

This is the same pattern I apply to an agent that maintains my own notes: the agent does the reading and the first draft, and the irreversible step stays with a human. An email, once sent, has no undo. Everything before send is free to redo, so that is where the agent should spend its effort.

Six gotchas that break threading and delivery

Each of these surfaced on the live mailbox, not in a test fixture.

Python folds long Message-IDs and breaks threading

RFC 5322 section 2.1.1 says a header line MUST be no more than 998 characters and SHOULD be no more than 78. Python's email package defaults max_line_length to 78, and when a single token is longer than a line it falls back to an RFC 2047 encoded word. Outlook's Message-IDs are long. The References header came out as =?utf-8?q?...?=, and the recipient's client no longer recognised it as part of the thread. The fix is one argument: build the message with email.policy.SMTP.clone(max_line_length=998), and at send time edit only the Date line instead of re-serialising the whole message.

Replying to an older message re-adds people

A reply takes its recipients from the message it answers. If the customer dropped a colleague from Cc three messages ago and the agent answers the first message, the colleague is back. The skill's draft refuses to reply to an Inbox message when a later one exists in the same conversation and names the later one. There is a --force flag, used only on my word.

Outlook cuts References to the parent

RFC 5322 section 3.6.4 says a reply's References carries the parent's references plus the parent's Message-ID, a full chain back to the root. Outlook and some webmail clients shorten it to the parent only, so a thread is not findable from its root. thread searches by normalised subject as well, then keeps only messages actually linked by Message-ID, In-Reply-To or References.

Form notifications need Reply-To honoured

Many contact-form and ticketing notifications arrive from a no-reply address with Reply-To set to the customer. Himalaya follows Reply-To when it builds the reply headers, so the answer goes to the person, threaded under the notification. Search for these by the ticket id or with body:, because the customer's address lives in the body, not the From line.

SMTP files no Sent copy

Covered above, and worth repeating as a test: after your first send, count the copies in Sent. Zero means your tool never appended one. Two means something appended it twice. One is the only correct answer.

Sending from home leaks your home IP

RFC 5321 section 4.4 has the receiving server record the connecting client's IP in a Received header. Webmail connects from the provider's own servers; a CLI on a home machine connects from your home connection, and that IP travels to every recipient in the headers. I found mine in a Gmail "show original". The fix is a mail-only WireGuard tunnel: wg-quick builds routes from the peer's AllowedIPs, so AllowedIPs = <smtp server ip>/32 sends only mail traffic through the tunnel and leaves the rest of the machine alone. Have send fail closed if the route to the SMTP server does not leave through the tunnel, because a provider IP change would otherwise quietly route around it.

Install the companion email skill

The whole thing is one Python file, one SKILL.md, a sample config and a sample ignore list, published as ph33nx/claude-code-email-skill under MIT. In Claude Code it installs as a plugin with /plugin marketplace add ph33nx/claude-code-email-skill and /plugin install email@ph33nx; a one-line installer then fetches himalaya and the markdown package at user level. Add an account and ask the agent "any new mail?". The skill follows the open Agent Skills SKILL.md format, so the same folder works in Codex, Cursor and other agents that read it. On Windows, run it inside WSL.

What I would keep if I rebuilt it from scratch is the contract, not the code: read cheap, write once into a file a human reviews, save to the real Drafts folder, send only on the word. That split between agent and human is the same one I describe in what senior developers do once coding agents arrive. The agent does the reading and the first draft. The human decides what goes out under their name. More on the ventures and practices this runs across.

FAQ

Can Claude Code read my email?

Yes, if you give it a tool that speaks IMAP. Claude Code has no built-in mailbox access. The lightest route is a CLI mail client such as himalaya, called from a skill: the agent runs a shell command, gets back one line per message or a trimmed thread, and never holds the password. The alternative is an email MCP server, which does the same job as a resident process, and on the stdio transport costs memory in every session that loads it.

Is himalaya safe to give an AI agent?

Himalaya itself only does what it is told, so safety lives in the wrapper. Keep the password in a mode 0600 file read through password.command, so it never appears in a config file or in the model's context. Expose read commands and a draft command freely, and gate send behind an explicit human "send" for each message. Reading never marks mail as seen, so the agent can look without changing the mailbox state.

How do I keep email replies threaded in Gmail and Outlook?

Set In-Reply-To to the parent's Message-ID and References to the parent's References plus that ID, as RFC 5322 section 3.6.4 describes, and keep a single Re: on the subject. Himalaya's message reply builds those headers. If you serialise the message in Python, raise max_line_length to 998, because the default 78 encodes long Outlook Message-IDs and the reply falls out of the thread.

Why does my sent email not appear in the Sent folder?

Because SMTP only delivers. Filing a copy in Sent is a separate IMAP APPEND that the sending client must perform. Webmail does it for you; a script or MCP server has to do it explicitly, and two open-source email MCP servers have had bugs filed for missing or silently failed Sent copies. With himalaya v2, message send --save sent sends and files the copy in one step.

Should I use an email MCP server or a CLI skill?

Use a remote HTTP MCP server when one exists for your provider, since it costs no local memory. For a plain IMAP mailbox, a skill over a CLI does the same work with nothing running between calls and no credentials in the client config. Local stdio MCP servers are the most expensive option: one process per session, 150 to 220 MB each on my machine.

Does this work with Gmail?

It works with any mailbox that offers IMAP and SMTP. Gmail and Google Workspace support both, typically with an app password on accounts that allow one. Mailbox folder names differ per provider, so run himalaya -a <name> imap list and point the sent and drafts aliases at the folders your provider actually uses. Because the skill speaks plain IMAP and SMTP, it needs no Gmail API access and no OAuth app of its own when an app password is available.