← Back to Skills Marketplace
psyb0t

docker-mailbox

by Ciprian Mandache · GitHub ↗ · v1.2.0 · MIT-0
cross-platform ⚠ pending
125
Downloads
0
Stars
0
Active Installs
3
Versions
Install in OpenClaw
/install docker-mailbox
Description
Multi-mailbox IMAP/SMTP control plane exposed as a REST API + MCP server (streamable HTTP) on a single port. Read, search, send, mark-seen, and delete mail a...
README (SKILL.md)

docker-mailbox

REST + MCP shim over IMAP/SMTP. Point it at one or more mail accounts via a YAML config, get back one HTTP API + one MCP server on the same port (MCP rides a streamable-HTTP endpoint at /mcp). No webmail. No DB. No message store. Stateless — restart it and nothing's lost because nothing was ever kept.

The killer endpoint is GET /inbox — it hits every IMAP account in parallel, runs the same structured search on each, merges newest-first, and tags every result with which mailbox it came from. "Show me everything from [email protected]," "what's unread right now," "what came in this morning" — one call, no fanout dance on the client side.

For installation and setup, see references/setup.md.

Setup

The API should already be running. Set the base URL and (if configured) the bearer token:

export MAILBOX_URL=http://localhost:8000
export MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config

Verify:

curl -s $MAILBOX_URL/health
# {"ok": true, "version": "0.1.0"}

curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" $MAILBOX_URL/mailboxes | jq

/health is always open — point liveness probes at it without worrying about auth.

Auth is optional. If auth.tokens is empty/missing in the server config, all endpoints are open. If it's set, every non-/health request needs Authorization: Bearer \x3Cone of auth.tokens> and returns 401 (with WWW-Authenticate: Bearer) on miss. Tokens are constant-time compared. The same gate covers /mcp.

How It Works

GET to read, POST to send/mark/create, DELETE to delete. All bodies are JSON. All responses are JSON.

Every error response:

{"detail": "description of what went wrong"}

Status codes:

Status When
401 Missing or invalid bearer (when auth is on).
404 Unknown mailbox name in the URL.
409 Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).
422 Request body validation failed (pydantic).
502 The IMAP / SMTP server upstream rejected the operation.

UIDs (not sequence numbers) are used for every message identifier so IDs stay stable across server-side mutations.

API Reference

Health

curl -s $MAILBOX_URL/health
# {"ok": true, "version": "0.1.0"}

Mailboxes

curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" $MAILBOX_URL/mailboxes
{
  "mailboxes": [
    { "name": "personal", "description": "Gmail", "imap": true, "smtp": true },
    { "name": "work",     "description": "",       "imap": true, "smtp": true }
  ]
}

name is the URL-safe handle (matches [a-zA-Z0-9_-]+, unique) used in every other path. The imap / smtp booleans tell you which protocols the server has configured for that mailbox — if imap: false, you can't list/fetch/delete; if smtp: false, you can't send.

Unified inbox (the main read endpoint)

GET /inbox fans out across every IMAP-configured mailbox in parallel, runs the same structured search against each one, merges newest-first, and tags each message with which account it came from. Per-mailbox failures land in errors instead of aborting the whole call.

Query param What it does
mailbox CSV filter by mailbox name (personal) or email address ([email protected]). Omit to search all IMAP mailboxes.
from, to, subject, body, text IMAP SEARCH predicates. text is full-text across headers + body.
since, before IMAP date filters, e.g. 1-Jan-2026.
unseen, seen, flagged, answered Boolean flag filters.
larger_than, smaller_than Size filters in bytes.
folder IMAP folder name (default INBOX).
limit Max merged results, ≤ 500 (default 50).
# everything from one sender, all accounts
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/[email protected]&limit=20" | jq

# unread mail in just two accounts
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/inbox?mailbox=personal,work&unseen=true" | jq

# everything since yesterday, full-text "invoice"
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/inbox?since=$(date -d 'yesterday' +%-d-%b-%Y)&text=invoice" | jq

# search a specific folder (e.g. Spam)
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/inbox?folder=Spam&limit=10" | jq

Response:

{
  "messages": [
    {
      "uid": "1234",
      "mailbox": "personal",
      "mailbox_address": "[email protected]",
      "from": "[email protected]",
      "to": "[email protected]",
      "subject": "weekly sync",
      "date": "Mon, 18 May 2026 09:15:00 +0000",
      "message_id": "\[email protected]>",
      "flags": ["\\Seen"]
    }
  ],
  "errors": [
    { "mailbox": "work", "error": "login failed: ..." }
  ]
}

Per-mailbox IMAP

When you want to target one account directly:

# Folders
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  $MAILBOX_URL/mailboxes/personal/folders

# List newest-first headers — raw IMAP SEARCH criteria
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/mailboxes/personal/messages?folder=INBOX&limit=20&search=UNSEEN"

