Skip to main content
Glama
danb0nd

imaplane

by danb0nd

Imaplane

A local IMAP control plane for AI agents and your own programs — anything you allow, not everything.

Agents use MCP. Scripts, backends, CLIs, and apps call the same local HTTP API (OpenAPI) to read, search, and move mail. First-class iCloud / Apple Mail and generic IMAP. Loopback by default. You start it on purpose.

IMAP plane · IMAP lane · I’m a plane.

Site: https://imaplane.com (Cloudflare Pages; also https://imaplane.pages.dev).

Authenticate with a bridge token. Mailbox passwords never leave this machine.

Default off: sending, scheduled sweeps, public bind, launchd.

Quick start

Requires Node 20+. Some dependencies (notably sanitize-html) may print an EBADENGINE warning unless you are on Node 22.12+; install and runtime still work on Node 20.

npm install
npm run build              # required before `npx imaplane` (the bin is dist/cli.js)

Then configure. Interactive wizard (needs a TTY; it mints BRIDGE_TOKEN):

npx imaplane init          # token + iCloud and/or IMAP account

Or env-only / headless (imaplane init refuses without a terminal — no flags):

cp .env.example .env
openssl rand -hex 32       # paste into BRIDGE_TOKEN= in .env (≥16 chars; do not leave empty)
# set ICLOUD_USER + ICLOUD_APP_PASSWORD (app-specific password, not your Apple ID password)
# or copy imaplane.yaml.example → imaplane.yaml for named / generic IMAP accounts
npm start                  # HTTP plane owns IMAP
curl -sS http://127.0.0.1:8787/v1/health

/v1/health is process liveness: it stays up if IMAP is down (wrong password, network, etc.). imap.connected will be false and imap.error explains the last connect/auth failure. A valid mailbox and app-specific password are still required for a healthy IMAP connection.

Env-only iCloud (no yaml) still works: .env with ICLOUD_USER, ICLOUD_APP_PASSWORD, and a generated BRIDGE_TOKEN.

Related MCP server: io.github.p-w-4-z/inbox-mcp

MCP (Claude, Codex, Grok, Cursor, …)

Keep npm start running. MCP is a thin stdio wrapper: it only calls http://127.0.0.1:8787/v1 with your bridge token. It never sees mailbox passwords.

1. Build once (if you haven’t):

npm run build

2. Absolute path to the MCP entry (copy yours):

# macOS / Linux
echo "$(pwd)/dist/mcp.js"
# Windows (PowerShell)
(Resolve-Path .\dist\mcp.js).Path
# or from cmd: %CD%\dist\mcp.js

node must be on your PATH on Windows as well (same as macOS/Linux).

3. Add a stdio MCP server named imaplane in your client:

Client

How

Claude Desktop

Edit claude_desktop_config.jsonmcpServers (JSON below)

Claude Code

claude mcp add imaplane -- node /ABS/PATH/TO/imaplane/dist/mcp.js

Codex

Add the same stdio server in Codex MCP settings / config

Grok

grok mcp add imaplane -- node /ABS/PATH/TO/imaplane/dist/mcp.js or paste grok.mcp.toml.example into ~/.grok/config.toml

Cursor

Settings → MCP → add stdio server with the same command / args

Other MCP hosts

Same pattern: command node, args ["/ABS/PATH/TO/imaplane/dist/mcp.js"]

Generic JSON (Claude Desktop and most hosts):

{
  "mcpServers": {
    "imaplane": {
      "command": "node",
      "args": ["/ABS/PATH/TO/imaplane/dist/mcp.js"]
    }
  }
}

On Windows, use a Windows-style absolute path in args (escaped backslashes in JSON), for example:

"args": ["C:\\Users\\YOU\\Programming\\imaplane\\dist\\mcp.js"]

Forward slashes often work in JSON too: C:/Users/YOU/imaplane/dist/mcp.js.

