Skip to content

feat(notification): support implicit-TLS SMTP email channels - #271

Open
excniesNIED wants to merge 14 commits into
astaxie:mainfrom
excniesNIED:feat/smtp-implicit-tls
Open

feat(notification): support implicit-TLS SMTP email channels#271
excniesNIED wants to merge 14 commits into
astaxie:mainfrom
excniesNIED:feat/smtp-implicit-tls

Conversation

@excniesNIED

@excniesNIED excniesNIED commented Aug 20, 2026

Copy link
Copy Markdown

Summary

Email notification channels previously could only use plaintext-plus-STARTTLS (typically port 587). Many SMTP relays expose SMTP exclusively over an implicit-TLS port such as 465, so sendEmail would open a plain TCP connection, the server would reset it waiting for a TLS ClientHello, and alert delivery failed. This change adds a smtp_encryption field to email notification channels that switches sendEmail to a direct TLS dial (implicit TLS) for such servers, exposes the field in the admin console, and documents it in all three administrator guides.

Related Issue

N/A

Changes

  • backend/internal/server/admin_notifications_http.go:
    • sendEmail opens an implicit-TLS connection when smtp_encryption is ssl, tls, smtps, or implicit; otherwise the previous STARTTLS path is unchanged.
    • smtp_encryption is the only opt-in switch; the generic encryption field is ignored so a free-form channel keeps its documented STARTTLS behavior.
    • Explicit smtp_encryption: "starttls" fails closed when the server does not advertise STARTTLS instead of downgrading to plaintext; legacy channels without the field keep the opportunistic path.
    • The implicit-TLS dial honours the caller context via tls.Dialer.DialContext.
  • backend/internal/server/smtp_direct_tls_test.go: in-process implicit-TLS SMTP delivery test, directSMTPTLSEnabled unit test, context-cancellation test, and a command-recording regression proving explicit STARTTLS fails closed before any MAIL FROM.
  • frontend/features/admin/resources/settings-config.tsx + payloads.tsx: smtp_encryption selector (starttls/ssl) with the field round-tripped on edit; legacy channels without the field are saved unset so their opportunistic semantics survive unrelated edits.
  • frontend/features/admin/i18n/notifications.tsx (+ en.tsx/ja.tsx split): EN/JA translations for the new field.
  • docs/administrator-guide.md, docs/zh-CN/administrator-guide.md, docs/ja/administrator-guide.md: document smtp_encryption with implicit TLS guidance on port 465.

Type of Change

  • New feature
  • Documentation

Verification

  • Backend: go vet ./...; SMTP tests (TestEmailDeliveryViaImplicitTLSSMTP, TestDirectSMTPTLSEnabled, TestDirectTLSSMTPDialHonorsContextCancellation, TestExplicitStartTLSFailsClosedWhenExtensionAbsent); full go test ./internal/server/ -count=1 — all passed.
  • Frontend: npm run lint, npm run typecheck (incl. source-line gate, 582 files), npm test (6 files / 11 tests), npm run build — all passed.
  • Gates: node tools/check-doc-translations.mjs --base origin/main --head HEAD, git diff --check — passed.

Compatibility, Security, and Operations

  • No environment variable changes; smtp_encryption is a runtime notification-channel field configured in the admin console, so .env.example/Compose/start.sh are untouched.
  • Fully backward compatible: channels without smtp_encryption keep the existing STARTTLS behavior, and legacy channels are never silently upgraded to the fail-closed mode by an unrelated edit.
  • Security: explicit starttls refuses to downgrade to plaintext (protects password-reset and alert bodies); implicit-TLS dials require TLS 1.2+ and honour caller cancellation.
  • SMTP credentials continue to be stored and masked as before; no credential handling changed.

Checklist

  • The PR title and body are written in English.
  • Tests were added or updated for behavior changes.
  • No credentials, local .env files, databases, backups, or runtime logs are included.
  • Environment variable changes are synchronized across examples, Compose, start.sh, and deployment documentation where applicable (none in this PR).
  • Shared user-facing behavior is documented consistently in English, Simplified Chinese, and Japanese where applicable.
  • data/model-catalog.yaml remains tracked and catalog changes were reviewed where applicable (none).
  • git diff --check passes.

