Skip to content

feat(plugin-email): SmtpTransport —— 真实 SMTP 投递、设置热替换、不再说谎的 mail/test (#5087) - #5106

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-5087-smtp-transport
Aug 4, 2026
Merged

feat(plugin-email): SmtpTransport —— 真实 SMTP 投递、设置热替换、不再说谎的 mail/test (#5087)#5106
os-zhuang merged 4 commits into
mainfrom
claude/issue-5087-smtp-transport

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5087

做了什么

设置页 Mail Delivery 一直把 SMTP 当默认 provider、给了完整的 host / port / TLS / user / password 表单,而后端什么都没有:applyMailSettingsprovider === 'smtp' 是 no-op(「transport unchanged」),mail/test 无条件返回 ok: true +「Configuration looks valid … Wire @objectstack/plugin-mail for actual delivery」——一封没人发出的邮件的成功提示,还指向一个从来不存在的包。国内部署因此没有任何可用发信通道。

本 PR 按 PM 决策(nodemailer,不自研 SMTP 客户端)落地:

  • SmtpTransport(packages/plugins/plugin-email/src/transports/smtp.ts),实现既有的 IEmailTransport,未改 packages/specnodemailerplugin-email 的正式 dependency,但在首次 send 时才 await import('nodemailer'),所以不选 SMTP 的部署与非 Node 运行时不会加载 node:net / node:tls
  • TLS 只有一个开关,连接行为由端口推导(各家服务商就是这么写文档的):465 → 隐式 TLS(SMTPS);其它端口 → 强制 STARTTLS 升级(服务器不肯升级就让这封信失败,而不是把凭据明文发出去);secure: false → 明文连接,服务器提供 STARTTLS 才机会性升级。逃生舱 transportOptions 可直接透传 nodemailer 选项。
  • 三个入口共用同一个选项读取器,不会各自漂移出不同的「Use TLS」语义:
    • 设置页热替换(smtp_host / smtp_port / smtp_secure / smtp_user / smtp_password,保存即换,不重启);
    • CLI os serve:OS_EMAIL_PROVIDER=smtp + 新增 OS_EMAIL_SMTP_HOST / _PORT / _SECURE / _USER / _PASSWORD(Prime Directive [WIP] Create a new release version #9OS_{DOMAIN}_{FEATURE} 形状,叠在 config.email.options 之上);
    • 构造期:new EmailServicePlugin({ provider: 'smtp', providerOptions: { host, ... } })
  • 响亮,不静默(本 issue 的缺陷本身就是静默):
    • 构造/CLI 路径缺 host 直接抛错,启动失败——不再退回 LogTransport 让服务器「每封都发成功、一封都没发出去」;
    • 设置热替换路径不抛错(一次保存不该把服务器打挂):保留旧 transport,但 error 级日志,并且按 AGENTS.md 的要求同时给出后果(SMTP 邮件没有在投递)和修复(Settings → Mail → Host / OS_MAIL_SMTP_HOST);
    • 只有「provider 还停在 manifest 默认值 smtp、从没配过」这一出厂状态走 info,否则每次 dev 冷启动都打一条 error,反而把 error 训练成可以跳过的噪音。
  • mail/test 真的发信:plugin-email 注册的 handler(覆盖 service-settings 里的内置 stub,与 storage/test 同一套路)对 smtp 总是用屏幕上的那份配置新建一次性 transport 去发,如实回报成功或 SMTP 服务器自己的报错(535 … authentication failed),测试信同样落 sys_email;transport 用完即关。LogTransport 状态下不再报成功,而是 ok: false + warning 说明「只写了日志」。
  • 内置 stub 也不再说谎:没挂 email 插件时它只能校验表单,于是返回 ok: false + 明确说明「没有发出任何测试邮件」,并指向 email capability。
  • 文档:content/docs/deployment/environment-variables.mdx 里「Real SMTP delivery requires the separate @objectstack/plugin-mail-smtp package」删除,补上新的 OS_EMAIL_SMTP_* 表格与设置页 / OS_MAIL_* 两条通道的关系;email-plugin.ts 同源注释一并改。未碰 content/docs/releases/
  • changeset:.changeset/smtp-transport-plugin-email.md(用户可见新能力)。

测试

新增 32 个用例,pnpm --filter @objectstack/plugin-email test 由 77 → 109 全绿:

  • transports/smtp.test.ts(mock createTransport):缺 host / 非法端口拒绝构造;首次 send 之前不加载 nodemailer;587 默认 + requireTLS、465 隐式 TLS、secure:false、有 user 才带 auth、transportOptions 覆盖;字段映射;535 认证失败原样抛出;构造失败不被缓存(改完设置下一次 send 就能好);describe() 不含密码;makeTransport('smtp') 缺 host 抛错。
  • transports/smtp.wire.test.ts(真 nodemailer + 进程内假 SMTP 服务器,不出网):完整跑通 EHLO / AUTH / MAIL / RCPT / DATA;中文主题按 RFC 2047 编码且能还原、中文 HTML 正文经传输编码后可还原、charset=utf-8;AUTH 被拒(535)时 send 抛错且服务器一条 DATA 都没收到。
  • email-plugin.mail-settings.test.ts:设置里的 SMTP 真的被换上(instanceof SmtpTransport + 连接参数)、再保存一次能再换;换上后发信走 nodemailer 且 sys_email 仍然记录(queued → sent + message_id,中文主题/正文原样落库);缺 host 时保留旧 transport + error 日志(出厂默认态则不吵);构造期 provider:'smtp' 无 host 抛错;mail/test 的四条路径(真发/535 失败/缺 host/LogTransport 不报成功,并断言旧文案已消失)。
  • packages/cli/src/commands/serve-email-capability.test.ts:新抽出的 resolveEmailCapabilityArg(与 resolveStorageCapabilityArg 同型)——OS_EMAIL_SMTP_* 组装、env 覆盖 config.email.optionssmtp 不再落进「缺 apiKey → 退回 log」那条分支、缺 host 抛错;resend/postmark 旧行为保持不变。

跑过:pnpm --filter @objectstack/plugin-email test(109 passed)、typecheck 干净;pnpm --filter @objectstack/service-settings test(197 passed);pnpm --filter @objectstack/cli test(69 files / 609 passed)、typecheck 干净;eslint 干净;check:doc-authoring / check:published-files / check:durability-log-level / check:startup-registry-verdict / check:adr-anchors 全绿。已 merge 最新 origin/main(#5097#5046)后重建并复跑受影响包。

顺带发现(未在本 PR 修)

🤖 Generated with Claude Code

https://claude.ai/code/session_017MCKJaEomEqg4tvz4SzdNd


Generated by Claude Code

claude added 2 commits August 4, 2026 04:39
… hot-swap, honest mail/test (#5087)

The Mail Delivery settings page defaults to SMTP and offers a full
host/port/TLS/user/password form, while nothing behind it delivered:
`applyMailSettings` treated `provider: 'smtp'` as a no-op and `mail/test`
answered `ok: true, "Configuration looks valid … Wire @objectstack/plugin-mail
for actual delivery"` — a success toast for a message nobody sent, naming a
package that has never existed. Deployments in China had no working channel at
all (Resend/Postmark are overseas SaaS).

- `SmtpTransport` (ADR-0012: SMTP in core via nodemailer), with nodemailer
  imported lazily on first send so non-SMTP deployments and non-Node runtimes
  never load node:net / node:tls.
- One TLS toggle, wire behaviour derived from the port: implicit TLS on 465,
  REQUIRED STARTTLS elsewhere (a server that will not upgrade fails the send
  instead of leaking credentials in the clear).
- Three doors share one options reader: settings hot-swap, `os serve`
  (OS_EMAIL_SMTP_HOST/_PORT/_SECURE/_USER/_PASSWORD, layered over
  config.email.options), and constructor providerOptions.
- Loud failure everywhere: the construction path throws (boot fails) rather
  than degrading to a LogTransport that reports success; the settings path
  keeps the previous transport but logs at error with consequence + fix; and
  `mail/test` now performs a REAL delivery and reports the SMTP server's own
  error text. The built-in fallback mail/test handler answers ok:false and says
  plainly that nothing was sent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MCKJaEomEqg4tvz4SzdNd
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 4:47am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/xl labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/plugin-email, @objectstack/service-settings.

27 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/flows.mdx (via @objectstack/plugin-email)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/plugin-email)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services/service-settings)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/services/service-settings)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services/service-settings)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/plugin-email, @objectstack/service-settings)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/plugin-email)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/service-settings)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)
  • content/docs/releases/v9.mdx (via @objectstack/service-settings)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits August 4, 2026 04:45
…y configured one (#5087)

The settings page legitimately carries no host in the deployment shape this
feature enables — SMTP configured through OS_EMAIL_SMTP_* at boot, Settings →
Mail never opened. Mail IS being delivered there, so the "no host configured"
error was a false alarm on every start. Report it only when nothing can
actually deliver; escalating a healthy boot is how error logs become skimmable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MCKJaEomEqg4tvz4SzdNd
@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 05:03
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 41c3b48 Aug 4, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5087-smtp-transport branch August 4, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-email: 实现 SMTP transport —— 设置页可选 SMTP 但后端无实现

2 participants