The MCP process loads BRIDGE_TOKEN from this project’s .env (or set BRIDGE_TOKEN / optional BRIDGE_URL in the server env block).

Tools: mail_health, mail_accounts, mail_folders, mail_create_folder, mail_list, mail_read, mail_search, mail_move, mail_flags, mail_folder_profile, mail_save_folder_profile, mail_apply_folder_profile, mail_rules_dry_run, mail_rules_apply.
mail_send appears only when sending is enabled.

Agent playbook: BOT.md (folder setup = interview → propose → confirm → create).

iCloud / Apple Mail

  1. appleid.apple.comSign-In and SecurityApp-Specific Passwords.

  2. Generate one labelled imaplane.

  3. Full Apple ID email as user (@icloud.com, @me.com, or @mac.com).

  4. App-specific password — not your Apple ID password.

Preset: imap.mail.me.com:993 TLS, smtp.mail.me.com:587 STARTTLS.

# imaplane.yaml
accounts:
  icloud:
    provider: icloud
    user: ${ICLOUD_USER}
    password: ${ICLOUD_APP_PASSWORD}

Generic IMAP

No OAuth. Host, port, TLS, username, password.

accounts:
  work:
    provider: imap
    user: ${WORK_USER}
    password: ${WORK_PASSWORD}
    imap:
      host: mail.example.com
      port: 993
      tls: true
    smtp:
      host: mail.example.com
      port: 587
      tls: false        # 587 STARTTLS; tls: true for 465

account=work on API/MCP selects it. default_account is used when omitted.

Config

Piece

Role

.env

Secrets (BRIDGE_TOKEN, passwords). gitignored

imaplane.yaml

Accounts, send, sweeps, folder profiles, rules. gitignored; see imaplane.yaml.example

folders.yaml / rules.yaml

Optional overlays

imaplane init writes .env (mode 600) and imaplane.yaml.

HTTP API

Same surface MCP uses. Call it from curl, a backend, a CLI, or generate a client from OpenAPI.

Base: http://127.0.0.1:8787/v1
Auth: Authorization: Bearer $BRIDGE_TOKEN except /health
Optional query/body: account

Method

Path

Purpose

GET

/health

Liveness (process up even if IMAP is down; see imap.connected / imap.error)

GET

/accounts

Named accounts

GET

/folders

List mailboxes

POST

/folders

{ "name": "BotMail/Action" }

GET

/folder-profile

Saved folder tree + protect list

PUT

/folder-profile

Save profile after the user confirms

POST

/folder-profile/apply

{ "dry_run": true } then false

GET

/messages?folder=INBOX&limit=50

Headers, newest first

GET

/messages/:uid?folder=INBOX

Body (text + sanitized html; attachment metadata)

GET

/search?folder=INBOX&q=...

IMAP SEARCH

POST

/messages/:uid/move

{ "from": "INBOX", "to": "BotMail/Action" }

POST

/messages/:uid/flags

{ "folder": "INBOX", "add": ["\\Seen"] }

GET

/rules

Loaded rules

POST

/rules/apply

{ "dry_run": true }

POST

/messages/send

Only if send.enabled

OpenAPI: openapi.yaml. Import it to generate a client in any language.

Folder profiles

Agents must not invent a full tree. Saved profile + protect list (e.g. Important) are first-class.

imaplane folders dry-run
imaplane folders apply

Example: examples/folders.botmail.yaml.

Rules and sweeps

File-based match → move/flags. Dry-run first.

imaplane rules dry-run
imaplane rules apply

Sweeps (sweeps.enabled) run those rules on interval (15m) or cron. Default off.

Sending

Default off. Set send.enabled: true (and per-account send: true if you want a subset). Uses SMTP, not IMAP APPEND. When off, the send HTTP route and MCP tool are absent.

