Mail Gateway - 企业邮件派发网关
业务系统接入邮件能力时,最容易从“调一个 SMTP”开始,最后却在每个服务里重复处理凭据、模板、限流、切换渠道和失败排查。mail-gateway 把这些横切问题收敛成一套统一入口:调用方只描述收件人、主题、正文或模板,网关负责选择平台、执行配额、调用 provider,并留下可查询的投递结果。
它不是一个异步营销邮件平台,而是一套同步邮件基础设施网关。当前重点是让内部系统通过稳定的 HTTP/gRPC 契约调用多个发信渠道,并把租户边界、凭据保护、资源上限和运维观测放进主链路。
两种协议,共用一套发送能力
HTTP API 默认监听 8900,提供单封发送、模板发送、批量发送、状态查询和重发;gRPC 默认监听 9090,protobuf wire contract 覆盖相同业务能力以及配置、模板、黑名单和统计管理。两条入口最终都调用同一个 EmailService,不会形成两套平台选择和投递语义。
服务调用方以 system_clients 为安全与配额边界。Token 模式使用 X-System-ID + X-API-Token;Signature 模式对 method、实际 path、raw query、RFC3339 timestamp 和原始 body 做 HMAC-SHA256,服务端拒绝超过正负五分钟的时间戳并使用常量时间比较。gRPC 把同一身份放进 metadata,签名内容改为 full method 与 deterministic protobuf bytes。
认证中间件只把已启用的 system client 放入上下文。状态查询和重发都额外带当前 system_id 过滤,普通 client 不能读取或重发其他系统的邮件。配置、Client、平台和模板写操作需要 admin 角色或 admin_system_ids 白名单;/metrics 也不再匿名暴露。
一封邮件如何选择发送渠道
请求首先经过全局 body 上限和业务字段校验。默认约束包括单批最多 100 封、单封最多 100 个收件人、10 个附件、单附件解码后 5 MiB、附件总计 10 MiB;批量任务使用配置化 worker pool,默认最多并发 10 个,不会为每封邮件无限创建 goroutine。
模板邮件先检查 system_template_mappings,只有分配给当前 system 的模板才能渲染主题和正文。普通邮件与模板邮件随后进入同一条候选平台链路:查询 system_email_platforms 和 email_platforms,要求映射与平台都处于启用状态,再把平台级 JSONB 配置和 system 的 config_override 合并、解密并构造运行时配置。
候选先按映射优先级、平台优先级和映射 ID 排序,进程内 cursor 会轮换起点,避免所有请求长期压在同一个渠道。每次尝试前,Redis Lua 脚本原子初始化窗口并同时扣减 system 日额度与平台时间窗额度;默认故障策略是 fail_closed,也可以显式配置为 fail_open。
Provider 调用失败后,网关会有界退还已经扣除的配额,记录平台、耗时和错误指标,再尝试下一个候选。成功或候选耗尽后,结果写入 email_send_logs,小时与日统计按 system/platform 聚合。调用方拿 trace_id 查询状态,失败记录可以在同一租户边界内重发。
这里有两个需要明确的当前边界。第一,黑名单模块已经支持 system/global 规则维护和独立检查接口,但当前 SendEmail 主链路没有自动调用 IsBlacklisted,因此它还不是强制投递门禁。第二,发送过程仍在请求生命周期内同步完成;持久任务队列、provider 回执 Webhook、bounce 和 complaint 闭环仍是后续演进项。
Provider 只实现发送,不决定业务策略
发送器工厂按 provider type 注册 builder,当前运行时注册了 SMTP、SendGrid、Resend、Brevo 和 OneSignal。新增渠道只需要实现统一 EmailSender 接口并注册构造器,租户映射、限流、fallback、日志和统计仍由上层服务负责。
sender 缓存不只使用 platform_identifier,还包含运行时配置的 SHA-256 指纹。这样两个 system 即使复用同一个平台标识,只要 API key、SMTP 密码、sender address 或 override 不同,就不会命中同一个 sender;平台配置和映射变化还会主动失效缓存。
所有 provider 接收调用方 context.Context,单次发送默认有 30 秒 timeout。SMTP 使用 wneessen/go-mail:465 走 implicit TLS,其他端口要求 STARTTLS,TLS 最低 1.2 并默认验证证书。即使 TCP 已建立,请求取消也会关闭仍卡在 greeting 或写入阶段的连接。附件在占用 SMTP 连接前完成 Base64 与 MIME 校验。
服务凭据与浏览器会话分开
服务到服务的长期凭据不应该因为增加管理后台就进入浏览器存储。管理前端只在 POST /api/v1/auth/session 提交一次 token;验证通过后,后端在 Redis 创建 256 位随机 opaque session,并下发 HttpOnly、SameSite Cookie。前端只在内存保留 profile 和 CSRF token,启动时通过 session API 恢复状态,同时主动清理历史 Web Storage 凭据。
Cookie 认证的 POST、PUT、PATCH 和 DELETE 必须携带常量时间匹配的 X-CSRF-Token。登录尝试按来源 IP 与 system ID 在 Redis 计数,Redis 不可用时登录和 Cookie session 都 fail closed。只要请求带了任意 Header 认证标记,服务端就固定按 Header 模式验证,不会在残缺或错误 Header 后降级使用 Cookie。
数据库中的 system_clients.api_secret、平台 API key、SMTP password、system override 和标记为敏感的配置使用版本化 v2: AES-256-GCM 密文。读取端兼容历史 CFB 与明文迁移窗口,但新写入统一带认证标签,密文被篡改会解密失败。32 字节主密钥只能从环境或 Secret 系统注入,真实 config.yaml 不会被复制进运行镜像。
数据、管理与可观测性
PostgreSQL 保存 system client、平台定义、system-platform 映射、模板授权、投递日志、黑名单和小时/日统计;Redis 保存配额窗口、管理会话和登录防刷状态。管理前端使用 Vue 3、Pinia 和 Vite,覆盖仪表盘、邮件查询、模板、黑名单、平台与 Client 配置。
Prometheus 指标记录 HTTP 请求、provider 耗时、成功、失败和限流结果,抓取端必须携带 admin Header。服务设置 ReadHeader/Read/Write/Idle timeout、Header 和 body 上限,并支持 HTTP 与 gRPC 优雅停机。gRPC 可以直接启用服务端 TLS;若 TLS 已在服务网格或反向代理终止,则可关闭应用层 TLS。
平台数据已经从历史的 email_platform_configs 与 system_platform_mappings 收敛到 email_platforms 和 system_email_platforms。运行时和路由不再依赖旧表,但生产删除仍要求先观察旧路由指标、核对迁移映射、创建逻辑快照并准备回滚,应用不会在启动或部署时自动执行破坏性清理。
mail-gateway 这个项目解决的不是“能不能发出一封邮件”,而是多个业务系统如何共享渠道又不串用凭据,平台失败时如何切换,浪涌下如何守住配额,浏览器如何管理而不持有服务密钥,以及一次发送如何被查询和度量。它已经具备内部邮件基础设施的完整主链路,也清楚保留了异步队列、投递回执和生产演练这些下一阶段边界。