Repository navigation
fix(plugin-email): saving mail provider "None (log only)" stops delivery instead of keeping the live SMTP transport - #25497
Conversation
…rt instead of keeping the live one Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
…ter an SMTP boot; changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
…s only Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 59e3609c4278eceda9102c6f7eaa1aa4d4a0c9b9 && git checkout 59e3609c4278eceda9102c6f7eaa1aa4d4a0c9b9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6308143731510c5883bd8af4d6fb92f9e6bd4671 fa561813a81f821f20c9e44fcb326dd9a09da475 && git checkout -B drift-repro 6308143731510c5883bd8af4d6fb92f9e6bd4671 && git merge --no-ff fa561813a81f821f20c9e44fcb326dd9a09da475
node scripts/docs-audit/affected-docs.mjs --json 6308143731510c5883bd8af4d6fb92f9e6bd4671
|
席位核对 · ACCEPT · 2026-10-23T16:50Z
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25450
Clause-②: no
Metadata-protocol: none
What was wrong
Setup → Email offers the provider
logas "None (log only — no real delivery)". Whenlogwas saved,applyMailSettings(packages/plugins/plugin-email/src/email-plugin.ts) returned without swapping the transport. It logged "transport unchanged — messages are logged and recorded in sys_email, never delivered". After a boot with SMTP, every message kept going out over SMTP. The live-status banner (landed with #25448) truthfully said SMTP was live. "Send test email" sent a real message and answered "Sent test email to …". The page label and the log line both said nothing was delivered.Measurement: was "transport unchanged" deliberate? No. Shape (A).
git log -S"transport unchanged"finds the line's origin infc9b204fe8("Add global scope + reactive settings client"). Theresmtpandlogshared ONE no-op branch, with the comment "No SMTP transport ships in core; settings-only edits become a no-op for transport". For both providers, nothing could be swapped in.41c3b48f1dshippedSmtpTransport(ADR-0012) and split the branch.smtpgot a real swap.logkept the no-op, and the docstring describing it says "provider = 'log'keeps the LogTransport". That line assumes the live transport is already the LogTransport. Neither commit, nor ADR-0012, names a boot-built transport (OS_EMAIL_SMTP_*/providerOptions) or a host-supplied one that a savedlogmust not replace.smtp-with-no-host branch. That is the out-of-the-box namespace, where the page has stated nothing. Env precedence over the page lives one layer up, in the settings env lock (OS_MAIL_*,SettingsService.get), not in keeping the transport.service-settings/src/manifests/mail.manifest.ts, thePROVIDER_OPTIONScomment) says oflog: "the label says so, the runtime honours it". After this PR, that is true.So nothing deliberate protects the old transport, and this PR takes (A). A saved
loginstalls theLogTransportand replaces whatever was live, the same way every other selected provider does. A host-supplied transport is not exempt. The settings page already replaces it for every other provider, so an exemption forlogalone would bring back the mismatch between the label and what the transport does.The change
applyMailSettings,provider === 'log': if the live transport is not already aLogTransport, install a newLogTransportand drop theliveSmtphandle. This matches the Resend/Postmark branch: the old SMTP pool is not closed under a possible in-flight send.the LogTransport now replaces the previous live transport (was: "Mail is delivered through SMTP (host:port)") — messages are logged and recorded in sys_email, never delivered.When nothing delivered before, it saysthe LogTransport stays live — ….live-transport-status.tsand the manifest/translations are unchanged. The banner and the label were already true statements; the transport was what disagreed with them.Why
Metadata-protocol: noneThis does change what a saved value DOES: a saved
lognow stops mail. Butmail.provideris a settings value that an administrator stores insys_settingthrough Setup. It is not metadata an author writes, and the change adds, narrows or re-means no key, value or type in the metadata protocol. The changeset carries the operator-facing consequence and the one-line remedy.Tests
New
packages/plugins/plugin-email/src/mail-log-provider.pin.test.ts. Each pin reads all four answers from one save: the live transport andisConfigured(); the page statement (registerLiveStatus); the re-apply's log line; and the "Send test email" answer, withnodemailer.sendMailnever called.loggives: LogTransport, banner "No mail leaves this machine", a line naming the replacedSMTP (smtp.saved.test:587), and Send testok: false/warning.OS_EMAIL_SMTP_*reaches the plugin asproviderOptions): the same.logstored and SMTP in the boot options boots onto the LogTransport.ok: false.Fixture re-judged:
email-plugin.queue-delivery.test.tsused a storedprovider: 'log'with a host transport, which relied on exactly the "transport unchanged" behaviour this PR removes. Its default namespace is now the out-of-the-box one:smtpat its manifest default, no host. That branch keeps the harness transport, as before.Runs (head
fa561813a8):pnpm --filter @objectstack/plugin-email testgives Test Files 38 passed (38), Tests 685 passed (685).pnpm --filter @objectstack/plugin-email typecheckgives exit 0 (tsc --noEmitpluscheck:test-typecheck: OK).Ablation (head
fa561813a8, throughscripts/ablation-replace.mjs): the anchorthis.service.setTransport(new LogTransport(ctx.logger));was replaced by a no-op. The tool reported "ok mutation landed: anchor 1 -> 0, blob 6e5d43794b5a -> 0a9522b85240". The pin went RED, with Tests 4 failed | 1 passed (5): all four SMTP/host legs failed and the CONTROL held. The restore reported "ok restored: blob == HEAD (6e5d43794b5a) andgit diff HEADis empty". The test imports./email-plugin.jsfromsrc, so nodistleg applies.Gates
node scripts/pm/dispatch-gates.mjs --commandsatfa561813a8derives 67 commands. All 67 ran locally and exited 0.--ran, with an exit code recorded for each, says: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero …)".check:engine-double-contractwas red on the first pass, because the new pin's fake engine declared anupdate(). The fake now answers reads only (persist: false), and the gate is green.Acceptance notes
provider === 'log'path tests the live service rather than the form's choice. A throwaway probe atfa561813a8(not committed) showedok: true, withsendMailcalled once. Reported to the seat as a separate finding.log. It is pre-existing, and nothing in this PR changes it.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy