内容
# Sink
**自主托管,无限制的Laravel应用测试邮件捕获服务,运行于Laravel Cloud。**
Sink是一个邮件捕获工具,您可以将其部署到自己的Laravel Cloud账户中。在任何Laravel应用中添加客户端,设置`MAIL_MAILER=sink`,所有发出的邮件都会被捕获到一个自主托管的收件箱中,而不是被投递。人类可以在网页界面中检查邮件;编码代理可以根据元数据和正文匹配布尔值,通过[MCP](https://modelcontextprotocol.io)进行验证。
> **一进,零出。** 无需计量,无需按消息计费,无需第三方收件箱存储您的测试邮件。
Sink仅用于捕获邮件。它不是SMTP服务器,不是转发中继,也不是`Mail::fake()`在单元测试中的替代品。
---
## Sink 存在的理由
像Mailpit这样的本地邮件捕获工具非常适合单个开发者的笔记本电脑,但它们是短暂的和本地的。像Mailtrap这样的托管沙盒非常完善,但它们增加了另一个供应商,计量计费和另一个收件箱来存储您的数据。
Sink是一个免费、自主托管的**基础**:一个持久的、共享的、代理可查询的测试/暂存收件箱,运行在您控制的基础设施上。存储和云资源是您唯一的成本。
**定位。** Mailpit是单个开发者的本地循环的正确工具,而Mailtrap是具有交付能力评分和广泛团队功能的完整托管产品。Sink是Laravel团队的基石,他们希望在自己的云账户中拥有一个持久的、共享的暂存捕获工具,并将编码代理作为一流的消费者。 当您不再需要时,可以升级到 **Mailtrap**。 Sink采用MIT许可证。
## 支持姿态
Sink是按照[Artisan Build](https://artisan.build)的方式编写的。我们会修复错误。功能请求可以通过fork实现。 客户端特定的功能不会**回填**到开源版本中。 如果您需要完整的托管产品,Mailtrap就是为您准备的。
---
## 架构
```
源应用 (MAIL_MAILER=sink)
└─ sink-client 传输
序列化 Symfony 消息 → 原始 MIME
生成幂等性 ULID + 薄信封
POST /ingest (Bearer SINK_TOKEN, 重试 + 回退, 冷启动容忍)
仅捕获 · 生产熔断器 · 耗尽时失败
│
▼
Sink 应用 (每个客户端一个隔离的 Laravel Cloud 环境)
/ingest → 令牌身份验证 → 信封版本检查 (如果发送者超前,则 4xx)
→ 按幂等键更新 → 原始 MIME 到对象存储 → 入队
│
▼
Redis 管理队列 (根据深度自动扩缩, 缩放为零)
│
▼
解析工作器 → 提取主题 / 发件人 / 收件人 · cc · bcc / 头部 / 链接 /
附件元数据 / 大小 → Postgres (元数据)
原始 MIME + 附件字节 → 对象存储
│
┌───────┴───────────────────────────────┐
▼ ▼
人类收件箱 UI MCP 服务器 (Bearer)
邀请函仅限 无正文断言工具
沙盒 HTML 渲染 ← 编码代理 / CI
头部 · 链接 · 原始 · 附件
│
▼
调度 sink:maintain → sink:prune
(默认保留 7 天 + 最大消息数 + 最大总字节数; 存储桶生命周期备份)
```
- **每个客户端一个隔离环境** 在 Laravel Cloud 上:它拥有自己的网络实例、Postgres 数据库、Redis、对象存储桶、托管队列和调度器。
- **每个环境无限源应用。** 源应用由发送它们的承载令牌标记,从而赋予 Sink 其 `应用` 维度。
- **跨边界传输是 HTTP,而不是共享 Redis。** 源应用是独立部署的;`POST /ingest` 是边界。
---
## 仓库布局
这是一个 **单体仓库**。三个软件包在 `packages/` 下开发,每个软件包都拆分为只读的独立仓库,并发布到 Packagist:
| 软件包 | 仓库 | 安装在 | 角色 |
| --- | --- | --- | --- |
| [`artisan-build/sink-contracts`](https://github.com/artisan-build/sink-contracts) | 只读拆分 | 两个软件包 | 版本化的线路信封。兼容性的单一位置。 |
| [`artisan-build/sink-client`](https://github.com/artisan-build/sink-client) | 只读拆分 | 源 Laravel 应用 | 发送端:`sink` 邮件传输、激活守卫、重试/回退、幂等键、`sink:install` 和 `sink:update`。 |
| [`artisan-build/sink-server`](https://github.com/artisan-build/sink-server) | 只读拆分 | Sink 应用 | 接收端:摄入、解析、对象存储、修剪、人类收件箱 UI 和 MCP 服务器。 |
**Sink 应用** 在此仓库的根目录是一个精简的 Laravel 外壳:身份验证、令牌发放和 `sink-server` 连接。它保持精简,以便 Sink 特有的业务逻辑不会转移到软件包中。
### 贡献
问题和拉取请求在三个拆分仓库中**禁用**,与 Laravel 自己的只读拆分模型相匹配。所有开发都在这个单体仓库中进行。参见 [`SECURITY.md`](SECURITY.md) 以进行私有漏洞披露;Sink 位于不受信任的摄入路径上。
---
## 兼容性与版本控制
在 N 个独立部署的发送者和一个自主托管的接收者之间,**版本偏差是正常状态,而不是错误。** 线路协议的设计可以容忍它:
- **版本化信封。** 每个有效载体都带有 `envelope_version`。信封在**主版本内累积演化**:仅新增可选字段,绝不删除或重新利用现有字段。
- **向后兼容的服务器。** 更新的 `sink-server` 可以解析它支持的每个旧信封主版本。
- **对危险情况的明确失败。** 当发送者使用服务器无法理解的新信封时,摄入会返回明确的 **4xx**:请先更新 Sink 应用。
- **不透明的消息有效载体。** 只有薄 Sink 信封对版本敏感;消息是 base64 编码的原始 RFC-822 MIME,并在服务器端解释。
- **规范的升级顺序:先更新 Sink 服务器,然后更新客户端。** 向后兼容的服务器可以安全地运行在其发送者之前;危险的是发送者超前于服务器。
- **保留流字段。** `stream` 在 v1 中存在但可为空。它为未来的每个运行隔离保留;v1 使用应用和时间窗口范围。
---
## 发布
发布是来自此单体仓库的锁定步骤 `v*` 标签。发布标签驱动 `php artisan kibble:split`,它将 `sink-contracts`、`sink-client` 和 `sink-server` 拆分为它们的只读镜像并发布软件包。保持软件包间的约束在同一主版本上。
---
## 在 Laravel Cloud 上部署
为每个客户端运行一个隔离的 Laravel Cloud 环境:Sink 应用、一个 Postgres 数据库、Redis、一个对象存储桶、一个网络实例、一个托管队列和调度器。调度器运行 `sink:maintain`,它运行 `sink:prune`;Redis 工作器排空解析作业。
> **使用编码代理?让技能为您完成。** 此仓库附带了一个 [`provisioning-sink-on-cloud`](.claude/skills/provisioning-sink-on-cloud/SKILL.md) 技能,该技能可以配置 Postgres、Redis、对象存储、网络、队列和调度器;连接 Sink 配置;部署;迁移;运行 `create-admin`;并颁发第一个源应用令牌。
所需的生产环境:
- `DB_CONNECTION=pgsql` 加上正常的 `DB_*` 值。
- 可选的 `SINK_DB_*` 值,当 Sink 元数据连接应与默认数据库不同时;否则 `sink` 连接镜像应用默认连接。
- 对象存储凭证和 `SINK_DISK` 用于原始 MIME 和附件字节。
- `QUEUE_CONNECTION=redis` 和 `SINK_QUEUE_CONNECTION=redis` 用于解析作业。
- `SINK_RETENTION_DAYS`,默认为 `7`,以及可选的 `SINK_MAX_MESSAGES` 和 `SINK_MAX_TOTAL_BYTES` 上限。
- `SINK_MCP_PATH`,默认为 `/mcp`,和 `SINK_MCP_LOCAL_NAME`,默认为 `sink`。
- 由 `artisan-build/built-for-cloud` 管理的应用身份验证密钥和承载令牌。
不要为 Sink 应用启用云托管邮件集成。Sink 就是收件箱。
## 添加源应用
在 Sink 服务器上,使用 `artisan-build/built-for-cloud` 命令本地颁发源应用令牌:
```shell
php artisan token:create <label>
```
该命令打印一次明文令牌,并仅在其哈希存储在 `api_tokens` 中。 使用 `token:rotate`、`token:revoke`、`token:list` 和 `token:usage` 旋转、撤销、列出和检查使用情况。
在源 Laravel 应用中:
```shell
composer require artisan-build/sink-client
php artisan sink:install --url=<SINK_URL> --token=<token>
```
然后显式选择:
```dotenv
MAIL_MAILER=sink
SINK_URL=<SINK_URL>
SINK_TOKEN=<token>
```
仅当选择 `MAIL_MAILER=sink` 时,安装软件包不会更改您的邮件发送者。 Sink 仅在捕获邮件时。生产熔断器拒绝在 `production` 中构造传输,除非 `SINK_ALLOW_PRODUCTION=true`,否则防止 Sink 静默地吞噬生产邮件。
> **在源应用中使用编码代理?** `sink-client` 提供了 `configuring-sink-client` 技能,位于安装后 `vendor/artisan-build/sink-client/skills/configuring-sink-client/`。 指向您的代理并要求它配置 Sink 客户端。 它涵盖了 `composer require`、`sink:install`、设置 `MAIL_MAILER=sink`、生产熔断器、由 Sink 操作员创建令牌、验证测试发送到达、以及故障排除。
## 连接编码代理(MCP)
HTTP MCP 服务器在 `SINK_MCP_PATH` 处注册,默认为 `/mcp`,并需要 `Authorization: Bearer <token>`。 没有解析的承载令牌的请求会以 `401` 封闭失败。 对于本地 Claude Code 使用,同一个服务器在 `SINK_MCP_LOCAL_NAME` 下注册为 stdio MCP 服务器,默认为 `sink`。
Sink 的 MCP 服务器是无正文的:没有工具返回渲染或原始正文内容。 代理获取信封元数据、收件人、头部、链接、附件元数据、计数和正文布尔断言。
MCP 工具:
- `list_apps` — 已发送的源应用,包含计数和最近一次查看。
- `list_recent` — 最近的消息元数据。无正文。
- `count_messages` — 按应用、主题、收件人、时间窗口和流匹配消息计数。
- `recipients` — 匹配筛选器的收件人地址。
- `assert_count` — 传递/失败计数断言,带有实际值与预期值的比较。
- `stats` — 按主题、应用或收件人域分组的计数。
- `message_detail` — 一个消息的完整元数据,包括头部、链接、附件元数据、大小和时间戳。无正文。
- `links` — 一个消息的规范提取 URL。
- `body_matches` — 仅返回布尔值和模式的匹配计数。绝不返回匹配文本。
- `purge` — 唯一改变的工具;删除匹配明确范围的消息,拒绝无范围删除。
## 升级
先升级 Sink 服务器,运行其迁移,然后更新源应用客户端。 客户端可以运行:
```shell
php artisan sink:update
```
该命令从 `SINK_URL` 推导出 `/capabilities`,当存在时去除结尾的 `/ingest`,并报告客户端的信封主版本是否在服务器支持的范围内。 如果源应用发送的信封比服务器理解的新版本,`/ingest` 将返回 4xx 升级消息,而不是接受它。
---
## 超出范围(v1 非目标)
- **没有 SMTP 服务器。** 仅通过 `sink` 传输进行 HTTP 摄入。
- **没有捕获和转发/BCC 存档。** 仅捕获;不投递任何内容。
- **v1 中没有每个运行的流。** 该字段保留并可为空,但 v1 按应用和时间窗口范围。
- **没有代理正文访问。** 仅 MCP 断言工具;人类在 UI 中查看正文。
- **没有开放注册/所有权移交 UI。** 管理员邀请用户;移交是操作员任务。
- **没有交付能力或垃圾邮件评分。** 那是 Mailtrap 的领域。
---
## 许可证
Sink 是根据 [MIT 许可证](LICENSE) 许可的开源软件。