mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 18:14:51 +00:00
feat: 添加SMTP邮件通知功能
新增功能: - 支持SMTP邮件通知平台,可通过邮件接收系统通知 - 支持配置SMTP服务器、端口、用户名、密码、发件人和收件人 - 支持TLS/SSL加密连接 - 提供美观的HTML邮件模板和纯文本备用格式 代码优化: - 重构邮件格式化逻辑,提取buildNotificationDetails减少重复代码 - 优化前端表单验证逻辑,提取validatePlatformForm统一验证 - 清理UI中的冗余提示信息和配置项 UI改进: - 移除SMTP配置说明文字 - 移除超时设置和忽略TLS证书验证选项 - 简化测试成功提示消息 - SMTP平台显示收件人邮箱而非URL 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const axios = require('axios')
|
||||
const crypto = require('crypto')
|
||||
const nodemailer = require('nodemailer')
|
||||
const logger = require('../utils/logger')
|
||||
const webhookConfigService = require('./webhookConfigService')
|
||||
const { getISOStringWithTimezone } = require('../utils/dateHelper')
|
||||
@@ -14,7 +15,8 @@ class WebhookService {
|
||||
slack: this.sendToSlack.bind(this),
|
||||
discord: this.sendToDiscord.bind(this),
|
||||
custom: this.sendToCustom.bind(this),
|
||||
bark: this.sendToBark.bind(this)
|
||||
bark: this.sendToBark.bind(this),
|
||||
smtp: this.sendToSMTP.bind(this)
|
||||
}
|
||||
this.timezone = appConfig.system.timezone || 'Asia/Shanghai'
|
||||
}
|
||||
@@ -243,6 +245,51 @@ class WebhookService {
|
||||
await this.sendHttpRequest(url, payload, platform.timeout || 10000)
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP邮件通知
|
||||
*/
|
||||
async sendToSMTP(platform, type, data) {
|
||||
try {
|
||||
// 创建SMTP传输器
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: platform.host,
|
||||
port: platform.port || 587,
|
||||
secure: platform.secure || false, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: platform.user,
|
||||
pass: platform.pass
|
||||
},
|
||||
// 可选的TLS配置
|
||||
tls: platform.ignoreTLS ? { rejectUnauthorized: false } : undefined,
|
||||
// 连接超时
|
||||
connectionTimeout: platform.timeout || 10000
|
||||
})
|
||||
|
||||
// 构造邮件内容
|
||||
const subject = this.getNotificationTitle(type)
|
||||
const htmlContent = this.formatMessageForEmail(type, data)
|
||||
const textContent = this.formatMessageForEmailText(type, data)
|
||||
|
||||
// 邮件选项
|
||||
const mailOptions = {
|
||||
from: platform.from || platform.user, // 发送者
|
||||
to: platform.to, // 接收者(必填)
|
||||
subject: `[Claude Relay Service] ${subject}`,
|
||||
text: textContent,
|
||||
html: htmlContent
|
||||
}
|
||||
|
||||
// 发送邮件
|
||||
const info = await transporter.sendMail(mailOptions)
|
||||
logger.info(`✅ 邮件发送成功: ${info.messageId}`)
|
||||
|
||||
return info
|
||||
} catch (error) {
|
||||
logger.error('SMTP邮件发送失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTTP请求
|
||||
*/
|
||||
@@ -459,6 +506,121 @@ class WebhookService {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通知详情数据
|
||||
*/
|
||||
buildNotificationDetails(data) {
|
||||
const details = []
|
||||
|
||||
if (data.accountName) {
|
||||
details.push({ label: '账号', value: data.accountName })
|
||||
}
|
||||
if (data.platform) {
|
||||
details.push({ label: '平台', value: data.platform })
|
||||
}
|
||||
if (data.status) {
|
||||
details.push({ label: '状态', value: data.status, color: this.getStatusColor(data.status) })
|
||||
}
|
||||
if (data.errorCode) {
|
||||
details.push({ label: '错误代码', value: data.errorCode, isCode: true })
|
||||
}
|
||||
if (data.reason) {
|
||||
details.push({ label: '原因', value: data.reason })
|
||||
}
|
||||
if (data.message) {
|
||||
details.push({ label: '消息', value: data.message })
|
||||
}
|
||||
if (data.quota) {
|
||||
details.push({ label: '配额', value: `${data.quota.remaining}/${data.quota.total}` })
|
||||
}
|
||||
if (data.usage) {
|
||||
details.push({ label: '使用率', value: `${data.usage}%` })
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化邮件HTML内容
|
||||
*/
|
||||
formatMessageForEmail(type, data) {
|
||||
const title = this.getNotificationTitle(type)
|
||||
const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone })
|
||||
const details = this.buildNotificationDetails(data)
|
||||
|
||||
let content = `
|
||||
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">${title}</h1>
|
||||
<p style="margin: 10px 0 0 0; opacity: 0.9;">Claude Relay Service</p>
|
||||
</div>
|
||||
<div style="background: #f8f9fa; padding: 20px; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 8px 8px;">
|
||||
<div style="background: white; padding: 16px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
`
|
||||
|
||||
// 使用统一的详情数据渲染
|
||||
details.forEach((detail) => {
|
||||
if (detail.isCode) {
|
||||
content += `<p><strong>${detail.label}:</strong> <code style="background: #f1f3f4; padding: 2px 6px; border-radius: 4px;">${detail.value}</code></p>`
|
||||
} else if (detail.color) {
|
||||
content += `<p><strong>${detail.label}:</strong> <span style="color: ${detail.color};">${detail.value}</span></p>`
|
||||
} else {
|
||||
content += `<p><strong>${detail.label}:</strong> ${detail.value}</p>`
|
||||
}
|
||||
})
|
||||
|
||||
content += `
|
||||
</div>
|
||||
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #e9ecef; font-size: 14px; color: #6c757d; text-align: center;">
|
||||
<p>发送时间: ${timestamp}</p>
|
||||
<p style="margin: 0;">此邮件由 Claude Relay Service 自动发送</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化邮件纯文本内容
|
||||
*/
|
||||
formatMessageForEmailText(type, data) {
|
||||
const title = this.getNotificationTitle(type)
|
||||
const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone })
|
||||
const details = this.buildNotificationDetails(data)
|
||||
|
||||
let content = `${title}\n`
|
||||
content += `=====================================\n\n`
|
||||
|
||||
// 使用统一的详情数据渲染
|
||||
details.forEach((detail) => {
|
||||
content += `${detail.label}: ${detail.value}\n`
|
||||
})
|
||||
|
||||
content += `\n发送时间: ${timestamp}\n`
|
||||
content += `服务: Claude Relay Service\n`
|
||||
content += `=====================================\n`
|
||||
content += `此邮件由系统自动发送,请勿回复。`
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态颜色
|
||||
*/
|
||||
getStatusColor(status) {
|
||||
const colors = {
|
||||
error: '#dc3545',
|
||||
unauthorized: '#fd7e14',
|
||||
blocked: '#6f42c1',
|
||||
disabled: '#6c757d',
|
||||
active: '#28a745',
|
||||
warning: '#ffc107'
|
||||
}
|
||||
return colors[status] || '#007bff'
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化通知详情
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user