Email notification channels gain an smtp_encryption field
(ssl|tls|smtps|implicit) that switches sendEmail to a direct TLS dial for mail
servers that only expose SMTP over implicit-TLS ports such as 465/994. Default
STARTTLS behavior on port 587 is unchanged.
Copilot AI balanced review requested due to automatic review settings August 20, 2026 09:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at 8a94c30. The direct-TLS dial path preserves the legacy STARTTLS behavior when smtp_encryption is absent, and the SMTP command flow is appropriate for implicit TLS.

[P1] Make the new implicit-TLS regression self-contained and runnable on the supported CI runners (backend/internal/server/smtp_direct_tls_test.go:79-83). The test writes the generated certificate to SSL_CERT_FILE and assumes Go's crypto/x509 will load that environment variable, but the exact test fails in the isolated macOS worktree with x509: certificate signed by unknown authority; the test therefore records a failed delivery rather than exercising SMTP. Pass a RootCAs pool explicitly to the production dial configuration through a test seam, or use a certificate trusted by the test process, and keep the end-to-end assertion green.

[P1] Required CI is missing. GitHub currently reports UNSTABLE with an empty status check rollup, so this head cannot be approved or considered merge-ready. Publish the required Backend/Frontend/Deployment/Repository gates and aggregate CI, then request re-review on the exact head.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up re-review of exact head 8a94c30d73a44f8468ed42cab9d1c9e20ce98650: all seven GitHub checks are now SUCCESS and the merge state is MERGEABLE/CLEAN. The direct implicit-TLS implementation and backward-compatible STARTTLS path remain sound.

The portability blocker remains in the new end-to-end regression: on the isolated macOS runner, TestEmailDeliveryViaImplicitTLSSMTP still fails with x509: certificate signed by unknown authority because the test relies on SSL_CERT_FILE rather than explicitly wiring the generated CA into the TLS configuration. go vet ./... passes and GitHub Linux CI is green, but the test is not self-contained across supported development environments. This remains comment-only; make the test trust setup explicit and request another review.

The end-to-end implicit-TLS test previously trusted its throwaway
certificate through the SSL_CERT_FILE env var, which Go's crypto/x509 does
not load in every environment (it fails in isolated macOS worktrees). Add a
Server.smtpRootCAs test seam that sendEmail wires into tls.Config.RootCAs on
the direct-TLS dial, and have the test inject the fake server's certificate
through that seam instead. The backward-compatible STARTTLS path is unchanged.
Copilot AI review requested due to automatic review settings August 20, 2026 10:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Fixed in 4353a80: the implicit-TLS regression no longer relies on SSL_CERT_FILE. The generated fixture CA is now wired into the production dial through a Server.smtpRootCAs test seam — sendEmail passes it to tls.Config.RootCAs on the direct-TLS path, and the test injects the fake server's certificate through that seam instead of the environment variable. The test is self-contained and runs identically on macOS and Linux CI.

Verified: go test ./internal/server/ -count=1 (includes the updated end-to-end test), go vet ./..., git diff --check, and the doc translation existence gate. Requesting re-review on the exact head.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at exact head 4353a80. The RootCAs test seam fixes the macOS portability blocker, and the full backend suite, go vet, repository tests and gates, git diff checks, and all GitHub checks pass.

[P1] The feature still cannot be configured through the admin console promised by the PR. notificationChannelConfig has no smtp_encryption field, notificationChannelPayload does not send it, and the edit form does not round-trip it. An operator using the documented UI therefore cannot enable implicit TLS; only a direct API or database edit can. Please add the email-only encryption selector, preserve it through create/edit payloads, add the required English/Japanese translations, and cover the form/payload round trip.

Also replace port 994 in all three administrator guides: 994 is assigned to IRC over TLS, while implicit-TLS SMTP submission uses port 465.

Please request another review after the console path and documentation are corrected.

The email notification channel form now has an SMTP encryption selector
(starttls default, or ssl for implicit TLS) that is preserved through the
create/update payload and round-tripped when editing, so operators can enable
implicit-TLS delivery from the admin console without direct API or database
edits. The three-language administrator guide documents the selector.
Copilot AI review requested due to automatic review settings August 21, 2026 02:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Addressed in 0a53238: the admin console's email notification channel form now exposes an SMTP encryption selector (starttls default, ssl for implicit TLS), preserved through the create/update payload and round-tripped on edit via toForm. The three-language administrator guide documents the selector. No backend change was needed — sendEmail already reads smtp_encryption from the channel fields.

