[{"data":1,"prerenderedAt":290},["ShallowReactive",2],{"work-\u002Fworks\u002Fmail-gateway":3,"work-surround-\u002Fworks\u002Fmail-gateway":281},{"id":4,"title":5,"body":6,"cover":260,"date":261,"demo":262,"description":263,"draft":264,"extension":265,"featured":266,"meta":267,"navigation":266,"path":268,"repo":262,"seo":269,"stack":270,"stem":279,"updated":262,"__hash__":280},"works\u002Fworks\u002Fmail-gateway.md","Mail Gateway - 企业邮件派发网关",{"type":7,"value":8,"toc":250},"minimark",[9,18,21,28,33,48,59,74,77,80,99,105,116,127,138,142,149,156,167,170,177,198,204,219,223,226,229,245],[10,11,12,13,17],"p",{},"业务系统接入邮件能力时，最容易从“调一个 SMTP”开始，最后却在每个服务里重复处理凭据、模板、限流、切换渠道和失败排查。",[14,15,16],"code",{},"mail-gateway"," 把这些横切问题收敛成一套统一入口：调用方只描述收件人、主题、正文或模板，网关负责选择平台、执行配额、调用 provider，并留下可查询的投递结果。",[10,19,20],{},"它不是一个异步营销邮件平台，而是一套同步邮件基础设施网关。当前重点是让内部系统通过稳定的 HTTP\u002FgRPC 契约调用多个发信渠道，并把租户边界、凭据保护、资源上限和运维观测放进主链路。",[10,22,23],{},[24,25],"img",{"alt":26,"src":27},"Mail Gateway 企业邮件网关总体架构","\u002Fimages\u002Fworks\u002Fmail-gateway\u002Fsystem-overview.svg",[29,30,32],"h2",{"id":31},"两种协议共用一套发送能力","两种协议，共用一套发送能力",[10,34,35,36,39,40,43,44,47],{},"HTTP API 默认监听 ",[14,37,38],{},"8900","，提供单封发送、模板发送、批量发送、状态查询和重发；gRPC 默认监听 ",[14,41,42],{},"9090","，protobuf wire contract 覆盖相同业务能力以及配置、模板、黑名单和统计管理。两条入口最终都调用同一个 ",[14,45,46],{},"EmailService","，不会形成两套平台选择和投递语义。",[10,49,50,51,54,55,58],{},"服务调用方以 ",[14,52,53],{},"system_clients"," 为安全与配额边界。Token 模式使用 ",[14,56,57],{},"X-System-ID + X-API-Token","；Signature 模式对 method、实际 path、raw query、RFC3339 timestamp 和原始 body 做 HMAC-SHA256，服务端拒绝超过正负五分钟的时间戳并使用常量时间比较。gRPC 把同一身份放进 metadata，签名内容改为 full method 与 deterministic protobuf bytes。",[10,60,61,62,65,66,69,70,73],{},"认证中间件只把已启用的 system client 放入上下文。状态查询和重发都额外带当前 ",[14,63,64],{},"system_id"," 过滤，普通 client 不能读取或重发其他系统的邮件。配置、Client、平台和模板写操作需要 admin 角色或 ",[14,67,68],{},"admin_system_ids"," 白名单；",[14,71,72],{},"\u002Fmetrics"," 也不再匿名暴露。",[29,75,76],{"id":76},"一封邮件如何选择发送渠道",[10,78,79],{},"请求首先经过全局 body 上限和业务字段校验。默认约束包括单批最多 100 封、单封最多 100 个收件人、10 个附件、单附件解码后 5 MiB、附件总计 10 MiB；批量任务使用配置化 worker pool，默认最多并发 10 个，不会为每封邮件无限创建 goroutine。",[10,81,82,83,86,87,90,91,94,95,98],{},"模板邮件先检查 ",[14,84,85],{},"system_template_mappings","，只有分配给当前 system 的模板才能渲染主题和正文。普通邮件与模板邮件随后进入同一条候选平台链路：查询 ",[14,88,89],{},"system_email_platforms"," 和 ",[14,92,93],{},"email_platforms","，要求映射与平台都处于启用状态，再把平台级 JSONB 配置和 system 的 ",[14,96,97],{},"config_override"," 合并、解密并构造运行时配置。",[10,100,101],{},[24,102],{"alt":103,"src":104},"Mail Gateway 邮件发送与失败回退流程","\u002Fimages\u002Fworks\u002Fmail-gateway\u002Fdelivery-failover-flow.svg",[10,106,107,108,111,112,115],{},"候选先按映射优先级、平台优先级和映射 ID 排序，进程内 cursor 会轮换起点，避免所有请求长期压在同一个渠道。每次尝试前，Redis Lua 脚本原子初始化窗口并同时扣减 system 日额度与平台时间窗额度；默认故障策略是 ",[14,109,110],{},"fail_closed","，也可以显式配置为 ",[14,113,114],{},"fail_open","。",[10,117,118,119,122,123,126],{},"Provider 调用失败后，网关会有界退还已经扣除的配额，记录平台、耗时和错误指标，再尝试下一个候选。成功或候选耗尽后，结果写入 ",[14,120,121],{},"email_send_logs","，小时与日统计按 system\u002Fplatform 聚合。调用方拿 ",[14,124,125],{},"trace_id"," 查询状态，失败记录可以在同一租户边界内重发。",[10,128,129,130,133,134,137],{},"这里有两个需要明确的当前边界。第一，黑名单模块已经支持 system\u002Fglobal 规则维护和独立检查接口，但当前 ",[14,131,132],{},"SendEmail"," 主链路没有自动调用 ",[14,135,136],{},"IsBlacklisted","，因此它还不是强制投递门禁。第二，发送过程仍在请求生命周期内同步完成；持久任务队列、provider 回执 Webhook、bounce 和 complaint 闭环仍是后续演进项。",[29,139,141],{"id":140},"provider-只实现发送不决定业务策略","Provider 只实现发送，不决定业务策略",[10,143,144,145,148],{},"发送器工厂按 provider type 注册 builder，当前运行时注册了 SMTP、SendGrid、Resend、Brevo 和 OneSignal。新增渠道只需要实现统一 ",[14,146,147],{},"EmailSender"," 接口并注册构造器，租户映射、限流、fallback、日志和统计仍由上层服务负责。",[10,150,151,152,155],{},"sender 缓存不只使用 ",[14,153,154],{},"platform_identifier","，还包含运行时配置的 SHA-256 指纹。这样两个 system 即使复用同一个平台标识，只要 API key、SMTP 密码、sender address 或 override 不同，就不会命中同一个 sender；平台配置和映射变化还会主动失效缓存。",[10,157,158,159,162,163,166],{},"所有 provider 接收调用方 ",[14,160,161],{},"context.Context","，单次发送默认有 30 秒 timeout。SMTP 使用 ",[14,164,165],{},"wneessen\u002Fgo-mail","：465 走 implicit TLS，其他端口要求 STARTTLS，TLS 最低 1.2 并默认验证证书。即使 TCP 已建立，请求取消也会关闭仍卡在 greeting 或写入阶段的连接。附件在占用 SMTP 连接前完成 Base64 与 MIME 校验。",[29,168,169],{"id":169},"服务凭据与浏览器会话分开",[10,171,172,173,176],{},"服务到服务的长期凭据不应该因为增加管理后台就进入浏览器存储。管理前端只在 ",[14,174,175],{},"POST \u002Fapi\u002Fv1\u002Fauth\u002Fsession"," 提交一次 token；验证通过后，后端在 Redis 创建 256 位随机 opaque session，并下发 HttpOnly、SameSite Cookie。前端只在内存保留 profile 和 CSRF token，启动时通过 session API 恢复状态，同时主动清理历史 Web Storage 凭据。",[10,178,179,180,183,184,183,187,90,190,193,194,197],{},"Cookie 认证的 ",[14,181,182],{},"POST","、",[14,185,186],{},"PUT",[14,188,189],{},"PATCH",[14,191,192],{},"DELETE"," 必须携带常量时间匹配的 ",[14,195,196],{},"X-CSRF-Token","。登录尝试按来源 IP 与 system ID 在 Redis 计数，Redis 不可用时登录和 Cookie session 都 fail closed。只要请求带了任意 Header 认证标记，服务端就固定按 Header 模式验证，不会在残缺或错误 Header 后降级使用 Cookie。",[10,199,200],{},[24,201],{"alt":202,"src":203},"Mail Gateway 认证、凭据与运维边界","\u002Fimages\u002Fworks\u002Fmail-gateway\u002Fsecurity-control-plane.svg",[10,205,206,207,210,211,214,215,218],{},"数据库中的 ",[14,208,209],{},"system_clients.api_secret","、平台 API key、SMTP password、system override 和标记为敏感的配置使用版本化 ",[14,212,213],{},"v2:"," AES-256-GCM 密文。读取端兼容历史 CFB 与明文迁移窗口，但新写入统一带认证标签，密文被篡改会解密失败。32 字节主密钥只能从环境或 Secret 系统注入，真实 ",[14,216,217],{},"config.yaml"," 不会被复制进运行镜像。",[29,220,222],{"id":221},"数据管理与可观测性","数据、管理与可观测性",[10,224,225],{},"PostgreSQL 保存 system client、平台定义、system-platform 映射、模板授权、投递日志、黑名单和小时\u002F日统计；Redis 保存配额窗口、管理会话和登录防刷状态。管理前端使用 Vue 3、Pinia 和 Vite，覆盖仪表盘、邮件查询、模板、黑名单、平台与 Client 配置。",[10,227,228],{},"Prometheus 指标记录 HTTP 请求、provider 耗时、成功、失败和限流结果，抓取端必须携带 admin Header。服务设置 ReadHeader\u002FRead\u002FWrite\u002FIdle timeout、Header 和 body 上限，并支持 HTTP 与 gRPC 优雅停机。gRPC 可以直接启用服务端 TLS；若 TLS 已在服务网格或反向代理终止，则可关闭应用层 TLS。",[10,230,231,232,235,236,239,240,90,242,244],{},"平台数据已经从历史的 ",[14,233,234],{},"email_platform_configs"," 与 ",[14,237,238],{},"system_platform_mappings"," 收敛到 ",[14,241,93],{},[14,243,89],{},"。运行时和路由不再依赖旧表，但生产删除仍要求先观察旧路由指标、核对迁移映射、创建逻辑快照并准备回滚，应用不会在启动或部署时自动执行破坏性清理。",[10,246,247,249],{},[14,248,16],{}," 这个项目解决的不是“能不能发出一封邮件”，而是多个业务系统如何共享渠道又不串用凭据，平台失败时如何切换，浪涌下如何守住配额，浏览器如何管理而不持有服务密钥，以及一次发送如何被查询和度量。它已经具备内部邮件基础设施的完整主链路，也清楚保留了异步队列、投递回执和生产演练这些下一阶段边界。",{"title":251,"searchDepth":252,"depth":252,"links":253},"",3,[254,256,257,258,259],{"id":31,"depth":255,"text":32},2,{"id":76,"depth":255,"text":76},{"id":140,"depth":255,"text":141},{"id":169,"depth":255,"text":169},{"id":221,"depth":255,"text":222},"\u002Fimages\u002Fworks\u002Fmail-gateway\u002Fproject-cover.svg","2025-11-21",null,"基于 Go 的多租户邮件聚合服务，通过 HTTP 与 gRPC 统一接入 SMTP、SendGrid、Resend、Brevo 和 OneSignal，并提供限流、失败回退、模板、统计与安全管理能力。",false,"md",true,{},"\u002Fworks\u002Fmail-gateway",{"title":5,"description":263},[271,272,273,274,275,276,277,278],"Go","Gin","gRPC","PostgreSQL","Redis","SMTP","Prometheus","Vue 3","works\u002Fmail-gateway","QB9TaHrewUKg0-onqqiA3Wjpw67ipF_HZkAL4u4EtzQ",[282,286],{"title":283,"path":284,"stem":285,"children":-1},"OmniCore - 企业业务能力中心","\u002Fworks\u002Fomnicore","works\u002Fomnicore",{"title":287,"path":288,"stem":289,"children":-1},"logstream — 终端日志追踪工具","\u002Fworks\u002Flogstream-cli","works\u002Flogstream-cli",1787046421831]