# Structured single-mailbox search — same query params as /inbox minus `mailbox`
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/mailboxes/personal/[email protected]&since=1-May-2026"

# Fetch one full message (decoded body_text + body_html + attachment metadata)
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX"

# Same but also get `body_reader` — HTML stripped to clean text/markdown
# (perfect for feeding into an LLM without all the table/style chrome)
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX&reader=true"

# Mark seen / unseen
curl -s -X POST -H "Authorization: Bearer $MAILBOX_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"seen": true}' \
  "$MAILBOX_URL/mailboxes/personal/messages/1234/seen?folder=INBOX"

# Delete (flag \Deleted + EXPUNGE — gone, really gone)
curl -s -X DELETE -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX"

/messages search is raw IMAP SEARCH (e.g. ALL, UNSEEN, FROM foo@bar, (UNSEEN FROM foo@bar)). /search is the structured query DSL — same params as /inbox minus mailbox. Use whichever's easier.

Full-message fetch returns:

{
  "uid": "1234",
  "from": "[email protected]",
  "to": "[email protected]",
  "cc": "",
  "subject": "weekly sync",
  "date": "Mon, 18 May 2026 09:15:00 +0000",
  "message_id": "\[email protected]>",
  "body_text": "plain text body",
  "body_html": "\x3Cp>html body\x3C/p>",
  "body_reader": null,
  "attachments": [
    {"filename": "agenda.pdf", "content_type": "application/pdf", "size": 12345}
  ]
}

body_reader is null unless you pass reader=true. When enabled it falls back to body_text if no HTML body exists, otherwise it's the HTML body stripped to readable markdown (links inline, images dropped, tables flattened, no styles/scripts).

How reader mode works

Runs the HTML body through html2text configured for LLM consumption: body_width=0 (no wrap), ignore_images=True (kills \x3Cimg> tracking pixels), unicode_snob=True (real unicode, no smart-quote mangling). \x3Cstyle>, \x3Cscript>, \x3Chead>, comments and all inline-style chrome get dropped. Headings → #, bold/italic preserved, \x3Ca href="x">text\x3C/a>[text](x) inline, lists/tables converted to markdown equivalents.

The original body_text and body_html are still returned — body_reader is additive. UI clients can render HTML, agents can read markdown, attachments stay as metadata.

Useful when the text/plain part is missing or an auto-generated "view in HTML client" stub (which is true for most marketing/transactional mail). Limitations: reply-quote chains aren't stripped, table-layout emails come through as pipe-tables (faithful but visually noisy).

SMTP

curl -s -X POST -H "Authorization: Bearer $MAILBOX_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "to":           ["[email protected]"],
    "cc":           ["[email protected]"],
    "bcc":          ["[email protected]"],
    "subject":      "hi",
    "body_text":    "plain text body",
    "body_html":    "\x3Cp>optional html body\x3C/p>",
    "from_address": "Me \[email protected]>",
    "reply_to":     "[email protected]"
  }' \
  $MAILBOX_URL/mailboxes/personal/send

Required: to (non-empty), subject, and at least one of body_text / body_html. Both bodies = multipart/alternative.

The SMTP client automatically sets Date, a domain-aligned Message-ID, and a Thunderbird-shaped User-Agent — provider spam filters get hostile when those are missing or sloppy, so we play the game. Response:

{
  "from":       "Me \[email protected]>",
  "to":         "[email protected]",
  "subject":    "hi",
  "message_id": "\[email protected]>"
}

MCP server

Same operations exposed as MCP tools over streamable HTTP at POST /mcp (same port, same bearer). One flat tool set — every per-mailbox op takes mailbox as a parameter (the configured name OR the email address), so the catalog stays constant-sized no matter how many accounts you configure:

mailboxes                   # discovery: list configured mailboxes + capabilities
inbox                       # unified read across all IMAP mailboxes (mailbox= filter)
list_folders                # (mailbox)
list_messages               # (mailbox, folder, limit, search)
search                      # (mailbox, from, subject, since, ...)
get_message                 # (mailbox, uid, reader=true → +body_reader)
delete_message              # (mailbox, uid)
mark_seen                   # (mailbox, uid, seen)
send                        # (mailbox, to, subject, body_text/html, ...)

Discovery flow for an agent: call mailboxes to see what's available, then pass the chosen name ("personal") or address ("[email protected]") as the mailbox argument. For cross-account reads use inboxinbox(from="[email protected]") fans out across every IMAP-enabled mailbox in one call. IMAP-only tools only appear if at least one mailbox has IMAP; same for SMTP. No dead buttons.

There is no stdio transport. Point MCP clients at $MAILBOX_URL/mcp. The endpoint speaks the full streamable-HTTP protocol (GET opens SSE, POST sends requests, DELETE terminates the session). .mcp.json snippet:

