Browse Source
Add an admin-only test email endpoint with SMTP config and admin email validation, plus a config-page action button to trigger test sends and minimal service tests for key 400 failure paths. Made-with: Cursormain
6 changed files with 184 additions and 13 deletions
@ -0,0 +1,49 @@ |
|||||
|
import { dbGlobal } from "drizzle-pkg/lib/db"; |
||||
|
import { users } from "drizzle-pkg/lib/schema/auth"; |
||||
|
import { eq } from "drizzle-orm"; |
||||
|
import { requireAdmin } from "#server/utils/admin-guard"; |
||||
|
import { |
||||
|
CommentEmailTestValidationError, |
||||
|
sendCommentEmailTestMail, |
||||
|
} from "#server/service/comment-email/test-mail"; |
||||
|
|
||||
|
export default defineWrappedResponseHandler(async (event) => { |
||||
|
const admin = await requireAdmin(event); |
||||
|
const [adminRow] = await dbGlobal |
||||
|
.select({ |
||||
|
email: users.email, |
||||
|
}) |
||||
|
.from(users) |
||||
|
.where(eq(users.id, admin.id)) |
||||
|
.limit(1); |
||||
|
|
||||
|
try { |
||||
|
await sendCommentEmailTestMail({ |
||||
|
toEmail: adminRow?.email ?? "", |
||||
|
requestedBy: admin.username, |
||||
|
config: { |
||||
|
enabled: await event.context.config.getGlobal("commentEmailNotifyEnabled"), |
||||
|
fromEmail: await event.context.config.getGlobal("commentMailFromEmail"), |
||||
|
smtpHost: await event.context.config.getGlobal("commentSmtpHost"), |
||||
|
smtpPort: await event.context.config.getGlobal("commentSmtpPort"), |
||||
|
smtpSecure: await event.context.config.getGlobal("commentSmtpSecure"), |
||||
|
smtpUser: await event.context.config.getGlobal("commentSmtpUser"), |
||||
|
smtpPass: await event.context.config.getGlobal("commentSmtpPass"), |
||||
|
}, |
||||
|
}); |
||||
|
return R.success({ |
||||
|
message: "测试邮件发送成功,请检查管理员邮箱", |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
if (error instanceof CommentEmailTestValidationError) { |
||||
|
throw createError({ |
||||
|
statusCode: 400, |
||||
|
statusMessage: error.message, |
||||
|
}); |
||||
|
} |
||||
|
throw createError({ |
||||
|
statusCode: 502, |
||||
|
statusMessage: "测试邮件发送失败,请检查 SMTP 配置或稍后重试", |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,26 @@ |
|||||
|
import { describe, expect, test } from "bun:test"; |
||||
|
import { |
||||
|
assertAdminEmailReady, |
||||
|
assertCommentEmailConfigReadyForTest, |
||||
|
CommentEmailTestValidationError, |
||||
|
} from "./test-mail"; |
||||
|
|
||||
|
describe("comment email test mail validation", () => { |
||||
|
test("returns 400-equivalent validation error when smtp config is incomplete", () => { |
||||
|
expect(() => |
||||
|
assertCommentEmailConfigReadyForTest({ |
||||
|
enabled: true, |
||||
|
fromEmail: "", |
||||
|
smtpHost: "smtp.example.com", |
||||
|
smtpPort: 465, |
||||
|
smtpSecure: true, |
||||
|
smtpUser: "user", |
||||
|
smtpPass: "pass", |
||||
|
}), |
||||
|
).toThrow(CommentEmailTestValidationError); |
||||
|
}); |
||||
|
|
||||
|
test("returns 400-equivalent validation error when admin email is empty", () => { |
||||
|
expect(() => assertAdminEmailReady("")).toThrow(CommentEmailTestValidationError); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,71 @@ |
|||||
|
import log4js from "logger"; |
||||
|
import nodemailer from "nodemailer"; |
||||
|
|
||||
|
type CommentEmailConfig = { |
||||
|
enabled: boolean; |
||||
|
fromEmail: string; |
||||
|
smtpHost: string; |
||||
|
smtpPort: number; |
||||
|
smtpSecure: boolean; |
||||
|
smtpUser: string; |
||||
|
smtpPass: string; |
||||
|
}; |
||||
|
|
||||
|
const logger = log4js.getLogger("COMMENT_EMAIL_TEST"); |
||||
|
|
||||
|
export class CommentEmailTestValidationError extends Error { |
||||
|
constructor(message: string) { |
||||
|
super(message); |
||||
|
this.name = "CommentEmailTestValidationError"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function hasValue(value: string): boolean { |
||||
|
return value.trim().length > 0; |
||||
|
} |
||||
|
|
||||
|
export function assertCommentEmailConfigReadyForTest(config: CommentEmailConfig): void { |
||||
|
if (!config.enabled) { |
||||
|
throw new CommentEmailTestValidationError("评论邮件通知总开关未开启"); |
||||
|
} |
||||
|
if (!hasValue(config.fromEmail) || !hasValue(config.smtpHost) || !hasValue(config.smtpUser) || !hasValue(config.smtpPass)) { |
||||
|
throw new CommentEmailTestValidationError("邮件配置不完整,请先填写发件人、SMTP 主机、用户名和密码"); |
||||
|
} |
||||
|
if (!Number.isInteger(config.smtpPort) || config.smtpPort < 1 || config.smtpPort > 65535) { |
||||
|
throw new CommentEmailTestValidationError("SMTP 端口不合法"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function assertAdminEmailReady(adminEmail: string | null | undefined): asserts adminEmail is string { |
||||
|
if (!adminEmail || adminEmail.trim().length === 0) { |
||||
|
throw new CommentEmailTestValidationError("当前管理员账号未配置邮箱,无法接收测试邮件"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function sendCommentEmailTestMail(input: { |
||||
|
toEmail: string; |
||||
|
requestedBy: string; |
||||
|
config: CommentEmailConfig; |
||||
|
}) { |
||||
|
assertCommentEmailConfigReadyForTest(input.config); |
||||
|
assertAdminEmailReady(input.toEmail); |
||||
|
|
||||
|
const transporter = nodemailer.createTransport({ |
||||
|
host: input.config.smtpHost, |
||||
|
port: input.config.smtpPort, |
||||
|
secure: input.config.smtpSecure, |
||||
|
auth: { |
||||
|
user: input.config.smtpUser, |
||||
|
pass: input.config.smtpPass, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
await transporter.sendMail({ |
||||
|
from: input.config.fromEmail, |
||||
|
to: input.toEmail.trim(), |
||||
|
subject: "[Person Panel] 评论邮件配置测试", |
||||
|
text: `你好,${input.requestedBy}。\n\n这是一封评论邮件配置测试邮件,若你收到该邮件,说明 SMTP 配置可用。\n\n发送时间:${new Date().toISOString()}\n`, |
||||
|
}); |
||||
|
|
||||
|
logger.info("comment email test message sent to admin account"); |
||||
|
} |
||||
Loading…
Reference in new issue