mail-shadow-mcp Server

MCP server for structured, read-only email access. Exposes a minimal, auditable API surface — AI agents can search and read emails, but cannot send, delete, or modify your mailbox.

Documentation

mail-shadow-mcp logo

Structured email access for AI agents — with built-in safety guarantees.

Build Latest Release Go Version Go Report Card License

mail-shadow-mcp is a Model Context Protocol (MCP) server that creates a local shadow copy of your IMAP mailboxes in a SQLite database. AI agents query the local database through well-defined MCP tools instead of connecting directly to your IMAP server.

[Remote IMAP Server] ──IMAP──▶ [Sync Engine] ──▶ [SQLite FTS5] ◀──▶ [MCP Server] ◀──▶ [AI Agent]

Features

  • Local shadow database — emails are synced into a local SQLite database; the AI agent never connects to your IMAP server directly
  • Read-only — no STORE, APPEND, or EXPUNGE commands; your mailbox is never modified
  • Incremental sync — only fetches messages newer than the last known UID
  • Full-text search — SQLite FTS5 index for fast body-text queries
  • Multi-account — sync any number of IMAP accounts simultaneously
  • IMAP IDLE — optional real-time push notifications; new mail detected within seconds instead of waiting for the next poll interval
  • Read/replied statusis_read and is_replied flags synced from IMAP and exposed as filters
  • Thread viewget_thread walks full email conversations via Message-ID / In-Reply-To headers
  • Paginated results — all list tools return total_count so agents can page through large result sets
  • On-demand attachments — attachment files are fetched from IMAP only when explicitly requested
  • Safe soft-delete — when an agent calls delete_mail, the MCP server performs an IMAP MOVE to a configurable trash folder; nothing is ever permanently deleted
  • Flexible transportstdio for local tools (Claude Desktop), http (StreamableHTTP) or sse for remote and Docker deployments
  • Docker-ready — official multi-arch image (linux/amd64, linux/arm64) published to ghcr.io on every release

Safety: Nothing Is Ever Really Deleted

mail-shadow-mcp gives AI agents a delete_mail tool, but this tool never issues a destructive IMAP command. Here is exactly what happens when an agent calls it:

  1. The MCP server looks up the email in the local database.
  2. It opens a short-lived IMAP connection and executes IMAP MOVE — moving the message to the trash_folder you specify in config.yaml (e.g. "llm_delete").
  3. The local database entry is removed so the agent can no longer see the mail in future queries.
  4. The email remains intact on the IMAP server, safely tucked away in the trash folder. You can inspect, restore, or permanently delete it yourself at any time.

The AI agent has no direct IMAP access. It cannot expunge messages, empty folders, or issue any write command other than this controlled move. If trash_folder is not configured for an account, delete_mail returns an error and does nothing.


MCP Tools

ToolDescription
list_accounts_and_foldersList all synced accounts and their folders
get_recent_activityN most recent emails with optional filters (is_read, has_attachments, pagination)
get_email_contentFull body text, read/replied status, and attachment list for a single email
search_emailsFTS5 full-text search with subject/sender/date/folder/is_read/sent_by filters
get_threadAll emails in the same thread as a given email, sorted by date ascending
download_attachmentsFetch attachment files from IMAP and save them to disk
get_download_linkGenerate a temporary HTTP download URL for attachments (optional fallback)
delete_mailSoft-delete an email by moving it to a configured trash folder (IMAP MOVE, no permanent deletion)

Quick Start

1. Build

make build          # current platform
make release        # cross-compile for all platforms into dist/

Requires Go 1.25+.

2. Configure

Copy the example config and fill in your IMAP credentials:

cp config.example.yaml config.yaml
sync_interval_min: 15

database:
  path: "data/mail.db"

attachment_dir: "data/attachments"

# Optional: structured JSON logs for Loki / Elasticsearch pipelines.
# log_format: json   # json | text (default: text)
# log_level: info    # debug | info | warn | error (default: info)
# log_file: "/var/log/mail-shadow-mcp.log"  # omit to use stderr

# Optional: lightweight HTTP server for temporary attachment download links.
# Only enable this if you need the get_download_link MCP tool (e.g. as a
# fallback when the AI agent cannot transfer files via its normal channels).
fileserver_port: 8787               # TCP port to listen on (disabled if omitted)
fileserver_ttl_min: 15              # minutes before a link expires (default: 15)
fileserver_host: "localhost"        # hostname/IP shown in generated URLs

accounts:
  - id: "[email protected]"
    host: "imap.example.com"
    port: 993
    username: "[email protected]"
    password: "$WORK_IMAP_PASS"   # or plain text
    folders: ["INBOX", "Archive"] # only sync the mentioned folders
    # idle_folders: ["INBOX"]     # optional: IMAP IDLE for real-time push on these folders
    # trash_folder: "llm_delete"  # optional: target for delete_mail (soft-delete via IMAP MOVE)
    
  - id: "[email protected]"
    host: "imap.example.com"
    port: 993
    username: "[email protected]"
    password: "$PRIVATE_IMAP_PASS"   # or plain text

Credentials can be stored as plain text or as $ENV_VAR references that are resolved at runtime.

3. Run

# Start the MCP server (syncs on startup, then every sync_interval_min minutes)
./mail-shadow-mcp serve

# One-shot sync without starting the server
./mail-shadow-mcp sync

# Query from the command line (output is JSON)
./mail-shadow-mcp query --subject "invoice" --body "Q1"
./mail-shadow-mcp query -q "budget" --attachments only   # only emails with attachments
./mail-shadow-mcp query --recent --attachments none      # recent emails without attachments
./mail-shadow-mcp query --recent --limit 10 --offset 10  # page 2