Verified: frontend lint, tsc --noEmit, npm run test (domain + vitest), git diff --check, and the doc translation existence gate. Requesting re-review on the exact head.

Comment thread backend/internal/server/admin_notifications_http.go Outdated
Comment thread frontend/features/admin/resources/settings-config.tsx Outdated
tls.DialWithDialer only honours the net.Dialer timeout, so a cancelled
alert or password-reset request could keep blocking on the implicit-TLS
path until the 5s dial timeout. Switch to tls.Dialer.DialContext so the
smtp_encryption=ssl path preserves the same cancellation semantics as the
STARTTLS branch, and lock it in with a context-cancellation test.
The generic field UI renders label and help through tx(), so English and
Japanese admin console users would have seen the new SMTP encryption copy
in Chinese. Add matching entries to both translation catalogs.
CompleteImageJob commits the completed status before observeGatewayCall
publishes the counters, so polling only the job status raced the metrics
assertion and failed intermittently under CI load (got 0). Also wait for
the overhead series — the last one ObserveGatewayCall writes — so the
assertions run after publication.
Copilot AI review requested due to automatic review settings August 21, 2026 09:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Both P1s and the CI failure are fixed at 4477770:

  1. smtp_encryption direct-TLS dial now goes through tls.Dialer{NetDialer: &dialer, ...}.DialContext(ctx, "tcp", addr), so a cancelled alert or password-reset request fails fast exactly like the STARTTLS branch. TestDirectTLSSMTPDialHonorsContextCancellation locks it in.
  2. SMTP 加密 and the STARTTLS/SSL help line have matching en and ja catalog entries, so the field renders localized instead of falling back to Chinese.
  3. The Backend check failure was TestMetricsImageJobAttributesAttempts: CompleteImageJob commits the completed status before observeGatewayCall publishes the counters, so polling only the job status raced the assertions. The test now also waits for the overhead series, the last one ObserveGatewayCall writes.

Verified: go vet ./internal/server/, SMTP + metrics tests (-count=10, plus -race), full go test ./internal/server/ -count=1 suite runs, frontend lint / tsc --noEmit / tests, git diff --check. Requesting re-review on the exact head.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at exact head 4477770d7f63dd0b00e149913d5493e7d32f13f9. The context-aware implicit-TLS dial and the English/Japanese catalog additions fix both inline findings, and I resolved those two threads. Focused SMTP and metrics tests, including the race run, go vet, frontend lint/tests, translation and environment gates, and diff checks pass.

Approval remains blocked:

[P1] Required CI is missing. GitHub reports no checks for this head.

[P1] npm run typecheck and the repository source-line gate fail because frontend/features/admin/i18n/en.tsx and ja.tsx are each 1502 lines, above the 1500-line limit. Move the new keys into an appropriate split catalog so the required gate passes.

The previously requested port 994 correction also remains open: the current UI help and all three administrator guides still advertise it for implicit SMTP. Please keep implicit SMTP guidance on port 465.

This remains comment-only.

Adding the two keys pushed en.tsx and ja.tsx past the 1500-line source
gate. Move them into a notifications catalog and merge it in
translations.tsx, following the existing per-domain split pattern.
The UI help text, all three administrator guides, and a test comment
advertised 994 alongside 465 for implicit-TLS SMTP. Standardize on 465,
the registered smtps port, so the guidance names one canonical port.
Copilot AI review requested due to automatic review settings August 21, 2026 10:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Both blockers fixed at 04deae2:

  1. Split catalogs: the two keys moved out of en.tsx/ja.tsx into a new frontend/features/admin/i18n/notifications.tsx, merged in translations.tsx like the other per-domain catalogs. Both base files are back to 1,499 lines and npm run typecheck (including the source-line gate) passes.
  2. Port 994 removed everywhere: UI help now reads "STARTTLS(端口 587,默认)或隐式 TLS/SSL(端口 465)。" with matching en/ja entries, and all three administrator guides plus a test comment say 465 only.

On CI: run 32468692789 for 4477770 is sitting in action_required — fork workflows need your approval on the Actions page before they start, which is why GitHub reports no checks. The push to 04deae2 queues a fresh run that also needs one approval.

