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);
|
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> {
|
export function fetchCommentSettings(): Promise<CommentSettingsResponse> {
|
||||||
return get<CommentSettingsResponse>('/admin/settings/comments');
|
return get<CommentSettingsResponse>('/admin/settings/comments');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,10 @@
|
|||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label class="form-label">授权码/密码</label>
|
<label class="form-label">授权码/密码</label>
|
||||||
<input v-model="smtpPass" class="form-input" type="password" placeholder="QQ邮箱请使用授权码" />
|
<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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -97,7 +101,11 @@
|
|||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</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">
|
<button class="card-button" :disabled="savingEmail" @click="saveEmail">
|
||||||
<span v-if="savingEmail">保存中...</span>
|
<span v-if="savingEmail">保存中...</span>
|
||||||
<span v-else>保存配置</span>
|
<span v-else>保存配置</span>
|
||||||
@@ -117,6 +125,7 @@ import {
|
|||||||
saveCommentSettings,
|
saveCommentSettings,
|
||||||
fetchEmailNotifySettings,
|
fetchEmailNotifySettings,
|
||||||
saveEmailNotifySettings,
|
saveEmailNotifySettings,
|
||||||
|
sendTestEmail
|
||||||
} from "../api/admin";
|
} from "../api/admin";
|
||||||
|
|
||||||
const email = ref("");
|
const email = ref("");
|
||||||
@@ -126,6 +135,7 @@ const commentAdminBadge = ref("");
|
|||||||
const avatarPrefix = ref("");
|
const avatarPrefix = ref("");
|
||||||
const commentAdminEnabled = ref(false);
|
const commentAdminEnabled = ref(false);
|
||||||
const savingEmail = ref(false);
|
const savingEmail = ref(false);
|
||||||
|
const testingEmail = ref(false);
|
||||||
const savingComment = ref(false);
|
const savingComment = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const message = ref("");
|
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() {
|
async function saveComment() {
|
||||||
savingComment.value = true;
|
savingComment.value = true;
|
||||||
message.value = "";
|
message.value = "";
|
||||||
@@ -400,6 +453,16 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
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 {
|
.card-button:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -453,4 +516,17 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #57606a;
|
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>
|
</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 { updateStatus } from './api/admin/updateStatus';
|
||||||
import { getAdminEmail } from './api/admin/getAdminEmail';
|
import { getAdminEmail } from './api/admin/getAdminEmail';
|
||||||
import { setAdminEmail } from './api/admin/setAdminEmail';
|
import { setAdminEmail } from './api/admin/setAdminEmail';
|
||||||
|
import { testEmail } from './api/admin/testEmail';
|
||||||
|
|
||||||
const app = new Hono<{ Bindings: Bindings }>();
|
const app = new Hono<{ Bindings: Bindings }>();
|
||||||
const VERSION = 'v0.0.1';
|
const VERSION = 'v0.0.1';
|
||||||
@@ -176,6 +177,8 @@ app.put('/admin/settings/email-notify', async (c) => {
|
|||||||
return c.json({ message: e.message || '保存失败' }, 500);
|
return c.json({ message: e.message || '保存失败' }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/admin/settings/email-test', testEmail);
|
||||||
app.get('/admin/settings/comments', async (c) => {
|
app.get('/admin/settings/comments', async (c) => {
|
||||||
try {
|
try {
|
||||||
const settings = await loadCommentSettings(c.env);
|
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) {
|
function parseEnabled(raw: string | undefined, defaultValue: boolean) {
|
||||||
if (raw === undefined) return defaultValue;
|
if (raw === undefined) return defaultValue;
|
||||||
return raw === '1';
|
return raw === '1';
|
||||||
|
|||||||
Reference in New Issue
Block a user