Repository navigation
plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现 #5087
Description
Activity
- addedenhancementNew feature or requestNew feature or requestpriority:p1High: required for production / M2High: required for production / M2
on Aug 4, 2026 - changed the title
[-]plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现,国内部署无可用发信通道[/-][+]plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现[/+]on Aug 4, 2026 认领:PM 循环第 1 轮
会话:session_017MCKJaEomEqg4tvz4SzdNd
分支:claude/issue-5087-smtp-transport
Worktree:objectstack-issue-5087
域:domain:services(落地包packages/plugins/plugin-email,按「域 = 修复落地的包」定级,不按标题词汇)
文件面:packages/plugins/plugin-email/**、packages/services/service-settings/src/manifests/mail.manifest.ts、packages/cli/src/commands/serve.ts、content/docs/deployment/environment-variables.mdx、.changeset/(越界即停,报告说明)分诊:前提核验(对
origin/main@ 30ff55b)issue 描述逐条属实,无过时前提:
git grep "SmtpTransport" origin/main -- packages/零命中;反查邻近词ResendTransport/PostmarkTransport命中正常,零命中成立。@objectstack/plugin-mail-smtp全仓只在注释与文档里出现(email-plugin.ts:478/499、content/docs/deployment/environment-variables.mdx:127/134),没有任何包,该指引确实无法落地。mail.manifest.ts的provider默认值就是'smtp',smtp_host 还是required: true;applyMailSettings对smtp走的是 no-op 分支;mailTestActionHandler一路返回ok: true+ 「Wire @objectstack/plugin-mail for actual delivery」——表单校验通过、测试按钮报成功、邮件不发,declared-but-not-delivered 成立。- 这几个文件今日无合并churn(最近一次
packages/plugins/plugin-email相关合并是 feat(spec,runtime,hono)!: 入站 rateLimit 从零建 seam —— 授权预算真正产生 429 (#4910, #4937) #5006),不存在同日改写。
顺带核实、不构成额外工作:
smtp_password虽未写encrypted: true,但SettingsService的注册逻辑是if (spec.encrypted || spec.type === 'password'),type: 'password'已隐式入encryptedKeys,静态存储侧无缺口——不要为此改 manifest。决策:第 4 条(nodemailer vs 自研最小 SMTP 客户端)取 nodemailer
这一条不上升给维护者 —— 它不改变产品语义,也不改变公共契约形状(两种做法都实现同一个既有的
IEmailTransport),且仓库既有规范已经给出答案。这是 PM 的决定,维护者拥有否决窗口而非审批门。轴一 · 项目长远合理性。 「仓库零依赖偏好」经核验不是仓库规范:
plugin-auth直接依赖better-auth/jose/@noble/hashes,driver-sql依赖knex并把pg/mysql2/tedious列 peer,plugin-hono-server依赖hono。Resend/Postmark 之所以是纯 fetch,是因为那两家本身就是 HTTP API,不是因为存在禁依赖的规定。方向上,ADR-0012 明确记载「M1 shipssmtpandsendgridtransports in core,SMTP is non-negotiable for on-prem/air-gapped(usingnodemailer)」;ADR-0030 取代它的是分层与收件箱漂移部分,对 transport 只字未改,故该方向未被推翻。反过来,自研 SMTP 要正确覆盖 STARTTLS 协商、AUTH PLAIN/LOGIN/CRAM-MD5、MIME multipart、quoted-printable/base64、RFC 2047 中文主题头编码、dot-stuffing、超时与连接复用——这正是 Prime Directive #5 所禁的临时补丁式选项。轴二 · 防 AI 犯错。 半成品 SMTP 客户端会把本 issue 报的缺陷下沉一层:它对多数服务器可用、只对 163/QQ 这类对 AUTH 与 TLS 行为挑剔的服务器静默失败,比现在这个「原地不动」更难被发现,也更容易被后来的 agent 用「放宽一点就通了」的方式改坏协议实现。成熟库 + 响亮错误,是让 AI 在结构上更难写错的那一侧。
配套约束(交派发词):
nodemailer作为plugin-email的正式 dependency,但在SmtpTransport内部惰性await import(),使非 SMTP 部署与非 Node 运行时不会加载node:net/node:tls;任何构建失败一律响亮——绝不允许回落成 LogTransport 后继续报成功,那等于原样保留本 issue 的缺陷。派发中(os-dev / opus),PR 落地后回帖。
Generated by Claude Code
- added a commit that references this issue
on Aug 4, 2026 实现完成,草稿 PR:#5106(分支
claude/issue-5087-smtp-transport,会话session_017MCKJaEomEqg4tvz4SzdNd)。按 PM 决策用
nodemailer(惰性await import),未碰packages/spec/objectql/lint/ objectui。三条入口(设置页热替换、CLIOS_EMAIL_SMTP_*、构造期providerOptions)共用同一份选项映射;构造期缺 host 直接抛错、设置期保留旧 transport 但打 error 日志,mail/test改为真实投递并如实回报 SMTP 服务器的报错——原来那句「Configuration looks valid … Wire @objectstack/plugin-mail for actual delivery」已从代码中消失(内置 stub 改为ok: false并说明未发送)。新增 33 个用例(plugin-email 77 → 110),含一个用真 nodemailer + 进程内假 SMTP 服务器跑完整 EHLO/AUTH/DATA 的 wire 测试,验证中文主题 RFC 2047 编码与中文 HTML 正文的传输编码可还原,不出外网。PR 在
4821d3eb3上 CI 全绿(TypeScript Type Check / ESLint / Test Core ×3 / Dogfood ×3 / Dogfood Verify CLI / Build Core / Build Docs / Temporal Conformance / Validate Package Dependencies(含 OSV 扫 nodemailer)/ Check Changeset 均 success)。顺带发现,已另行立单:#5104(本 issue 的 sub-issue ——
packages/spec的EmailProviderSchema仍缺'smtp',TSDoc 还写着 SMTP 不随 plugin-email 发布,落地后为假;因禁止碰 spec 而拆出)、#5105(finding—— config-resolution 文档示例给出并不存在的 email provider)。SendGrid / SES 的同形缺口已在 #5094,本 PR 不涉及。
Generated by Claude Code
Generated by Claude Code
复核结论:ACCEPT —— PR #5106,转 ready 并入合并队列。
判据取自 GitHub,不取报告自述:最终 head
4821d3eb上 8 个工作流全部 success(CI、Lint & Type Check、Validate Dependencies、Spec Liveness Check、Duplicate Fix Guard、Console Pin Freshness、Docs Drift Check、PR Automation)。入队前另核两项:本 PR 未碰.gitattributes里merge=os-regen的七条生成物路径中的任何一条(改的content/docs/deployment/environment-variables.mdx是手写页,不在content/docs/references/**下),因此不存在那个「exit 0、零冲突标记却静默吞掉一侧改动」的风险;plugin-email/serve.ts/mail.manifest.ts在飞期间 main 上无他人改动。落地了什么
三条入口共用同一份
smtpOptionsFromMailSettings映射(设置页热替换 / CLIOS_EMAIL_SMTP_*/ 构造期providerOptions),不会各自漂出不同的「Use TLS」语义。TLS 一个开关、连接行为由端口推导:465 隐式 TLS,其余端口强制 STARTTLS —— 服务器不肯升级就让这封信失败,而不是把凭据明文发出去。nodemailer是正式 dependency 但首次 send 才惰性 import,不选 SMTP 的部署与非 Node 运行时不加载node:net/node:tls;OSV 扫描确认 9.0.3 零传递依赖。本 issue 的核心缺陷——「说谎的成功提示」——已从代码里彻底消失:原来那句
ok: true+「Configuration looks valid … Wire @objectstack/plugin-mail for actual delivery」被换成由 plugin-email 注册的真实投递 handler(与storage/test同套路),用屏幕上的配置新建一次性 transport 去发,如实回传 SMTP 服务器原话(535 …),测试信同样落sys_email,用完即关。dev 还多堵了两个我没要求的洞:没挂 email 插件时的内置兜底改成ok: false并说明「没有发出任何邮件」;LogTransport 状态下按测试按钮也不再报成功。响亮性分两路处理,我认可这个判断:构造/CLI 路径缺 host 直接抛错让启动失败(不再退回 LogTransport 那种「每封都成功、一封都没发出」);设置热替换路径不抛错(一次保存不该把跑着的服务器打挂),保留旧 transport 但打 error 级日志并同时给出后果与修复。并且用 settings 的
source区分出厂默认态与运维显式选择,避免把 error 训练成可跳过的噪音。dev 自己发现并修掉的一个真实缺陷
首版有个误报:通过
OS_EMAIL_SMTP_*在启动期配好 SMTP、而设置页从未填过 host 时,applyMailSettings会喊「没配 host,邮件没在投递」——但邮件其实正在正常投递。e42a568c加了this.service.options.transport instanceof SmtpTransport的判断改走 info,并补了针对这一部署形态的用例。这正是本 issue 要消灭的那类假信号的镜像版本(这次是假警报而非假成功),自查出来并修掉是好事。派生工作(均已归位,不在本 PR)
- spec: EmailProviderSchema 缺 'smtp',TSDoc 仍称 SMTP 不随 plugin-email 发布(#5087 落地后为假) #5104(本单 sub-issue):spec 的
EmailProviderSchema仍缺'smtp'、TSDoc 仍写着「SMTP 不随 plugin-email 发布」——本 PR 落地后即为假话。这块残留是我的派发约束造成的(为避开 E1(#5040 执行器契约面):IMetadataService.matchEndpoint?/generateOpenApi?+IHttpServer.setFallbackHandler?可选契约方法与契约测试 #5080/未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 在 spec 内撞车而禁止碰 spec),已排队,待本 PR 合入且 spec 车道空出后派发;运行时不受影响(CLI 直接读config.email,不过该 schema)。 - docs: config-resolution 示例给出并不存在的 email provider(console / mailhog)与 fromAddress 键 #5105:
config-resolution.mdx示例给出并不存在的 provider 与键,已晋级派发中。 - plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094:SendGrid / SES 同形缺口,
Blocked-by本单,合入即派。顺带一提,本 PR 之后那条路径已从静默不发变成mail/test报错 + error 日志。
Generated by Claude Code
- spec: EmailProviderSchema 缺 'smtp',TSDoc 仍称 SMTP 不随 plugin-email 发布(#5087 落地后为假) #5104(本单 sub-issue):spec 的
路过发现,记在这里而不另开单 —— 修复面正好落在本单(与 PR #5106)已经要改的文件里,单开会是 #5104 的近似孪生。
@objectstack/plugin-mail-smtp这个包从来不存在做 #5105(config-resolution 的 email 示例)核对 provider 取值时,顺带全仓搜了一遍。除 #5104 已经记录的 spec TSDoc(「SMTP 不随 plugin-email 发布,自带
IEmailTransport」)之外,还有另外两处给出了一个不同的补救说法 —— 让用户去装一个独立包:content/docs/deployment/environment-variables.mdx:127——OS_EMAIL_PROVIDER那一行:「Real SMTP delivery requires the separate@objectstack/plugin-mail-smtppackage.」- 同文件
:132-134的引述块重复一遍,并称该包「reads ...」 packages/plugins/plugin-email/src/email-plugin.ts:478与:499的注释:「delivery requires@objectstack/plugin-mail-smtp, which is not ...」/「wanting real SMTP install@objectstack/plugin-mail-smtp」
而
packages/plugins/下只有plugin-email,全仓git grep plugin-mail-smtp origin/main的命中全部是上面这四处自我引用 —— 没有该包的目录、没有 package.json、没有任何发布痕迹。也就是说照文档执行npm i @objectstack/plugin-mail-smtp今天直接 404。为什么挂在本单而不是 #5104
两点:
- 文件面属于本单。 spec: EmailProviderSchema 缺 'smtp',TSDoc 仍称 SMTP 不随 plugin-email 发布(#5087 落地后为假) #5104 被 PM 明确圈在
packages/spec+ 生成物email-config.mdx;上面四处都在它范围外,而email-plugin.ts正是本单要动的文件,environment-variables.mdx的OS_EMAIL_PROVIDER枚举行也会因本单新增smtp而必须改。 - 它与 spec: EmailProviderSchema 缺 'smtp',TSDoc 仍称 SMTP 不随 plugin-email 发布(#5087 落地后为假) #5104 不是同一个断言。 spec: EmailProviderSchema 缺 'smtp',TSDoc 仍称 SMTP 不随 plugin-email 发布(#5087 落地后为假) #5104 说的是「TSDoc 称 SMTP 不随包发布,plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现 #5087 落地后为假」;这里是另一条补救路径 —— 「去装那个独立包」—— 而它指向的包从未存在过,与 plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现 #5087 落不落地无关,今天就是错的。
建议
本单重写这些文案时,请连带删掉/改写这四处对
@objectstack/plugin-mail-smtp的指引,而不是只把「不支持 SMTP」改成「支持 SMTP」—— 后者容易把这个假包名原样抄进新文案里。落地后正确说法应是:SMTP 由@objectstack/plugin-email内置(nodemailer 惰性 import),无需额外安装包。若认为这超出本单验收范围,我可以按 Prime Directive #10 另立一单,请示下。
Generated by Claude Code
Generated by Claude Code
- added 4 commits that reference this issue
on Aug 4, 2026 - added a commit that references this issue
on Oct 7, 2026
问题
plugin-email目前只有 Resend / Postmark 两个真实投递 transport,没有 SMTP transport。但 Mail Delivery 设置页(packages/services/service-settings/src/manifests/mail.manifest.ts)把 SMTP 列为默认 provider,并提供 host / port / TLS / username / password 完整表单——用户填完保存后,transport 实际原地不动(仍是 LogTransport),邮件只写日志和sys_email表,不会真正发出。界面承诺与后端实现之间存在缺口。代码里的现状注释(
packages/plugins/plugin-email/src/email-plugin.ts→applyMailSettings):而
@objectstack/plugin-mail-smtp这个包在仓库中并不存在,该指引目前无法落地。影响
建议范围
plugin-email新增SmtpTransport(实现IEmailTransport),支持 host / port / secure(TLS) / user / password,与mail设置命名空间现有字段(smtp_host/smtp_port/smtp_secure/smtp_user/smtp_password)对齐。applyMailSettings中provider === 'smtp'分支从「no-op + 日志」改为真实构建/热替换 SMTP transport;mail/test动作(发送测试邮件)同步走通。serve装配路径(OS_EMAIL_PROVIDER=smtp+ 相应 env)打通。不在本 issue 范围
设置页还列出 SendGrid / Amazon SES 选项,同样后端无实现——属另一问题,按一事一议另行处理(SMTP 打通后,这两家也可直接走各自的 SMTP 端点作为过渡)。