Security

  • App-specific or mailbox password; rotatable

  • Bridge token on every non-health request (≥16 chars)

  • One IMAP connection per account; ops serialised per account

  • Logs subjects/from; never passwords, tokens, or full bodies

  • Bind 127.0.0.1 unless you are reaching it over a mesh VPN (below). Do not port-forward on the router or use Tailscale Funnel.

Remote access (Tailscale)

The plane is loopback-only on purpose. To use it from another machine (phone, laptop, a Grok host on the same tailnet), put a private overlay in front — Tailscale, Headscale, or WireGuard. Same idea for all of them: the IMAP passwords stay on the Mac; the remote side only gets BRIDGE_TOKEN + HTTP.

Do not publish Imaplane on the public internet.

1. Preferred — keep 127.0.0.1, proxy with Serve

HTTP stays on loopback. Tailscale terminates TLS on your tailnet hostname.

# on the Mac that runs `npm start`
tailscale serve --bg 8787
# → https://<machine>.<tailnet>.ts.net/

From another device on the tailnet:

curl -sS -H "Authorization: Bearer $BRIDGE_TOKEN" \
  https://<machine>.<tailnet>.ts.net/v1/health

MCP on that remote device (stdio still local; it calls HTTP over the tailnet):

# .env next to imaplane, or the MCP process environment
BRIDGE_TOKEN=…                  # same token as the Mac
BRIDGE_URL=https://<machine>.<tailnet>.ts.net/v1

tailscale funnel would expose this to the whole internet — don’t.

2. SSH tunnel

No bind change. From the remote machine:

ssh -N -L 8787:127.0.0.1:8787 user@<machine>
# then http://127.0.0.1:8787/v1 as if you were on the Mac

Works with tailscale ssh the same way.

3. Bind only the Tailscale interface

If a client cannot use Serve or SSH, bind the tailnet IP — not 0.0.0.0, not your LAN.

tailscale ip -4          # e.g. 100.x.y.z
# .env on the Mac
HOST=100.x.y.z
PORT=8787

Or in imaplane.yaml: host: 100.x.y.z. Restart. Imaplane logs a warning when it is off loopback — keep BRIDGE_TOKEN long (openssl rand -hex 32). Restrict Tailscale ACLs so only your nodes can hit port 8787.

Remote MCP:

BRIDGE_URL=http://100.x.y.z:8787/v1
BRIDGE_TOKEN=…

Headscale / plain WireGuard: same pattern (Serve if you have it, otherwise bind the mesh IP or tunnel). Always HTTPS or a tunnel; never a public A record.

CLI

imaplane init | start | mcp | rules dry-run|apply | folders apply|--dry-run | version

npm start is the same as imaplane start. launchd scripts exist under scripts/ if you want login start — not installed by default.

Landing page

Public site: https://imaplane.com (Cloudflare Pages). Optional local marketing page lives in landing-page/ (gitignored). Open landing-page/index.html on your machine if you have it.

Help make it better

Issues and PRs welcome on GitHub. Ideas especially welcome for folder profiles, rules, providers, and MCP docs.

Develop

npm test          # mocked IMAP, no real credentials
npm run build

Node 20+ (some deps may warn for 22.12+; see Quick start). Spec: SPEC.md. Roadmap: ROADMAP.md. License: MIT.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A local MCP server that provides LLM clients with read/write access to email and calendar data from Gmail, iCloud, and generic IMAP providers. It runs entirely on your machine, keeping data private while enabling email management, calendar operations, and task handling through natural language.
    39
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Provider-agnostic email MCP server that connects any IMAP mailbox to AI assistants, enabling email management through natural language.
    8
    AGPL 3.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Open-source, self-hosted Inbox-as-a-Service API for AI agents. It enables agents to manage email inboxes, send/receive emails, search messages, and wait for replies via REST or MCP.
    27
    Apache 2.0
  • F
    license
    Not graded
    quality
    C
    maintenance
    Local IMAP/SMTP MCP server that lets Claude read, search, draft, send, flag, and move mail across multiple IMAP mailboxes. Credentials stay on your machine.
    -