feat(邮件通知): 添加测试邮件发送功能
在邮件通知设置页面添加发送测试邮件按钮,用于验证SMTP配置是否正确。同时添加QQ邮箱授权码的使用提示,帮助用户正确配置。 - 新增测试邮件API接口 - 前端添加测试邮件按钮及相关逻辑 - 优化错误提示,特别是针对QQ邮箱的535错误 - 添加QQ邮箱授权码使用说明文档
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
31
cwd-comments-api/src/api/admin/testEmail.ts
Normal file
31
cwd-comments-api/src/api/admin/testEmail.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user