imap-rest-mailcow
v0.6.2
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
REST facade and full-featured webmail for mailcow-dockerized.
Authenticates against mailcow's Dovecot via HTTP Basic Auth or Bearer
sessions, pools IMAP connections, and exposes the mailbox surface plus
SMTP send, CalDAV calendar, S3-backed drive, web-push notifications,
sender-policy management, mail rules via Sieve, an AI chat panel, and
a Svelte SPA (desktop + mobile) at /webmail/.
The same API is also exposed as a Model Context Protocol
server (bin/imap-rest-mcp) so an LLM can drive the mailbox directly.
For the architectural decisions, working norms, and pitfalls that have
caught AI agents out, see AGENTS.md.
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 for AI features: an LLM provider key (
LLM_API_KEY— works with Mistral, OpenAI, Groq, Together, Ollama, Perplexity, OpenRouter, or Anthropic viaLLM_PROVIDER=anthropic). - Optional for OCR:
MISTRAL_API_KEY. - Optional for the Drive feature: a Backblaze B2 / S3-compatible
bucket and per-user credentials in
S3_DRIVE_USERS_JSON. - Optional for push notifications: VAPID keys.
- Optional for voice replies: an ElevenLabs API key.
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:
The full list lives in .env.example. Most-used:
| 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 the trusted nginx proxy; set false for direct exposure |
| CREDENTIAL_ENCRYPTION_KEY | — | Required in production. Persistent 32-byte base64 or 64-character hex key for encrypted SQLite session credentials |
| PUBLIC_API_DOCS | false | Set true only to intentionally publish Swagger UI and /openapi.json |
| IMAP_HOST | dovecot-mailcow | mailcow Dovecot service |
| IMAP_PORT | 993 | IMAP port (STARTTLS or implicit TLS) |
| IMAP_SECURE | true | true = implicit TLS (993); false = STARTTLS (143) |
| IMAP_TLS_REJECT_UNAUTHORIZED | true | Disable only for self-signed dev certs |
| CACHE_TTL_VALID_MS | 300000 | Positive-auth cache TTL |
| CACHE_TTL_INVALID_MS | 10000 | Negative-auth cache TTL |
| SESSION_MAX_LIFETIME_MS | 86400000 | Hard cap on Bearer-session lifetime regardless of activity |
| 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 |
| SMTP_HOST | postfix-mailcow (in compose) | mailcow's postfix container — /v1/send works out of the box |
| SMTP_PORT / SMTP_SECURE | 587 / false | STARTTLS on 587 by default |
| SOGO_URL | http://sogo-mailcow:20000/SOGo (in compose) | CalDAV endpoint; calendar routes register if set |
| CALDAV_TLS_REJECT_UNAUTHORIZED | true | Disable only for self-signed SOGo certs |
| MAILCOW_DB_HOST / _PORT / _USER / _NAME | mysql-mailcow / 3306 / mailcow / mailcow | Mailcow MariaDB; only MAILCOW_DB_PASS must come from .env |
| MAILCOW_DB_PASS | — | From /opt/mailcow-dockerized/mailcow.conf DBPASS= |
| LLM_PROVIDER | openai | openai (covers all openai-compat presets) or anthropic |
| LLM_PRESET | empty | mistral / openai / groq / together / ollama / perplexity / openrouter |
| LLM_API_KEY | — | Enables /v1/ai/* endpoints and the SPA AI chat panel |
| LLM_MODEL / LLM_BASE_URL | preset defaults | Override model and endpoint URL |
| LLM_ALLOW_CLIENT_OVERRIDE | false | Lets the SPA pass an arbitrary baseUrl (SSRF risk — opt in deliberately) |
| MISTRAL_API_KEY | — | OCR (separate from chat key); also acts as LLM_API_KEY with LLM_PRESET=mistral for back-compat |
| MISTRAL_OCR_MODEL | mistral-ocr-latest | Pin to mistral-ocr-2512 for stable output |
| 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 |
| S3_DRIVE_ENABLED | false | Enables /v1/drive/* and the SPA Drive panel |
| S3_DRIVE_PROVIDER | json | json (per-user file) or b2 (Backblaze B2 master credentials) |
| S3_DRIVE_USERS_JSON | ./data/drive-users.json | Per-user S3 endpoint/bucket/keys when provider=json |
| B2_KEY_ID / B2_APPLICATION_KEY | — | Master credentials when provider=b2 |
| S3_DRIVE_DEFAULT_QUOTA_GB | 5 | Per-user storage quota |
| VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY | — | Web Push subscription keys; without these /v1/push/* accepts subscriptions but the poller doesn't deliver |
| VAPID_SUBJECT | mailto:[email protected] | RFC 8030 subscriber identity |
| PUSH_POLL_INTERVAL_MS | 300000 | How often the push-sender checks for new mail per session |
| ELEVENLABS_API_KEY | — | Enables TTS for voice replies in the webmail |
| IMAGE_PROXY_MAX_BYTES_PER_DAY | 1073741824 | Per-user daily quota for the email image proxy (1 GiB) |
| COMPANY_SHORTCUTS | [] | JSON array of admin-defined sidebar links/popups/iframes (see src/config.js for shape) |
| WEBMAIL_ENABLED | true | Mounts the Svelte SPA at /webmail/ and /webmail/mobile/ |
| WEBMAIL_DIST | ./webmail/dist | Path to the built SPA; container image rebuilds in CI |
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.
Production hardening checklist
- Keep the API behind mailcow nginx. Do not publish its Docker port or set
BIND_ADDR=0.0.0.0unless a separate TLS terminator and source-IP policy are in place. - Set
IP_ALLOWLISTto the narrowest practical VPN, office, or proxy CIDRs. If the API is ever directly exposed, setTRUST_PROXY=false; otherwise a client can forgeX-Forwarded-Forand bypass an IP allowlist or per-client rate limit. - Generate and retain
CREDENTIAL_ENCRYPTION_KEYoutside the data volume:openssl rand -base64 32. Losing or rotating it invalidates sessions; it must not be stored alongside the SQLite backups it protects. - Leave
PUBLIC_API_DOCS=false. The OpenAPI document enumerates the full authenticated API surface. Enable it only on a deliberately public developer endpoint. - Treat
.env, Docker secrets, and data-volume backups as sensitive: they can contain mailcow DB, S3/B2, AI-provider, VAPID, webhook, and session encryption credentials. Use a secret manager or root-readable Docker secret file; never commit or copy them into images. - Set conservative session, request, and outbound-send limits for the mailbox population. Monitor rejected limits and revoke sessions after any suspected mailbox compromise.
- Keep both npm lockfiles current. Dependabot opens weekly update PRs and the dependency-review workflow blocks introduced moderate-or-worse advisories; review and merge those updates promptly.
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).
Features:
- Send via mailcow's postfix — full RFC822 build, attachments, reply/forward, append-to-sent, optional tracking pixel.
- Calendar via SOGo CalDAV — events, recurring, free/busy, iCal share links, external subscriptions.
- Drive — per-user S3-compatible storage (Backblaze B2 ready), PDF preview, image proxy, quota.
- AI panel — chat over the inbox, summarize, draft reply,
inbox-sort, phishing-scan, error-doctor (requires
LLM_API_KEY). - Voice — ElevenLabs TTS for reading messages and dictating
replies (requires
ELEVENLABS_API_KEY). - Push notifications — Web Push for new mail (requires VAPID keys).
- Mail rules via Sieve — block/redirect/copy senders without leaving the UI; coexists with mailcow's existing Sieve scripts.
- Sender policies — block/whitelist senders via mailcow DB.
- Mobile build — separate Svelte entry at
/webmail/mobile/with swipe gestures, pull-to-refresh, action sheet, bottom nav. - OCR for image/PDF attachments inline (requires
MISTRAL_API_KEY).
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.
/v1/ai/config returns 501.
LLM_API_KEY is unset. AI features (chat, summarize, draft, sort,
phishing-scan, error-doctor) all gate on this. Set it in .env,
pick LLM_PRESET (or LLM_PROVIDER=anthropic), and recreate the
container so the env file is re-read.
/v1/send returns 501 "sending not enabled".
SMTP_HOST is unset. The default docker-compose.yml points it at
postfix-mailcow so this should work out of the box on a mailcow
host — confirm the container is on the mailcow-network external
network (docker network inspect mailcowdockerized_mailcow-network).
Calendar / mailbox-info / sender-policy endpoints return empty
data even though credentials are valid.
SOGO_URL (calendar) or MAILCOW_DB_PASS (mailbox-info, aliases,
sender-policy) is missing. The routes register either way and return
graceful empty payloads — set the missing env to enable real data.
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/mailboxes/:path/messages— APPEND raw RFC822 (message/rfc822)POST /v1/send— full SMTP send (subject/body/attachments/reply-to/in-reply-to). RequiresSMTP_HOST.
Sessions
POST /v1/auth/session— exchange Basic credentials for a Bearer token (24h hard cap)DELETE /v1/auth/session— revoke
AI (require LLM_API_KEY)
GET /v1/ai/capabilities— public probeGET /v1/ai/config— provider config exposed to the SPA so it can call the LLM directlyPOST /v1/ai/summarize,/draft-reply,/sort-inbox,/phishing-scan,/error-doctor— task endpointsPOST /v1/ai/chat— generic chat passthrough
Calendar (requires SOGO_URL)
GET /v1/calendars,GET /v1/calendars/:id/events,POST /v1/calendars/:id/events,PUT/DELETEper-eventGET /v1/calendar-subscriptions,POST /v1/calendar-subscriptions— share via iCal URL or subscribe to external Google/iCloud calendars
Drive (requires S3_DRIVE_ENABLED=true + per-user creds)
GET /v1/drive/config,GET /v1/drive/quotaGET /v1/drive/list?prefix=…,POST /v1/drive/upload,GET /v1/drive/download/*,DELETE /v1/drive/*
Mailcow integration (requires MAILCOW_DB_PASS)
GET /v1/me/mailbox,/me/aliases,/me/temp-aliases,/me/logins,/me/send-fromGET/POST/DELETE /v1/me/blocked-senders,/me/allowed-senders
Mail rules (Sieve, requires mailcow DB)
GET/POST/PUT/DELETE /v1/me/mail-rules— block / redirect / copyGET/POST/DELETE /v1/me/blocked-recipients— legacy; same Sieve script
Push notifications (requires VAPID keys)
POST /v1/push/subscribe,DELETE /v1/push/subscribe
Tracking pixels
GET /v1/tracking/pixel/:id.gif(public)GET /v1/tracking/events
Proxies
GET /v1/proxy/icon?u=…(public, allowlisted hosts)GET /v1/proxy/image?u=…(auth, SSRF-hardened, byte-capped)
TTS (requires ELEVENLABS_API_KEY)
POST /v1/tts/synthesize
Public
GET /health— capabilities probeGET /— Swagger UIGET /openapi.json— OpenAPI 3.1 documentGET /webmail/— desktop SPA,GET /webmail/mobile/— mobile 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
37 tools cover the full surface — mailboxes, messages, attachments,
OCR, send + delivery status, sender / recipient policies, mail rules,
mailbox info / aliases / temp aliases / send-from, and CalDAV
calendars / events. Run npx --yes --package imap-rest-mailcow imap-rest-mcp
under your MCP client to see the complete tool list with schemas, or
read src/mcp/index.js.
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.
CI/CD
GitHub Actions workflows live in .github/workflows/:
publish-image— fires on push tomasterand onv*tag push. Builds the multi-arch container and pushes toghcr.io/jr551/imap-rest-mailcowwith tagsmaster,latest(tag pushes),:{semver},:sha-{short}.publish-npm— fires onv*tag push. Runsnpm testthennpm publish --access public. Failing tests silently block the npm release — keepnpm testgreen onmaster. There is deliberately nodeployworkflow. This repo publishes an npm/MCP package and a container image; it is not deployed anywhere, andserver-delivering-emailruns two different repos —jr551/mailcow-rest-api(backend) andjr551/mailcow-rest-api-webmail(SPA). A change that needs to reach that server belongs in one of those. SeeAGENTS.mdfor the full story.
For AI coding agents
AGENTS.md is the single source of truth on:
- Project layout and where the interesting code lives.
- Architectural decisions that look like bugs but aren't (intentional secret-returning endpoints, plaintext password storage tradeoff, graceful-degrade route registrations, etc.).
- The CI/CD chain and how to verify it.
- Pitfalls that have wasted prior agents' time (stale containers, SPA-fallback 404s, baseline-test-failure traps).
- Working norms with the maintainer.
Read it before modifying anything load-bearing.
License
MIT — see LICENSE.
This is a fresh project, not a fork. It depends on
imapflow (MIT) for IMAP
client behaviour.