Verified: npm run typecheck / lint / tests, doc-translation gate (--base origin/main), go vet, SMTP + metrics tests, git diff --check.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at exact head 04deae237a9907cb6ca377b8e06a36dd7a0496d2. The split notification catalog fixes the source-line gate, and the UI help, synchronized administrator guides, and test comment now consistently use port 465. Standards and spec reviews are otherwise clean. Full local backend tests and go vet, repeated focused race tests, frontend lint/typecheck/tests/build/E2E, all repository tests and gates, the explicit base/head documentation translation check, and git diff --check pass.

Approval remains blocked:

[P1] GitHub reports this PR as CONFLICTING/DIRTY against main. Resolve the merge conflict, preserving both the catalogs added on main and this PR’s notificationTranslations merge, then request review on the resulting head.

[P1] The exact current head has no CI checks. The most recent listed workflow is action_required on the previous 4477770 head, and there is no required-check result for 04deae2. The required checks must run and pass on the post-conflict head before approval.

[P2] The PR Summary still says implicit-TLS SMTP uses “465 or 994”, even though the code and guides were corrected. Remove the remaining 994 reference from the PR description so it matches the corrected guidance.

This remains comment-only; no workflow was triggered.

# Conflicts:
#	frontend/features/admin/i18n/translations.tsx
@excniesNIED

Copy link
Copy Markdown
Author

Merge conflict resolved at 8f7c3f5 (merges afc9c93, 63 commits): the only conflict was the catalog merge list in translations.tsx; the resolution keeps main's apiKeyUsage and dbEvolution catalogs and this branch's notificationTranslations. PR summary updated to drop the remaining 994 mention.

Verified on the merge head: source-line gate / lint / vitest / next build, doc-translation check (--base origin/main), go vet ./..., SMTP + metrics tests, full go test ./internal/server/ -count=1, git diff --check.

CI note: run 32505020693 for the new head is again in action_required — fork workflows stay queued until approved on the Actions tab, which is why no checks appear. Once approved it runs to completion; requesting review on 8f7c3f5.

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified that the current head resolves the previous merge conflict and preserves the intended translation catalog changes. The focused backend checks, go vet, frontend build, repository gates, and explicit base-to-head translation checks pass locally.

I cannot approve this head yet because GitHub reports no CI checks for the exact commit and the merge state remains UNSTABLE; the workflow is action_required. Please obtain trusted CI results for this exact head. I did not trigger or approve the workflow.

@excniesNIED

Copy link
Copy Markdown
Author

