Skip to main content
Glama
buzzer

imap-mcp

by buzzer

imap-mcp

A small MCP server that gives an AI assistant (e.g. Claude Desktop) read access to IMAP mailboxes and the ability to create drafts. It was written for a Hetzner mailbox plus an iCloud account on a Raspberry Pi, but it works with any IMAP server that supports IMAPS (port 993).

Status: a personal project, published as-is. It is meant for a trusted home network, not for the open internet. See Security and SECURITY.md.

What it does

Tools: list_accounts, list_folders, list_messages, search_messages, read_message, mark_read, create_draft. Every tool has an account parameter; if omitted, the first configured account is used.

  • Reading is read-only: messages are fetched with BODY.PEEK, nothing is marked as read, moved or deleted by default.

  • The only write access to existing messages is the \Seen flag, set by mark_read or, if enabled, by read_message itself. See Read status.

  • Sending is deliberately not possible. create_draft stores a draft in the drafts folder; you review and send it yourself in your mail client.

  • Drafts can carry attachments (PDF, PNG, JPEG) taken from one configured folder (ATTACHMENT_DIR, off by default). See Attachments.

  • Senders are limited to the account address plus configured aliases (MAIL_ALIASES, wildcards like *@example.com allowed). For replies, the address the original mail was sent to is chosen automatically.

  • At most 2 MB of a message are loaded; read_message reports message_truncated when a mail was cut off.

  • One IMAP connection per account is reused between calls.

  • Mail content is treated as untrusted data (see the server instructions and Security).

Related MCP server: postfach-mcp

Requirements

  • Python 3.10+

  • An IMAP account with an app password where required (iCloud, Gmail, ...)

  • For the server itself: mcp and uvicorn (see requirements.txt); the mail logic uses only the standard library.

1. Installation

git clone https://github.com/<you>/imap-mcp.git
cd imap-mcp
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

cp config.env.example config.env
chmod 600 config.env
$EDITOR config.env          # user, password, aliases

.venv/bin/python -m unittest discover -s tests   # offline tests
.venv/bin/python server.py --check                # test the real IMAP connections

--check prints one line per account, for example OK [hetzner] user@example.com @ mail.your-server.de, Drafts: INBOX.Drafts, Senders: ….

The client starts the server on demand over SSH: no open port, no token, encryption by SSH. Use a key without passphrase prompt (ssh -o BatchMode=yes host echo ok must work). Example claude_desktop_config.json:

{
  "mcpServers": {
    "mail": {
      "command": "ssh",
      "args": [
        "-T", "-o", "BatchMode=yes", "-o", "ServerAliveInterval=30",
        "user@your-host",
        "/path/to/imap-mcp/.venv/bin/python",
        "/path/to/imap-mcp/server.py"
      ]
    }
  }
}

2b. Alternative: HTTP service on the LAN

Useful if several machines should use the server.

python3 -c 'import secrets; print(secrets.token_urlsafe(32))'   # generate a token
# in config.env: MCP_AUTH_TOKEN=<token>

scripts/make-cert.sh        # self-signed certificate for this host
# in config.env:
# TLS_CERT=/path/to/imap-mcp/certs/server.crt
# TLS_KEY=/path/to/imap-mcp/certs/server.key

sudo cp imap-mcp.service /etc/systemd/system/    # adjust User= and the paths first
sudo systemctl daemon-reload
sudo systemctl enable --now imap-mcp

The endpoint is https://<host>:8765/mcp and requires Authorization: Bearer <token>. Without --host the server listens on 127.0.0.1 only; the systemd unit passes --host 0.0.0.0 (all interfaces), so change it to your LAN address if you prefer. Anything but plain HTTP requests (e.g. WebSocket) is refused. Without TLS_CERT/TLS_KEY the server keeps running in plain text and logs a warning. The certificate is valid for 730 days; run make-cert.sh again afterwards and redistribute it.

Clients that only speak stdio can use the mcp-remote bridge (needs Node.js). The self-signed certificate must be trusted by Node, so point NODE_EXTRA_CA_CERTS to a copy of server.crt on the client machine:

{
  "mcpServers": {
    "mail": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://<host>:8765/mcp",
               "--header", "Authorization:${AUTH}"],
      "env": { "AUTH": "Bearer <token>",
               "NODE_EXTRA_CA_CERTS": "/path/on/the/client/server.crt" }
    }
  }
}

The custom connectors in Claude's settings do not work for a LAN address, because they connect from Anthropic's cloud.

Configuration

See config.env.example for all keys (MAIL_USER, MAIL_PASSWORD, MAIL_ADDRESS, MAIL_FROM_NAME, MAIL_ALIASES, IMAP_HOST, IMAP_PORT, IMAP_TIMEOUT, DRAFTS_FOLDER, ATTACHMENT_DIR, ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_TOTAL_BYTES, ATTACHMENT_MAX_COUNT, MARK_READ_ON_READ, BCC_SELF, ACCOUNTS, ACCOUNT_NAME, MCP_AUTH_TOKEN, TLS_CERT, TLS_KEY). The account keys up to ATTACHMENT_MAX_COUNT can be prefixed with <NAME>_ for additional accounts; the remaining keys are global. Every key can also be set as an environment variable; MAIL_MCP_CONFIG or --config selects another config file.

Additional account: iCloud

