{"id":"2d72958d-7827-450d-bfe5-c37d5518cc65","entityType":"agent","slug":"clawhub-psyb0t-docker-mailbox","name":"docker-mailbox","canonicalUrl":"https://www.xpersona.co/agent/clawhub-psyb0t-docker-mailbox","canonicalPath":"/agent/clawhub-psyb0t-docker-mailbox","generatedAt":"2026-10-11T00:31:38.498Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:17:22.290Z","emptyReason":null},"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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.2K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:docker-mailbox","sourceUrl":"https://clawhub.ai/psyb0t/docker-mailbox","homepage":"https://clawhub.ai/psyb0t/skills/docker-mailbox","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/psyb0t/docker-mailbox","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/psyb0t/skills/docker-mailbox","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"docker-mailbox technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:17:22.290Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:17:22.290Z","emptyReason":null},"stars":null,"forks":null,"downloads":1244,"packageName":null,"latestVersion":"1.2.3","tractionLabel":"1.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:17:22.217Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T22:17:22.290Z","lastCrawledAt":"2026-10-10T22:17:22.217Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T22:17:22.217Z","lastVerifiedAt":null,"highlights":[{"version":"1.2.3","createdAt":"2026-08-20T14:39:02.946Z","changelog":"- Updated setup and reference documentation in `references/setup.md`. - Removed redundant `skill-card.md` file. - No user-facing functionality changes; documentation cleanup only.","fileCount":4,"zipByteSize":12822},{"version":"1.2.2","createdAt":"2026-07-26T02:23:49.158Z","changelog":"docker-mailbox 1.2.2 - Updated SKILL.md language for improved clarity and conciseness, especially in security warnings and deletion guidance. - Expanded search example in `/inbox` documentation to include searching a specific folder. - Removed old skill-card.md file. - No functional or API changes; documentation only.","fileCount":4,"zipByteSize":12819},{"version":"1.2.1","createdAt":"2026-07-26T01:34:29.493Z","changelog":"- Added a \"Security & safety\" section to the documentation, detailing important warnings about destructive deletes, authentication/config best practices, and data handling. - Removed the file skill-card.md from the project. - No changes to functionality or API; changes are documentation and file organization only.","fileCount":4,"zipByteSize":12976},{"version":"1.2.0","createdAt":"2026-05-18T04:23:22.707Z","changelog":"No visible file changes detected for version 1.2.0. No changes to functionality or documentation.","fileCount":4,"zipByteSize":11964},{"version":"1.1.0","createdAt":"2026-05-18T03:31:38.641Z","changelog":"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.","fileCount":3,"zipByteSize":9882},{"version":"1.0.0","createdAt":"2026-05-18T03:17:43.482Z","changelog":"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.","fileCount":3,"zipByteSize":9736}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:docker-mailbox","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:docker-mailbox` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/psyb0t/docker-mailbox before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T00:31:38.495Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-docker-mailbox/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:17:22.290Z","emptyReason":null},"readme":"Skill: docker-mailbox\n\nOwner: psyb0t\n\nSummary: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\n\nTags: latest:1.2.3\n\nVersion history:\n\nv1.2.3 | 2026-08-20T14:39:02.946Z | auto\n\n- Updated setup and reference documentation in `references/setup.md`.\n- Removed redundant `skill-card.md` file.\n- No user-facing functionality changes; documentation cleanup only.\n\nv1.2.2 | 2026-07-26T02:23:49.158Z | auto\n\ndocker-mailbox 1.2.2\n\n- Updated SKILL.md language for improved clarity and conciseness, especially in security warnings and deletion guidance.\n- Expanded search example in `/inbox` documentation to include searching a specific folder.\n- Removed old skill-card.md file.\n- No functional or API changes; documentation only.\n\nv1.2.1 | 2026-07-26T01:34:29.493Z | auto\n\n- Added a \"Security & safety\" section to the documentation, detailing important warnings about destructive deletes, authentication/config best practices, and data handling.\n- Removed the file skill-card.md from the project.\n- No changes to functionality or API; changes are documentation and file organization only.\n\nv1.2.0 | 2026-05-18T04:23:22.707Z | user\n\nNo visible file changes detected for version 1.2.0. No changes to functionality or documentation.\n\nv1.1.0 | 2026-05-18T03:31:38.641Z | user\n\nNo file changes detected for this release.\n\n- No user-facing changes or updates in this version.\n- Functionality and documentation remain the same as the previous release.\n\nv1.0.0 | 2026-05-18T03:17:43.482Z | user\n\ndocker-mailbox v1.0.0\n\n- Initial release providing a unified REST API and MCP server for controlling multiple IMAP/SMTP mailboxes on a single port.\n- Supports reading, searching, sending, marking as seen, and deleting mail across multiple inboxes in one call.\n- Unified `GET /inbox` endpoint fans out searches across all configured mailboxes and merges results newest-first.\n- Optional bearer-token authentication; `/health` endpoint is always open for liveness checks.\n- No state, message store, or per-provider client library required—uses Python stdlib and FastAPI.\n- Supports granular mailbox operations, full structured search, and simple per-mailbox requests.\n\nArchive index:\n\nArchive v1.2.3: 4 files, 12822 bytes\n\nFiles: references/setup.md (10629b), skill-card.md (2675b), SKILL.md (17810b), _meta.json (133b)\n\nFile v1.2.3:SKILL.md\n\n---\nname: docker-mailbox\ndescription: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\ncompatibility: Requires curl and a running mailboxd instance. MAILBOX_URL env var must be set. MAILBOX_TOKEN is optional — only needed if the server was started with `auth.tokens` configured.\nmetadata:\n  author: psyb0t\n  homepage: https://github.com/psyb0t/docker-mailbox\n---\n\n# docker-mailbox\n\nREST + 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.\n\nThe 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.\n\nFor installation and setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Deleting mail is permanent** — the delete endpoint flags a message `\\Deleted` and `EXPUNGE`s it immediately (no trash bin, no undo). Only delete specific message UIDs the user has confirmed; never bulk-delete straight from a broad `/inbox` or `/search` result — list first, show what matched, confirm, then delete. On a multi-mailbox instance, always confirm which `mailbox`/UID you're targeting so you don't touch the wrong account.\n- **No auth when `auth.tokens` is empty.** With it unset the HTTP API AND `/mcp` are UNAUTHENTICATED — anyone who can reach the port gets full read/send/delete access to every configured mailbox. NEVER expose such an instance on a network or to untrusted agents; set `auth.tokens` and bind to loopback / behind an authenticating proxy.\n- **Every call sends your mail data to whatever `MAILBOX_URL` points at.** Point it only at a `mailboxd` instance you run or explicitly trust; prefer HTTPS if it's reachable over a network.\n\n## Setup\n\nThe API should already be running. Set the base URL and (if configured) the bearer token:\n\n```bash\nexport MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config\n```\n\n**Verify:**\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq\n```\n\n`/health` is **always open** — point liveness probes at it without worrying about auth.\n\nAuth 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 <one of auth.tokens>` and returns `401` (with `WWW-Authenticate: Bearer`) on miss. Tokens are constant-time compared. The same gate covers `/mcp`.\n\n## How It Works\n\n`GET` to read, `POST` to send/mark/create, `DELETE` to delete. All bodies are JSON. All responses are JSON.\n\nEvery error response:\n\n```json\n{\"detail\": \"description of what went wrong\"}\n```\n\nStatus codes:\n\n| Status | When                                                                                       |\n| ------ | ------------------------------------------------------------------------------------------ |\n| `401`  | Missing or invalid bearer (when auth is on).                                                |\n| `404`  | Unknown mailbox name in the URL.                                                            |\n| `409`  | Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).        |\n| `422`  | Request body validation failed (pydantic).                                                  |\n| `502`  | The IMAP / SMTP server upstream rejected the operation.                                     |\n\nUIDs (not sequence numbers) are used for every message identifier so IDs stay stable across server-side mutations.\n\n## API Reference\n\n### Health\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n```\n\n### Mailboxes\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes\n```\n\n```json\n{\n  \"mailboxes\": [\n    { \"name\": \"personal\", \"description\": \"Gmail\", \"imap\": true, \"smtp\": true },\n    { \"name\": \"work\",     \"description\": \"\",       \"imap\": true, \"smtp\": true }\n  ]\n}\n```\n\n`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.\n\n### Unified inbox (the main read endpoint)\n\n`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.\n\n| Query param                                      | What it does                                                                                                                              |\n| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `mailbox`                                        | CSV filter by mailbox name (`personal`) **or** email address (`me@gmail.com`). Omit to search all IMAP mailboxes.                          |\n| `from`, `to`, `subject`, `body`, `text`          | IMAP SEARCH predicates. `text` is full-text across headers + body.                                                                         |\n| `since`, `before`                                | IMAP date filters, e.g. `1-Jan-2026`.                                                                                                      |\n| `unseen`, `seen`, `flagged`, `answered`          | Boolean flag filters.                                                                                                                      |\n| `larger_than`, `smaller_than`                    | Size filters in bytes.                                                                                                                     |\n| `folder`                                         | IMAP folder name (default `INBOX`).                                                                                                        |\n| `limit`                                          | Max merged results, ≤ 500 (default 50).                                                                                                    |\n\n```bash\n# everything from one sender, all accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=boss@corp.com&limit=20\" | jq\n\n# unread mail in just two accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?mailbox=personal,work&unseen=true\" | jq\n\n# everything since yesterday, full-text \"invoice\"\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?since=$(date -d 'yesterday' +%-d-%b-%Y)&text=invoice\" | jq\n\n# search a specific folder (e.g. Spam)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?folder=Spam&limit=10\" | jq\n```\n\nResponse:\n\n```json\n{\n  \"messages\": [\n    {\n      \"uid\": \"1234\",\n      \"mailbox\": \"personal\",\n      \"mailbox_address\": \"me@gmail.com\",\n      \"from\": \"boss@corp.com\",\n      \"to\": \"me@gmail.com\",\n      \"subject\": \"weekly sync\",\n      \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n      \"message_id\": \"<...@corp.com>\",\n      \"flags\": [\"\\\\Seen\"]\n    }\n  ],\n  \"errors\": [\n    { \"mailbox\": \"work\", \"error\": \"login failed: ...\" }\n  ]\n}\n```\n\n### Per-mailbox IMAP\n\nWhen you want to target one account directly:\n\n```bash\n# Folders\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  $MAILBOX_URL/mailboxes/personal/folders\n\n# List newest-first headers — raw IMAP SEARCH criteria\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages?folder=INBOX&limit=20&search=UNSEEN\"\n\n# Structured single-mailbox search — same query params as /inbox minus `mailbox`\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/search?from=boss@corp.com&since=1-May-2026\"\n\n# Fetch one full message (decoded body_text + body_html + attachment metadata)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n\n# Same but also get `body_reader` — HTML stripped to clean text/markdown\n# (perfect for feeding into an LLM without all the table/style chrome)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX&reader=true\"\n\n# Mark seen / unseen\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"seen\": true}' \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234/seen?folder=INBOX\"\n\n# Delete (flag \\Deleted + EXPUNGE — gone, really gone)\ncurl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n```\n\n`DELETE /mailboxes/<name>/messages/<uid>` permanently removes a message (`\\Deleted` + `EXPUNGE`, no undo). Confirm the target `mailbox`/UID first — see [Security & safety](#security--safety).\n\n`/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.\n\nFull-message fetch returns:\n\n```json\n{\n  \"uid\": \"1234\",\n  \"from\": \"boss@corp.com\",\n  \"to\": \"me@gmail.com\",\n  \"cc\": \"\",\n  \"subject\": \"weekly sync\",\n  \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n  \"message_id\": \"<...@corp.com>\",\n  \"body_text\": \"plain text body\",\n  \"body_html\": \"<p>html body</p>\",\n  \"body_reader\": null,\n  \"attachments\": [\n    {\"filename\": \"agenda.pdf\", \"content_type\": \"application/pdf\", \"size\": 12345}\n  ]\n}\n```\n\n`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).\n\n#### How reader mode works\n\nRuns the HTML body through [html2text](https://github.com/Alir3z4/html2text) configured for LLM consumption: `body_width=0` (no wrap), `ignore_images=True` (kills `<img>` tracking pixels), `unicode_snob=True` (real unicode, no smart-quote mangling). `<style>`, `<script>`, `<head>`, comments and all inline-style chrome get dropped. Headings → `#`, bold/italic preserved, `<a href=\"x\">text</a>` → `[text](x)` inline, lists/tables converted to markdown equivalents.\n\nThe 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.\n\nUseful 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).\n\n### SMTP\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"to\":           [\"dest@example.com\"],\n    \"cc\":           [\"copy@example.com\"],\n    \"bcc\":          [\"hidden@example.com\"],\n    \"subject\":      \"hi\",\n    \"body_text\":    \"plain text body\",\n    \"body_html\":    \"<p>optional html body</p>\",\n    \"from_address\": \"Me <me@example.com>\",\n    \"reply_to\":     \"noreply@example.com\"\n  }' \\\n  $MAILBOX_URL/mailboxes/personal/send\n```\n\nRequired: `to` (non-empty), `subject`, and at least one of `body_text` / `body_html`. Both bodies = `multipart/alternative`.\n\nThe 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:\n\n```json\n{\n  \"from\":       \"Me <me@example.com>\",\n  \"to\":         \"dest@example.com\",\n  \"subject\":    \"hi\",\n  \"message_id\": \"<177906914784.1.7220590975517922818@example.com>\"\n}\n```\n\n## MCP server\n\nSame 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:\n\n```\nmailboxes                   # discovery: list configured mailboxes + capabilities\ninbox                       # unified read across all IMAP mailboxes (mailbox= filter)\nlist_folders                # (mailbox)\nlist_messages               # (mailbox, folder, limit, search)\nsearch                      # (mailbox, from, subject, since, ...)\nget_message                 # (mailbox, uid, reader=true → +body_reader)\ndelete_message              # (mailbox, uid)\nmark_seen                   # (mailbox, uid, seen)\nsend                        # (mailbox, to, subject, body_text/html, ...)\n```\n\nDiscovery flow for an agent: call `mailboxes` to see what's available, then pass the chosen name (`\"personal\"`) or address (`\"me@gmail.com\"`) as the `mailbox` argument. For cross-account reads use `inbox` — `inbox(from=\"boss@corp.com\")` 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.\n\nThere 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:\n\n```json\n{\n  \"mcpServers\": {\n    \"mailbox\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:8000/mcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer YOUR_TOKEN_HERE\"\n      }\n    }\n  }\n}\n```\n\nDrop the `headers` block if you're running without `auth.tokens`.\n\n## Common Workflows\n\n### Find and delete\n\nDeletion is permanent (see [Security & safety](#security--safety)). Don't chain step 1 into step 2 automatically: run step 1 (list-only), show the matched `mailbox`/`uid`/`from`/`subject` to the user, get explicit confirmation of which UIDs to remove, then run step 2. A loose `from`/`subject`/`text` filter can match more than intended, so never remove every hit from a broad search unseen.\n\n```bash\n# 1. Find UIDs matching the criteria (dry-run: list only, delete nothing yet)\nHITS=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=newsletter@spam.io&limit=500\" | jq -r '.messages[] | \"\\(.mailbox) \\(.uid) \\(.subject)\"')\necho \"$HITS\"   # <-- review/confirm with the user before deleting anything\n\n# 2. Only after explicit user confirmation of the specific UIDs above,\n#    delete each (per-mailbox endpoint since DELETE is single-mailbox)\necho \"$HITS\" | while read -r mailbox uid _subject; do\n  curl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/mailboxes/$mailbox/messages/$uid\"\ndone\n```\n\n### Send-to-self e2e sanity check\n\n```bash\nMARKER=\"e2e-$(uuidgen | cut -c1-8)\"\n\n# 1. Send marker to self\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"to\\\": [\\\"me@gmail.com\\\"], \\\"subject\\\": \\\"ping $MARKER\\\", \\\"body_text\\\": \\\"$MARKER\\\"}\" \\\n  \"$MAILBOX_URL/mailboxes/personal/send\"\n\n# 2. Search for it (may take a few seconds to land)\nfor i in 1 2 3 4 5; do\n  sleep 2\n  FOUND=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/inbox?subject=$MARKER\" | jq -r '.messages | length')\n  [ \"$FOUND\" -gt 0 ] && break\ndone\n```\n\n### Pull unread across everything, format for a digest\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?unseen=true&limit=100\" \\\n  | jq -r '.messages[] | \"\\(.mailbox)\\t\\(.from)\\t\\(.subject)\"' \\\n  | column -t -s $'\\t'\n```\n\n## Tips\n\n- Date filters (`since`, `before`) use **IMAP date format** (`1-Jan-2026`), not ISO — `date -d ... +%-d-%b-%Y` is your friend.\n- `larger_than` / `smaller_than` are in **bytes**.\n- `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/<name>/folders` to discover.\n- 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.\n- Gmail / Yahoo / etc. need **app passwords**, not your account password. Generate one in the provider's security settings.\n- `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.\n- Per-mailbox blowups in `/inbox` come back in the `errors` array — always check it, one dead account shouldn't blind you to the rest.\n- Bearer tokens live in the server's `config.yaml` under `auth.tokens` — list with multiple tokens to rotate without downtime.\n\nFile v1.2.3:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"docker-mailbox\",\n  \"version\": \"1.2.3\",\n  \"publishedAt\": 1787236742946\n}\n\nFile v1.2.3:references/setup.md\n\n# docker-mailbox setup\n\n## Requirements\n\n- Docker + Docker Compose\n- One or more email accounts with IMAP and/or SMTP credentials\n- For Gmail / Yahoo / Outlook / iCloud / most major providers: an **app password** (your normal account password will not work; you need to enable 2FA and generate an app-specific password in the provider's security settings)\n\n## Quick Install\n\n```bash\ngit clone https://github.com/psyb0t/docker-mailbox\ncd docker-mailbox\ncp config.example.yaml config.yaml\n# Edit config.yaml — add your mailboxes and (optionally) auth.tokens\n```\n\nThen either:\n\n```bash\n# Plain docker run\ndocker run --rm -p 8000:8000 \\\n  -v \"$PWD/config.yaml:/etc/mailboxd/config.yaml:ro\" \\\n  psyb0t/mailbox:latest\n\n# Or docker compose (recommended)\ndocker compose up -d\n```\n\nVerify it came up:\n\n```bash\ncurl -s http://localhost:8000/health\n# {\"ok\": true, \"version\": \"...\"}\n```\n\n## Configuration\n\n### `config.yaml`\n\nOne YAML file. Lives at `MAILBOXD_CONFIG`, or `--config`, or `/etc/mailboxd/config.yaml` by default (which is where the prod container expects the read-only mount).\n\n```yaml\nlog_level: INFO\n\n# Bearer-token gate. Guards the HTTP API AND /mcp. Empty / missing = no auth.\n# Multi-token list = rotate without downtime: add a new one, swap clients\n# over, retire the old one.\nauth:\n  tokens:\n    - \"long-random-token-1\"        # generate with: openssl rand -hex 32\n    # - \"long-random-token-2\"\n\nmailboxes:\n  - name: personal                  # URL-safe handle; matches [a-zA-Z0-9_-]+, must be unique\n    description: \"Gmail\"\n\n    imap:\n      host: imap.gmail.com\n      port: 993                     # default 993\n      tls: ssl                      # ssl | starttls | none   (default ssl)\n      username: me@gmail.com\n      password: \"app-password\"      # not your real account password\n      default_folder: INBOX         # default folder when callers don't specify\n\n    smtp:\n      host: smtp.gmail.com\n      port: 465                     # default 587\n      tls: ssl                      # default starttls\n      username: me@gmail.com\n      password: \"app-password\"\n      from_address: \"Me <me@gmail.com>\"\n\n  - name: work\n    imap: { host: mail.work.com, port: 143, tls: starttls, username: me, password: \"...\", default_folder: INBOX }\n    smtp: { host: mail.work.com, port: 587, tls: starttls, username: me, password: \"...\", from_address: me@work.com }\n```\n\n### Config rules\n\n- **At least one mailbox** is required.\n- Each mailbox must declare at least one of `imap` / `smtp`. Both is fine. Neither is a config error.\n- **`name`** is the URL path segment AND the MCP tool prefix. Matches `[a-zA-Z0-9_-]+`. Must be unique across the file.\n- **Defaults**: IMAP `993/ssl`, SMTP `587/starttls`. Override per-mailbox if your provider is weird.\n- **The config file holds plaintext passwords and your bearer tokens.** Treat it like a credential vault: gitignore it (the repo already does), `chmod 600`, mount read-only into the container, don't paste it in chat.\n\n### Provider quick-reference\n\n| Provider     | IMAP host           | IMAP port | TLS       | SMTP host           | SMTP port | TLS       | Auth requirement                                          |\n| ------------ | ------------------- | --------- | --------- | ------------------- | --------- | --------- | --------------------------------------------------------- |\n| Gmail        | `imap.gmail.com`    | 993       | `ssl`     | `smtp.gmail.com`    | 465       | `ssl`     | 2FA + app password                                        |\n| GMX          | `imap.gmx.com`      | 993       | `ssl`     | `mail.gmx.com`      | 587       | `starttls`| Regular account password works                            |\n| Yahoo        | `imap.mail.yahoo.com` | 993     | `ssl`     | `smtp.mail.yahoo.com` | 587     | `starttls`| 2FA + app password (16 lowercase chars)                   |\n| Outlook/Hotmail | `outlook.office365.com` | 993 | `ssl`    | `smtp.office365.com` | 587      | `starttls`| App password if 2FA, OAuth not supported                  |\n| iCloud       | `imap.mail.me.com`  | 993       | `ssl`     | `smtp.mail.me.com`  | 587       | `starttls`| 2FA + app-specific password                               |\n| FastMail     | `imap.fastmail.com` | 993       | `ssl`     | `smtp.fastmail.com` | 465       | `ssl`     | App password                                              |\n| ProtonMail   | (via Bridge `127.0.0.1`) | 1143 | `starttls`| (via Bridge `127.0.0.1`) | 1025 | `starttls`| Bridge running locally; per-app credentials               |\n\nConfirm with the provider's docs — these change occasionally.\n\n## Ports\n\n| Port | Service                                                                                  |\n| ---- | ---------------------------------------------------------------------------------------- |\n| 8000 | HTTP API (`/health`, `/mailboxes`, `/inbox`, `/mailboxes/<name>/...`) AND MCP at `/mcp`  |\n\nThat's it. One port, one process, two surfaces sharing the same bearer-auth gate. Override the host port with `-p HOST:8000` on `docker run` (or set `ports:` in compose).\n\n## Auth\n\nWhen `auth.tokens` is configured, every request except `GET /health` must carry:\n\n```\nAuthorization: Bearer <one of auth.tokens>\n```\n\n- Missing / malformed / wrong token → `401` with `WWW-Authenticate: Bearer`.\n- Tokens are compared in constant time (no timing leaks).\n- The same gate covers `/mcp` — there's no second auth system to learn.\n- Multiple tokens in the list let you rotate without downtime: add the new one, switch clients over, remove the old one.\n\nWhen `auth.tokens` is empty / omitted, all endpoints are open. Fine for \"this is bound to `127.0.0.1` and there's a reverse proxy in front.\" Catastrophic otherwise.\n\n## Management\n\n```bash\n# Lifecycle\ndocker compose up -d         # start\ndocker compose down          # stop\ndocker compose logs -f       # tail logs\ndocker compose pull          # grab a new image\ndocker compose restart       # restart after editing config.yaml\n\n# Dev (in-repo workflow)\nmake help                    # list dev targets\nmake dev-image               # build the sandboxed dev container\nmake shell                   # drop into it\nmake run                     # boot the server locally (mounts CONFIG=path/to/config.yaml)\nmake test                    # full suite (unit + docker-in-docker integration)\nmake test-unit               # in-process only, fast feedback\nmake lint                    # flake8 + mypy\nmake sec                     # semgrep + bandit + pip-audit -> sec.sarif (reports, never fails)\nmake format                  # isort + black\n```\n\n## Logs\n\nmailboxd logs to stdout in the format `%(asctime)s %(levelname)s %(name)s %(message)s`. Control level with `log_level: DEBUG|INFO|WARNING|ERROR` in `config.yaml`. Read with `docker compose logs -f` or whatever your orchestrator does with container logs.\n\n## Public Access via Cloudflare Tunnel (optional)\n\nExpose mailboxd to the internet without opening firewall ports. **Make sure `auth.tokens` is set first** — anything reachable from the internet without auth is a credential-exfiltration vector.\n\n```bash\n# Install cloudflared\ncurl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared\nsudo install /tmp/cloudflared /usr/local/bin/cloudflared\n\n# Authenticate and create tunnel\ncloudflared tunnel login\ncloudflared tunnel create mailbox\n\n# Route a subdomain\ncloudflared tunnel route dns mailbox mailbox.yourdomain.com\n\n# Stash creds\nmkdir -p .data/cloudflared\ncp ~/.cloudflared/<tunnel-id>.json .data/cloudflared/creds.json\n```\n\nCreate `.data/cloudflared/config.yml`:\n\n```yaml\ntunnel: <tunnel-id>\ncredentials-file: /etc/cloudflared/creds.json\n\ningress:\n  - hostname: mailbox.yourdomain.com\n    service: http://mailbox:8000\n  - service: http_status:404\n```\n\nAdd a sidecar to `docker-compose.yml`:\n\n```yaml\nservices:\n  mailbox:\n    image: psyb0t/mailbox:latest\n    volumes:\n      - ./config.yaml:/etc/mailboxd/config.yaml:ro\n    # no `ports:` — only cloudflared reaches it\n\n  cloudflared:\n    image: cloudflare/cloudflared:latest\n    command: tunnel --config /etc/cloudflared/config.yml run\n    volumes:\n      - ./.data/cloudflared:/etc/cloudflared:ro\n    depends_on:\n      - mailbox\n```\n\nHit `https://mailbox.yourdomain.com` from anywhere. The bearer token is now your only line of defense — keep it long, keep it secret, rotate it.\n\nNote: Cloudflare's free Universal SSL covers `*.yourdomain.com` but not deeper subdomains like `*.mail.yourdomain.com`. Stick with one-level subdomains under your root.\n\n## Troubleshooting\n\n| Symptom                                                  | Likely cause                                                                                              |\n| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `401 missing or invalid Bearer token`                    | Forgot `-H \"Authorization: Bearer ...\"` or token doesn't match anything in `auth.tokens`.                  |\n| `502 login failed`                                       | Wrong password OR you used your account password instead of an app password (Gmail/Yahoo/iCloud/Outlook).  |\n| `502 [AUTHENTICATIONFAILED] LOGIN Invalid credentials`   | Yahoo: app passwords are **16 lowercase letters**. If yours has digits/symbols/caps, it's the wrong one.   |\n| `409 mailbox 'X' has no imap configured`                 | You called an IMAP endpoint on an SMTP-only mailbox (or vice versa). Check `/mailboxes` for capabilities.  |\n| `404 unknown mailbox`                                    | Mailbox `name` in the URL doesn't match anything in `config.yaml`.                                         |\n| Self-send lands in Spam (especially GMX)                 | Provider-side self-send heuristic — search `folder=Spam`. Not a mailboxd bug; the headers are well-formed. |\n| Container won't start, `config not found: ...`           | Your config mount didn't land at `/etc/mailboxd/config.yaml`. Check the volume path.                       |\n| `config ... invalid: ...` on startup                     | Pydantic validation failure — read the message, it points at the bad field.                                |\n| MCP client gets `Task group is not initialized`          | Server isn't fully started yet (lifespan hasn't completed). Retry after a moment.                          |\n| `/inbox` `errors` array has entries                      | Those mailboxes failed to connect/auth. The rest still returned results — check the per-mailbox `error`.   |\n\nFile v1.2.3:skill-card.md\n\n## Description:\n\ndocker-mailbox lets agents use a REST API and streamable-HTTP MCP server to read, search, send, mark seen, and delete messages across one or more IMAP/SMTP mailboxes.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and operators use this skill to let an agent or script interact with real email accounts through mailboxd for mailbox discovery, message retrieval, search, sending, mark-seen, and carefully confirmed deletion.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A deployment without auth.tokens can expose mailbox read, send, and delete access to anyone who can reach the service.\n\nMitigation: Set long random bearer tokens, bind the service to loopback or place it behind authenticated HTTPS, and keep tokens secret.\n\nRisk: Delete operations permanently expunge messages and do not provide a trash bin or undo path.\n\nMitigation: List matching messages first, show the mailbox and UID, and require explicit human confirmation before deleting any message.\n\nRisk: Configuration and tunnel credential files can contain mail passwords, bearer tokens, or tunnel secrets.\n\nMitigation: Keep these files private, mount them read-only where possible, and avoid sharing their contents in chat or logs.\n\nRisk: Mutable Docker images or downloaded tools can change after deployment.\n\nMitigation: Pin Docker images and downloaded tools for environments where reproducibility or supply-chain control matters.\n\nRisk: Mail contents are sent to the mailboxd endpoint selected by MAILBOX_URL.\n\nMitigation: Point MAILBOX_URL only at a mailboxd instance the user runs or explicitly trusts, and prefer HTTPS for network access.\n\n## Reference(s):\n\n- [docker-mailbox setup](references/setup.md)\n- [docker-mailbox repository](https://github.com/psyb0t/docker-mailbox)\n- [html2text](https://github.com/Alir3z4/html2text)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with JSON configuration snippets and inline shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires MAILBOX_URL and, when configured, MAILBOX_TOKEN; mailbox service responses are JSON.]\n\n## Skill Version(s):\n\n1.2.3 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.2: 4 files, 12819 bytes\n\nFiles: references/setup.md (10580b), skill-card.md (2993b), SKILL.md (17810b), _meta.json (133b)\n\nFile v1.2.2:SKILL.md\n\n---\nname: docker-mailbox\ndescription: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\ncompatibility: Requires curl and a running mailboxd instance. MAILBOX_URL env var must be set. MAILBOX_TOKEN is optional — only needed if the server was started with `auth.tokens` configured.\nmetadata:\n  author: psyb0t\n  homepage: https://github.com/psyb0t/docker-mailbox\n---\n\n# docker-mailbox\n\nREST + 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.\n\nThe 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.\n\nFor installation and setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Deleting mail is permanent** — the delete endpoint flags a message `\\Deleted` and `EXPUNGE`s it immediately (no trash bin, no undo). Only delete specific message UIDs the user has confirmed; never bulk-delete straight from a broad `/inbox` or `/search` result — list first, show what matched, confirm, then delete. On a multi-mailbox instance, always confirm which `mailbox`/UID you're targeting so you don't touch the wrong account.\n- **No auth when `auth.tokens` is empty.** With it unset the HTTP API AND `/mcp` are UNAUTHENTICATED — anyone who can reach the port gets full read/send/delete access to every configured mailbox. NEVER expose such an instance on a network or to untrusted agents; set `auth.tokens` and bind to loopback / behind an authenticating proxy.\n- **Every call sends your mail data to whatever `MAILBOX_URL` points at.** Point it only at a `mailboxd` instance you run or explicitly trust; prefer HTTPS if it's reachable over a network.\n\n## Setup\n\nThe API should already be running. Set the base URL and (if configured) the bearer token:\n\n```bash\nexport MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config\n```\n\n**Verify:**\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq\n```\n\n`/health` is **always open** — point liveness probes at it without worrying about auth.\n\nAuth 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 <one of auth.tokens>` and returns `401` (with `WWW-Authenticate: Bearer`) on miss. Tokens are constant-time compared. The same gate covers `/mcp`.\n\n## How It Works\n\n`GET` to read, `POST` to send/mark/create, `DELETE` to delete. All bodies are JSON. All responses are JSON.\n\nEvery error response:\n\n```json\n{\"detail\": \"description of what went wrong\"}\n```\n\nStatus codes:\n\n| Status | When                                                                                       |\n| ------ | ------------------------------------------------------------------------------------------ |\n| `401`  | Missing or invalid bearer (when auth is on).                                                |\n| `404`  | Unknown mailbox name in the URL.                                                            |\n| `409`  | Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).        |\n| `422`  | Request body validation failed (pydantic).                                                  |\n| `502`  | The IMAP / SMTP server upstream rejected the operation.                                     |\n\nUIDs (not sequence numbers) are used for every message identifier so IDs stay stable across server-side mutations.\n\n## API Reference\n\n### Health\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n```\n\n### Mailboxes\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes\n```\n\n```json\n{\n  \"mailboxes\": [\n    { \"name\": \"personal\", \"description\": \"Gmail\", \"imap\": true, \"smtp\": true },\n    { \"name\": \"work\",     \"description\": \"\",       \"imap\": true, \"smtp\": true }\n  ]\n}\n```\n\n`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.\n\n### Unified inbox (the main read endpoint)\n\n`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.\n\n| Query param                                      | What it does                                                                                                                              |\n| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `mailbox`                                        | CSV filter by mailbox name (`personal`) **or** email address (`me@gmail.com`). Omit to search all IMAP mailboxes.                          |\n| `from`, `to`, `subject`, `body`, `text`          | IMAP SEARCH predicates. `text` is full-text across headers + body.                                                                         |\n| `since`, `before`                                | IMAP date filters, e.g. `1-Jan-2026`.                                                                                                      |\n| `unseen`, `seen`, `flagged`, `answered`          | Boolean flag filters.                                                                                                                      |\n| `larger_than`, `smaller_than`                    | Size filters in bytes.                                                                                                                     |\n| `folder`                                         | IMAP folder name (default `INBOX`).                                                                                                        |\n| `limit`                                          | Max merged results, ≤ 500 (default 50).                                                                                                    |\n\n```bash\n# everything from one sender, all accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=boss@corp.com&limit=20\" | jq\n\n# unread mail in just two accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?mailbox=personal,work&unseen=true\" | jq\n\n# everything since yesterday, full-text \"invoice\"\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?since=$(date -d 'yesterday' +%-d-%b-%Y)&text=invoice\" | jq\n\n# search a specific folder (e.g. Spam)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?folder=Spam&limit=10\" | jq\n```\n\nResponse:\n\n```json\n{\n  \"messages\": [\n    {\n      \"uid\": \"1234\",\n      \"mailbox\": \"personal\",\n      \"mailbox_address\": \"me@gmail.com\",\n      \"from\": \"boss@corp.com\",\n      \"to\": \"me@gmail.com\",\n      \"subject\": \"weekly sync\",\n      \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n      \"message_id\": \"<...@corp.com>\",\n      \"flags\": [\"\\\\Seen\"]\n    }\n  ],\n  \"errors\": [\n    { \"mailbox\": \"work\", \"error\": \"login failed: ...\" }\n  ]\n}\n```\n\n### Per-mailbox IMAP\n\nWhen you want to target one account directly:\n\n```bash\n# Folders\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  $MAILBOX_URL/mailboxes/personal/folders\n\n# List newest-first headers — raw IMAP SEARCH criteria\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages?folder=INBOX&limit=20&search=UNSEEN\"\n\n# Structured single-mailbox search — same query params as /inbox minus `mailbox`\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/search?from=boss@corp.com&since=1-May-2026\"\n\n# Fetch one full message (decoded body_text + body_html + attachment metadata)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n\n# Same but also get `body_reader` — HTML stripped to clean text/markdown\n# (perfect for feeding into an LLM without all the table/style chrome)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX&reader=true\"\n\n# Mark seen / unseen\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"seen\": true}' \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234/seen?folder=INBOX\"\n\n# Delete (flag \\Deleted + EXPUNGE — gone, really gone)\ncurl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n```\n\n`DELETE /mailboxes/<name>/messages/<uid>` permanently removes a message (`\\Deleted` + `EXPUNGE`, no undo). Confirm the target `mailbox`/UID first — see [Security & safety](#security--safety).\n\n`/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.\n\nFull-message fetch returns:\n\n```json\n{\n  \"uid\": \"1234\",\n  \"from\": \"boss@corp.com\",\n  \"to\": \"me@gmail.com\",\n  \"cc\": \"\",\n  \"subject\": \"weekly sync\",\n  \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n  \"message_id\": \"<...@corp.com>\",\n  \"body_text\": \"plain text body\",\n  \"body_html\": \"<p>html body</p>\",\n  \"body_reader\": null,\n  \"attachments\": [\n    {\"filename\": \"agenda.pdf\", \"content_type\": \"application/pdf\", \"size\": 12345}\n  ]\n}\n```\n\n`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).\n\n#### How reader mode works\n\nRuns the HTML body through [html2text](https://github.com/Alir3z4/html2text) configured for LLM consumption: `body_width=0` (no wrap), `ignore_images=True` (kills `<img>` tracking pixels), `unicode_snob=True` (real unicode, no smart-quote mangling). `<style>`, `<script>`, `<head>`, comments and all inline-style chrome get dropped. Headings → `#`, bold/italic preserved, `<a href=\"x\">text</a>` → `[text](x)` inline, lists/tables converted to markdown equivalents.\n\nThe 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.\n\nUseful 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).\n\n### SMTP\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"to\":           [\"dest@example.com\"],\n    \"cc\":           [\"copy@example.com\"],\n    \"bcc\":          [\"hidden@example.com\"],\n    \"subject\":      \"hi\",\n    \"body_text\":    \"plain text body\",\n    \"body_html\":    \"<p>optional html body</p>\",\n    \"from_address\": \"Me <me@example.com>\",\n    \"reply_to\":     \"noreply@example.com\"\n  }' \\\n  $MAILBOX_URL/mailboxes/personal/send\n```\n\nRequired: `to` (non-empty), `subject`, and at least one of `body_text` / `body_html`. Both bodies = `multipart/alternative`.\n\nThe 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:\n\n```json\n{\n  \"from\":       \"Me <me@example.com>\",\n  \"to\":         \"dest@example.com\",\n  \"subject\":    \"hi\",\n  \"message_id\": \"<177906914784.1.7220590975517922818@example.com>\"\n}\n```\n\n## MCP server\n\nSame 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:\n\n```\nmailboxes                   # discovery: list configured mailboxes + capabilities\ninbox                       # unified read across all IMAP mailboxes (mailbox= filter)\nlist_folders                # (mailbox)\nlist_messages               # (mailbox, folder, limit, search)\nsearch                      # (mailbox, from, subject, since, ...)\nget_message                 # (mailbox, uid, reader=true → +body_reader)\ndelete_message              # (mailbox, uid)\nmark_seen                   # (mailbox, uid, seen)\nsend                        # (mailbox, to, subject, body_text/html, ...)\n```\n\nDiscovery flow for an agent: call `mailboxes` to see what's available, then pass the chosen name (`\"personal\"`) or address (`\"me@gmail.com\"`) as the `mailbox` argument. For cross-account reads use `inbox` — `inbox(from=\"boss@corp.com\")` 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.\n\nThere 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:\n\n```json\n{\n  \"mcpServers\": {\n    \"mailbox\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:8000/mcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer YOUR_TOKEN_HERE\"\n      }\n    }\n  }\n}\n```\n\nDrop the `headers` block if you're running without `auth.tokens`.\n\n## Common Workflows\n\n### Find and delete\n\nDeletion is permanent (see [Security & safety](#security--safety)). Don't chain step 1 into step 2 automatically: run step 1 (list-only), show the matched `mailbox`/`uid`/`from`/`subject` to the user, get explicit confirmation of which UIDs to remove, then run step 2. A loose `from`/`subject`/`text` filter can match more than intended, so never remove every hit from a broad search unseen.\n\n```bash\n# 1. Find UIDs matching the criteria (dry-run: list only, delete nothing yet)\nHITS=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=newsletter@spam.io&limit=500\" | jq -r '.messages[] | \"\\(.mailbox) \\(.uid) \\(.subject)\"')\necho \"$HITS\"   # <-- review/confirm with the user before deleting anything\n\n# 2. Only after explicit user confirmation of the specific UIDs above,\n#    delete each (per-mailbox endpoint since DELETE is single-mailbox)\necho \"$HITS\" | while read -r mailbox uid _subject; do\n  curl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/mailboxes/$mailbox/messages/$uid\"\ndone\n```\n\n### Send-to-self e2e sanity check\n\n```bash\nMARKER=\"e2e-$(uuidgen | cut -c1-8)\"\n\n# 1. Send marker to self\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"to\\\": [\\\"me@gmail.com\\\"], \\\"subject\\\": \\\"ping $MARKER\\\", \\\"body_text\\\": \\\"$MARKER\\\"}\" \\\n  \"$MAILBOX_URL/mailboxes/personal/send\"\n\n# 2. Search for it (may take a few seconds to land)\nfor i in 1 2 3 4 5; do\n  sleep 2\n  FOUND=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/inbox?subject=$MARKER\" | jq -r '.messages | length')\n  [ \"$FOUND\" -gt 0 ] && break\ndone\n```\n\n### Pull unread across everything, format for a digest\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?unseen=true&limit=100\" \\\n  | jq -r '.messages[] | \"\\(.mailbox)\\t\\(.from)\\t\\(.subject)\"' \\\n  | column -t -s $'\\t'\n```\n\n## Tips\n\n- Date filters (`since`, `before`) use **IMAP date format** (`1-Jan-2026`), not ISO — `date -d ... +%-d-%b-%Y` is your friend.\n- `larger_than` / `smaller_than` are in **bytes**.\n- `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/<name>/folders` to discover.\n- 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.\n- Gmail / Yahoo / etc. need **app passwords**, not your account password. Generate one in the provider's security settings.\n- `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.\n- Per-mailbox blowups in `/inbox` come back in the `errors` array — always check it, one dead account shouldn't blind you to the rest.\n- Bearer tokens live in the server's `config.yaml` under `auth.tokens` — list with multiple tokens to rotate without downtime.\n\nFile v1.2.2:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"docker-mailbox\",\n  \"version\": \"1.2.2\",\n  \"publishedAt\": 1785032629158\n}\n\nFile v1.2.2:references/setup.md\n\n# docker-mailbox setup\n\n## Requirements\n\n- Docker + Docker Compose\n- One or more email accounts with IMAP and/or SMTP credentials\n- For Gmail / Yahoo / Outlook / iCloud / most major providers: an **app password** (your normal account password will not work; you need to enable 2FA and generate an app-specific password in the provider's security settings)\n\n## Quick Install\n\n```bash\ngit clone https://github.com/psyb0t/docker-mailbox\ncd docker-mailbox\ncp config.example.yaml config.yaml\n# Edit config.yaml — add your mailboxes and (optionally) auth.tokens\n```\n\nThen either:\n\n```bash\n# Plain docker run\ndocker run --rm -p 8000:8000 \\\n  -v \"$PWD/config.yaml:/etc/mailboxd/config.yaml:ro\" \\\n  psyb0t/mailbox:latest\n\n# Or docker compose (recommended)\ndocker compose up -d\n```\n\nVerify it came up:\n\n```bash\ncurl -s http://localhost:8000/health\n# {\"ok\": true, \"version\": \"...\"}\n```\n\n## Configuration\n\n### `config.yaml`\n\nOne YAML file. Lives at `MAILBOXD_CONFIG`, or `--config`, or `/etc/mailboxd/config.yaml` by default (which is where the prod container expects the read-only mount).\n\n```yaml\nlog_level: INFO\n\n# Bearer-token gate. Guards the HTTP API AND /mcp. Empty / missing = no auth.\n# Multi-token list = rotate without downtime: add a new one, swap clients\n# over, retire the old one.\nauth:\n  tokens:\n    - \"long-random-token-1\"        # generate with: openssl rand -hex 32\n    # - \"long-random-token-2\"\n\nmailboxes:\n  - name: personal                  # URL-safe handle; matches [a-zA-Z0-9_-]+, must be unique\n    description: \"Gmail\"\n\n    imap:\n      host: imap.gmail.com\n      port: 993                     # default 993\n      tls: ssl                      # ssl | starttls | none   (default ssl)\n      username: me@gmail.com\n      password: \"app-password\"      # not your real account password\n      default_folder: INBOX         # default folder when callers don't specify\n\n    smtp:\n      host: smtp.gmail.com\n      port: 465                     # default 587\n      tls: ssl                      # default starttls\n      username: me@gmail.com\n      password: \"app-password\"\n      from_address: \"Me <me@gmail.com>\"\n\n  - name: work\n    imap: { host: mail.work.com, port: 143, tls: starttls, username: me, password: \"...\", default_folder: INBOX }\n    smtp: { host: mail.work.com, port: 587, tls: starttls, username: me, password: \"...\", from_address: me@work.com }\n```\n\n### Config rules\n\n- **At least one mailbox** is required.\n- Each mailbox must declare at least one of `imap` / `smtp`. Both is fine. Neither is a config error.\n- **`name`** is the URL path segment AND the MCP tool prefix. Matches `[a-zA-Z0-9_-]+`. Must be unique across the file.\n- **Defaults**: IMAP `993/ssl`, SMTP `587/starttls`. Override per-mailbox if your provider is weird.\n- **The config file holds plaintext passwords and your bearer tokens.** Treat it like a credential vault: gitignore it (the repo already does), `chmod 600`, mount read-only into the container, don't paste it in chat.\n\n### Provider quick-reference\n\n| Provider     | IMAP host           | IMAP port | TLS       | SMTP host           | SMTP port | TLS       | Auth requirement                                          |\n| ------------ | ------------------- | --------- | --------- | ------------------- | --------- | --------- | --------------------------------------------------------- |\n| Gmail        | `imap.gmail.com`    | 993       | `ssl`     | `smtp.gmail.com`    | 465       | `ssl`     | 2FA + app password                                        |\n| GMX          | `imap.gmx.com`      | 993       | `ssl`     | `mail.gmx.com`      | 587       | `starttls`| Regular account password works                            |\n| Yahoo        | `imap.mail.yahoo.com` | 993     | `ssl`     | `smtp.mail.yahoo.com` | 587     | `starttls`| 2FA + app password (16 lowercase chars)                   |\n| Outlook/Hotmail | `outlook.office365.com` | 993 | `ssl`    | `smtp.office365.com` | 587      | `starttls`| App password if 2FA, OAuth not supported                  |\n| iCloud       | `imap.mail.me.com`  | 993       | `ssl`     | `smtp.mail.me.com`  | 587       | `starttls`| 2FA + app-specific password                               |\n| FastMail     | `imap.fastmail.com` | 993       | `ssl`     | `smtp.fastmail.com` | 465       | `ssl`     | App password                                              |\n| ProtonMail   | (via Bridge `127.0.0.1`) | 1143 | `starttls`| (via Bridge `127.0.0.1`) | 1025 | `starttls`| Bridge running locally; per-app credentials               |\n\nConfirm with the provider's docs — these change occasionally.\n\n## Ports\n\n| Port | Service                                                                                  |\n| ---- | ---------------------------------------------------------------------------------------- |\n| 8000 | HTTP API (`/health`, `/mailboxes`, `/inbox`, `/mailboxes/<name>/...`) AND MCP at `/mcp`  |\n\nThat's it. One port, one process, two surfaces sharing the same bearer-auth gate. Override the host port with `-p HOST:8000` on `docker run` (or set `ports:` in compose).\n\n## Auth\n\nWhen `auth.tokens` is configured, every request except `GET /health` must carry:\n\n```\nAuthorization: Bearer <one of auth.tokens>\n```\n\n- Missing / malformed / wrong token → `401` with `WWW-Authenticate: Bearer`.\n- Tokens are compared in constant time (no timing leaks).\n- The same gate covers `/mcp` — there's no second auth system to learn.\n- Multiple tokens in the list let you rotate without downtime: add the new one, switch clients over, remove the old one.\n\nWhen `auth.tokens` is empty / omitted, all endpoints are open. Fine for \"this is bound to `127.0.0.1` and there's a reverse proxy in front.\" Catastrophic otherwise.\n\n## Management\n\n```bash\n# Lifecycle\ndocker compose up -d         # start\ndocker compose down          # stop\ndocker compose logs -f       # tail logs\ndocker compose pull          # grab a new image\ndocker compose restart       # restart after editing config.yaml\n\n# Dev (in-repo workflow)\nmake help                    # list dev targets\nmake dev-image               # build the sandboxed dev container\nmake shell                   # drop into it\nmake run                     # boot the server locally (mounts CONFIG=path/to/config.yaml)\nmake test                    # full suite (unit + docker-in-docker integration)\nmake test-unit               # in-process only — fast feedback\nmake lint                    # flake8 + mypy\nmake format                  # isort + black\nmake check                   # lint + tests\n```\n\n## Logs\n\nmailboxd logs to stdout in the format `%(asctime)s %(levelname)s %(name)s %(message)s`. Control level with `log_level: DEBUG|INFO|WARNING|ERROR` in `config.yaml`. Read with `docker compose logs -f` or whatever your orchestrator does with container logs.\n\n## Public Access via Cloudflare Tunnel (optional)\n\nExpose mailboxd to the internet without opening firewall ports. **Make sure `auth.tokens` is set first** — anything reachable from the internet without auth is a credential-exfiltration vector.\n\n```bash\n# Install cloudflared\ncurl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared\nsudo install /tmp/cloudflared /usr/local/bin/cloudflared\n\n# Authenticate and create tunnel\ncloudflared tunnel login\ncloudflared tunnel create mailbox\n\n# Route a subdomain\ncloudflared tunnel route dns mailbox mailbox.yourdomain.com\n\n# Stash creds\nmkdir -p .data/cloudflared\ncp ~/.cloudflared/<tunnel-id>.json .data/cloudflared/creds.json\n```\n\nCreate `.data/cloudflared/config.yml`:\n\n```yaml\ntunnel: <tunnel-id>\ncredentials-file: /etc/cloudflared/creds.json\n\ningress:\n  - hostname: mailbox.yourdomain.com\n    service: http://mailbox:8000\n  - service: http_status:404\n```\n\nAdd a sidecar to `docker-compose.yml`:\n\n```yaml\nservices:\n  mailbox:\n    image: psyb0t/mailbox:latest\n    volumes:\n      - ./config.yaml:/etc/mailboxd/config.yaml:ro\n    # no `ports:` — only cloudflared reaches it\n\n  cloudflared:\n    image: cloudflare/cloudflared:latest\n    command: tunnel --config /etc/cloudflared/config.yml run\n    volumes:\n      - ./.data/cloudflared:/etc/cloudflared:ro\n    depends_on:\n      - mailbox\n```\n\nHit `https://mailbox.yourdomain.com` from anywhere. The bearer token is now your only line of defense — keep it long, keep it secret, rotate it.\n\nNote: Cloudflare's free Universal SSL covers `*.yourdomain.com` but not deeper subdomains like `*.mail.yourdomain.com`. Stick with one-level subdomains under your root.\n\n## Troubleshooting\n\n| Symptom                                                  | Likely cause                                                                                              |\n| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `401 missing or invalid Bearer token`                    | Forgot `-H \"Authorization: Bearer ...\"` or token doesn't match anything in `auth.tokens`.                  |\n| `502 login failed`                                       | Wrong password OR you used your account password instead of an app password (Gmail/Yahoo/iCloud/Outlook).  |\n| `502 [AUTHENTICATIONFAILED] LOGIN Invalid credentials`   | Yahoo: app passwords are **16 lowercase letters**. If yours has digits/symbols/caps, it's the wrong one.   |\n| `409 mailbox 'X' has no imap configured`                 | You called an IMAP endpoint on an SMTP-only mailbox (or vice versa). Check `/mailboxes` for capabilities.  |\n| `404 unknown mailbox`                                    | Mailbox `name` in the URL doesn't match anything in `config.yaml`.                                         |\n| Self-send lands in Spam (especially GMX)                 | Provider-side self-send heuristic — search `folder=Spam`. Not a mailboxd bug; the headers are well-formed. |\n| Container won't start, `config not found: ...`           | Your config mount didn't land at `/etc/mailboxd/config.yaml`. Check the volume path.                       |\n| `config ... invalid: ...` on startup                     | Pydantic validation failure — read the message, it points at the bad field.                                |\n| MCP client gets `Task group is not initialized`          | Server isn't fully started yet (lifespan hasn't completed). Retry after a moment.                          |\n| `/inbox` `errors` array has entries                      | Those mailboxes failed to connect/auth. The rest still returned results — check the per-mailbox `error`.   |\n\nFile v1.2.2:skill-card.md\n\n## Description: <br>\ndocker-mailbox lets agents and scripts read, search, send, mark seen, and delete messages across one or more configured IMAP/SMTP mailboxes through a REST API and streamable HTTP MCP server. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and operators use this skill to connect an agent to a running mailboxd service so it can inspect, search, send, and manage mail across configured accounts. It is suited to real mailbox workflows where the user can configure authentication and confirm destructive actions. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can operate real mailboxes, including reading and sending messages. <br>\nMitigation: Install only for mailboxes the user intends an agent or script to control, and point MAILBOX_URL only at a trusted mailboxd instance. <br>\nRisk: If auth.tokens is empty, reachable HTTP and MCP endpoints have full mailbox access without authentication. <br>\nMitigation: Configure long random bearer tokens, keep the service bound locally or behind an authenticating proxy, and avoid public exposure unless remote access is required. <br>\nRisk: Message deletion is permanent because deleted messages are expunged immediately. <br>\nMitigation: Require explicit user confirmation of the exact mailbox and UID before deletion, and avoid bulk deletion directly from broad search results. <br>\nRisk: The configuration file and tunnel credentials contain sensitive mailbox passwords, bearer tokens, and remote access credentials. <br>\nMitigation: Protect config.yaml and tunnel credential files, keep them out of version control and chats, use restrictive permissions, and mount configuration read-only where possible. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/docker-mailbox) <br>\n- [Publisher profile](https://clawhub.ai/user/psyb0t) <br>\n- [Setup reference](references/setup.md) <br>\n- [Project homepage](https://github.com/psyb0t/docker-mailbox) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown with inline shell commands, REST examples, MCP configuration snippets, and operational guidance] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Outputs may include mailbox search, read, send, mark-seen, and delete instructions that require a running mailboxd service and configured credentials.] <br>\n\n## Skill Version(s): <br>\n1.2.2 (source: server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.2.1: 4 files, 12976 bytes\n\nFiles: references/setup.md (10580b), skill-card.md (2914b), SKILL.md (18544b), _meta.json (133b)\n\nFile v1.2.1:SKILL.md\n\n---\nname: docker-mailbox\ndescription: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\ncompatibility: Requires curl and a running mailboxd instance. MAILBOX_URL env var must be set. MAILBOX_TOKEN is optional — only needed if the server was started with `auth.tokens` configured.\nmetadata:\n  author: psyb0t\n  homepage: https://github.com/psyb0t/docker-mailbox\n---\n\n# docker-mailbox\n\nREST + 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.\n\nThe 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.\n\nFor installation and setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **`DELETE /mailboxes/<name>/messages/<uid>` is destructive & irreversible.** It flags the message `\\Deleted` and immediately `EXPUNGE`s it — there is no trash bin, no undo. An agent must NEVER call it unless the user explicitly asked for that exact deletion; confirm the specific message(s)/UID(s) with the user before deleting, never enumerate a broad `GET /inbox` or `/search` result and bulk-delete straight from it, and prefer listing/searching first (dry-run) so the user can review what would be deleted. On a multi-mailbox instance this can destroy mail in accounts other than the one the user meant — always confirm which `mailbox` name/UID pair you're targeting.\n- **No auth when `auth.tokens` is empty.** With it unset the HTTP API AND `/mcp` are UNAUTHENTICATED — anyone who can reach the port gets full read/send/delete access to every configured mailbox. NEVER expose such an instance on a network or to untrusted agents; set `auth.tokens` and bind to loopback / behind an authenticating proxy.\n- **Every call sends your mail data to whatever `MAILBOX_URL` points at.** Point it only at a `mailboxd` instance you run or explicitly trust; prefer HTTPS if it's reachable over a network.\n\n## Setup\n\nThe API should already be running. Set the base URL and (if configured) the bearer token:\n\n```bash\nexport MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config\n```\n\n**Verify:**\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq\n```\n\n`/health` is **always open** — point liveness probes at it without worrying about auth.\n\nAuth 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 <one of auth.tokens>` and returns `401` (with `WWW-Authenticate: Bearer`) on miss. Tokens are constant-time compared. The same gate covers `/mcp`.\n\n## How It Works\n\n`GET` to read, `POST` to send/mark/create, `DELETE` to delete. All bodies are JSON. All responses are JSON.\n\nEvery error response:\n\n```json\n{\"detail\": \"description of what went wrong\"}\n```\n\nStatus codes:\n\n| Status | When                                                                                       |\n| ------ | ------------------------------------------------------------------------------------------ |\n| `401`  | Missing or invalid bearer (when auth is on).                                                |\n| `404`  | Unknown mailbox name in the URL.                                                            |\n| `409`  | Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).        |\n| `422`  | Request body validation failed (pydantic).                                                  |\n| `502`  | The IMAP / SMTP server upstream rejected the operation.                                     |\n\nUIDs (not sequence numbers) are used for every message identifier so IDs stay stable across server-side mutations.\n\n## API Reference\n\n### Health\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n```\n\n### Mailboxes\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes\n```\n\n```json\n{\n  \"mailboxes\": [\n    { \"name\": \"personal\", \"description\": \"Gmail\", \"imap\": true, \"smtp\": true },\n    { \"name\": \"work\",     \"description\": \"\",       \"imap\": true, \"smtp\": true }\n  ]\n}\n```\n\n`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.\n\n### Unified inbox (the main read endpoint)\n\n`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.\n\n| Query param                                      | What it does                                                                                                                              |\n| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `mailbox`                                        | CSV filter by mailbox name (`personal`) **or** email address (`me@gmail.com`). Omit to search all IMAP mailboxes.                          |\n| `from`, `to`, `subject`, `body`, `text`          | IMAP SEARCH predicates. `text` is full-text across headers + body.                                                                         |\n| `since`, `before`                                | IMAP date filters, e.g. `1-Jan-2026`.                                                                                                      |\n| `unseen`, `seen`, `flagged`, `answered`          | Boolean flag filters.                                                                                                                      |\n| `larger_than`, `smaller_than`                    | Size filters in bytes.                                                                                                                     |\n| `folder`                                         | IMAP folder name (default `INBOX`).                                                                                                        |\n| `limit`                                          | Max merged results, ≤ 500 (default 50).                                                                                                    |\n\n```bash\n# everything from one sender, all accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=boss@corp.com&limit=20\" | jq\n\n# unread mail in just two accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?mailbox=personal,work&unseen=true\" | jq\n\n# everything since yesterday, full-text \"invoice\"\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?since=$(date -d 'yesterday' +%-d-%b-%Y)&text=invoice\" | jq\n\n# search a specific folder (e.g. Spam)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?folder=Spam&limit=10\" | jq\n```\n\nResponse:\n\n```json\n{\n  \"messages\": [\n    {\n      \"uid\": \"1234\",\n      \"mailbox\": \"personal\",\n      \"mailbox_address\": \"me@gmail.com\",\n      \"from\": \"boss@corp.com\",\n      \"to\": \"me@gmail.com\",\n      \"subject\": \"weekly sync\",\n      \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n      \"message_id\": \"<...@corp.com>\",\n      \"flags\": [\"\\\\Seen\"]\n    }\n  ],\n  \"errors\": [\n    { \"mailbox\": \"work\", \"error\": \"login failed: ...\" }\n  ]\n}\n```\n\n### Per-mailbox IMAP\n\nWhen you want to target one account directly:\n\n```bash\n# Folders\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  $MAILBOX_URL/mailboxes/personal/folders\n\n# List newest-first headers — raw IMAP SEARCH criteria\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages?folder=INBOX&limit=20&search=UNSEEN\"\n\n# Structured single-mailbox search — same query params as /inbox minus `mailbox`\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/search?from=boss@corp.com&since=1-May-2026\"\n\n# Fetch one full message (decoded body_text + body_html + attachment metadata)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n\n# Same but also get `body_reader` — HTML stripped to clean text/markdown\n# (perfect for feeding into an LLM without all the table/style chrome)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX&reader=true\"\n\n# Mark seen / unseen\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"seen\": true}' \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234/seen?folder=INBOX\"\n\n# Delete (flag \\Deleted + EXPUNGE — gone, really gone)\ncurl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n```\n\n**Destructive & irreversible.** `DELETE /mailboxes/<name>/messages/<uid>` sets `\\Deleted` and `EXPUNGE`s with no undo. An agent must NEVER call it unless the user explicitly asked for that exact action; confirm the specific message/UID first; scope it to the current task; never enumerate a broad `/inbox` or `/search` result and bulk-delete straight from it — list/search first, show the user what matched, get confirmation, then delete. On a shared/multi-mailbox instance this can destroy mail in an account other than the one the user meant.\n\n`/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.\n\nFull-message fetch returns:\n\n```json\n{\n  \"uid\": \"1234\",\n  \"from\": \"boss@corp.com\",\n  \"to\": \"me@gmail.com\",\n  \"cc\": \"\",\n  \"subject\": \"weekly sync\",\n  \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n  \"message_id\": \"<...@corp.com>\",\n  \"body_text\": \"plain text body\",\n  \"body_html\": \"<p>html body</p>\",\n  \"body_reader\": null,\n  \"attachments\": [\n    {\"filename\": \"agenda.pdf\", \"content_type\": \"application/pdf\", \"size\": 12345}\n  ]\n}\n```\n\n`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).\n\n#### How reader mode works\n\nRuns the HTML body through [html2text](https://github.com/Alir3z4/html2text) configured for LLM consumption: `body_width=0` (no wrap), `ignore_images=True` (kills `<img>` tracking pixels), `unicode_snob=True` (real unicode, no smart-quote mangling). `<style>`, `<script>`, `<head>`, comments and all inline-style chrome get dropped. Headings → `#`, bold/italic preserved, `<a href=\"x\">text</a>` → `[text](x)` inline, lists/tables converted to markdown equivalents.\n\nThe 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.\n\nUseful 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).\n\n### SMTP\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"to\":           [\"dest@example.com\"],\n    \"cc\":           [\"copy@example.com\"],\n    \"bcc\":          [\"hidden@example.com\"],\n    \"subject\":      \"hi\",\n    \"body_text\":    \"plain text body\",\n    \"body_html\":    \"<p>optional html body</p>\",\n    \"from_address\": \"Me <me@example.com>\",\n    \"reply_to\":     \"noreply@example.com\"\n  }' \\\n  $MAILBOX_URL/mailboxes/personal/send\n```\n\nRequired: `to` (non-empty), `subject`, and at least one of `body_text` / `body_html`. Both bodies = `multipart/alternative`.\n\nThe 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:\n\n```json\n{\n  \"from\":       \"Me <me@example.com>\",\n  \"to\":         \"dest@example.com\",\n  \"subject\":    \"hi\",\n  \"message_id\": \"<177906914784.1.7220590975517922818@example.com>\"\n}\n```\n\n## MCP server\n\nSame 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:\n\n```\nmailboxes                   # discovery: list configured mailboxes + capabilities\ninbox                       # unified read across all IMAP mailboxes (mailbox= filter)\nlist_folders                # (mailbox)\nlist_messages               # (mailbox, folder, limit, search)\nsearch                      # (mailbox, from, subject, since, ...)\nget_message                 # (mailbox, uid, reader=true → +body_reader)\ndelete_message              # (mailbox, uid)\nmark_seen                   # (mailbox, uid, seen)\nsend                        # (mailbox, to, subject, body_text/html, ...)\n```\n\nDiscovery flow for an agent: call `mailboxes` to see what's available, then pass the chosen name (`\"personal\"`) or address (`\"me@gmail.com\"`) as the `mailbox` argument. For cross-account reads use `inbox` — `inbox(from=\"boss@corp.com\")` 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.\n\nThere 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:\n\n```json\n{\n  \"mcpServers\": {\n    \"mailbox\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:8000/mcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer YOUR_TOKEN_HERE\"\n      }\n    }\n  }\n}\n```\n\nDrop the `headers` block if you're running without `auth.tokens`.\n\n## Common Workflows\n\n### Find and delete\n\n**Destructive & irreversible.** `DELETE` is a real `\\Deleted` + `EXPUNGE` with no undo (see [Security & safety](#security--safety)). An agent must NEVER chain step 1 straight into step 2 automatically. Run step 1 (dry-run / list-only), show the matched `mailbox`/`uid`/`from`/`subject` pairs to the user, and get explicit confirmation of which specific UIDs to delete before running step 2. Never bulk-delete every hit from a broad `/inbox` search unseen — a loose `from`/`subject`/`text` filter can match more than the user intended.\n\n```bash\n# 1. Find UIDs matching the criteria (dry-run: list only, delete nothing yet)\nHITS=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=newsletter@spam.io&limit=500\" | jq -r '.messages[] | \"\\(.mailbox) \\(.uid) \\(.subject)\"')\necho \"$HITS\"   # <-- review/confirm with the user before deleting anything\n\n# 2. Only after explicit user confirmation of the specific UIDs above,\n#    delete each (per-mailbox endpoint since DELETE is single-mailbox)\necho \"$HITS\" | while read -r mailbox uid _subject; do\n  curl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/mailboxes/$mailbox/messages/$uid\"\ndone\n```\n\n### Send-to-self e2e sanity check\n\n```bash\nMARKER=\"e2e-$(uuidgen | cut -c1-8)\"\n\n# 1. Send marker to self\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"to\\\": [\\\"me@gmail.com\\\"], \\\"subject\\\": \\\"ping $MARKER\\\", \\\"body_text\\\": \\\"$MARKER\\\"}\" \\\n  \"$MAILBOX_URL/mailboxes/personal/send\"\n\n# 2. Search for it (may take a few seconds to land)\nfor i in 1 2 3 4 5; do\n  sleep 2\n  FOUND=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/inbox?subject=$MARKER\" | jq -r '.messages | length')\n  [ \"$FOUND\" -gt 0 ] && break\ndone\n```\n\n### Pull unread across everything, format for a digest\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?unseen=true&limit=100\" \\\n  | jq -r '.messages[] | \"\\(.mailbox)\\t\\(.from)\\t\\(.subject)\"' \\\n  | column -t -s $'\\t'\n```\n\n## Tips\n\n- Date filters (`since`, `before`) use **IMAP date format** (`1-Jan-2026`), not ISO — `date -d ... +%-d-%b-%Y` is your friend.\n- `larger_than` / `smaller_than` are in **bytes**.\n- `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/<name>/folders` to discover.\n- 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.\n- Gmail / Yahoo / etc. need **app passwords**, not your account password. Generate one in the provider's security settings.\n- `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.\n- Per-mailbox blowups in `/inbox` come back in the `errors` array — always check it, one dead account shouldn't blind you to the rest.\n- Bearer tokens live in the server's `config.yaml` under `auth.tokens` — list with multiple tokens to rotate without downtime.\n\nFile v1.2.1:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"docker-mailbox\",\n  \"version\": \"1.2.1\",\n  \"publishedAt\": 1785029669493\n}\n\nFile v1.2.1:references/setup.md\n\n# docker-mailbox setup\n\n## Requirements\n\n- Docker + Docker Compose\n- One or more email accounts with IMAP and/or SMTP credentials\n- For Gmail / Yahoo / Outlook / iCloud / most major providers: an **app password** (your normal account password will not work; you need to enable 2FA and generate an app-specific password in the provider's security settings)\n\n## Quick Install\n\n```bash\ngit clone https://github.com/psyb0t/docker-mailbox\ncd docker-mailbox\ncp config.example.yaml config.yaml\n# Edit config.yaml — add your mailboxes and (optionally) auth.tokens\n```\n\nThen either:\n\n```bash\n# Plain docker run\ndocker run --rm -p 8000:8000 \\\n  -v \"$PWD/config.yaml:/etc/mailboxd/config.yaml:ro\" \\\n  psyb0t/mailbox:latest\n\n# Or docker compose (recommended)\ndocker compose up -d\n```\n\nVerify it came up:\n\n```bash\ncurl -s http://localhost:8000/health\n# {\"ok\": true, \"version\": \"...\"}\n```\n\n## Configuration\n\n### `config.yaml`\n\nOne YAML file. Lives at `MAILBOXD_CONFIG`, or `--config`, or `/etc/mailboxd/config.yaml` by default (which is where the prod container expects the read-only mount).\n\n```yaml\nlog_level: INFO\n\n# Bearer-token gate. Guards the HTTP API AND /mcp. Empty / missing = no auth.\n# Multi-token list = rotate without downtime: add a new one, swap clients\n# over, retire the old one.\nauth:\n  tokens:\n    - \"long-random-token-1\"        # generate with: openssl rand -hex 32\n    # - \"long-random-token-2\"\n\nmailboxes:\n  - name: personal                  # URL-safe handle; matches [a-zA-Z0-9_-]+, must be unique\n    description: \"Gmail\"\n\n    imap:\n      host: imap.gmail.com\n      port: 993                     # default 993\n      tls: ssl                      # ssl | starttls | none   (default ssl)\n      username: me@gmail.com\n      password: \"app-password\"      # not your real account password\n      default_folder: INBOX         # default folder when callers don't specify\n\n    smtp:\n      host: smtp.gmail.com\n      port: 465                     # default 587\n      tls: ssl                      # default starttls\n      username: me@gmail.com\n      password: \"app-password\"\n      from_address: \"Me <me@gmail.com>\"\n\n  - name: work\n    imap: { host: mail.work.com, port: 143, tls: starttls, username: me, password: \"...\", default_folder: INBOX }\n    smtp: { host: mail.work.com, port: 587, tls: starttls, username: me, password: \"...\", from_address: me@work.com }\n```\n\n### Config rules\n\n- **At least one mailbox** is required.\n- Each mailbox must declare at least one of `imap` / `smtp`. Both is fine. Neither is a config error.\n- **`name`** is the URL path segment AND the MCP tool prefix. Matches `[a-zA-Z0-9_-]+`. Must be unique across the file.\n- **Defaults**: IMAP `993/ssl`, SMTP `587/starttls`. Override per-mailbox if your provider is weird.\n- **The config file holds plaintext passwords and your bearer tokens.** Treat it like a credential vault: gitignore it (the repo already does), `chmod 600`, mount read-only into the container, don't paste it in chat.\n\n### Provider quick-reference\n\n| Provider     | IMAP host           | IMAP port | TLS       | SMTP host           | SMTP port | TLS       | Auth requirement                                          |\n| ------------ | ------------------- | --------- | --------- | ------------------- | --------- | --------- | --------------------------------------------------------- |\n| Gmail        | `imap.gmail.com`    | 993       | `ssl`     | `smtp.gmail.com`    | 465       | `ssl`     | 2FA + app password                                        |\n| GMX          | `imap.gmx.com`      | 993       | `ssl`     | `mail.gmx.com`      | 587       | `starttls`| Regular account password works                            |\n| Yahoo        | `imap.mail.yahoo.com` | 993     | `ssl`     | `smtp.mail.yahoo.com` | 587     | `starttls`| 2FA + app password (16 lowercase chars)                   |\n| Outlook/Hotmail | `outlook.office365.com` | 993 | `ssl`    | `smtp.office365.com` | 587      | `starttls`| App password if 2FA, OAuth not supported                  |\n| iCloud       | `imap.mail.me.com`  | 993       | `ssl`     | `smtp.mail.me.com`  | 587       | `starttls`| 2FA + app-specific password                               |\n| FastMail     | `imap.fastmail.com` | 993       | `ssl`     | `smtp.fastmail.com` | 465       | `ssl`     | App password                                              |\n| ProtonMail   | (via Bridge `127.0.0.1`) | 1143 | `starttls`| (via Bridge `127.0.0.1`) | 1025 | `starttls`| Bridge running locally; per-app credentials               |\n\nConfirm with the provider's docs — these change occasionally.\n\n## Ports\n\n| Port | Service                                                                                  |\n| ---- | ---------------------------------------------------------------------------------------- |\n| 8000 | HTTP API (`/health`, `/mailboxes`, `/inbox`, `/mailboxes/<name>/...`) AND MCP at `/mcp`  |\n\nThat's it. One port, one process, two surfaces sharing the same bearer-auth gate. Override the host port with `-p HOST:8000` on `docker run` (or set `ports:` in compose).\n\n## Auth\n\nWhen `auth.tokens` is configured, every request except `GET /health` must carry:\n\n```\nAuthorization: Bearer <one of auth.tokens>\n```\n\n- Missing / malformed / wrong token → `401` with `WWW-Authenticate: Bearer`.\n- Tokens are compared in constant time (no timing leaks).\n- The same gate covers `/mcp` — there's no second auth system to learn.\n- Multiple tokens in the list let you rotate without downtime: add the new one, switch clients over, remove the old one.\n\nWhen `auth.tokens` is empty / omitted, all endpoints are open. Fine for \"this is bound to `127.0.0.1` and there's a reverse proxy in front.\" Catastrophic otherwise.\n\n## Management\n\n```bash\n# Lifecycle\ndocker compose up -d         # start\ndocker compose down          # stop\ndocker compose logs -f       # tail logs\ndocker compose pull          # grab a new image\ndocker compose restart       # restart after editing config.yaml\n\n# Dev (in-repo workflow)\nmake help                    # list dev targets\nmake dev-image               # build the sandboxed dev container\nmake shell                   # drop into it\nmake run                     # boot the server locally (mounts CONFIG=path/to/config.yaml)\nmake test                    # full suite (unit + docker-in-docker integration)\nmake test-unit               # in-process only — fast feedback\nmake lint                    # flake8 + mypy\nmake format                  # isort + black\nmake check                   # lint + tests\n```\n\n## Logs\n\nmailboxd logs to stdout in the format `%(asctime)s %(levelname)s %(name)s %(message)s`. Control level with `log_level: DEBUG|INFO|WARNING|ERROR` in `config.yaml`. Read with `docker compose logs -f` or whatever your orchestrator does with container logs.\n\n## Public Access via Cloudflare Tunnel (optional)\n\nExpose mailboxd to the internet without opening firewall ports. **Make sure `auth.tokens` is set first** — anything reachable from the internet without auth is a credential-exfiltration vector.\n\n```bash\n# Install cloudflared\ncurl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared\nsudo install /tmp/cloudflared /usr/local/bin/cloudflared\n\n# Authenticate and create tunnel\ncloudflared tunnel login\ncloudflared tunnel create mailbox\n\n# Route a subdomain\ncloudflared tunnel route dns mailbox mailbox.yourdomain.com\n\n# Stash creds\nmkdir -p .data/cloudflared\ncp ~/.cloudflared/<tunnel-id>.json .data/cloudflared/creds.json\n```\n\nCreate `.data/cloudflared/config.yml`:\n\n```yaml\ntunnel: <tunnel-id>\ncredentials-file: /etc/cloudflared/creds.json\n\ningress:\n  - hostname: mailbox.yourdomain.com\n    service: http://mailbox:8000\n  - service: http_status:404\n```\n\nAdd a sidecar to `docker-compose.yml`:\n\n```yaml\nservices:\n  mailbox:\n    image: psyb0t/mailbox:latest\n    volumes:\n      - ./config.yaml:/etc/mailboxd/config.yaml:ro\n    # no `ports:` — only cloudflared reaches it\n\n  cloudflared:\n    image: cloudflare/cloudflared:latest\n    command: tunnel --config /etc/cloudflared/config.yml run\n    volumes:\n      - ./.data/cloudflared:/etc/cloudflared:ro\n    depends_on:\n      - mailbox\n```\n\nHit `https://mailbox.yourdomain.com` from anywhere. The bearer token is now your only line of defense — keep it long, keep it secret, rotate it.\n\nNote: Cloudflare's free Universal SSL covers `*.yourdomain.com` but not deeper subdomains like `*.mail.yourdomain.com`. Stick with one-level subdomains under your root.\n\n## Troubleshooting\n\n| Symptom                                                  | Likely cause                                                                                              |\n| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `401 missing or invalid Bearer token`                    | Forgot `-H \"Authorization: Bearer ...\"` or token doesn't match anything in `auth.tokens`.                  |\n| `502 login failed`                                       | Wrong password OR you used your account password instead of an app password (Gmail/Yahoo/iCloud/Outlook).  |\n| `502 [AUTHENTICATIONFAILED] LOGIN Invalid credentials`   | Yahoo: app passwords are **16 lowercase letters**. If yours has digits/symbols/caps, it's the wrong one.   |\n| `409 mailbox 'X' has no imap configured`                 | You called an IMAP endpoint on an SMTP-only mailbox (or vice versa). Check `/mailboxes` for capabilities.  |\n| `404 unknown mailbox`                                    | Mailbox `name` in the URL doesn't match anything in `config.yaml`.                                         |\n| Self-send lands in Spam (especially GMX)                 | Provider-side self-send heuristic — search `folder=Spam`. Not a mailboxd bug; the headers are well-formed. |\n| Container won't start, `config not found: ...`           | Your config mount didn't land at `/etc/mailboxd/config.yaml`. Check the volume path.                       |\n| `config ... invalid: ...` on startup                     | Pydantic validation failure — read the message, it points at the bad field.                                |\n| MCP client gets `Task group is not initialized`          | Server isn't fully started yet (lifespan hasn't completed). Retry after a moment.                          |\n| `/inbox` `errors` array has entries                      | Those mailboxes failed to connect/auth. The rest still returned results — check the per-mailbox `error`.   |\n\nFile v1.2.1:skill-card.md\n\n## Description: <br>\ndocker-mailbox lets agents use a REST API or streamable-HTTP MCP server to read, search, send, mark seen, and delete mail across one or more IMAP/SMTP mailboxes. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers, engineers, and external agent operators use this skill to connect an agent or script to real mailboxes through a single HTTP API and MCP endpoint. It supports multi-account mailbox discovery, search, message retrieval, sending, seen-state updates, and explicitly confirmed deletion workflows. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can delete mailbox messages irreversibly through mailboxd delete operations. <br>\nMitigation: Require explicit user confirmation of the exact mailbox and UID before deletion, and use list or search calls as a dry run before any destructive action. <br>\nRisk: If auth.tokens is empty, the HTTP API and MCP endpoint can be unauthenticated for anyone who can reach the service. <br>\nMitigation: Configure long random bearer tokens, bind to localhost or place the service behind an authenticating proxy, and avoid exposing unauthenticated instances to networks or untrusted agents. <br>\nRisk: Mailbox credentials, bearer tokens, and tunnel credentials can expose private mail access if mishandled. <br>\nMitigation: Keep credentials out of git and chat, protect the config file as sensitive material, and mount configuration read-only where possible. <br>\nRisk: Mailbox content is sent to the configured MAILBOX_URL service. <br>\nMitigation: Point agents only at a mailboxd instance the operator runs or explicitly trusts, and prefer HTTPS for network-reachable deployments. <br>\n\n\n## Reference(s): <br>\n- [docker-mailbox setup](references/setup.md) <br>\n- [docker-mailbox homepage](https://github.com/psyb0t/docker-mailbox) <br>\n- [html2text project](https://github.com/Alir3z4/html2text) <br>\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/docker-mailbox) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown guidance with shell commands, JSON examples, and configuration snippets] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [The skill guides an agent to call mailboxd HTTP and MCP operations; responses from the service are JSON.] <br>\n\n## Skill Version(s): <br>\n1.2.1 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.2.0: 4 files, 11964 bytes\n\nFiles: references/setup.md (10580b), skill-card.md (2637b), SKILL.md (16021b), _meta.json (133b)\n\nFile v1.2.0:SKILL.md\n\n---\nname: docker-mailbox\ndescription: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\ncompatibility: Requires curl and a running mailboxd instance. MAILBOX_URL env var must be set. MAILBOX_TOKEN is optional — only needed if the server was started with `auth.tokens` configured.\nmetadata:\n  author: psyb0t\n  homepage: https://github.com/psyb0t/docker-mailbox\n---\n\n# docker-mailbox\n\nREST + 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.\n\nThe 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.\n\nFor installation and setup, see [references/setup.md](references/setup.md).\n\n## Setup\n\nThe API should already be running. Set the base URL and (if configured) the bearer token:\n\n```bash\nexport MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config\n```\n\n**Verify:**\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq\n```\n\n`/health` is **always open** — point liveness probes at it without worrying about auth.\n\nAuth 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 <one of auth.tokens>` and returns `401` (with `WWW-Authenticate: Bearer`) on miss. Tokens are constant-time compared. The same gate covers `/mcp`.\n\n## How It Works\n\n`GET` to read, `POST` to send/mark/create, `DELETE` to delete. All bodies are JSON. All responses are JSON.\n\nEvery error response:\n\n```json\n{\"detail\": \"description of what went wrong\"}\n```\n\nStatus codes:\n\n| Status | When                                                                                       |\n| ------ | ------------------------------------------------------------------------------------------ |\n| `401`  | Missing or invalid bearer (when auth is on).                                                |\n| `404`  | Unknown mailbox name in the URL.                                                            |\n| `409`  | Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).        |\n| `422`  | Request body validation failed (pydantic).                                                  |\n| `502`  | The IMAP / SMTP server upstream rejected the operation.                                     |\n\nUIDs (not sequence numbers) are used for every message identifier so IDs stay stable across server-side mutations.\n\n## API Reference\n\n### Health\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n```\n\n### Mailboxes\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes\n```\n\n```json\n{\n  \"mailboxes\": [\n    { \"name\": \"personal\", \"description\": \"Gmail\", \"imap\": true, \"smtp\": true },\n    { \"name\": \"work\",     \"description\": \"\",       \"imap\": true, \"smtp\": true }\n  ]\n}\n```\n\n`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.\n\n### Unified inbox (the main read endpoint)\n\n`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.\n\n| Query param                                      | What it does                                                                                                                              |\n| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `mailbox`                                        | CSV filter by mailbox name (`personal`) **or** email address (`me@gmail.com`). Omit to search all IMAP mailboxes.                          |\n| `from`, `to`, `subject`, `body`, `text`          | IMAP SEARCH predicates. `text` is full-text across headers + body.                                                                         |\n| `since`, `before`                                | IMAP date filters, e.g. `1-Jan-2026`.                                                                                                      |\n| `unseen`, `seen`, `flagged`, `answered`          | Boolean flag filters.                                                                                                                      |\n| `larger_than`, `smaller_than`                    | Size filters in bytes.                                                                                                                     |\n| `folder`                                         | IMAP folder name (default `INBOX`).                                                                                                        |\n| `limit`                                          | Max merged results, ≤ 500 (default 50).                                                                                                    |\n\n```bash\n# everything from one sender, all accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=boss@corp.com&limit=20\" | jq\n\n# unread mail in just two accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?mailbox=personal,work&unseen=true\" | jq\n\n# everything since yesterday, full-text \"invoice\"\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?since=$(date -d 'yesterday' +%-d-%b-%Y)&text=invoice\" | jq\n\n# search a specific folder (e.g. Spam)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?folder=Spam&limit=10\" | jq\n```\n\nResponse:\n\n```json\n{\n  \"messages\": [\n    {\n      \"uid\": \"1234\",\n      \"mailbox\": \"personal\",\n      \"mailbox_address\": \"me@gmail.com\",\n      \"from\": \"boss@corp.com\",\n      \"to\": \"me@gmail.com\",\n      \"subject\": \"weekly sync\",\n      \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n      \"message_id\": \"<...@corp.com>\",\n      \"flags\": [\"\\\\Seen\"]\n    }\n  ],\n  \"errors\": [\n    { \"mailbox\": \"work\", \"error\": \"login failed: ...\" }\n  ]\n}\n```\n\n### Per-mailbox IMAP\n\nWhen you want to target one account directly:\n\n```bash\n# Folders\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  $MAILBOX_URL/mailboxes/personal/folders\n\n# List newest-first headers — raw IMAP SEARCH criteria\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages?folder=INBOX&limit=20&search=UNSEEN\"\n\n# Structured single-mailbox search — same query params as /inbox minus `mailbox`\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/search?from=boss@corp.com&since=1-May-2026\"\n\n# Fetch one full message (decoded body_text + body_html + attachment metadata)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n\n# Same but also get `body_reader` — HTML stripped to clean text/markdown\n# (perfect for feeding into an LLM without all the table/style chrome)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX&reader=true\"\n\n# Mark seen / unseen\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"seen\": true}' \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234/seen?folder=INBOX\"\n\n# Delete (flag \\Deleted + EXPUNGE — gone, really gone)\ncurl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n```\n\n`/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.\n\nFull-message fetch returns:\n\n```json\n{\n  \"uid\": \"1234\",\n  \"from\": \"boss@corp.com\",\n  \"to\": \"me@gmail.com\",\n  \"cc\": \"\",\n  \"subject\": \"weekly sync\",\n  \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n  \"message_id\": \"<...@corp.com>\",\n  \"body_text\": \"plain text body\",\n  \"body_html\": \"<p>html body</p>\",\n  \"body_reader\": null,\n  \"attachments\": [\n    {\"filename\": \"agenda.pdf\", \"content_type\": \"application/pdf\", \"size\": 12345}\n  ]\n}\n```\n\n`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).\n\n#### How reader mode works\n\nRuns the HTML body through [html2text](https://github.com/Alir3z4/html2text) configured for LLM consumption: `body_width=0` (no wrap), `ignore_images=True` (kills `<img>` tracking pixels), `unicode_snob=True` (real unicode, no smart-quote mangling). `<style>`, `<script>`, `<head>`, comments and all inline-style chrome get dropped. Headings → `#`, bold/italic preserved, `<a href=\"x\">text</a>` → `[text](x)` inline, lists/tables converted to markdown equivalents.\n\nThe 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.\n\nUseful 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).\n\n### SMTP\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"to\":           [\"dest@example.com\"],\n    \"cc\":           [\"copy@example.com\"],\n    \"bcc\":          [\"hidden@example.com\"],\n    \"subject\":      \"hi\",\n    \"body_text\":    \"plain text body\",\n    \"body_html\":    \"<p>optional html body</p>\",\n    \"from_address\": \"Me <me@example.com>\",\n    \"reply_to\":     \"noreply@example.com\"\n  }' \\\n  $MAILBOX_URL/mailboxes/personal/send\n```\n\nRequired: `to` (non-empty), `subject`, and at least one of `body_text` / `body_html`. Both bodies = `multipart/alternative`.\n\nThe 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:\n\n```json\n{\n  \"from\":       \"Me <me@example.com>\",\n  \"to\":         \"dest@example.com\",\n  \"subject\":    \"hi\",\n  \"message_id\": \"<177906914784.1.7220590975517922818@example.com>\"\n}\n```\n\n## MCP server\n\nSame 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:\n\n```\nmailboxes                   # discovery: list configured mailboxes + capabilities\ninbox                       # unified read across all IMAP mailboxes (mailbox= filter)\nlist_folders                # (mailbox)\nlist_messages               # (mailbox, folder, limit, search)\nsearch                      # (mailbox, from, subject, since, ...)\nget_message                 # (mailbox, uid, reader=true → +body_reader)\ndelete_message              # (mailbox, uid)\nmark_seen                   # (mailbox, uid, seen)\nsend                        # (mailbox, to, subject, body_text/html, ...)\n```\n\nDiscovery flow for an agent: call `mailboxes` to see what's available, then pass the chosen name (`\"personal\"`) or address (`\"me@gmail.com\"`) as the `mailbox` argument. For cross-account reads use `inbox` — `inbox(from=\"boss@corp.com\")` 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.\n\nThere 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:\n\n```json\n{\n  \"mcpServers\": {\n    \"mailbox\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:8000/mcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer YOUR_TOKEN_HERE\"\n      }\n    }\n  }\n}\n```\n\nDrop the `headers` block if you're running without `auth.tokens`.\n\n## Common Workflows\n\n### Find and delete\n\n```bash\n# 1. Find UIDs matching the criteria\nHITS=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=newsletter@spam.io&limit=500\" | jq -r '.messages[] | \"\\(.mailbox) \\(.uid)\"')\n\n# 2. Delete each (per-mailbox endpoint since DELETE is single-mailbox)\necho \"$HITS\" | while read -r mailbox uid; do\n  curl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/mailboxes/$mailbox/messages/$uid\"\ndone\n```\n\n### Send-to-self e2e sanity check\n\n```bash\nMARKER=\"e2e-$(uuidgen | cut -c1-8)\"\n\n# 1. Send marker to self\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"to\\\": [\\\"me@gmail.com\\\"], \\\"subject\\\": \\\"ping $MARKER\\\", \\\"body_text\\\": \\\"$MARKER\\\"}\" \\\n  \"$MAILBOX_URL/mailboxes/personal/send\"\n\n# 2. Search for it (may take a few seconds to land)\nfor i in 1 2 3 4 5; do\n  sleep 2\n  FOUND=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/inbox?subject=$MARKER\" | jq -r '.messages | length')\n  [ \"$FOUND\" -gt 0 ] && break\ndone\n```\n\n### Pull unread across everything, format for a digest\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?unseen=true&limit=100\" \\\n  | jq -r '.messages[] | \"\\(.mailbox)\\t\\(.from)\\t\\(.subject)\"' \\\n  | column -t -s $'\\t'\n```\n\n## Tips\n\n- Date filters (`since`, `before`) use **IMAP date format** (`1-Jan-2026`), not ISO — `date -d ... +%-d-%b-%Y` is your friend.\n- `larger_than` / `smaller_than` are in **bytes**.\n- `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/<name>/folders` to discover.\n- 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.\n- Gmail / Yahoo / etc. need **app passwords**, not your account password. Generate one in the provider's security settings.\n- `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.\n- Per-mailbox blowups in `/inbox` come back in the `errors` array — always check it, one dead account shouldn't blind you to the rest.\n- Bearer tokens live in the server's `config.yaml` under `auth.tokens` — list with multiple tokens to rotate without downtime.\n\nFile v1.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"docker-mailbox\",\n  \"version\": \"1.2.0\",\n  \"publishedAt\": 1779078202707\n}\n\nFile v1.2.0:references/setup.md\n\n# docker-mailbox setup\n\n## Requirements\n\n- Docker + Docker Compose\n- One or more email accounts with IMAP and/or SMTP credentials\n- For Gmail / Yahoo / Outlook / iCloud / most major providers: an **app password** (your normal account password will not work; you need to enable 2FA and generate an app-specific password in the provider's security settings)\n\n## Quick Install\n\n```bash\ngit clone https://github.com/psyb0t/docker-mailbox\ncd docker-mailbox\ncp config.example.yaml config.yaml\n# Edit config.yaml — add your mailboxes and (optionally) auth.tokens\n```\n\nThen either:\n\n```bash\n# Plain docker run\ndocker run --rm -p 8000:8000 \\\n  -v \"$PWD/config.yaml:/etc/mailboxd/config.yaml:ro\" \\\n  psyb0t/mailbox:latest\n\n# Or docker compose (recommended)\ndocker compose up -d\n```\n\nVerify it came up:\n\n```bash\ncurl -s http://localhost:8000/health\n# {\"ok\": true, \"version\": \"...\"}\n```\n\n## Configuration\n\n### `config.yaml`\n\nOne YAML file. Lives at `MAILBOXD_CONFIG`, or `--config`, or `/etc/mailboxd/config.yaml` by default (which is where the prod container expects the read-only mount).\n\n```yaml\nlog_level: INFO\n\n# Bearer-token gate. Guards the HTTP API AND /mcp. Empty / missing = no auth.\n# Multi-token list = rotate without downtime: add a new one, swap clients\n# over, retire the old one.\nauth:\n  tokens:\n    - \"long-random-token-1\"        # generate with: openssl rand -hex 32\n    # - \"long-random-token-2\"\n\nmailboxes:\n  - name: personal                  # URL-safe handle; matches [a-zA-Z0-9_-]+, must be unique\n    description: \"Gmail\"\n\n    imap:\n      host: imap.gmail.com\n      port: 993                     # default 993\n      tls: ssl                      # ssl | starttls | none   (default ssl)\n      username: me@gmail.com\n      password: \"app-password\"      # not your real account password\n      default_folder: INBOX         # default folder when callers don't specify\n\n    smtp:\n      host: smtp.gmail.com\n      port: 465                     # default 587\n      tls: ssl                      # default starttls\n      username: me@gmail.com\n      password: \"app-password\"\n      from_address: \"Me <me@gmail.com>\"\n\n  - name: work\n    imap: { host: mail.work.com, port: 143, tls: starttls, username: me, password: \"...\", default_folder: INBOX }\n    smtp: { host: mail.work.com, port: 587, tls: starttls, username: me, password: \"...\", from_address: me@work.com }\n```\n\n### Config rules\n\n- **At least one mailbox** is required.\n- Each mailbox must declare at least one of `imap` / `smtp`. Both is fine. Neither is a config error.\n- **`name`** is the URL path segment AND the MCP tool prefix. Matches `[a-zA-Z0-9_-]+`. Must be unique across the file.\n- **Defaults**: IMAP `993/ssl`, SMTP `587/starttls`. Override per-mailbox if your provider is weird.\n- **The config file holds plaintext passwords and your bearer tokens.** Treat it like a credential vault: gitignore it (the repo already does), `chmod 600`, mount read-only into the container, don't paste it in chat.\n\n### Provider quick-reference\n\n| Provider     | IMAP host           | IMAP port | TLS       | SMTP host           | SMTP port | TLS       | Auth requirement                                          |\n| ------------ | ------------------- | --------- | --------- | ------------------- | --------- | --------- | --------------------------------------------------------- |\n| Gmail        | `imap.gmail.com`    | 993       | `ssl`     | `smtp.gmail.com`    | 465       | `ssl`     | 2FA + app password                                        |\n| GMX          | `imap.gmx.com`      | 993       | `ssl`     | `mail.gmx.com`      | 587       | `starttls`| Regular account password works                            |\n| Yahoo        | `imap.mail.yahoo.com` | 993     | `ssl`     | `smtp.mail.yahoo.com` | 587     | `starttls`| 2FA + app password (16 lowercase chars)                   |\n| Outlook/Hotmail | `outlook.office365.com` | 993 | `ssl`    | `smtp.office365.com` | 587      | `starttls`| App password if 2FA, OAuth not supported                  |\n| iCloud       | `imap.mail.me.com`  | 993       | `ssl`     | `smtp.mail.me.com`  | 587       | `starttls`| 2FA + app-specific password                               |\n| FastMail     | `imap.fastmail.com` | 993       | `ssl`     | `smtp.fastmail.com` | 465       | `ssl`     | App password                                              |\n| ProtonMail   | (via Bridge `127.0.0.1`) | 1143 | `starttls`| (via Bridge `127.0.0.1`) | 1025 | `starttls`| Bridge running locally; per-app credentials               |\n\nConfirm with the provider's docs — these change occasionally.\n\n## Ports\n\n| Port | Service                                                                                  |\n| ---- | ---------------------------------------------------------------------------------------- |\n| 8000 | HTTP API (`/health`, `/mailboxes`, `/inbox`, `/mailboxes/<name>/...`) AND MCP at `/mcp`  |\n\nThat's it. One port, one process, two surfaces sharing the same bearer-auth gate. Override the host port with `-p HOST:8000` on `docker run` (or set `ports:` in compose).\n\n## Auth\n\nWhen `auth.tokens` is configured, every request except `GET /health` must carry:\n\n```\nAuthorization: Bearer <one of auth.tokens>\n```\n\n- Missing / malformed / wrong token → `401` with `WWW-Authenticate: Bearer`.\n- Tokens are compared in constant time (no timing leaks).\n- The same gate covers `/mcp` — there's no second auth system to learn.\n- Multiple tokens in the list let you rotate without downtime: add the new one, switch clients over, remove the old one.\n\nWhen `auth.tokens` is empty / omitted, all endpoints are open. Fine for \"this is bound to `127.0.0.1` and there's a reverse proxy in front.\" Catastrophic otherwise.\n\n## Management\n\n```bash\n# Lifecycle\ndocker compose up -d         # start\ndocker compose down          # stop\ndocker compose logs -f       # tail logs\ndocker compose pull          # grab a new image\ndocker compose restart       # restart after editing config.yaml\n\n# Dev (in-repo workflow)\nmake help                    # list dev targets\nmake dev-image               # build the sandboxed dev container\nmake shell                   # drop into it\nmake run                     # boot the server locally (mounts CONFIG=path/to/config.yaml)\nmake test                    # full suite (unit + docker-in-docker integration)\nmake test-unit               # in-process only — fast feedback\nmake lint                    # flake8 + mypy\nmake format                  # isort + black\nmake check                   # lint + tests\n```\n\n## Logs\n\nmailboxd logs to stdout in the format `%(asctime)s %(levelname)s %(name)s %(message)s`. Control level with `log_level: DEBUG|INFO|WARNING|ERROR` in `config.yaml`. Read with `docker compose logs -f` or whatever your orchestrator does with container logs.\n\n## Public Access via Cloudflare Tunnel (optional)\n\nExpose mailboxd to the internet without opening firewall ports. **Make sure `auth.tokens` is set first** — anything reachable from the internet without auth is a credential-exfiltration vector.\n\n```bash\n# Install cloudflared\ncurl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared\nsudo install /tmp/cloudflared /usr/local/bin/cloudflared\n\n# Authenticate and create tunnel\ncloudflared tunnel login\ncloudflared tunnel create mailbox\n\n# Route a subdomain\ncloudflared tunnel route dns mailbox mailbox.yourdomain.com\n\n# Stash creds\nmkdir -p .data/cloudflared\ncp ~/.cloudflared/<tunnel-id>.json .data/cloudflared/creds.json\n```\n\nCreate `.data/cloudflared/config.yml`:\n\n```yaml\ntunnel: <tunnel-id>\ncredentials-file: /etc/cloudflared/creds.json\n\ningress:\n  - hostname: mailbox.yourdomain.com\n    service: http://mailbox:8000\n  - service: http_status:404\n```\n\nAdd a sidecar to `docker-compose.yml`:\n\n```yaml\nservices:\n  mailbox:\n    image: psyb0t/mailbox:latest\n    volumes:\n      - ./config.yaml:/etc/mailboxd/config.yaml:ro\n    # no `ports:` — only cloudflared reaches it\n\n  cloudflared:\n    image: cloudflare/cloudflared:latest\n    command: tunnel --config /etc/cloudflared/config.yml run\n    volumes:\n      - ./.data/cloudflared:/etc/cloudflared:ro\n    depends_on:\n      - mailbox\n```\n\nHit `https://mailbox.yourdomain.com` from anywhere. The bearer token is now your only line of defense — keep it long, keep it secret, rotate it.\n\nNote: Cloudflare's free Universal SSL covers `*.yourdomain.com` but not deeper subdomains like `*.mail.yourdomain.com`. Stick with one-level subdomains under your root.\n\n## Troubleshooting\n\n| Symptom                                                  | Likely cause                                                                                              |\n| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `401 missing or invalid Bearer token`                    | Forgot `-H \"Authorization: Bearer ...\"` or token doesn't match anything in `auth.tokens`.                  |\n| `502 login failed`                                       | Wrong password OR you used your account password instead of an app password (Gmail/Yahoo/iCloud/Outlook).  |\n| `502 [AUTHENTICATIONFAILED] LOGIN Invalid credentials`   | Yahoo: app passwords are **16 lowercase letters**. If yours has digits/symbols/caps, it's the wrong one.   |\n| `409 mailbox 'X' has no imap configured`                 | You called an IMAP endpoint on an SMTP-only mailbox (or vice versa). Check `/mailboxes` for capabilities.  |\n| `404 unknown mailbox`                                    | Mailbox `name` in the URL doesn't match anything in `config.yaml`.                                         |\n| Self-send lands in Spam (especially GMX)                 | Provider-side self-send heuristic — search `folder=Spam`. Not a mailboxd bug; the headers are well-formed. |\n| Container won't start, `config not found: ...`           | Your config mount didn't land at `/etc/mailboxd/config.yaml`. Check the volume path.                       |\n| `config ... invalid: ...` on startup                     | Pydantic validation failure — read the message, it points at the bad field.                                |\n| MCP client gets `Task group is not initialized`          | Server isn't fully started yet (lifespan hasn't completed). Retry after a moment.                          |\n| `/inbox` `errors` array has entries                      | Those mailboxes failed to connect/auth. The rest still returned results — check the per-mailbox `error`.   |\n\nFile v1.2.0:skill-card.md\n\n## Description: <br>\ndocker-mailbox lets agents use a running mailboxd server to read, search, send, mark-seen, and delete mail across multiple IMAP/SMTP accounts through REST and MCP. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and agent operators use this skill to connect an agent or script to configured real email accounts for unified mailbox search, message retrieval, sending, deletion, and mailbox state changes. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can give an agent access to real email accounts, including read, send, mark-seen, and delete actions. <br>\nMitigation: Use it only with accounts and scopes intended for agent access, require explicit review before sending or deleting mail, and monitor mailbox activity. <br>\nRisk: An unauthenticated or publicly exposed mailboxd service could expose email contents or allow mailbox actions. <br>\nMitigation: Configure strong auth.tokens before any non-local exposure, keep the service bound locally or behind a protected proxy, and rotate tokens when needed. <br>\nRisk: config.yaml contains mailbox passwords and bearer tokens. <br>\nMitigation: Treat config.yaml as a secret, keep it out of source control and chat transcripts, restrict file permissions, and mount it read-only into containers. <br>\nRisk: Using an unverified Docker image or repository can introduce supply-chain risk. <br>\nMitigation: Verify the Docker image and repository before deployment and pin trusted image versions for production use. <br>\n\n\n## Reference(s): <br>\n- [Setup guide](references/setup.md) <br>\n- [Project homepage](https://github.com/psyb0t/docker-mailbox) <br>\n- [html2text](https://github.com/Alir3z4/html2text) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [guidance, shell commands, configuration, API calls, JSON] <br>\n**Output Format:** [Markdown with curl commands, JSON request and response examples, and MCP configuration snippets] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Requires a running mailboxd service, MAILBOX_URL, and optional MAILBOX_TOKEN.] <br>\n\n## Skill Version(s): <br>\n1.2.0 (source: server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.1.0: 3 files, 9882 bytes\n\nFiles: references/setup.md (10580b), SKILL.md (14493b), _meta.json (133b)\n\nFile v1.1.0:SKILL.md\n\n---\nname: docker-mailbox\ndescription: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\ncompatibility: Requires curl and a running mailboxd instance. MAILBOX_URL env var must be set. MAILBOX_TOKEN is optional — only needed if the server was started with `auth.tokens` configured.\nmetadata:\n  author: psyb0t\n  homepage: https://github.com/psyb0t/docker-mailbox\n---\n\n# docker-mailbox\n\nREST + 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.\n\nThe 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.\n\nFor installation and setup, see [references/setup.md](references/setup.md).\n\n## Setup\n\nThe API should already be running. Set the base URL and (if configured) the bearer token:\n\n```bash\nexport MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config\n```\n\n**Verify:**\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq\n```\n\n`/health` is **always open** — point liveness probes at it without worrying about auth.\n\nAuth 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 <one of auth.tokens>` and returns `401` (with `WWW-Authenticate: Bearer`) on miss. Tokens are constant-time compared. The same gate covers `/mcp`.\n\n## How It Works\n\n`GET` to read, `POST` to send/mark/create, `DELETE` to delete. All bodies are JSON. All responses are JSON.\n\nEvery error response:\n\n```json\n{\"detail\": \"description of what went wrong\"}\n```\n\nStatus codes:\n\n| Status | When                                                                                       |\n| ------ | ------------------------------------------------------------------------------------------ |\n| `401`  | Missing or invalid bearer (when auth is on).                                                |\n| `404`  | Unknown mailbox name in the URL.                                                            |\n| `409`  | Mailbox doesn't have the requested protocol (IMAP endpoint on an SMTP-only mailbox).        |\n| `422`  | Request body validation failed (pydantic).                                                  |\n| `502`  | The IMAP / SMTP server upstream rejected the operation.                                     |\n\nUIDs (not sequence numbers) are used for every message identifier so IDs stay stable across server-side mutations.\n\n## API Reference\n\n### Health\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n```\n\n### Mailboxes\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes\n```\n\n```json\n{\n  \"mailboxes\": [\n    { \"name\": \"personal\", \"description\": \"Gmail\", \"imap\": true, \"smtp\": true },\n    { \"name\": \"work\",     \"description\": \"\",       \"imap\": true, \"smtp\": true }\n  ]\n}\n```\n\n`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.\n\n### Unified inbox (the main read endpoint)\n\n`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.\n\n| Query param                                      | What it does                                                                                                                              |\n| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `mailbox`                                        | CSV filter by mailbox name (`personal`) **or** email address (`me@gmail.com`). Omit to search all IMAP mailboxes.                          |\n| `from`, `to`, `subject`, `body`, `text`          | IMAP SEARCH predicates. `text` is full-text across headers + body.                                                                         |\n| `since`, `before`                                | IMAP date filters, e.g. `1-Jan-2026`.                                                                                                      |\n| `unseen`, `seen`, `flagged`, `answered`          | Boolean flag filters.                                                                                                                      |\n| `larger_than`, `smaller_than`                    | Size filters in bytes.                                                                                                                     |\n| `folder`                                         | IMAP folder name (default `INBOX`).                                                                                                        |\n| `limit`                                          | Max merged results, ≤ 500 (default 50).                                                                                                    |\n\n```bash\n# everything from one sender, all accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=boss@corp.com&limit=20\" | jq\n\n# unread mail in just two accounts\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?mailbox=personal,work&unseen=true\" | jq\n\n# everything since yesterday, full-text \"invoice\"\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?since=$(date -d 'yesterday' +%-d-%b-%Y)&text=invoice\" | jq\n\n# search a specific folder (e.g. Spam)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?folder=Spam&limit=10\" | jq\n```\n\nResponse:\n\n```json\n{\n  \"messages\": [\n    {\n      \"uid\": \"1234\",\n      \"mailbox\": \"personal\",\n      \"mailbox_address\": \"me@gmail.com\",\n      \"from\": \"boss@corp.com\",\n      \"to\": \"me@gmail.com\",\n      \"subject\": \"weekly sync\",\n      \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n      \"message_id\": \"<...@corp.com>\",\n      \"flags\": [\"\\\\Seen\"]\n    }\n  ],\n  \"errors\": [\n    { \"mailbox\": \"work\", \"error\": \"login failed: ...\" }\n  ]\n}\n```\n\n### Per-mailbox IMAP\n\nWhen you want to target one account directly:\n\n```bash\n# Folders\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  $MAILBOX_URL/mailboxes/personal/folders\n\n# List newest-first headers — raw IMAP SEARCH criteria\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages?folder=INBOX&limit=20&search=UNSEEN\"\n\n# Structured single-mailbox search — same query params as /inbox minus `mailbox`\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/search?from=boss@corp.com&since=1-May-2026\"\n\n# Fetch one full message (decoded body_text + body_html + attachment metadata)\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n\n# Mark seen / unseen\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"seen\": true}' \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234/seen?folder=INBOX\"\n\n# Delete (flag \\Deleted + EXPUNGE — gone, really gone)\ncurl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/mailboxes/personal/messages/1234?folder=INBOX\"\n```\n\n`/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.\n\nFull-message fetch returns:\n\n```json\n{\n  \"uid\": \"1234\",\n  \"from\": \"boss@corp.com\",\n  \"to\": \"me@gmail.com\",\n  \"cc\": \"\",\n  \"subject\": \"weekly sync\",\n  \"date\": \"Mon, 18 May 2026 09:15:00 +0000\",\n  \"message_id\": \"<...@corp.com>\",\n  \"body_text\": \"plain text body\",\n  \"body_html\": \"<p>html body</p>\",\n  \"attachments\": [\n    {\"filename\": \"agenda.pdf\", \"content_type\": \"application/pdf\", \"size\": 12345}\n  ]\n}\n```\n\n### SMTP\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"to\":           [\"dest@example.com\"],\n    \"cc\":           [\"copy@example.com\"],\n    \"bcc\":          [\"hidden@example.com\"],\n    \"subject\":      \"hi\",\n    \"body_text\":    \"plain text body\",\n    \"body_html\":    \"<p>optional html body</p>\",\n    \"from_address\": \"Me <me@example.com>\",\n    \"reply_to\":     \"noreply@example.com\"\n  }' \\\n  $MAILBOX_URL/mailboxes/personal/send\n```\n\nRequired: `to` (non-empty), `subject`, and at least one of `body_text` / `body_html`. Both bodies = `multipart/alternative`.\n\nThe 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:\n\n```json\n{\n  \"from\":       \"Me <me@example.com>\",\n  \"to\":         \"dest@example.com\",\n  \"subject\":    \"hi\",\n  \"message_id\": \"<177906914784.1.7220590975517922818@example.com>\"\n}\n```\n\n## MCP server\n\nSame 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:\n\n```\nmailboxes                   # discovery: list configured mailboxes + capabilities\ninbox                       # unified read across all IMAP mailboxes (mailbox= filter)\nlist_folders                # (mailbox)\nlist_messages               # (mailbox, folder, limit, search)\nsearch                      # (mailbox, from, subject, since, ...)\nget_message                 # (mailbox, uid)\ndelete_message              # (mailbox, uid)\nmark_seen                   # (mailbox, uid, seen)\nsend                        # (mailbox, to, subject, body_text/html, ...)\n```\n\nDiscovery flow for an agent: call `mailboxes` to see what's available, then pass the chosen name (`\"personal\"`) or address (`\"me@gmail.com\"`) as the `mailbox` argument. For cross-account reads use `inbox` — `inbox(from=\"boss@corp.com\")` 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.\n\nThere 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:\n\n```json\n{\n  \"mcpServers\": {\n    \"mailbox\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:8000/mcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer YOUR_TOKEN_HERE\"\n      }\n    }\n  }\n}\n```\n\nDrop the `headers` block if you're running without `auth.tokens`.\n\n## Common Workflows\n\n### Find and delete\n\n```bash\n# 1. Find UIDs matching the criteria\nHITS=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?from=newsletter@spam.io&limit=500\" | jq -r '.messages[] | \"\\(.mailbox) \\(.uid)\"')\n\n# 2. Delete each (per-mailbox endpoint since DELETE is single-mailbox)\necho \"$HITS\" | while read -r mailbox uid; do\n  curl -s -X DELETE -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/mailboxes/$mailbox/messages/$uid\"\ndone\n```\n\n### Send-to-self e2e sanity check\n\n```bash\nMARKER=\"e2e-$(uuidgen | cut -c1-8)\"\n\n# 1. Send marker to self\ncurl -s -X POST -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"to\\\": [\\\"me@gmail.com\\\"], \\\"subject\\\": \\\"ping $MARKER\\\", \\\"body_text\\\": \\\"$MARKER\\\"}\" \\\n  \"$MAILBOX_URL/mailboxes/personal/send\"\n\n# 2. Search for it (may take a few seconds to land)\nfor i in 1 2 3 4 5; do\n  sleep 2\n  FOUND=$(curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n    \"$MAILBOX_URL/inbox?subject=$MARKER\" | jq -r '.messages | length')\n  [ \"$FOUND\" -gt 0 ] && break\ndone\n```\n\n### Pull unread across everything, format for a digest\n\n```bash\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" \\\n  \"$MAILBOX_URL/inbox?unseen=true&limit=100\" \\\n  | jq -r '.messages[] | \"\\(.mailbox)\\t\\(.from)\\t\\(.subject)\"' \\\n  | column -t -s $'\\t'\n```\n\n## Tips\n\n- Date filters (`since`, `before`) use **IMAP date format** (`1-Jan-2026`), not ISO — `date -d ... +%-d-%b-%Y` is your friend.\n- `larger_than` / `smaller_than` are in **bytes**.\n- `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/<name>/folders` to discover.\n- 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.\n- Gmail / Yahoo / etc. need **app passwords**, not your account password. Generate one in the provider's security settings.\n- `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.\n- Per-mailbox blowups in `/inbox` come back in the `errors` array — always check it, one dead account shouldn't blind you to the rest.\n- Bearer tokens live in the server's `config.yaml` under `auth.tokens` — list with multiple tokens to rotate without downtime.\n\nFile v1.1.0:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"docker-mailbox\",\n  \"version\": \"1.1.0\",\n  \"publishedAt\": 1779075098641\n}\n\nFile v1.1.0:references/setup.md\n\n# docker-mailbox setup\n\n## Requirements\n\n- Docker + Docker Compose\n- One or more email accounts with IMAP and/or SMTP credentials\n- For Gmail / Yahoo / Outlook / iCloud / most major providers: an **app password** (your normal account password will not work; you need to enable 2FA and generate an app-specific password in the provider's security settings)\n\n## Quick Install\n\n```bash\ngit clone https://github.com/psyb0t/docker-mailbox\ncd docker-mailbox\ncp config.example.yaml config.yaml\n# Edit config.yaml — add your mailboxes and (optionally) auth.tokens\n```\n\nThen either:\n\n```bash\n# Plain docker run\ndocker run --rm -p 8000:8000 \\\n  -v \"$PWD/config.yaml:/etc/mailboxd/config.yaml:ro\" \\\n  psyb0t/mailbox:latest\n\n# Or docker compose (recommended)\ndocker compose up -d\n```\n\nVerify it came up:\n\n```bash\ncurl -s http://localhost:8000/health\n# {\"ok\": true, \"version\": \"...\"}\n```\n\n## Configuration\n\n### `config.yaml`\n\nOne YAML file. Lives at `MAILBOXD_CONFIG`, or `--config`, or `/etc/mailboxd/config.yaml` by default (which is where the prod container expects the read-only mount).\n\n```yaml\nlog_level: INFO\n\n# Bearer-token gate. Guards the HTTP API AND /mcp. Empty / missing = no auth.\n# Multi-token list = rotate without downtime: add a new one, swap clients\n# over, retire the old one.\nauth:\n  tokens:\n    - \"long-random-token-1\"        # generate with: openssl rand -hex 32\n    # - \"long-random-token-2\"\n\nmailboxes:\n  - name: personal \n\nArchive v1.0.0: 3 files, 9736 bytes\n\nFiles: references/setup.md (10580b), SKILL.md (14049b), _meta.json (133b)","readmeExcerpt":"Skill: docker-mailbox Owner: psyb0t Summary: 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 across multiple inboxes in one call — GET /inbox fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"export MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config"},{"language":"bash","snippet":"curl -s $MAILBOX_URL/health"},{"language":"bash","snippet":"curl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq"},{"language":"bash","snippet":"curl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_URL/mailboxes | jq"},{"language":"json","snippet":"{\"detail\": \"description of what went wrong\"}"},{"language":"bash","snippet":"curl -s $MAILBOX_URL/health"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: docker-mailbox\ndescription: 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 across multiple inboxes in one call — `GET /inbox` fans out across every IMAP account in parallel and returns a merged, newest-first feed. Use when you need an agent (or a script, or a curl one-liner) to drive one or more real mail accounts without standing up a webmail UI, a message store, or any per-provider client library. Stdlib `imaplib`/`smtplib` under the hood, FastAPI on top, bearer-token auth optional.\ncompatibility: Requires curl and a running mailboxd instance. MAILBOX_URL env var must be set. MAILBOX_TOKEN is optional — only needed if the server was started with `auth.tokens` configured.\nmetadata:\n  author: psyb0t\n  homepage: https://github.com/psyb0t/docker-mailbox\n---\n\n# docker-mailbox\n\nREST + 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.\n\nThe 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.\n\nFor installation and setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Deleting mail is permanent** — the delete endpoint flags a message `\\Deleted` and `EXPUNGE`s it immediately (no trash bin, no undo). Only delete specific message UIDs the user has confirmed; never bulk-delete straight from a broad `/inbox` or `/search` result — list first, show what matched, confirm, then delete. On a multi-mailbox instance, always confirm which `mailbox`/UID you're targeting so you don't touch the wrong account.\n- **No auth when `auth.tokens` is empty.** With it unset the HTTP API AND `/mcp` are UNAUTHENTICATED — anyone who can reach the port gets full read/send/delete access to every configured mailbox. NEVER expose such an instance on a network or to untrusted agents; set `auth.tokens` and bind to loopback / behind an authenticating proxy.\n- **Every call sends your mail data to whatever `MAILBOX_URL` points at.** Point it only at a `mailboxd` instance you run or explicitly trust; prefer HTTPS if it's reachable over a network.\n\n## Setup\n\nThe API should already be running. Set the base URL and (if configured) the bearer token:\n\n```bash\nexport MAILBOX_URL=http://localhost:8000\nexport MAILBOX_TOKEN=your_token_here   # omit if auth.tokens is empty in config\n```\n\n**Verify:**\n\n```bash\ncurl -s $MAILBOX_URL/health\n# {\"ok\": true, \"version\": \"0.1.0\"}\n\ncurl -s -H \"Authorization: Bearer $MAILBOX_TOKEN\" $MAILBOX_U"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"docker-mailbox\",\n  \"version\": \"1.2.3\",\n  \"publishedAt\": 1787236742946\n}"},{"path":"references/setup.md","content":"# docker-mailbox setup\n\n## Requirements\n\n- Docker + Docker Compose\n- One or more email accounts with IMAP and/or SMTP credentials\n- For Gmail / Yahoo / Outlook / iCloud / most major providers: an **app password** (your normal account password will not work; you need to enable 2FA and generate an app-specific password in the provider's security settings)\n\n## Quick Install\n\n```bash\ngit clone https://github.com/psyb0t/docker-mailbox\ncd docker-mailbox\ncp config.example.yaml config.yaml\n# Edit config.yaml — add your mailboxes and (optionally) auth.tokens\n```\n\nThen either:\n\n```bash\n# Plain docker run\ndocker run --rm -p 8000:8000 \\\n  -v \"$PWD/config.yaml:/etc/mailboxd/config.yaml:ro\" \\\n  psyb0t/mailbox:latest\n\n# Or docker compose (recommended)\ndocker compose up -d\n```\n\nVerify it came up:\n\n```bash\ncurl -s http://localhost:8000/health\n# {\"ok\": true, \"version\": \"...\"}\n```\n\n## Configuration\n\n### `config.yaml`\n\nOne YAML file. Lives at `MAILBOXD_CONFIG`, or `--config`, or `/etc/mailboxd/config.yaml` by default (which is where the prod container expects the read-only mount).\n\n```yaml\nlog_level: INFO\n\n# Bearer-token gate. Guards the HTTP API AND /mcp. Empty / missing = no auth.\n# Multi-token list = rotate without downtime: add a new one, swap clients\n# over, retire the old one.\nauth:\n  tokens:\n    - \"long-random-token-1\"        # generate with: openssl rand -hex 32\n    # - \"long-random-token-2\"\n\nmailboxes:\n  - name: personal                  # URL-safe handle; matches [a-zA-Z0-9_-]+, must be unique\n    description: \"Gmail\"\n\n    imap:\n      host: imap.gmail.com\n      port: 993                     # default 993\n      tls: ssl                      # ssl | starttls | none   (default ssl)\n      username: me@gmail.com\n      password: \"app-password\"      # not your real account password\n      default_folder: INBOX         # default folder when callers don't specify\n\n    smtp:\n      host: smtp.gmail.com\n      port: 465                     # default 587\n      tls: ssl                      # default starttls\n      username: me@gmail.com\n      password: \"app-password\"\n      from_address: \"Me <me@gmail.com>\"\n\n  - name: work\n    imap: { host: mail.work.com, port: 143, tls: starttls, username: me, password: \"...\", default_folder: INBOX }\n    smtp: { host: mail.work.com, port: 587, tls: starttls, username: me, password: \"...\", from_address: me@work.com }\n```\n\n### Config rules\n\n- **At least one mailbox** is required.\n- Each mailbox must declare at least one of `imap` / `smtp`. Both is fine. Neither is a config error.\n- **`name`** is the URL path segment AND the MCP tool prefix. Matches `[a-zA-Z0-9_-]+`. Must be unique across the file.\n- **Defaults**: IMAP `993/ssl`, SMTP `587/starttls`. Override per-mailbox if your provider is weird.\n- **The config file holds plaintext passwords and your bearer tokens.** Treat it like a credential vault: gitignore it (the repo already does), `chmod 600`, mount read-only into the container, don't paste it in chat.\n\n### Provider quick-reference\n"},{"path":"skill-card.md","content":"## Description:\n\ndocker-mailbox lets agents use a REST API and streamable-HTTP MCP server to read, search, send, mark seen, and delete messages across one or more IMAP/SMTP mailboxes.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and operators use this skill to let an agent or script interact with real email accounts through mailboxd for mailbox discovery, message retrieval, search, sending, mark-seen, and carefully confirmed deletion.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A deployment without auth.tokens can expose mailbox read, send, and delete access to anyone who can reach the service.\n\nMitigation: Set long random bearer tokens, bind the service to loopback or place it behind authenticated HTTPS, and keep tokens secret.\n\nRisk: Delete operations permanently expunge messages and do not provide a trash bin or undo path.\n\nMitigation: List matching messages first, show the mailbox and UID, and require explicit human confirmation before deleting any message.\n\nRisk: Configuration and tunnel credential files can contain mail passwords, bearer tokens, or tunnel secrets.\n\nMitigation: Keep these files private, mount them read-only where possible, and avoid sharing their contents in chat or logs.\n\nRisk: Mutable Docker images or downloaded tools can change after deployment.\n\nMitigation: Pin Docker images and downloaded tools for environments where reproducibility or supply-chain control matters.\n\nRisk: Mail contents are sent to the mailboxd endpoint selected by MAILBOX_URL.\n\nMitigation: Point MAILBOX_URL only at a mailboxd instance the user runs or explicitly trusts, and prefer HTTPS for network access.\n\n## Reference(s):\n\n- [docker-mailbox setup](references/setup.md)\n- [docker-mailbox repository](https://github.com/psyb0t/docker-mailbox)\n- [html2text](https://github.com/Alir3z4/html2text)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with JSON configuration snippets and inline shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires MAILBOX_URL and, when configured, MAILBOX_TOKEN; mailbox service responses are JSON.]\n\n## Skill Version(s):\n\n1.2.3 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1809,"uniquenessScore":41,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T22:17:22.290Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T22:17:22.290Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T00:31:38.498Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}