imap-rest-mailcow
v0.5.0
Published
Stateless REST facade for IMAP, designed as a third-party addon for mailcow-dockerized. Optional Mistral OCR for attachments.
Maintainers
Readme
imap-rest-mailcow
Stateless REST facade for IMAP, designed as a third-party addon for mailcow-dockerized. Authenticates every request against mailcow's Dovecot using HTTP Basic Auth, pools IMAP connections, and exposes a small set of mailbox/message endpoints. Optional Mistral OCR endpoint returns attachment contents as text.
No database of accounts, no credential store, no persistent state — just HTTP Basic Auth in front of IMAP, with a short-lived auth cache.
Overview
Every API call carries Authorization: Basic <base64(email:password)>.
On each request:
- The credentials hash is looked up in a SQLite (WAL) auth cache.
- On miss, the addon attempts an IMAP LOGIN against mailcow's Dovecot. The result (valid/invalid) is cached with a short TTL.
- Valid requests acquire a pooled IMAP connection for that user, run the operation against mailcow's Dovecot, and release the connection.
No credentials are ever written to disk. The cache stores only SHA-256 hashes with expiry timestamps.
Prerequisites
- A running mailcow-dockerized install on the same host.
- Docker + Docker Compose v2.
- Optional: a Mistral API key, only if you want the OCR endpoint enabled.
Install
This addon is designed to live outside /opt/mailcow-dockerized/,
so mailcow's update.sh (which resets its own working tree) never
touches it.
git clone <this-repo> /opt/imap-rest-mailcow
cd /opt/imap-rest-mailcow
cp .env.example .env # tweak if you want non-defaults
sudo install/setup.sh # builds image, copies nginx config, restarts nginx-mailcowAfter install the API is reachable at
https://<your-mailcow-host>/imap-rest/. Browse there in any browser
to load the interactive Swagger UI.
The setup script is idempotent — re-run after git pull to upgrade.
The only file it drops into mailcow's tree is
data/conf/nginx/site.imap-rest.custom, which mailcow preserves
across update.sh runs.
If your mailcow lives somewhere other than /opt/mailcow-dockerized,
override it: sudo MAILCOW_PATH=/srv/mailcow install/setup.sh.
Use the prebuilt image
Each push to master and each tagged release publishes a container
image to GHCR. To use it instead of building locally, edit
docker-compose.yml so the imap-rest service uses image: instead
of build::
services:
imap-rest:
image: ghcr.io/<owner>/imap-rest-mailcow:latest
# remove or comment out the `build:` block aboveAvailable tags: latest (latest tagged release), master (head of
master), <version> (e.g. 0.2.0), sha-<short> (specific commit).
Configuration
All settings are environment variables; see .env.example for the full
list. The most common:
| Var | Default | Notes |
|---|---|---|
| PORT | 3001 | Host listen port |
| BIND_ADDR | 127.0.0.1 | Set to 0.0.0.0 only when bypassing mailcow's nginx |
| IP_ALLOWLIST | empty | Comma-separated CIDRs / IPs allowed to reach the API. Empty = all. Loopback always allowed. |
| TRUST_PROXY | true | Honor X-Forwarded-For from upstream proxies |
| IMAP_HOST | dovecot-mailcow | mailcow Dovecot service |
| IMAP_PORT | 143 | Plain or STARTTLS port |
| IMAP_SECURE | false | true = implicit TLS (993) |
| CACHE_TTL_VALID_MS | 300000 | Positive-auth cache TTL |
| CACHE_TTL_INVALID_MS | 10000 | Negative-auth cache TTL |
| POOL_MAX | 50 | Max live IMAP connections |
| POOL_IDLE_MS | 30000 | Idle connection eviction |
| TLS_CERT / TLS_KEY | — | Optional HTTPS termination |
| LOG_LEVEL | info | pino level |
| MAILCOW_NETWORK | mailcowdockerized_mailcow-network | Override if mailcow renames it |
| MISTRAL_API_KEY | — | Optional. Enables OCR + AI summarize/draft. |
| MISTRAL_OCR_MODEL | mistral-ocr-latest | Pin to mistral-ocr-2512 for stable output |
| MISTRAL_OCR_TIMEOUT_MS | 60000 | Outbound OCR request timeout |
| MISTRAL_CHAT_MODEL | mistral-small-latest | Used by /v1/ai/summarize and /v1/ai/draft-reply |
| MISTRAL_CHAT_TIMEOUT_MS | 30000 | Outbound chat request timeout |
| OCR_CACHE_ENABLED | true | Cache OCR results by content hash; no TTL |
| OCR_CACHE_MAX_ENTRIES | 1000 | Max cached OCR results before oldest are evicted |
| WEBMAIL_ENABLED | true | Mounts the Svelte SPA at /webmail/. Set false to disable. |
| WEBMAIL_DIST | ./webmail/dist | Path to the built SPA. Containerized image rebuilds in CI. |
| SMTP_HOST / SMTP_USER / SMTP_PASS | — | Reserved. Send is stubbed; future release will wire it up. |
Public exposure
The recommended path is behind mailcow's nginx, which is what
install/setup.sh configures. The addon listens only on 127.0.0.1
by default, and nginx-mailcow reaches it via the docker-network alias
imap-rest:3001. The addon is published at
https://<mailcow-host>/imap-rest/ using mailcow's existing TLS.
Use IP_ALLOWLIST (e.g. IP_ALLOWLIST=10.0.0.0/8,203.0.113.5) to
further restrict who can reach the API. Loopback is always allowed so
the docker healthcheck still works.
Advanced: bypass mailcow's proxy
Set BIND_ADDR=0.0.0.0 and either configure TLS_CERT/TLS_KEY to
terminate TLS in the addon, or front it with your own TLS terminator.
This is not the recommended path — never expose the addon directly
without TLS.
API docs
Browse to the addon's root path in any browser to load the Swagger UI:
- Behind mailcow's proxy:
https://<mailcow-host>/imap-rest/ - Direct:
http://<host>:3001/
The page loads without authentication; click Authorize and enter
your mailcow email + password (use a SOGo App Password if your account
has 2FA enabled). The raw OpenAPI document is at /openapi.json on
the same base URL.
Webmail UI
The container ships with a Svelte single-page webmail at /webmail/:
- Behind mailcow's proxy:
https://<mailcow-host>/imap-rest/webmail/ - Direct:
http://<host>:3001/webmail/
Polished light + dark themes (auto-detected, user-overridable). Three-pane
Gmail-style layout with keyboard shortcuts (j/k next/prev, s star,
u toggle read, # trash, c compose, / focus search, Esc close).
AI features (require MISTRAL_API_KEY):
- Summarize the open message into 3–5 bullets.
- Draft reply with optional intent ("decline politely", "ask for an extension"…).
- OCR any image / PDF attachment inline.
Send is intentionally stubbed for v1: the compose modal posts to
POST /v1/messages/send which returns 501 Not Implemented with a
friendly explanation. SMTP wiring will land in a future release behind
the reserved SMTP_* env vars.
To rebuild the SPA locally (only needed if you change webmail/):
npm run build:webmailScreenshots of the UI live under webmail/test/screenshots/.
Upgrade safety
This addon is upgrade-safe by design:
- Lives outside
/opt/mailcow-dockerized/, so mailcow'supdate.shcannot touch it. - Joins mailcow's network as
external; if mailcow renames the network in a future major version, overrideMAILCOW_NETWORKin.env. - The optional nginx site file (
install/site.imap-rest.custom) lives underdata/conf/, which mailcow preserves across updates.
To upgrade the addon itself: git pull && sudo install/setup.sh.
Uninstall
cd /opt/imap-rest-mailcow
docker compose down -v
rm -rf /opt/imap-rest-mailcowIf you installed the nginx snippet, also:
rm /opt/mailcow-dockerized/data/conf/nginx/site.imap-rest.custom
docker compose -f /opt/mailcow-dockerized/docker-compose.yml restart nginx-mailcowTroubleshooting
/health returns 200 but /v1/mailboxes returns 401.
The credentials Basic-Auth'd in are not valid mailcow accounts. Check
mailcow's SOGo/admin UI to confirm the mailbox exists.
Container can't reach dovecot-mailcow.
The mailcow-network external network may have been renamed. Run
docker network ls | grep mailcow and set MAILCOW_NETWORK in .env.
OCR endpoint returns 501.
MISTRAL_API_KEY is unset. The addon ships with OCR disabled by default.
Set the env var in .env and docker compose up -d to enable.
OCR endpoint returns 502. The addon reached Mistral but Mistral rejected our credentials (401), denied access (403), or returned a 5xx. Check the container logs.
API
All authenticated routes require Authorization: Basic <base64(email:password)>.
Errors follow RFC 7807 (application/problem+json).
Mailboxes
GET /v1/mailboxesPOST /v1/mailboxes—{path}PUT /v1/mailboxes/:path—{newPath}DELETE /v1/mailboxes/:path
Messages
GET /v1/mailboxes/:path/messages?page=0&pageSize=20&search=...GET /v1/mailboxes/:path/messages/:uidGET /v1/mailboxes/:path/messages/:uid/rawGET /v1/mailboxes/:path/messages/:uid/attachments/:idGET /v1/mailboxes/:path/messages/:uid/attachments/:id/text— OCR (requiresMISTRAL_API_KEY)PUT /v1/mailboxes/:path/messages/:uid/flags—{add?,remove?,set?}PUT /v1/mailboxes/:path/messages/:uid/move—{path}DELETE /v1/mailboxes/:path/messages/:uidPOST /v1/messages/send— stubbed; returns501until SMTP is wired
AI (require MISTRAL_API_KEY)
POST /v1/ai/summarize—{text, maxWords?}→{content, model}POST /v1/ai/draft-reply—{thread, intent?}→{content, model}
Public
GET /healthGET /— Swagger UIGET /openapi.json— OpenAPI 3.1 documentGET /webmail/— Svelte webmail SPA
:path is URL-encoded to support IMAP namespaces and delimiters.
Attachment OCR
# Plain text (page markdowns joined with "\n\n---\n\n")
curl -u '[email protected]:password' \
https://mail.example.com/imap-rest/v1/mailboxes/INBOX/messages/42/attachments/2/text
# Full Mistral response (per-page markdown, usage_info, bbox)
curl -u '[email protected]:password' \
'https://mail.example.com/imap-rest/v1/mailboxes/INBOX/messages/42/attachments/2/text?format=json'If MISTRAL_API_KEY is unset, the endpoint returns 501 Not Implemented.
Attachments above 50 MB return 413 Payload Too Large (Mistral's hard
limit). On 429, the addon forwards Mistral's Retry-After header.
OCR results are cached by content hash (sha256 of attachment bytes +
model name) in the same SQLite file as the auth cache. Two messages
containing the identical attachment share the cache entry, so the second
read returns instantly without calling Mistral again. The cache has no
TTL — OCR output is deterministic for given (bytes, model) — and is
bounded by row count via OCR_CACHE_MAX_ENTRIES (default 1000). Set
OCR_CACHE_ENABLED=false to disable.
MCP server
This package ships an optional Model Context
Protocol server that exposes the REST
API as tools an LLM can call. The MCP server runs as a local subprocess
over stdio (the standard MCP pattern) and talks to the REST API over
HTTP, so it works against any running imap-rest-mailcow instance —
local or remote.
Tools
| Tool | Wraps |
|---|---|
| list_mailboxes | GET /v1/mailboxes |
| create_mailbox | POST /v1/mailboxes |
| rename_mailbox | PUT /v1/mailboxes/:path |
| delete_mailbox | DELETE /v1/mailboxes/:path |
| list_messages | GET /v1/mailboxes/:path/messages |
| get_message | GET /v1/mailboxes/:path/messages/:uid |
| ocr_attachment | GET .../attachments/:id/text (Mistral OCR) |
| flag_message | PUT .../messages/:uid/flags |
| move_message | PUT .../messages/:uid/move |
| delete_message | DELETE .../messages/:uid |
Configure
The most reliable setup is to point your MCP client at this checkout
explicitly with node, rather than assuming imap-rest-mcp is
installed globally on PATH.
Claude Desktop / Claude Code
{
"mcpServers": {
"imap-rest-mailcow": {
"command": "node",
"args": ["/opt/imap-rest-mailcow/bin/imap-rest-mcp"],
"env": {
"IMAP_REST_BASE_URL": "http://127.0.0.1:3001",
"IMAP_REST_USER": "[email protected]",
"IMAP_REST_PASS": "your-mailcow-password"
}
}
}
}Codex
Add to your Codex MCP config:
{
"mcpServers": {
"imap-rest-mailcow": {
"command": "node",
"args": ["/opt/imap-rest-mailcow/bin/imap-rest-mcp"],
"env": {
"IMAP_REST_BASE_URL": "http://127.0.0.1:3001",
"IMAP_REST_USER": "[email protected]",
"IMAP_REST_PASS": "your-mailcow-password"
}
}
}
}Kimi CLI
kimi mcp add --transport stdio \
-e IMAP_REST_BASE_URL=http://127.0.0.1:3001 \
-e [email protected] \
-e IMAP_REST_PASS=your-mailcow-password \
imap-rest-mailcow -- \
node /opt/imap-rest-mailcow/bin/imap-rest-mcpThen verify:
kimi mcp test imap-rest-mailcowInstall via npm / npx
You can also run the MCP server without cloning the repo:
npx --yes --package imap-rest-mailcow imap-rest-mcpOr configure your MCP client to invoke it via npx:
{
"mcpServers": {
"imap-rest-mailcow": {
"command": "npx",
"args": ["--yes", "--package", "imap-rest-mailcow", "imap-rest-mcp"],
"env": {
"IMAP_REST_BASE_URL": "http://127.0.0.1:3001",
"IMAP_REST_USER": "[email protected]",
"IMAP_REST_PASS": "your-mailcow-password"
}
}
}
}If you prefer a global binary, you can also install the package and use:
{
"mcpServers": {
"imap-rest-mailcow": {
"command": "imap-rest-mcp",
"env": {
"IMAP_REST_BASE_URL": "http://127.0.0.1:3001",
"IMAP_REST_USER": "[email protected]",
"IMAP_REST_PASS": "your-mailcow-password"
}
}
}
}but that requires imap-rest-mcp to be installed somewhere your MCP
client can execute it from.
The MCP server uses static credentials from the env — single-user. It includes Basic Auth on every REST call. Per-call credentials are not supported.
Running tests
Server unit tests (mocked imapflow + mocked undici, no network):
npm install
npm testWebmail end-to-end tests (Playwright + Chromium against a Vite preview server with mocked API responses):
cd webmail
npm install
npx playwright install chromium
npm run test:e2eThe Playwright config runs serially with a single worker — fine on
RAM-constrained hosts. Screenshots land in webmail/test/screenshots/
and on failure under webmail/test-results/.
There is no integration test tier in this project; if you add one,
ensure your tests trap "docker compose down -v" EXIT so no Docker
state is left behind.
License
MIT — see LICENSE.
This is a fresh project, not a fork. It depends on
imapflow (MIT) for IMAP
client behaviour.