ACCOUNTS=hetzner,icloud
ICLOUD_MAIL_USER=first.last                # login, usually the part before @icloud.com
ICLOUD_MAIL_ADDRESS=first.last@icloud.com
ICLOUD_MAIL_PASSWORD=abcd-efgh-ijkl-mnop   # app-specific password

The password is an app-specific password from account.apple.com (Sign-In and Security → App-Specific Passwords), not the Apple ID password. imap.mail.me.com:993 is preconfigured for the account name icloud and mail.your-server.de:993 for hetzner. Every other account name needs <NAME>_IMAP_HOST; the server never guesses a host, so a password is not sent to the wrong provider.

Read status

So that your local mail client shows what the assistant has already looked at, read_message can set the \Seen flag on every message it returns:

  • MARK_READ_ON_READ=true in config.env switches this on for all accounts (one global option, off by default). There is no per-call or per-account setting.

  • Only read_message does this. Listing and searching show headers only and never change a flag; mark_read stays available for messages processed otherwise.

  • The result has unread (state before reading) and marked_read. If the flag cannot be set, the message is still returned and marked_read is false.

  • Nothing but \Seen is ever set; messages that are already read are left untouched.

Bcc to yourself

With BCC_SELF=true in config.env (global for all accounts, off by default), create_draft adds the sender address of the draft as Bcc, so a sent copy also lands in that mailbox. It is the address actually used as sender (account address or alias, see from_address). It is not added twice if it is already in To, Cc or Bcc. The result shows the final bcc, and list_accounts reports bcc_self.

Attachments

create_draft accepts attachments, a list of file paths relative to ATTACHMENT_DIR. The feature is off until that key is set in config.env (list_accounts shows attachments_enabled per account). Rules, all enforced on the server:

  • only .pdf, .png, .jpg, .jpeg, and the content must start with the matching magic bytes (a renamed file is refused);

  • only files below ATTACHMENT_DIR: absolute paths, .., symbolic links and hidden files or folders (.env, .ssh, ...) are refused;

  • at most ATTACHMENT_MAX_COUNT files (default 5), ATTACHMENT_MAX_BYTES each (default 10 MB) and ATTACHMENT_MAX_TOTAL_BYTES together (default 15 MB);

  • the draft is only stored if every attachment passes; the model never sees the file content, only the names.

Suggested setup

Use a dedicated, flat folder that holds nothing but files you are happy to attach. Everything in it can be attached by the model.

mkdir -p ~/mail-attachments
chmod 700 ~/mail-attachments
ATTACHMENT_DIR=/home/<user>/mail-attachments
  • With the stdio transport over SSH the server reads the files as the SSH user.

  • Paths in the tool call are relative to that folder, e.g. invoice-2026-10.pdf or invoices/2026/invoice-2026-10.pdf; absolute paths are refused.

  • The server only reads. Put files there yourself (scp file.pdf host:mail-attachments/, rsync, Syncthing or a Samba share), then ask the assistant to attach them by name.

  • Several accounts can use separate folders with the prefix, e.g. ICLOUD_ATTACHMENT_DIR=/home/<user>/mail-attachments-icloud.

  • With the systemd unit (ProtectHome=read-only) the service user needs read access; the simplest way is a folder outside /home, e.g. /opt/imap-mcp/attachments.

  • Remove files you no longer need.

Security

  • config.env contains mailbox passwords: chmod 600, never commit it (the server warns if the file is readable by others).

  • Mail content is untrusted text. The server instructions tell the model not to follow requests contained in mails. Since nothing can be sent or deleted, the possible damage is limited to drafts and to leaking mail content to the model.

  • HTML mail is converted to text for the model without content a reader would not see (display:none, hidden, opacity:0, collapsed preheaders, script/style/head), which removes the most common hiding places for invisible prompt injection. Text that is only unreadable (white on white, tiny fonts set on child elements) is still passed on, so the protection is a mitigation, not a guarantee.

  • Search terms containing control characters (CR/LF) are rejected, so they cannot inject additional IMAP commands. Recipient addresses with line breaks are rejected.

  • The HTTP transport uses a single static bearer token without rate limiting. Use it only on a trusted network, with TLS, and rotate the token if it leaks.

  • Failed HTTP authentication attempts are logged with the client IP; tool calls (account, folder, UID) are logged without mail content.

Known limitations

  • Drafts are plain text only. The attribution line of a quoted reply ("Am … schrieb …:") and the placeholder subject "(kein Betreff)" are in German.

  • Attachments are limited to PDF/PNG/JPEG from one folder; no moving/deleting of messages; the only flag that can be set is \Seen (via mark_read).

  • Tested against Hetzner (Dovecot) and iCloud only.

Development

python -m unittest discover -s tests    # no network and no MCP SDK needed

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to read, search, and manage emails via IMAP with secure, read-only access to email accounts.
    6
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI assistants to securely access IMAP mailboxes with read-focused tools for searching, reading, listing messages, and creating drafts, while deliberately preventing sending or deleting emails.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables read-only access to multiple email accounts, including Gmail via App Password and generic IMAP providers, so an agent can list accounts and folders, search messages, and fetch messages or attachments. All operations are strictly read-only, with no sending, writing, deleting, or moving.
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI assistants to securely manage multiple email accounts via IMAP and SMTP, including searching, reading, sending, replying, forwarding, organizing folders, marking, and bulk deleting messages.
    1,152 npm
    MIT