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