内容
# mailbox
[](https://github.com/psyb0t/docker-mailbox/actions/workflows/pipeline.yml)
[](https://github.com/psyb0t/docker-mailbox/releases)
[](LICENSE)
[](https://hub.docker.com/r/psyb0t/mailbox)
您的收件箱,即插即用。您可以将此工具连接到任意多个 IMAP 和 SMTP 邮箱,然后通过 **一个 HTTP API 和一个 MCP 服务器,均在同一端口** (MCP 通过流式 HTTP 通道在 `/mcp` 处提供)来读取邮件、发送邮件和删除邮件,涵盖所有账户。无需网络邮件。无需数据库。无需拥有三千个开关的桌面应用程序。只需:“这是一些电子邮件凭证”→“现在我的代理 / shell 脚本 / 凌晨 3 点的 curl 管道可以控制收件箱。”
其他所有“统一收件箱”工具都想要**拥有**您的邮件——将所有邮件吸入他们的云中,永远向您收费,并在下一个季度将其丢失在一次泄露中。而这款工具**存储零字节**。重启容器后,什么都不会丢失,因为本来就没有东西需要丢失。连接按需建立,执行完任务后就会关闭,在 `finally` 块中。
标准库 `imaplib` + `smtplib` 在底层,FastAPI 在上层,官方 MCP Python SDK 协同工作(流式 HTTP,无 stdio 乱象),供应链 `exclude-newer` 门控确保恶意 pip 发布不会在凌晨 3 点偷偷溜进来,并且有真正的 SMTP 服务器集成测试,实际将字节放在套接字上。
## 目录
- [内部结构](#内部结构)
- [快速开始](#快速开始)
- [配置](#配置)
- [HTTP API](#http-api)
- [身份验证](#身份验证)
- [MCP 服务器](#mcp-服务器)
- [代理集成](#代理集成)
- [架构](#架构)
- [开发](#开发)
- [许可证](#许可证)
## 内部结构
| 界面 | 功能 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **HTTP API** | `GET /inbox` 跨所有账户并行搜索(按邮箱、发件人、主题、日期、标志等过滤)。每个邮箱的读取和删除。SMTP 发送。 |
| **MCP 服务器** | 在 `/mcp` 处提供流式 HTTP MCP - 相同的端口,相同的认证令牌,相同的操作(`mailboxes`、`inbox`、`list_messages`、`send`等),工具集是扁平的。 |
| **认证令牌** | YAML 中的一个令牌列表保护 API 和 `/mcp`。空列表 = 完全开放(您自担风险)。多个令牌 = 零停机时间轮换。 |
| **协议** | IMAP(SSL / STARTTLS / 明文)和 SMTP(SSL / STARTTLS / 明文)。故意保持标准。 |
| **配置** | 一个 YAML 文件。添加邮箱,重启,完成。每个邮箱声明 `{imap, smtp}` 的子集。 |
| **状态** | 没有。真正没有。没有数据库,没有队列,没有缓存,没有“哦,就这个小 Redis”。连接打开,做完工作,关闭。下次继续。 |
## 快速开始
1. 在旁边放置一个 `config.yaml`(如果懒惰,可以复制 `config.example.yaml`)。
2. 启动:
```bash
docker run --rm \
-p 8000:8000 \
-v "$PWD/config.yaml:/etc/mailboxd/config.yaml:ro" \
psyb0t/mailbox:latest
```
3. 测试:
```bash
# 无认证?这样直接使用。设置 auth.tokens 后,添加:-H "Authorization: Bearer YOUR_TOKEN"
TOKEN="在此处粘贴来自配置的令牌"
curl -s http://localhost:8000/health | jq # 健康检查始终开放
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/mailboxes | jq
curl -s -H "Authorization: Bearer $TOKEN" 'http://localhost:8000/inbox?limit=5' | jq
```
### docker compose
```yaml
services:
mailbox:
image: psyb0t/mailbox:latest
ports: ["8000:8000"]
volumes:
# config.yaml 包含您的 IMAP/SMTP 密码和 auth.tokens —
# 请忽略它,限制其文件系统权限,像对待 SSH 密钥一样对待它。
- ./config.yaml:/etc/mailboxd/config.yaml:ro
```
## 配置
一个 YAML 文件。位于 `MAILBOXD_CONFIG`,或 `--config`,或 `/etc/mailboxd/config.yaml`(如果您不关心的话)。
```yaml
log_level: INFO
# 认证令牌门控。保护 HTTP API 和 /mcp。空/缺失 = 无认证(祝你好运)。多令牌列表 = 轮换而无需停机:添加一个,
# 切换客户端,淘汰旧的。
auth:
tokens:
- "长随机令牌-1"
- "长随机令牌-2"
mailboxes:
- name: personal # URL 安全句柄;显示在 /mailboxes/<name>/... 中,并作为 MCP 工具前缀
description: "Gmail"
imap:
host: imap.gmail.com
port: 993 # 默认 993
tls: ssl # ssl | starttls | none (默认 ssl)
username: me@gmail.com
password: "应用密码" # Gmail/Yahoo/等需要应用密码,而不是您的真实密码
default_folder: INBOX # 调用者未指定时的默认文件夹
smtp:
host: smtp.gmail.com
port: 465 # 默认 587
tls: ssl # 默认 starttls
username: me@gmail.com
password: "应用密码"
from_address: "我 <me@gmail.com>"
- name: work
imap: { host: mail.work.com, port: 143, tls: starttls, username: me, password: "...", default_folder: INBOX }
smtp: { host: mail.work.com, port: 587, tls: starttls, username: me, password: "...", from_address: me@work.com }
```
注意事项:
- **至少一个邮箱。** 每个邮箱至少需要一个 `imap` / `smtp`。两者皆可。两者都不是配置错误。
- **`name`** 匹配 `[a-zA-Z0-9_-]+` 且唯一 — 它是 URL 路径段和 MCP 工具前缀,因此不要包含空格或表情符号。
- **默认值**:IMAP `993/ssl`,SMTP `587/starttls`。如果您的提供商很奇怪,请重写。
- **配置文件包含纯文本密码和认证令牌。** 请像凭证库一样对待它:忽略它,`chmod 600`,只读挂载,不要在 Slack 中粘贴。
## HTTP API
### 身份验证
如果设置了 `auth.tokens`,**除 `GET /health` 之外的每个请求** 都必须携带认证令牌:
```
Authorization: Bearer <其中一个 auth.tokens>
```
无头部、错误格式、错误值 → `401` 及 `WWW-Authenticate: Bearer`。令牌进行恒定时间比较,以防止通过计时泄露。同一认证门控涵盖 `/mcp` — 无需学习第二套认证系统。
留空 `auth.tokens`(或跳过该块),则所有内容都开放。对于“绑定到 127.0.0.1 且前面有反向代理”,这样是安全的。否则则灾难性。由您决定。
### 错误
所有内容均为 JSON。错误如下所示:`{"detail": "..."}`:
| 状态码 | 何时发生 |
| ------ | ----------------------------------------------------------------------------------------------- |
| `401` | 缺失或无效的认证令牌(当启用认证时)。 |
| `404` | URL 中未知的邮箱名称。 |
| `409` | 您为仅支持 SMTP 的邮箱请求 IMAP 端点(反之亦然)。 |
| `502` | 上游 IMAP / SMTP 服务器拒绝。 |
### `GET /health`
```json
{ "ok": true, "version": "0.1.0" }
```
始终开放,无需认证令牌。您的存活探测可以指向此并忽略它。
### `GET /mailboxes`
```json
{
"mailboxes": [
{ "name": "personal", "description": "Gmail",
"imap": true, "smtp": true }
]
}
```
### 统一收件箱(主要事件)
`GET /inbox` 是您 90% 的时间想要的读取端点。它**并行**命中**每个配置了 IMAP 的邮箱**,在每个邮箱上运行相同的结构化搜索,合并最新的结果,并为每个结果标记其来源的账户。“显示来自 `boss@corp.com` 的所有内容”,“当前未读的内容”,“今天早上收到的内容” — 相同的调用,无需客户端端的扩展舞。
| 查询参数 | 功能 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `mailbox` | 按邮箱名称(`personal`)或电子邮件地址(`me@gmail.com`)的 CSV 筛选。省略以搜索所有邮箱。 |
| `from`、`to`、`subject`、`body`、`text` | IMAP 搜索谓词。`text` 是标题和正文的全文搜索。 |
| `since`、`before` | IMAP 日期,例如 `1-Jan-2026`。 |
| `unseen`、`seen`、`flagged`、`answered` | 标志过滤器。设置您想要的标志为 true。 |
| `larger_than`、`smaller_than` | 字节。 |
| `folder` | IMAP 文件夹(默认 `INBOX`)。 |
| `limit` | 硬上限为 500。默认 50。 |
响应:
```json
{
"messages": [
{
"uid": "1234",
"mailbox": "personal",
"mailbox_address": "me@gmail.com",
"from": "boss@corp.com",
"subject": "weekly sync",
"date": "..."
}
],
"errors": [
{ "mailbox": "work", "error": "login failed: ..." }
]
}
```
每个邮箱的故障都记录在 `errors` 中,而不是导致调用失败。 一个死账户不会蒙蔽你对其他九个账户的视线。
```bash
# 来自一个发件人的所有内容,跨所有账户
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/inbox?from=boss@corp.com&limit=20' | jq
# 仅在两个账户中未读邮件
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/inbox?mailbox=personal,work&unseen=true' | jq
```
### 每个邮箱的 IMAP
当您想聚焦在一个账户上:
| 方法 | 路径 | 功能 |
| -------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/mailboxes/{name}/folders` | 列出 IMAP 文件夹。 |
| `GET` | `/mailboxes/{name}/messages?folder=&limit=&search=` | 最新-优先的标题。`search` 是原始 IMAP(`ALL`、`UNSEEN`、`FROM foo@bar`)。`limit` ≤ 500。 |
| `GET` | `/mailboxes/{name}/search?from=&subject=&...` | 与 `/inbox` 相同的结构化查询,但仅限一个账户,无 `mailbox`。 |
| `GET` | `/mailboxes/{name}/messages/{uid}?folder=&reader=` | 一条消息,完全解码(`body_text` + `body_html` + 附件元数据)。添加 `reader=true` 还可以获取 `body_reader` — HTML 剥离为干净的文本/ Markdown(无表格、样式、跟踪像素)。 |
| `DELETE` | `/mailboxes/{name}/messages/{uid}?folder=` | `\Deleted` + EXPUNGE。消失。真的消失。 |
| `POST` | `/mailboxes/{name}/messages/{uid}/seen?folder=` | 正文 `{"seen": true|false}` — 翻转 `\Seen` 标志。 |
UID 无处不在,从不使用序列号 — 标识符在邮箱在您下方移动时保持稳定。
### 阅读模式
在 `GET /mailboxes/{name}/messages/{uid}` 处添加 `?reader=true`(或在 MCP 的 `get_message` 工具中添加 `reader=true`)会添加一个 `body_reader` 字段 — HTML 正文被展平为干净的 Markdown。适用于 LLM 和不想阅读原始 `<table><tr><td style="…">…</td></tr></table>` 结构的 人类。
工作原理:
1. HTML 正文通过 [html2text](https://github.com/Alir3z4/html2text) 运行,参数为 `ignore_images=True`、`body_width=0`(无硬换行)、`unicode_snob=True`。样式、脚本、注释、头部和 `<img>` 标签被丢弃。
2. 标题 → `#`,粗体/斜体保留,`<a href="x">text</a>` → `[text](x)` 行内,列表/表格转换为 Markdown 等价物。
3. 如果没有 HTML 正文,`body_reader` 默认为修剪后的 `body_text`。
4. 原始的 `body_text` 和 `body_html` 仍然与 `body_reader` 一起返回 — `body_reader` 是附加的。UI 客户端可以渲染 HTML;代理可以读取 Markdown; nobody 丢失任何内容。
为什么不直接使用 `body_text`?大多数营销/交易邮件都带有 `multipart/alternative`,其中 `text/plain` 部分丢失,“在 HTML 中查看”存根,或自动生成的噪音。真正的内容存在于 HTML 部分。阅读模式提取它。为什么不选择 readability/trafilatura?它们被调谐为在网页中查找文章,带有导航和广告。而电子邮件 **是** 内容 — 噪音是样式,这正是 html2text 剥离的内容。无需 DOM 提取。
回复引用链不会被剥离(您获得完整的线程),表格布局电子邮件以管道表格形式出现(忠实但嘈杂),附件仅作为元数据。
### SMTP
| 方法 | 路径 | 功能 |
| ------ | ----------------------------- | -------------- |
| `POST` | `/mailboxes/{name}/send` | 发送电子邮件。 |
正文:
```json
{
"to": ["dest@example.com"],
"cc": ["copy@example.com"],
"bcc": ["hidden@example.com"],
"subject": "hi",
"body_text": "纯文本正文",
"body_html": "<p>可选的 HTML 正文</p>",
"from_address": "override@example.com",
"reply_to": "noreply@example.com"
}
```
至少需要 `body_text` / `body_html` 之一。两者 = `multipart/alternative`,像一个体面的邮件客户端一样。我们还添加了一个 Thunderbird 形状的 `User-Agent`,一个 domain-aligned 的 `Message-ID`,和一个 `Date` 头 — 提供商的垃圾邮件过滤器在这些缺失或粗糙时变得敌对,所以我们配合。
## MCP 服务器
与 HTTP API 相同的操作,通过 **流式 HTTP** 在 `/mcp` 处暴露为 MCP 工具(相同的端口,相同的认证令牌)。一套扁平的工具集 — 每个邮箱操作都带有一个 `mailbox` 参数(名称或地址),因此 100 个收件箱仍然附带相同的工具集:
```
mailboxes # 发现:列出配置的邮箱 + 功能
inbox # 跨所有 IMAP 邮箱的统一读取(mailbox= 过滤)
list_folders # (邮箱)
list_messages # (邮箱,文件夹,限制,搜索)
search # (邮箱,发件人,主题,时间,...)
get_message # (邮箱,UID,reader=true → +body_reader)
delete_message # (邮箱,UID)
mark_seen # (邮箱,UID,seen)
send # (邮箱,到,主题,body_text/html,...)
```
代理通过 `mailboxes` 找到可用的工具,然后将选定的名称(`"personal"`)或地址(`"me@gmail.com"`)作为 `mailbox` 参数传递。对于跨账户查询,请使用 `inbox` — `inbox(from="boss@corp.com")` 同时扇出到每个启用的 IMAP 邮箱。
仅当至少一个邮箱具有 IMAP 时,才会出现 IMAP 专用工具。SMTP 亦如此。没有无效按钮。
### 传输
`/mcp` 处的纯旧流式 HTTP MCP。端点会发出完整的传输:`GET` 为客户端开启 SSE 流,`POST` 发送请求,`DELETE` 终止会话。您的 MCP 主机可以执行的操作,使用 `make run`;相同的进程提供 REST API 和 `/mcp`。将您的客户端指向 `http://host:8000/mcp`,您就完成了。
### 将其连接到 Claude / Pi / 任意 MCP 主机
大多数 MCP 主机使用这样的 `.mcp.json`(或等效文件):
```json
{
"mcpServers": {
"mailbox": {
"transport": "streamable-http",
"url": "http://localhost:8000/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN_HERE"
}
}
}
}
```
如果您在没有 `auth.tokens`,则丢弃 `headers` 块。如果您珍视睡眠,请保留它。
## 代理集成
[技能](.agents/skills/docker-mailbox)在任何读取 `.agents/skills/` 的代理中运行,并在以下客户端中原生安装:
### Claude 代码
```bash
claude plugin marketplace add psyb0t/agents
claude plugin install mailbox@psyb0t
```
Claude Code 提示您输入 mailboxd URL 和(如果启用了身份验证)认证令牌 — 令牌存储在您的 OS 密钥链中。
### Codex
```bash
codex plugin marketplace add psyb0t/agents
codex plugin add mailbox@psyb0t
```
通过市场安装后,Codex 调用技能为 `$mailbox:docker-mailbox`。Codex 还会自动拾取技能,无需安装,在任何包含 `.agents/skills/` 的存储库中 — 在那里它直接调用为 `$docker-mailbox`。
### OpenClaw
该技能在每次发布时都会发布到 ClawHub:
```bash
openclaw skills install @psyb0t/docker-mailbox
```
对于支持本地标准输入输出的 MCP 客户端,[`@psyb0t/mailbox`](.agents/plugins/mailbox) 插件桥接到 mailboxd 的 `/mcp` 端点:
```bash
openclaw plugins install clawhub:@psyb0t/mailbox
```
然后设置 `MAILBOX_URL`(如果服务器启动时配置了 `auth.tokens`,还需设置 `MAILBOX_TOKEN`)。
## 架构
```
┌────────────┐ ┌──────────────────────────┐ ┌──────────────┐
│ HTTP CLI │───▶│ FastAPI │ │ IMAP 服务器 │
│ / curl │ │ /inbox, /mailboxes, … │───▶│ SMTP 服务器 │
└────────────┘ │ │ └──────────────┘
┌────────────┐ │ bearer-auth 网关 │
│ MCP 主机 │───▶│ ─── 共享操作 ─── │
│ (Claude…) │ │ │
└────────────┘ │ /mcp (可流式 HTTP) │
│ mailboxes/inbox/send/… │
└──────────────────────────┘
│
▼
┌──────────────────────┐
│ config.yaml │
│ auth.tokens │
│ 每个邮箱一个条目 │
└──────────────────────┘
```
无状态。无数据库。无队列。无缓存。每个请求打开连接,在 `finally` 中关闭。 在运行中杀死容器 — 没有什么可恢复的,因为没有任何东西被持久化。重启它。同样的故事。故意保持简单。
## 开发
```bash
make help # 列出所有目标
make dev-image # 构建沙盒开发容器
make shell # 进入容器
make run # 启动服务器(REST + /mcp);挂载 CONFIG=path/to/config.yaml
make test # 完整测试套件(单元测试 + Docker-in-Docker 集成测试)
make test-unit # 仅进程内测试 — 快速反馈循环
make lint # flake8 + mypy
make format # isort + black
make check # lint + 测试
```
### 包管理 — 供应链防御
我们使用 `uv` 的 `exclude-newer` 来拒绝在固定截止日期后发布的任何软件包版本。截止日期通过每个 `pkg-*` make 目标自动**更新为今天**,因此新发布的恶意软件包无法在下一次 `pkg-add` 时潜入。
```bash
make pkg-add PKG=foo==1.2.3
make pkg-remove PKG=foo
make pkg-update PKG=foo
make pkg-lock
make pkg-upgrade
```
不要手动编辑 `[tool.uv].exclude-newer`。让目标来处理它。
## 许可证
WTFPL — 见 [LICENSE](LICENSE)。随便你怎么搞。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器