16 KiB
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.tsfor new config keys and validation. - Modify
app/composables/useGlobalConfig.tsfor global config typing updates. - Modify
app/pages/me/admin/config/index.vuefor SMTP form and test-send action. - Create
server/api/config/global/comment-email-test.post.tsfor admin test email.
- Modify
- Comment domain
- Modify
packages/drizzle-pkg/database/sqlite/schema/content.tsto add guest email fields. - Create a migration under
packages/drizzle-pkg/migrations. - Modify
server/service/post-comments/index.tsto validate/store guest email + anonymous. - Modify
app/components/PostComments.vueto collect guest email + anonymous toggle and render user hints.
- Modify
- Notification domain
- Create
server/service/comment-notify/index.tsfor send gating + dispatch. - Integrate notify trigger in comment create flow routes.
- Create
- User preference
- Modify
app/pages/me/profile/index.vueand/api/config/meusage to addcommentNotifyEnabled.
- Modify
- Tests
- Modify/create tests in
server/utils/post-comment-guest.test.ts, comment service tests, and config validation tests.
- Modify/create tests in
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
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
commentEmailNotifyEnabled: defineConfig<boolean>({ key: "commentEmailNotifyEnabled", scope: "global", valueType: "boolean", defaultValue: false, userOverridable: false }),
commentMailFromEmail: defineConfig<string>({
key: "commentMailFromEmail",
scope: "global",
valueType: "string",
defaultValue: "",
userOverridable: false,
validate: (value: string) => !value.trim() || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()),
}),
commentSmtpHost: defineConfig<string>({ key: "commentSmtpHost", scope: "global", valueType: "string", defaultValue: "", userOverridable: false }),
commentSmtpPort: defineConfig<number>({
key: "commentSmtpPort",
scope: "global",
valueType: "number",
defaultValue: 465,
userOverridable: false,
validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 65535,
}),
commentSmtpSecure: defineConfig<boolean>({ key: "commentSmtpSecure", scope: "global", valueType: "boolean", defaultValue: true, userOverridable: false }),
commentSmtpUser: defineConfig<string>({ key: "commentSmtpUser", scope: "global", valueType: "string", defaultValue: "", userOverridable: false }),
commentSmtpPass: defineConfig<string>({ key: "commentSmtpPass", scope: "global", valueType: "string", defaultValue: "", userOverridable: false }),
commentNotifyEnabled: defineConfig<boolean>({ 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
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)
// 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
// useGlobalConfig.ts
export type GlobalConfig = {
siteName: string;
allowRegister: boolean;
commentEmailNotifyEnabled: boolean;
commentMailFromEmail: string;
commentSmtpHost: string;
commentSmtpPort: number;
commentSmtpSecure: boolean;
commentSmtpUser: string;
};
<!-- me/admin/config/index.vue -->
<UFormField label="评论邮件通知总开关"><UCheckbox v-model="commentEmailNotifyEnabled" label="开启" /></UFormField>
<UFormField label="发件人邮箱"><UInput v-model="commentMailFromEmail" placeholder="noreply@example.com" /></UFormField>
<UFormField label="SMTP Host"><UInput v-model="commentSmtpHost" /></UFormField>
<UFormField label="SMTP Port"><UInput v-model.number="commentSmtpPort" type="number" /></UFormField>
<UFormField label="SMTP Secure"><UCheckbox v-model="commentSmtpSecure" label="TLS/SSL" /></UFormField>
<UFormField label="SMTP User"><UInput v-model="commentSmtpUser" /></UFormField>
<UFormField label="SMTP Password"><UInput v-model="commentSmtpPass" type="password" autocomplete="new-password" /></UFormField>
- Step 4: Re-run checks
Run: bun run typecheck
Expected: PASS for touched types/components.
- Step 5: Commit
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
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
// 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 });
<UButton :loading="testingMail" variant="soft" @click="testSendMail">发送测试邮件</UButton>
- Step 4: Run test again
Run: bun test server/api/config/global/comment-email-test.post.test.ts
Expected: PASS.
- Step 5: Commit
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
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
guestEmail: text("guest_email"),
guestIsAnonymous: integer("guest_is_anonymous", { mode: "boolean" }).notNull().default(false),
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
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
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
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;
}
<UCheckbox v-model="guestIsAnonymous" label="匿名评论(可不填邮箱)" />
<UInput v-model="guestEmail" :required="!guestIsAnonymous" placeholder="你的邮箱" />
- Step 4: Run tests and typecheck
Run: bun test server/utils/post-comment-guest.test.ts && bun run typecheck
Expected: PASS.
- Step 5: Commit
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
// 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
state.commentNotifyEnabled = typeof cfg.commentNotifyEnabled === "boolean" ? cfg.commentNotifyEnabled : true;
await fetchData('/api/config/me', {
method: 'PUT',
notify: false,
body: { key: 'commentNotifyEnabled', value: state.commentNotifyEnabled },
});
<UCheckbox v-model="state.commentNotifyEnabled" label="接收评论邮件通知" />
- Step 4: Re-run typecheck
Run: bun run typecheck
Expected: PASS.
- Step 5: Commit
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
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
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
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
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:
- Admin saves SMTP config and test-send succeeds.
- Guest comment requires email unless anonymous checked.
- Logged-in user without email can comment but sees warning.
- User can toggle notify preference and it affects send decision.
- Step 4: Commit verification/doc touch-ups
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/TBDplaceholders 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.