feat(邮件通知): 添加测试邮件发送功能

在邮件通知设置页面添加发送测试邮件按钮,用于验证SMTP配置是否正确。同时添加QQ邮箱授权码的使用提示,帮助用户正确配置。

- 新增测试邮件API接口
- 前端添加测试邮件按钮及相关逻辑
- 优化错误提示,特别是针对QQ邮箱的535错误
- 添加QQ邮箱授权码使用说明文档
This commit is contained in:
anghunk
2026-01-20 12:38:48 +08:00
parent 6d3bdb2b11
commit 1c9e8c02b6
5 changed files with 165 additions and 1 deletions

View File

@@ -99,6 +99,19 @@ export function saveEmailNotifySettings(data: {
return put<{ message: string }>('/admin/settings/email-notify', data);
}
export function sendTestEmail(data: {
toEmail: string;
smtp?: {
host?: string;
port?: number;
user?: string;
pass?: string;
secure?: boolean;
};
}): Promise<{ message: string }> {
return post<{ message: string }>('/admin/settings/email-test', data);
}
export function fetchCommentSettings(): Promise<CommentSettingsResponse> {
return get<CommentSettingsResponse>('/admin/settings/comments');
}

View File

@@ -89,6 +89,10 @@
<div class="form-item">
<label class="form-label">授权码/密码</label>
<input v-model="smtpPass" class="form-input" type="password" placeholder="QQ邮箱请使用授权码" />
<div v-if="smtpProvider === 'qq'" class="form-hint">
注意QQ邮箱必须使用<a href="https://service.mail.qq.com/detail/0/75" target="_blank">授权码</a>而非QQ密码<br/>
请登录QQ邮箱网页版设置-账户中开启 POP3/SMTP 服务并生成授权码
</div>
</div>
<div
@@ -97,7 +101,11 @@
>
{{ message }}
</div>
<div class="card-actions">
<div class="card-actions" style="justify-content: space-between;">
<button class="card-button secondary" :disabled="testingEmail" @click="testEmail">
<span v-if="testingEmail">发送中...</span>
<span v-else>发送测试邮件</span>
</button>
<button class="card-button" :disabled="savingEmail" @click="saveEmail">
<span v-if="savingEmail">保存中...</span>
<span v-else>保存配置</span>
@@ -117,6 +125,7 @@ import {
saveCommentSettings,
fetchEmailNotifySettings,
saveEmailNotifySettings,
sendTestEmail
} from "../api/admin";
const email = ref("");
@@ -126,6 +135,7 @@ const commentAdminBadge = ref("");
const avatarPrefix = ref("");
const commentAdminEnabled = ref(false);
const savingEmail = ref(false);
const testingEmail = ref(false);
const savingComment = ref(false);
const loading = ref(false);
const message = ref("");
@@ -225,6 +235,49 @@ async function saveEmail() {
}
}
async function testEmail() {
if (!email.value) {
message.value = "请输入管理员通知邮箱作为测试接收邮箱";
messageType.value = "error";
return;
}
if (!smtpUser.value || !smtpPass.value) {
message.value = "请先填写 SMTP 账号和密码";
messageType.value = "error";
return;
}
testingEmail.value = true;
message.value = "";
try {
const res = await sendTestEmail({
toEmail: email.value,
smtp: {
host: smtpHost.value,
port: smtpPort.value,
user: smtpUser.value,
pass: smtpPass.value,
secure: smtpSecure.value
}
});
showToast(res.message || "发送成功,请查收邮件", "success");
} catch (e: any) {
// 显示详细错误信息
console.error(e);
let errorMsg = e.message || "发送失败";
// 针对 QQ 邮箱 535 错误的友好提示
if (errorMsg.includes('535') && (errorMsg.includes('Login fail') || errorMsg.includes('authentication failed'))) {
errorMsg = "验证失败 (535):请检查 1. QQ邮箱是否已开启 POP3/SMTP 服务2. 密码栏是否填写了“授权码”非QQ密码。";
}
message.value = errorMsg;
messageType.value = "error";
} finally {
testingEmail.value = false;
}
}
async function saveComment() {
savingComment.value = true;
message.value = "";
@@ -400,6 +453,16 @@ onMounted(() => {
cursor: pointer;
}
.card-button.secondary {
background-color: #f6f8fa;
color: #24292f;
border: 1px solid #d0d7de;
}
.card-button.secondary:hover {
background-color: #f3f4f6;
border-color: #d0d7de;
}
.card-button:disabled {
opacity: 0.7;
cursor: default;
@@ -453,4 +516,17 @@ onMounted(() => {
font-size: 14px;
color: #57606a;
}
.form-hint {
font-size: 12px;
color: #57606a;
margin-top: 4px;
line-height: 1.5;
}
.form-hint a {
color: #0969da;
text-decoration: none;
}
.form-hint a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,31 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { sendTestEmail, EmailNotificationSettings, isValidEmail } from '../../utils/email';
export const testEmail = async (c: Context<{ Bindings: Bindings }>) => {
try {
const body = await c.req.json();
const toEmail = body.toEmail;
if (!toEmail || !isValidEmail(toEmail)) {
return c.json({ message: '请输入有效的接收邮箱' }, 400);
}
const smtp: EmailNotificationSettings['smtp'] = body.smtp;
if (!smtp || !smtp.user || !smtp.pass) {
return c.json({ message: 'SMTP 配置不完整' }, 400);
}
const result = await sendTestEmail(c.env, toEmail, smtp);
if (result.success) {
return c.json({ message: '邮件发送成功' });
} else {
return c.json({ message: '邮件发送失败: ' + result.message }, 500);
}
} catch (e: any) {
return c.json({ message: e.message || '测试失败' }, 500);
}
};

View File

@@ -16,6 +16,7 @@ import { listComments } from './api/admin/listComments';
import { updateStatus } from './api/admin/updateStatus';
import { getAdminEmail } from './api/admin/getAdminEmail';
import { setAdminEmail } from './api/admin/setAdminEmail';
import { testEmail } from './api/admin/testEmail';
const app = new Hono<{ Bindings: Bindings }>();
const VERSION = 'v0.0.1';
@@ -176,6 +177,8 @@ app.put('/admin/settings/email-notify', async (c) => {
return c.json({ message: e.message || '保存失败' }, 500);
}
});
app.post('/admin/settings/email-test', testEmail);
app.get('/admin/settings/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);

View File

@@ -94,6 +94,47 @@ async function dispatchMail(
}
}
export async function sendTestEmail(
env: Bindings,
to: string,
smtp: EmailNotificationSettings['smtp']
): Promise<{ success: boolean; message?: string }> {
if (!smtp || !smtp.user || !smtp.pass) {
return { success: false, message: 'SMTP 配置不完整' };
}
try {
const transporter = createTransport({
host: smtp.host,
port: smtp.port,
secure: smtp.secure,
auth: { user: smtp.user, pass: smtp.pass }
});
// 尝试验证连接配置
await transporter.verify();
await transporter.sendMail({
from: `"Test" <${smtp.user}>`,
to: to,
subject: 'CWD Comments 邮件配置测试',
html: `
<div style="padding: 20px; font-family: sans-serif;">
<h2 style="color: #059669;">配置成功!</h2>
<p>这就是一封来自 CWD Comments 的测试邮件。</p>
<p>如果您收到了这封邮件,说明您的 SMTP 配置是正确的。</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">发送时间:${new Date().toLocaleString()}</p>
</div>
`
});
return { success: true };
} catch (e: any) {
console.error('TestEmail:error', e);
return { success: false, message: e.message || String(e) };
}
}
function parseEnabled(raw: string | undefined, defaultValue: boolean) {
if (raw === undefined) return defaultValue;
return raw === '1';