{
  "mcpServers": {
    "mailbox": {
      "transport": "streamable-http",
      "url": "http://localhost:8000/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN_HERE"
      }
    }
  }
}

Drop the headers block if you're running without auth.tokens.

Common Workflows

Find and delete

# 1. Find UIDs matching the criteria
HITS=$(curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/[email protected]&limit=500" | jq -r '.messages[] | "\(.mailbox) \(.uid)"')

# 2. Delete each (per-mailbox endpoint since DELETE is single-mailbox)
echo "$HITS" | while read -r mailbox uid; do
  curl -s -X DELETE -H "Authorization: Bearer $MAILBOX_TOKEN" \
    "$MAILBOX_URL/mailboxes/$mailbox/messages/$uid"
done

Send-to-self e2e sanity check

MARKER="e2e-$(uuidgen | cut -c1-8)"

# 1. Send marker to self
curl -s -X POST -H "Authorization: Bearer $MAILBOX_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{\"to\": [\"[email protected]\"], \"subject\": \"ping $MARKER\", \"body_text\": \"$MARKER\"}" \
  "$MAILBOX_URL/mailboxes/personal/send"

# 2. Search for it (may take a few seconds to land)
for i in 1 2 3 4 5; do
  sleep 2
  FOUND=$(curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
    "$MAILBOX_URL/inbox?subject=$MARKER" | jq -r '.messages | length')
  [ "$FOUND" -gt 0 ] && break
done

Pull unread across everything, format for a digest

curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/inbox?unseen=true&limit=100" \
  | jq -r '.messages[] | "\(.mailbox)	\(.from)	\(.subject)"' \
  | column -t -s $'	'

Tips

  • Date filters (since, before) use IMAP date format (1-Jan-2026), not ISO — date -d ... +%-d-%b-%Y is your friend.
  • larger_than / smaller_than are in bytes.
  • folder defaults to the mailbox's default_folder (usually INBOX). Provider-specific folder names: Gmail = [Gmail]/Spam, GMX/Yahoo = Spam, Outlook = Junk Email. Use GET /mailboxes/\x3Cname>/folders to discover.
  • A self-send may land in Spam on some providers (GMX especially) due to provider-side self-send heuristics even with proper headers — search folder=Spam if you don't see it in INBOX.
  • Gmail / Yahoo / etc. need app passwords, not your account password. Generate one in the provider's security settings.
  • delete is a real EXPUNGE — there is no trash bin equivalent unless the server moves to a Trash folder first. If you want soft delete, MOVE first then delete; mailboxd doesn't expose move yet.
  • Per-mailbox blowups in /inbox come back in the errors array — always check it, one dead account shouldn't blind you to the rest.
  • Bearer tokens live in the server's config.yaml under auth.tokens — list with multiple tokens to rotate without downtime.
Capability Tags
cryptorequires-oauth-tokenrequires-sensitive-credentials
How to Use
  1. Make sure OpenClaw is installed (local or Docker)
  2. Run the install command in chat: /install docker-mailbox
  3. After installation, invoke the skill by name or use /docker-mailbox
  4. Provide required inputs per the skill's parameter spec and get structured output
Version History
v1.2.0
No visible file changes detected for version 1.2.0. No changes to functionality or documentation.
v1.1.0
No file changes detected for this release. - No user-facing changes or updates in this version. - Functionality and documentation remain the same as the previous release.
v1.0.0
docker-mailbox v1.0.0 - Initial release providing a unified REST API and MCP server for controlling multiple IMAP/SMTP mailboxes on a single port. - Supports reading, searching, sending, marking as seen, and deleting mail across multiple inboxes in one call. - Unified `GET /inbox` endpoint fans out searches across all configured mailboxes and merges results newest-first. - Optional bearer-token authentication; `/health` endpoint is always open for liveness checks. - No state, message store, or per-provider client library required—uses Python stdlib and FastAPI. - Supports granular mailbox operations, full structured search, and simple per-mailbox requests.
Metadata
Slug docker-mailbox
Version 1.2.0
License MIT-0
All-time Installs 0
Active Installs 0
Total Versions 3
Frequently Asked Questions

What is docker-mailbox?

Multi-mailbox IMAP/SMTP control plane exposed as a REST API + MCP server (streamable HTTP) on a single port. Read, search, send, mark-seen, and delete mail a... It is an AI Agent Skill for Claude Code / OpenClaw, with 125 downloads so far.

How do I install docker-mailbox?

Run "/install docker-mailbox" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.

Is docker-mailbox free?

Yes, docker-mailbox is completely free, licensed under MIT-0. You can download, install and use it at no cost.

Which platforms does docker-mailbox support?

docker-mailbox is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).

Who created docker-mailbox?

It is built and maintained by Ciprian Mandache (@psyb0t); the current version is v1.2.0.

💬 Comments