Posthorn:自托管项目的统一出站邮件层
Posthorn 是一个自托管的电子邮件网关,可将来自您的应用程序的出站邮件统一到 Postmark、Resend 和 AWS SES 等事务性提供商。
问题:自托管者的出站邮件一团糟
没有人想在 2026 年运行邮件服务器。自托管运营商使用 Postmark、Resend、Mailgun 或 AWS SES,因为它们便宜、能妥善处理投递能力,并且其他人会操心 SPF/DKIM/DMARC/退信/发件人声誉。
但是,您自托管的每个应用程序都必须独立与该服务集成。您的联系表单、Ghost 博客的管理员邮件、Gitea 的魔法链接、Mastodon 的通知、当有人点击链接时触发密码重置邮件的 Cloudflare Worker。每个都需要自己的 API 密钥副本、自己的集成代码、自己在重试和退信处理方面的怪癖。相同的出站关注点在整个技术栈中重复出现。
而在阻止出站 SMTP 的云主机上——DigitalOcean、AWS Lightsail、Linode、Vultr——纯 SMTP 的应用程序在没有变通方法的情况下根本无法工作。
引入 Posthorn
Posthorn 是桥梁。一个容器、一个配置、一组凭据。您的应用程序指向 Posthorn。Posthorn 与您的提供商通信。
它提供三种入口形态:
- HTTP 表单(联系表单、注册、警报 webhook)——蜜罐 + Origin/Referer + 速率限制 + 可选的 CSRF;模板化邮件;发送
- HTTP API 模式(Worker、cron、支付处理器、内部服务)——
Authorization: Bearer认证;JSON 主体;幂等重试;用于事务性发送的每请求to_override - SMTP 监听器(Ghost、Gitea、Mastodon、Matrix、NextCloud、Authentik,以及任何发出 SMTP 的应用程序)——AUTH PLAIN 或客户端证书;需要 STARTTLS;发件人 + 收件人白名单;解析 MIME;通过 HTTP API 传输转发
所有三个入口汇聚到一个 transport.Message 和一个出站提供商——从 Postmark、Resend、Mailgun、AWS SES 或出站 SMTP 中继中选择。
Posthorn 不是什么
为了避免您走弯路:
| 它做什么 | 请改用 |
|---|---|
| 不是邮件服务器——没有邮箱存储、没有 IMAP/JMAP、没有 DKIM 密钥管理、没有 MX 目标 | Stalwart、Mailcow、iRedMail |
| 不是自己的出站基础设施——Posthorn 通过您选择的提供商中继;它不运行自己的 SMTP 集群或管理 IP 声誉 | Postal、Hyvor Relay |
| 不是营销电子邮件平台——没有列表管理、没有细分、没有活动仪表板 | Listmonk |
| 不是网络邮件/邮箱 UI——没有用于阅读邮件的界面 | Roundcube、Snappymail(配合邮件服务器) |
楔子在于您的自托管应用程序与您已选择的事务性提供商之间的集成层。
快速入门(Docker)
# docker-compose.yml
services:
posthorn:
image: ghcr.io/craigmccaskill/posthorn:latest
restart: unless-stopped
volumes:
- ./posthorn.toml:/etc/posthorn/config.toml:ro
environment:
POSTMARK_API_KEY: ${POSTMARK_API_KEY}
ports:
- "127.0.0.1:8080:8080" # 绑定到回环;从您的前门进行反向代理
# posthorn.toml
[[endpoints]]
path = "/api/contact"
to = ["[email protected]"]
from = "联系表单 <[email protected]>"
honeypot = "_gotcha"
allowed_origins = ["https://example.com"]
required = ["name", "email", "message"]
subject = "来自 {{.name}} 的联系"
body = """
来自:{{.name}} <{{.email}}>
{{.message}}
"""
redirect_success = "/thank-you"
[endpoints.transport]
type = "postmark"
[endpoints.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
[endpoints.rate_limit]
count = 5
interval = "1m"
从您的前门(Caddy、nginx、Traefik)将 /api/contact 反向代理到 http://posthorn:8080。将表单的 action 指向 /api/contact。完成。
API 模式(服务器到服务器)
适用于 Worker、cron 作业、内部服务——任何使用 JSON 而非表单的:
[[endpoints]]
path = "/api/transactional"
to = ["[email protected]"]
from = "YourApp <[email protected]>"
auth = "api-key"
api_keys = ["${env.WORKER_KEY_PRIMARY}", "${env.WORKER_KEY_BACKUP}"]
required = ["subject_line", "message"]
subject = "{{.subject_line}}"
body = "{{.message}}"
[endpoints.transport]
type = "postmark"
[endpoints.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
curl -X POST https://posthorn.yourdomain.com/api/transactional \
-H "Authorization: Bearer $WORKER_KEY_PRIMARY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reset:user-123:$(date -u +%FT%H)" \
--data '{
"to_override": "[email protected]",
"subject_line": "重置您的密码",
"message": "点击此处:https://app.example.com/reset/abc"
}'
SMTP 监听器(Ghost / Gitea / Mastodon / Authentik)
适用于原生使用 SMTP 且无法重新配置为调用 HTTP API 的应用程序:
[smtp_listener]
listen = ":2525"
require_tls = true
tls_cert = "/etc/posthorn/cert.pem"
tls_key = "/etc/posthorn/key.pem"
auth_required = "smtp-auth"
allowed_senders = ["*@yourdomain.com"]
max_recipients_per_session = 10
max_message_size = "1MB"
[[smtp_listener.smtp_users]]
username = "ghost"
password = "${env.GHOST_SMTP_PASSWORD}"
[smtp_listener.transport]
type = "postmark"
[smtp_listener.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
将 Ghost(或任何应用程序的 SMTP 配置)指向 posthorn.yourdomain.com:2525,并使用上述用户名/密码。Posthorn 解析 MIME,构建 transport.Message,通过 Postmark 转发。
选择传输方式
| 传输方式 | 最适合 | 认证 | 主体 |
|---|---|---|---|
| Postmark | 事务性电子邮件,强大的投递能力默认值 | X-Postmark-Server-Token | JSON |
| Resend | 现代 HTTP API,开发者友好的仪表板 | Authorization: Bearer | JSON |
| Mailgun | 较大量的事务性邮件,美国和欧盟区域 | HTTP Basic | multipart/form-data |
| AWS SES | AWS 原生部署,批量时最便宜 | AWS SigV4(定制) | JSON |
| 出站 SMTP | 任何支持 STARTTLS 的中继(Mailtrap、您的 Postfix 智能主机等) | AUTH PLAIN | SMTP DATA |
切换提供商只需编辑 TOML——每个传输方式都实现相同的 Transport 接口。
生产环境检查清单
在将真实流量指向 Posthorn 之前:
- DNS——在您的发送域上设置 SPF、DKIM 和 DMARC 记录。没有这些,您的邮件会进入垃圾邮件。
- 反向代理——Posthorn 不终止 TLS。在 Caddy、nginx 或 Traefik 后面运行它。
allowed_origins(表单模式端点)——设置此项以将提交锁定到您的域。没有它,任何人都可以向您的端点发送 POST 请求。rate_limit——为每个端点设置一个严格的桶(对于公共联系表单,5/分钟是一个合理的默认值;API 模式按匹配的密钥进行速率限制)。trusted_proxies——如果在反向代理后面,列出其 CIDR(或使用cloudflare命名预设),以便速率限制器看到真实的客户端 IP。/healthz和/metrics——自动注册在同一个监听器上。将您的 Docker 健康检查或 Prometheus 抓取连接到这些。
v1.0 包含的内容
| 模块 | 详情 |
|---|---|
| 表单入口 | 表单编码 + multipart 主体;蜜罐、Origin/Referer 失败关闭、速率限制、可选的 CSRF 令牌 |
| API 模式 | auth = "api-key" 使用 Bearer 令牌(常量时间比较);JSON 内容类型;幂等键(24 小时,内存 LRU);每请求 to_override |
| 传输方式 | Postmark、Resend、Mailgun、AWS SES(定制 SigV4)、出站 SMTP 中继 |
| SMTP 监听器 | 使用 AUTH PLAIN / 客户端证书的 TCP 监听器、需要 STARTTLS、发件人 + 收件人白名单、大小限制、MIME → transport.Message |
| 运维 | /healthz、/metrics(Prometheus 展示)、干运行模式、IP 剥离、命名 trusted_proxies 预设(Cloudflare) |
| 故障处理 | 对临时/5xx 重试 1 次(1 秒),对 429 重试 1 次(5 秒),10 秒硬超时 |
| 日志记录 | 结构化 JSON;UUIDv4 提交 ID 和 SMTP 会话 ID;submission_sent 中的 transport_message_id |
| 部署 | 单个 Go 二进制文件,多架构 distroless Docker 镜像,位于 ghcr.io/craigmccaskill/posthorn |
整个模块中只有三个外部 Go 依赖项:TOML 解析器、UUID 库、LRU 缓存。每个传输方式都是定制的——传输代码中没有供应商 SDK。
路线图
- v2——平台成熟度。SQLite 提交日志、跨重启的重试队列、抑制列表(硬退信自动)、持久幂等性、通过 HMAC 签名 webhook 的生命周期事件回调、RFC 8058 一键退订、文件附件、HTML 主体、每个端点的多个输出(邮件 + webhook + 日志扇出)、多租户 SMTP 路由。
- v3——推测性。管理 UI、工作量证明垃圾邮件挑战、PGP 加密。取决于社区推动。
从源码构建
需要 Go 1.25+。
git clone https://github.com/craigmccaskill/posthorn
cd posthorn/core
go build -o /tmp/posthorn ./cmd/posthorn
/tmp/posthorn version
结论
Posthorn 解决了自托管运营商的一个真实痛点:出站邮件集成的碎片化。通过提供一个统一的网关,具有多种入口形态和传输后端,它极大地简化了您的电子邮件基础设施。一个容器、一个配置、一组凭据——就完成了。