← 返回
未分类

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...
多邮箱 IMAP/SMTP 控制面通过 REST API + MCP 服务器(流式 HTTP)在单一端口暴露,提供读取、搜索、发送、标记已读和删除邮件等功能。
psyb0t
未分类 clawhub v1.2.0 1 版本 100000 Key: 无需
★ 0
Stars
📥 318
下载
💾 0
安装
1
版本
#latest

概述

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 boss@corp.com," "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 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:

StatusWhen
------------------------------------------------------------------------------------------------
401Missing or invalid bearer (when auth is on).
404Unknown mailbox name in the URL.
409Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).
422Request body validation failed (pydantic).
502The 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 paramWhat it does
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
mailboxCSV filter by mailbox name (personal) or email address (me@gmail.com). Omit to search all IMAP mailboxes.
from, to, subject, body, textIMAP SEARCH predicates. text is full-text across headers + body.
since, beforeIMAP date filters, e.g. 1-Jan-2026.
unseen, seen, flagged, answeredBoolean flag filters.
larger_than, smaller_thanSize filters in bytes.
folderIMAP folder name (default INBOX).
limitMax merged results, ≤ 500 (default 50).
# everything from one sender, all accounts
curl -s -H "Authorization: Bearer $MAILBOX_TOKEN" \
  "$MAILBOX_URL/inbox?from=boss@corp.com&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": "me@gmail.com",
      "from": "boss@corp.com",
      "to": "me@gmail.com",
      "subject": "weekly sync",
      "date": "Mon, 18 May 2026 09:15:00 +0000",
      "message_id": "<...@corp.com>",
      "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/search?from=boss@corp.com&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": "boss@corp.com",
  "to": "me@gmail.com",
  "cc": "",
  "subject": "weekly sync",
  "date": "Mon, 18 May 2026 09:15:00 +0000",
  "message_id": "<...@corp.com>",
  "body_text": "plain text body",
  "body_html": "<p>html body</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 tracking pixels), unicode_snob=True (real unicode, no smart-quote mangling).