Thanks for approving the workflow and verifying the merge locally — CI is now running on 8f7c3f5 (https://github.com/astaxie/TokenHub/actions/runs/32505020693) and the Frontend, Repository gates, Deployment, N-1 contract, PostgreSQL and browser smoke checks are already green, with Backend still in progress.

For the record: because this PR comes from an external fork, each commit's Actions run lands in action_required and the "Approve and run" button is only visible to maintainers, so I can't trigger or approve fork workflows myself. Appreciate you clicking it through.

Will follow up once Backend finishes — should be ready for final approval if everything stays green.

@excniesNIED

Copy link
Copy Markdown
Author

CI is now green for 8f7c3f5. Run 32505020693 passed all 7 checks (Backend, Backend PostgreSQL, N-1 contract, Deployment, Frontend, Frontend browser smoke, and Repository gates), and the merge state is CLEAN.

All blockers from the previous review are cleared. Requesting final approval and merge on this head.

Comment thread frontend/features/admin/resources/settings-config.tsx Outdated
Channels created before the smtp_encryption field existed have no value,
so toForm returned an empty string and the select showed the blank
placeholder, and saving wrote an empty value back. Round-trip the
backend's effective default by falling back to starttls.
Copilot AI review requested due to automatic review settings August 22, 2026 05:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Fixed at 4d2dc8a: toForm now falls back to "starttls" when smtp_encryption is absent, so editing a legacy email channel shows the documented default instead of the blank placeholder and no longer writes an empty value back. Matches the backend's missing-or-blank-is-STARTTLS semantics.

Verified: npm run typecheck / lint / vitest / build and git diff --check all pass. This will need your "Approve and run" on the new head's workflow as usual (fork-run approval is maintainer-only).

Comment thread backend/internal/server/metrics_test.go Outdated
Comment thread backend/internal/server/admin_notifications_http.go Outdated
This flaky-test fix is unrelated to the implicit-TLS SMTP feature and
will be resubmitted as its own PR.
Notification channel fields are free-form, so a pre-existing generic
encryption field must not silently flip an email channel to direct TLS.
Keep the predicate scoped to smtp_encryption to preserve the documented
opt-in boundary.
Copilot AI review requested due to automatic review settings August 22, 2026 07:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Both points addressed at 95e92a0:

  1. [P2] directSMTPTLSEnabled now reads only smtp_encryption — the generic encryption fallback is gone, so a free-form channel with an unrelated encryption: "ssl" keeps the documented STARTTLS path. TestDirectSMTPTLSEnabled updated: the alias case now asserts the generic field does NOT enable direct TLS.

  2. [P3] The image metrics wait-loop fix is split out into its own PR: test(metrics): wait for observation after image job completion #285 — this PR no longer touches metrics_test.go (reverted to match main).

Verified: go vet ./internal/server/, SMTP direct-TLS tests, git diff --check. New head will need the usual "Approve and run" on its workflow (maintainer-only for fork PRs).

@astaxie astaxie left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the complete current diff at exact head 95e92a0fb8d9d8af3371d7c1d1ae9c5eb14a5c8c. The generic encryption alias is removed, the unrelated metrics change is split into #285, all prior threads are resolved, and all GitHub CI checks are green.

One security-sensitive behavior still blocks approval: an explicitly configured smtp_encryption: "starttls" remains opportunistic. At admin_notifications_http.go:687-693, if the SMTP server does not advertise STARTTLS, TokenHub continues and sends the message over the plaintext connection. smtp.PlainAuth itself refuses non-local plaintext authentication, but the alert or password-reset email body can still be transmitted; the latter contains a live reset token. This contradicts the UI/docs contract that selecting STARTTLS means STARTTLS and makes downgrade or server misconfiguration unsafe.

Please fail closed when smtp_encryption is explicitly starttls and the extension is absent. If preserving the legacy unset-field behavior is required, that compatibility path can remain opportunistic, but an explicit secure mode must be enforced. Add an in-process SMTP regression that omits STARTTLS and verifies no MAIL FROM/message body is sent.

Focused SMTP/notification tests, go vet ./..., frontend lint/typecheck/tests/build, the serial browser smoke suite (3/3), and git diff --check passed locally. The first browser run collided with another review worktree on the suite's fixed port; the isolated rerun passed.

Explicit smtp_encryption "starttls" must not downgrade to plaintext
when the server does not advertise STARTTLS: the alert or
password-reset body could be transmitted in the clear.
Return an error instead; legacy channels without the field keep
the opportunistic STARTTLS path. Add a regression that runs an
in-process server without STARTTLS and asserts no MAIL FROM or
message body is sent.
Copilot AI review requested due to automatic review settings August 22, 2026 11:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Fixed at ba5f7cc: explicit smtp_encryption: "starttls" now fails closed. If the server does not advertise STARTTLS, sendEmail returns an error before any MAIL FROM or body is transmitted. Legacy channels without the field keep the opportunistic STARTTLS path unchanged.

Added TestExplicitStartTLSFailsClosedWhenExtensionAbsent: an in-process plain SMTP server (no STARTTLS advertised) plus smtp_encryption: "starttls", asserting the call errors and no message body reaches the wire.

Verified: go vet ./internal/server/, SMTP direct-TLS + fail-closed tests, git diff --check. New head ba5f7cc workflow is queued as action_required again — needs the usual "Approve and run" (maintainer-only for fork PRs).

whatsapp_api_version: stringifyValue(item.fields?.whatsapp_api_version || item.fields?.api_version || "v20.0"),
smtp_host: stringifyValue(item.fields?.smtp_host),
smtp_port: stringifyValue(item.fields?.smtp_port),
smtp_encryption: stringifyValue(item.fields?.smtp_encryption) || "starttls",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This default now changes legacy channel behavior on any edit. A channel created before smtp_encryption existed has the legacy opportunistic STARTTLS semantics, but toForm turns the missing field into "starttls", and notificationChannelPayload writes that value back on save. With the new fail-closed handling for explicit STARTTLS, an unrelated UI edit can convert a previously working plaintext-fallback channel into one that fails whenever the server does not advertise STARTTLS. Please preserve the unset value through edit/update, or otherwise distinguish the displayed default from the persisted explicit starttls value.

// advertise the extension: downgrading to plaintext would transmit the alert
// or password-reset body in the clear. The plain serveTestSMTPConnection helper
// never advertises STARTTLS, so no MAIL FROM or message body may be sent.
func TestExplicitStartTLSFailsClosedWhenExtensionAbsent(t *testing.T) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] The regression only proves no DATA body is accepted, not the full no-downgrade boundary requested in the previous review. serveTestSMTPConnection records completed message bodies, so this test would still pass if a future refactor sent MAIL FROM before discovering STARTTLS was unavailable. Please have the fake server record commands and assert that no MAIL FROM (and therefore no DATA body) is sent before the explicit STARTTLS failure.

