# Comment Email Config Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add comment email infrastructure with admin SMTP config, guest anonymous email rules, per-user notification preference, and admin test-send capability without blocking comment creation. **Architecture:** Extend the existing config registry (global + user) for mail settings and notify preferences, then wire those values into comment creation and notification service logic. Keep notification sending best-effort and side-effect-safe: comment write succeeds even if email send fails. Reuse existing `/api/config/*`, profile page, and comment service patterns. **Tech Stack:** Nuxt 3, Nitro server routes, Drizzle (SQLite), Bun test, existing config service in `server/service/config`. --- ## File Structure Map - **Config domain** - Modify `server/service/config/registry.ts` for new config keys and validation. - Modify `app/composables/useGlobalConfig.ts` for global config typing updates. - Modify `app/pages/me/admin/config/index.vue` for SMTP form and test-send action. - Create `server/api/config/global/comment-email-test.post.ts` for admin test email. - **Comment domain** - Modify `packages/drizzle-pkg/database/sqlite/schema/content.ts` to add guest email fields. - Create a migration under `packages/drizzle-pkg/migrations`. - Modify `server/service/post-comments/index.ts` to validate/store guest email + anonymous. - Modify `app/components/PostComments.vue` to collect guest email + anonymous toggle and render user hints. - **Notification domain** - Create `server/service/comment-notify/index.ts` for send gating + dispatch. - Integrate notify trigger in comment create flow routes. - **User preference** - Modify `app/pages/me/profile/index.vue` and `/api/config/me` usage to add `commentNotifyEnabled`. - **Tests** - Modify/create tests in `server/utils/post-comment-guest.test.ts`, comment service tests, and config validation tests. --- ### Task 1: Add Config Keys and Validation **Files:** - Modify: `server/service/config/registry.ts` - Test: `server/service/config/registry.test.ts` (create if absent) - [ ] **Step 1: Write the failing validation tests** ```ts import { describe, expect, test } from "bun:test"; import { validateConfigValue } from "./registry"; describe("comment mail config validation", () => { test("accepts valid from email", () => { expect(validateConfigValue("commentMailFromEmail", "sender@example.com")).toBe(true); }); test("rejects invalid from email", () => { expect(validateConfigValue("commentMailFromEmail", "bad-email")).toBe(false); }); test("accepts comment notify default", () => { expect(validateConfigValue("commentNotifyEnabled", true)).toBe(true); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `bun test server/service/config/registry.test.ts` Expected: FAIL with unknown key/type validation mismatch. - [ ] **Step 3: Implement minimal registry entries** ```ts commentEmailNotifyEnabled: defineConfig({ key: "commentEmailNotifyEnabled", scope: "global", valueType: "boolean", defaultValue: false, userOverridable: false }), commentMailFromEmail: defineConfig({ key: "commentMailFromEmail", scope: "global", valueType: "string", defaultValue: "", userOverridable: false, validate: (value: string) => !value.trim() || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()), }), commentSmtpHost: defineConfig({ key: "commentSmtpHost", scope: "global", valueType: "string", defaultValue: "", userOverridable: false }), commentSmtpPort: defineConfig({ key: "commentSmtpPort", scope: "global", valueType: "number", defaultValue: 465, userOverridable: false, validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 65535, }), commentSmtpSecure: defineConfig({ key: "commentSmtpSecure", scope: "global", valueType: "boolean", defaultValue: true, userOverridable: false }), commentSmtpUser: defineConfig({ key: "commentSmtpUser", scope: "global", valueType: "string", defaultValue: "", userOverridable: false }), commentSmtpPass: defineConfig({ key: "commentSmtpPass", scope: "global", valueType: "string", defaultValue: "", userOverridable: false }), commentNotifyEnabled: defineConfig({ key: "commentNotifyEnabled", scope: "both", valueType: "boolean", defaultValue: true, userOverridable: true }), ``` - [ ] **Step 4: Run test to verify it passes** Run: `bun test server/service/config/registry.test.ts` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add server/service/config/registry.ts server/service/config/registry.test.ts git commit -m "feat(config): add comment email and notify preference keys" ``` --- ### Task 2: Extend Admin Global Config UI and Global Composable **Files:** - Modify: `app/composables/useGlobalConfig.ts` - Modify: `app/pages/me/admin/config/index.vue` - [ ] **Step 1: Write a focused UI behavior test (or component-level assertion)** ```ts // Pseudocode if no mounted test infra exists yet: // assert payload includes commentEmailNotifyEnabled/commentSmtpHost/commentSmtpPort/commentSmtpSecure/commentSmtpUser/commentSmtpPass/commentMailFromEmail ``` - [ ] **Step 2: Run existing frontend checks** Run: `bun run typecheck` Expected: FAIL due to missing new config fields in types. - [ ] **Step 3: Implement minimal type + form changes** ```ts // useGlobalConfig.ts export type GlobalConfig = { siteName: string; allowRegister: boolean; commentEmailNotifyEnabled: boolean; commentMailFromEmail: string; commentSmtpHost: string; commentSmtpPort: number; commentSmtpSecure: boolean; commentSmtpUser: string; }; ``` ```vue ``` - [ ] **Step 4: Re-run checks** Run: `bun run typecheck` Expected: PASS for touched types/components. - [ ] **Step 5: Commit** ```bash git add app/composables/useGlobalConfig.ts app/pages/me/admin/config/index.vue git commit -m "feat(admin-config): add comment mail smtp settings form" ``` --- ### Task 3: Add Admin Test Send Endpoint and Button **Files:** - Create: `server/api/config/global/comment-email-test.post.ts` - Modify: `app/pages/me/admin/config/index.vue` - Create/Modify Test: `server/api/config/global/comment-email-test.post.test.ts` - [ ] **Step 1: Write failing API tests** ```ts test("returns 400 when smtp config incomplete", async () => { // setup admin session + empty smtp config // call POST /api/config/global/comment-email-test // expect 400 }); ``` - [ ] **Step 2: Run targeted test** Run: `bun test server/api/config/global/comment-email-test.post.test.ts` Expected: FAIL with route missing. - [ ] **Step 3: Implement endpoint and UI action** ```ts // comment-email-test.post.ts await requireAdmin(event); const user = await event.context.auth.requireUser(); const cfg = await getCommentMailConfig(event.context.config); assertMailConfigReady(cfg); if (!user.email) throw createError({ statusCode: 400, statusMessage: "管理员未配置邮箱" }); await sendCommentTestMail({ to: user.email, cfg }); return R.success({ ok: true }); ``` ```vue 发送测试邮件 ``` - [ ] **Step 4: Run test again** Run: `bun test server/api/config/global/comment-email-test.post.test.ts` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add server/api/config/global/comment-email-test.post.ts server/api/config/global/comment-email-test.post.test.ts app/pages/me/admin/config/index.vue git commit -m "feat(mail): add admin comment email test-send endpoint" ``` --- ### Task 4: Add DB Fields for Guest Email and Anonymous Flag **Files:** - Modify: `packages/drizzle-pkg/database/sqlite/schema/content.ts` - Create: `packages/drizzle-pkg/migrations/0008_comment_guest_email.sql` (use next available index) - Modify: `packages/drizzle-pkg/migrations/meta/_journal.json` (if migration tooling requires) - [ ] **Step 1: Write failing service test for guest email rules** ```ts test("guest non-anonymous must provide email", async () => { await expect(createComment({ postId, ownerUserId, parentId: null, viewer: null, guestDisplayName: "访客", guestIsAnonymous: false, guestEmail: "", body: "hello", })).rejects.toThrow("请填写邮箱"); }); ``` - [ ] **Step 2: Run test to confirm fail** Run: `bun test server/service/post-comments/index.test.ts` Expected: FAIL due to unknown fields / missing validation. - [ ] **Step 3: Add schema + migration** ```ts guestEmail: text("guest_email"), guestIsAnonymous: integer("guest_is_anonymous", { mode: "boolean" }).notNull().default(false), ``` ```sql ALTER TABLE post_comments ADD COLUMN guest_email text; ALTER TABLE post_comments ADD COLUMN guest_is_anonymous integer DEFAULT 0 NOT NULL; ``` - [ ] **Step 4: Run migration/test checks** Run: `bun test server/service/post-comments/index.test.ts` Expected: still FAIL until service is implemented (this is expected at this stage). - [ ] **Step 5: Commit** ```bash git add packages/drizzle-pkg/database/sqlite/schema/content.ts packages/drizzle-pkg/migrations/0008_comment_guest_email.sql packages/drizzle-pkg/migrations/meta/_journal.json git commit -m "feat(db): add guest email and anonymous fields for comments" ``` --- ### Task 5: Implement Comment Service Validation and Persist New Fields **Files:** - Modify: `server/service/post-comments/index.ts` - Modify: `server/utils/post-comment-guest.ts` - Modify: `server/utils/post-comment-guest.test.ts` - Modify: `app/components/PostComments.vue` - [ ] **Step 1: Expand failing validation tests** ```ts test("anonymous guest can omit email", () => { expect(validateGuestCommentEmail("", true)).toBe(""); }); test("non-anonymous guest rejects invalid email", () => { expect(() => validateGuestCommentEmail("abc", false)).toThrow(GuestCommentValidationError); }); ``` - [ ] **Step 2: Run guest validation tests** Run: `bun test server/utils/post-comment-guest.test.ts` Expected: FAIL with `validateGuestCommentEmail` undefined. - [ ] **Step 3: Implement validation and form payload** ```ts export function validateGuestCommentEmail(raw: string, isAnonymous: boolean): string { const t = raw.trim(); if (isAnonymous) return t; if (!t) throw new GuestCommentValidationError("请填写邮箱"); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)) throw new GuestCommentValidationError("邮箱格式不正确"); return t; } ``` ```vue ``` - [ ] **Step 4: Run tests and typecheck** Run: `bun test server/utils/post-comment-guest.test.ts && bun run typecheck` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add server/service/post-comments/index.ts server/utils/post-comment-guest.ts server/utils/post-comment-guest.test.ts app/components/PostComments.vue git commit -m "feat(comments): enforce guest email rules with anonymous override" ``` --- ### Task 6: Add User Notification Preference in Profile **Files:** - Modify: `app/pages/me/profile/index.vue` - Modify: `server/service/config/registry.ts` (if not already included in Task 1) - [ ] **Step 1: Write failing profile load/save assertion** ```ts // verify /api/config/me result drives state.commentNotifyEnabled and save writes key ``` - [ ] **Step 2: Run typecheck before implementation** Run: `bun run typecheck` Expected: FAIL due to missing `commentNotifyEnabled` state/binding. - [ ] **Step 3: Implement minimal state + save wiring** ```ts state.commentNotifyEnabled = typeof cfg.commentNotifyEnabled === "boolean" ? cfg.commentNotifyEnabled : true; ``` ```ts await fetchData('/api/config/me', { method: 'PUT', notify: false, body: { key: 'commentNotifyEnabled', value: state.commentNotifyEnabled }, }); ``` ```vue ``` - [ ] **Step 4: Re-run typecheck** Run: `bun run typecheck` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add app/pages/me/profile/index.vue git commit -m "feat(profile): add comment email notify preference toggle" ``` --- ### Task 7: Implement Notification Dispatch on Comment Create **Files:** - Create: `server/service/comment-notify/index.ts` - Modify: `server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts` - Modify: `server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts` - Create/Modify Tests: `server/service/comment-notify/index.test.ts` - [ ] **Step 1: Write failing notify gating tests** ```ts test("skips when global switch off", async () => { // expect no send call }); test("skips when target user has notify disabled", async () => { // expect no send call }); ``` - [ ] **Step 2: Run targeted tests** Run: `bun test server/service/comment-notify/index.test.ts` Expected: FAIL (service missing). - [ ] **Step 3: Implement best-effort notify service** ```ts export async function notifyOnCommentReply(input: { postId: number; commentId: number; actorUserId: number | null; parentId: number | null }) { if (!input.parentId) return; // load target comment author -> user // check global config + smtp readiness + user pref + user email // send and catch errors with logger.error(...) } ``` - [ ] **Step 4: Integrate into POST handlers** ```ts const id = await createComment(...); void notifyOnCommentReply({ postId, commentId: id, actorUserId: viewer?.id ?? null, parentId: body.parentId ?? null }); return R.success({ id }); ``` - [ ] **Step 5: Run tests** Run: `bun test server/service/comment-notify/index.test.ts` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add server/service/comment-notify/index.ts server/service/comment-notify/index.test.ts server/api/public/profile/[publicSlug]/posts/[postSlug]/comments.post.ts server/api/public/unlisted/[publicSlug]/[shareToken]/comments.post.ts git commit -m "feat(notify): send best-effort comment reply emails" ``` --- ### Task 8: End-to-End Verification and Documentation Update **Files:** - Modify (if needed): `docs/superpowers/specs/2026-04-20-comment-email-config-design.md` - Modify/Create: any README/admin docs that mention config keys - [ ] **Step 1: Run full relevant test suite** Run: `bun test server/utils/post-comment-guest.test.ts server/service/comment-notify/index.test.ts server/service/config/registry.test.ts` Expected: PASS. - [ ] **Step 2: Run static verification** Run: `bun run typecheck` Expected: PASS. - [ ] **Step 3: Manually verify key flows** Run app and validate: 1. Admin saves SMTP config and test-send succeeds. 2. Guest comment requires email unless anonymous checked. 3. Logged-in user without email can comment but sees warning. 4. User can toggle notify preference and it affects send decision. - [ ] **Step 4: Commit verification/doc touch-ups** ```bash git add docs/superpowers/specs/2026-04-20-comment-email-config-design.md git commit -m "docs(comment-email): align spec notes with implementation checks" ``` --- ## Self-Review - **Spec coverage:** All approved requirements are mapped to tasks: global SMTP config, guest anonymous/email rule, user notify preference default true, non-blocking send, and admin test-send. - **Placeholder scan:** No `TODO/TBD` placeholders remain; each code step has concrete snippet and command. - **Type consistency:** Config keys use a consistent prefix (`comment*`) across registry, UI, and notification service tasks.