# Download attachments for a specific email
./mail-shadow-mcp attachments --id "[email protected]:INBOX:42"

Docker

Pre-built multi-architecture images (linux/amd64, linux/arm64) are published to the GitHub Container Registry on every release:

docker pull ghcr.io/dryas/mail-shadow-mcp:latest

Required volumes

VolumePurpose
/configMust contain config.yaml. Mount read-only.
/dataPersistent storage for the SQLite database and downloaded attachments. Survives container restarts.

Quick Docker run

Step 1 — prepare a config.yaml with transport: http and paths pointing to /data:

transport: http
http_addr: ":8080"
http_bearer_token: "your-secret-token"   # see below for how to generate one

database:
  path: "/data/mail.db"

attachment_dir: "/data/attachments"

accounts:
  - id: "[email protected]"
    host: "imap.example.com"
    port: 993
    username: "[email protected]"
    password: "$WORK_IMAP_PASS"

Step 2 — run the container:

docker run -d \
  --name mail-shadow-mcp \
  -v ./config.yaml:/config/config.yaml:ro \
  -v mail-shadow-data:/data \
  -p 8080:8080 \
  ghcr.io/dryas/mail-shadow-mcp:latest

The MCP server is now reachable at http://localhost:8080/mcp.

docker-compose example

services:
  mail-shadow-mcp:
    image: ghcr.io/dryas/mail-shadow-mcp:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./config.yaml:/config/config.yaml:ro   # your config — mount read-only
      - mail-shadow-data:/data                 # persistent DB + attachments
    environment:
      - WORK_IMAP_PASS=your_password_here      # referenced as $WORK_IMAP_PASS in config

volumes:
  mail-shadow-data:

Passwords as environment variables: In config.yaml you can reference passwords as $ENV_VAR — the server resolves them at startup. Pass them via environment: in docker-compose or via -e with docker run. This way no plaintext password ends up in the config file.

Authentication (Bearer Token)

When using http or sse transport, always set http_bearer_token — otherwise the MCP endpoint is reachable by anyone who can access the port.

Generate a cryptographically secure token:

# Linux / macOS / WSL
openssl rand -hex 32

# PowerShell
[System.Convert]::ToBase64String((1..32 | ForEach-Object { [byte](Get-Random -Max 256) }))

Set it in your config.yaml:

http_bearer_token: "a3f1c2e8b4d9..."   # paste your generated token here

Every request to the MCP endpoint must then include the header:

Authorization: Bearer a3f1c2e8b4d9...

Most MCP clients (Claude Desktop, Cursor, etc.) support Bearer auth natively — see the connection example below.

Connecting an AI agent to the Docker container

Point your MCP client at http://localhost:8080/mcp using the StreamableHTTP transport:

{
  "mcpServers": {
    "mail_shadow": {
      "url": "http://localhost:8080/mcp",
      "headers": {
        "Authorization": "Bearer your-secret-token"
      }
    }
  }
}

Build the image yourself

docker build --build-arg VERSION=dev -t mail-shadow-mcp .

Integrating with an AI Agent

Configure your MCP client to launch the server via stdio.

Example for Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "mail_shadow": {
      "command": "/path/to/mail-shadow-mcp",
      "args": ["serve", "--config", "/path/to/config.yaml"]
    }
  }
}

Example for Hermes Agent (config.yaml):

mcp_servers:
  mail_shadow:
    command: "/path/to/mail-shadow-mcp"
    args: ["serve", "--config", "/path/to/config.yaml"]

Example for OpenClaw (~/.openclaw/openclaw.json):

{
  "mcpServers": {
    "mail_shadow": {
      "command": "/path/to/mail-shadow-mcp",
      "args": ["serve", "--config", "/path/to/config.yaml"],
      "transport": "stdio"
    }
  }
}

TLS Modes

tls_modePortDescription
tls993Implicit TLS (default)
starttls143STARTTLS upgrade
none143No encryption — localhost/testing only

Set tls_skip_verify: true to accept self-signed certificates.


IMAP IDLE (Real-time Push)

By default, mail-shadow-mcp polls for new messages every sync_interval_min minutes. For folders where you want near-instant notifications, enable IMAP IDLE:

accounts:
  - id: "[email protected]"
    # ...
    idle_folders: ["INBOX"]   # IDLE runs on top of regular polling
  • One dedicated IMAP connection is opened per entry in idle_folders
  • When the server sends an EXISTS notification, a sync is triggered immediately
  • Regular polling continues unchanged for all other folders
  • Falls back to polling automatically if the server does not support IDLE
  • Exponential backoff (30 s → 5 min) on persistent connection errors

Attachment Download Server

The optional built-in HTTP server lets the AI agent generate temporary, single-use download links for attachment files — useful as a fallback when the agent cannot transfer files through its normal communication channels (e.g. WhatsApp, email).

Enable it in config.yaml:

fileserver_port: 8787        # TCP port to listen on
fileserver_ttl_min: 15       # minutes before a link expires (default: 15)
fileserver_host: "localhost" # hostname/IP shown in generated URLs

When enabled, the get_download_link MCP tool becomes available. It downloads the attachments, saves them to attachment_dir, and returns one temporary URL per file:

[
  {
    "file": "data/attachments/[email protected]/INBOX/42/invoice.pdf",
    "url": "http://localhost:8787/dl/3f8a1c.../invoice.pdf"
  }
]

Each link is single-use and expires after fileserver_ttl_min minutes. The tool description instructs the AI agent to prefer direct file transfer and only fall back to this mechanism when necessary.


License

Apache 2.0 — see LICENSE for details.
Copyright (c) 2026 Benjamin Kaiser.