@astaxie

astaxie commented Aug 22, 2026

Copy link
Copy Markdown
Owner

The current PR body still under-reports verification for this head. The diff changes frontend resources/i18n files, but the Verification section only lists backend Go checks, git diff --check, and the docs translation check. Please update the PR body to include the frontend checks and repository gates that apply to this change (npm run lint, npm run typecheck, npm test, npm run build, browser smoke/E2E if run, and the root gates), or explicitly explain any skipped checks.

…ression

Only persist smtp_encryption when the channel already has an explicit
value or the user picked a non-default option, so an unrelated edit
cannot silently upgrade a legacy channel to the fail-closed mode.
Rewrite the explicit-STARTTLS regression to record every command and
assert no MAIL FROM is sent, not just that no message body arrives.
Copilot AI review requested due to automatic review settings August 22, 2026 12:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@excniesNIED

Copy link
Copy Markdown
Author

Addressed all three points at 8bf2fcd:

  1. [P1] Legacy channels now keep their unset smtp_encryption through edit/update. notificationChannelPayload only writes the field when the channel already had an explicit value, or the user picked something other than the displayed default (starttls). A legacy channel edited without touching the encryption selector is saved with the field still unset, preserving the opportunistic STARTTLS path — no unrelated edit can flip it to the fail-closed mode. New channels are created without the field (backend default = STARTTLS). The selector still shows starttls as the displayed default per the earlier feedback.

  2. [P3] TestExplicitStartTLSFailsClosedWhenExtensionAbsent now uses a fake server that records every command, and asserts no MAIL FROM (and therefore no DATA body) is sent before the explicit-STARTTLS failure — not just that no message body arrives.

  3. PR body updated: Verification section now lists the frontend checks this diff touches, and the Changes section reflects the fail-closed and scope behavior.

Verification (this head):

  • Backend: go vet ./..., SMTP tests (TestExplicitStartTLSFailsClosedWhenExtensionAbsent, TestEmailDeliveryViaImplicitTLSSMTP, TestDirectSMTPTLSEnabled, TestDirectTLSSMTPDialHonorsContextCancellation), full go test ./internal/server/ -count=1
  • Frontend: npm run lint, npm run typecheck (incl. source-line gate, 582 files), npm test (6 files / 11 tests), npm run build
  • Gates: node tools/check-doc-translations.mjs --base origin/main --head HEAD, git diff --check

New head will need the usual "Approve and run" on its workflow (maintainer-only for fork PRs).

let smtpEncryption: string | undefined;
if (existingEncryption) {
smtpEncryption = values.smtp_encryption;
} else if (values.smtp_encryption && values.smtp_encryption !== "starttls") {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This makes the UI unable to save explicit fail-closed STARTTLS for the default path. On create, existing is absent and the form default is starttls, so the payload omits smtp_encryption; the backend then treats the channel as legacy/opportunistic STARTTLS and may continue over plaintext when the server lacks STARTTLS. The same happens for legacy channels with no existing field, where the selector displays starttls but saving keeps it unset, so an operator cannot opt into the explicit starttls behavior from this form. Please distinguish the legacy/opportunistic state from explicit STARTTLS (for example, persist starttls on create and expose a separate legacy/auto value or dirty-state handling for existing unset channels), and keep the docs aligned with whichever behavior is intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants