上一个文章:
Komari 美化通知邮件模板 - 更新 3.0 版本
3.0展示:
🎉 本次更新:修复 Outlook 等客户端中文乱码问题 | 新增消息智能翻译 | 优化移动端显示 | 更丰富的服务器信息展示
✨ 3.0 版本新特性
| 特性 | 说明 |
|---|---|
| 🔧 编码修复 | 添加 UTF-8 字符集声明,彻底解决 Outlook 等客户端中文乱码 |
| 🌐 智能翻译 | 自动将英文系统消息翻译为中文(如 This is a test message → 这是一条来自 Komari 的测试通知) |
| 📱 响应式优化 | 移动端胶囊布局自动适配,小屏幕下信息不重叠 |
| 💻 硬件信息 | 新增 CPU 核心数、内存、硬盘容量展示 |
| 🌍 地区识别 | 国旗 Emoji 自动转换为文字地区代码(如 🇨🇳 → CN) |
| 📊 告警详情 | 流量/CPU/内存超限提示更直观,显示具体数值 |
🚀 快速开始
前置要求
- 支持API的邮件发送服务(Mailgun / Resend / 自建 SMTP-to-API/其他的发给AI改就可以了很简单)
- 推荐 Mailgun(每日 100 封免费)或 Resend(每月 3,000 封免费)
⚙️ 配置步骤
第一步:获取 API 凭证
📮 Mailgun 配置
| 配置项 | 获取位置 | 示例 |
|---|---|---|
key |
Settings → API Keys → Private API Key | key-xxxxxxxxxx-xxxxxxxx |
dom |
Sending → Domains | mg.yourdomain.com |
from |
自定义发件人 | Komari Monitor <[email protected]> |
📧 Resend 配置(未经过测试,AI直接改的)
| 配置项 | 获取位置 | 示例 |
|---|---|---|
key |
Dashboard → API Keys | re_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx |
from |
需先验证域名或使用测试域名 | Komari Monitor <[email protected]> |
第二步:填入 Komari
- 打开 Komari 面板 → 设置 → 通知
- 开启通知开关
- 通知渠道选择 JavaScript 代码
- 粘贴下方对应版本的代码
- 点击保存,发送测试验证
📋 完整代码
🔷 Mailgun API 版本
async function sendEvent(event) {
const C = {
key: "YOUR_MAILGUN_API_KEY", // ← 替换为你的 Private API Key
dom: "YOUR_MAILGUN_DOMAIN", // ← 替换为你的发送域名
from: "Komari Monitor <[email protected]>", // ← 自定义发件人
to: "[email protected]" // ← 接收通知的邮箱
};
// Base64 编码(兼容浏览器/Node.js)
const b64 = s => {
try { if (typeof btoa != 'undefined') return btoa(s); } catch (e) {}
try { if (typeof Buffer != 'undefined') return Buffer.from(s).toString('base64'); } catch (e) {}
let o = "", c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", i = 0, a, b, d;
while (i < s.length) {
a = s.charCodeAt(i++); b = s.charCodeAt(i++); d = s.charCodeAt(i++);
o += c.charAt(a >> 2) + c.charAt((a & 3) << 4 | b >> 4) + c.charAt((b & 15) << 2 | d >> 6) + c.charAt(d & 63).replace(64, '=');
}
return o;
};
// 字节格式化(自动转换 MB/GB)
const fmt = bytes => {
if (!bytes) return '0';
const k = 1024, gb = bytes / (k**3);
return gb < 1 ? Math.round(bytes / (k**2)) + 'M' : Math.round(gb) + 'G';
};
// 国旗 Emoji 转地区代码
const flagToTxt = str => {
if (!str) return 'UNK';
return Array.from(str).map(char => {
const code = char.codePointAt(0);
return (code >= 0x1F1E6 && code <= 0x1F1FF) ? String.fromCharCode(code - 127397) : char;
}).join('').toUpperCase();
};
// 智能翻译系统消息
const translateMsg = msg => {
if (!msg) return '';
return msg
.replace(/This is a test message from Komari\.?/ig, "这是一条来自 Komari 的测试通知。")
.replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=sum/ig, "流量或硬盘总量超限!已使用 $1 (具体数值:$2)")
.replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=cpu/ig, "CPU 负载过高!当前占用 $1 ($2)")
.replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=memory/ig, "内存占用过高!当前已用 $1 ($2)")
.replace(/type=sum/ig, "触发类型:总量累计")
.replace(/used/ig, "已使用");
};
// 发送邮件
const send = async (html, sub) => {
try {
const resp = await fetch(`https://api.mailgun.net/v3/${C.dom}/messages`, {
method: 'POST',
headers: {
'Authorization': `Basic ${b64('api:' + C.key)}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `from=${encodeURIComponent(C.from)}&to=${encodeURIComponent(C.to)}&subject=${encodeURIComponent(sub)}&html=${encodeURIComponent(html)}`
});
return resp.ok;
} catch (e) { return false; }
};
try {
const c = event.clients || [];
const map = {
'Offline': ['#e11d48', '离线警告'],
'Online': ['#059669', '恢复上线'],
'Alert': ['#d97706', '系统告警'],
'Renew': ['#2563eb', '自动续费'],
'Expire': ['#dc2626', '到期提醒'],
'Test': ['#7c3aed', '测试通知']
};
const [color, title] = map[event.event] || ['#4b5563', '系统通知'];
// 北京时间转换
const d = new Date(new Date(event.time).getTime() + 28800000);
const date = `${d.getUTCFullYear()}/${d.getUTCMonth() + 1}/${d.getUTCDate()} ${d.getUTCHours().toString().padStart(2, '0')}:${d.getUTCMinutes().toString().padStart(2, '0')}`;
const fontBase = "font-family:'MiSans',system-ui,-apple-system,BlinkMacSystemFont,sans-serif";
const translatedMessage = translateMsg(event.message);
// 服务器信息行
const rows = c.map(x => {
const cpu = x.cpu_cores ? x.cpu_cores + 'C' : '?';
const specs = `${cpu} · ${fmt(x.mem_total)} · ${fmt(x.disk_total)}`;
const ip = x.ipv4 || x.ipv6 || 'No IP';
const region = flagToTxt(x.region || 'UNK');
return `<tr>
<td style="padding:10px 0;border-bottom:1px solid #f3f4f6">
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:6px;">
<div style="flex:1 1 230px;max-width:100%;">
<div style="display:inline-block;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:3px 10px;line-height:1.2;margin-bottom:4px;max-width:100%;box-sizing:border-box;">
<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:${color};margin-right:6px;vertical-align:middle"></span>
<span style="font-weight:500;color:#1f2937;font-size:13.5px;letter-spacing:-0.2px;vertical-align:middle;word-break:break-all;">${x.name || '?'}</span>
</div>
<div style="padding-left:4px;font-size:10px;color:#9ca3af;${fontBase};letter-spacing:0.3px;">${specs}</div>
</div>
<div class="capsule-box" style="flex:0 1 auto;text-align:right;">
<div style="display:inline-block;background:#f9fafb;border:1px solid #e5e7eb;border-radius:20px;padding:4px 10px;line-height:1.2;white-space:nowrap;">
<span style="font-size:11px;color:#6b7280;vertical-align:middle;position:relative;top:-0.5px;${fontBase}">${ip}</span>
<span style="color:#e5e7eb;margin:0 1px;font-size:11px;vertical-align:middle;position:relative;top:-1px">|</span>
<span style="font-size:11px;font-weight:600;color:#4b5563;vertical-align:middle;position:relative;top:-0.5px;${fontBase}">${region}</span>
</div>
</div>
</div>
</td>
</tr>`;
}).join('');
// ✅ 3.0 关键修复:添加 charset="UTF-8" 防止乱码
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<style>@media only screen and (max-width:480px){.capsule-box{text-align:left!important;margin-top:6px!important;flex-basis:100%!important}}</style>
</head>
<body style="margin:0;padding:0;background-color:#fff">
<div style="font-family:'MiSans','MiLanPro','MiLan',ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:#fff;padding:20px 0">
<div style="width:94%;max-width:480px;margin:0 auto;background:#fff;border-radius:12px;border:0;border-top:4px solid ${color};box-shadow:inset 0 3px 6px rgba(0,0,0,0.06),0 0 0 1px #e5e7eb,0 10px 25px -5px rgba(0,0,0,0.18),0 8px 10px -6px rgba(0,0,0,0.1);overflow:hidden">
<div style="padding:24px 5%">
<h1 style="font-size:18px;margin:0;color:#111827;font-weight:600;letter-spacing:-0.5px">${title}</h1>
<p style="color:#6b7280;font-size:11px;margin:4px 0 2px 0;font-weight:500">${date}</p>
${translatedMessage ? `<div style="margin:14px 0;background:#f9fafb;border-radius:0 6px 6px 0;padding:12px 16px;color:#374151;font-size:13px;font-weight:normal;line-height:1.6;border-left:4px solid ${color};word-wrap:break-word">${translatedMessage}</div>` : ''}
${rows ? `<div style="margin-top:8px"><table style="width:100%;border-collapse:collapse">${rows}</table></div>` : ''}
</div>
<div style="background:#fafafa;padding:10px;text-align:center;font-size:10px;color:#9ca3af;border-top:1px solid #f3f4f6;font-weight:500;letter-spacing:0.5px">Komari Monitoring</div>
</div>
</div>
</body>
</html>`;
// 邮件主题
let sub = `[${title}] ` + (!c.length ? 'System' : (c.length === 1 ? c[0].name : `${c.length} Servers Update`));
if (sub.length > 80) sub = sub.slice(0, 77) + '...';
return await send(html, sub);
} catch (e) {
return await send(`Error: ${e.message}`, 'Script Error');
}
}
🔶 Resend API 版本
async function sendEvent(event) {
const C = {
key: "re_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx", // ← 替换为 Resend API Key
from: "Komari Monitor <[email protected]>", // ← 替换为验证后的发件地址
to: "[email protected]" // ← 接收通知的邮箱
};
// 字节格式化(自动转换 MB/GB)
const fmt = bytes => {
if (!bytes) return '0';
const k = 1024, gb = bytes / (k**3);
return gb < 1 ? Math.round(bytes / (k**2)) + 'M' : Math.round(gb) + 'G';
};
// 国旗 Emoji 转地区代码
const flagToTxt = str => {
if (!str) return 'UNK';
return Array.from(str).map(char => {
const code = char.codePointAt(0);
return (code >= 0x1F1E6 && code <= 0x1F1FF) ? String.fromCharCode(code - 127397) : char;
}).join('').toUpperCase();
};
// 智能翻译系统消息
const translateMsg = msg => {
if (!msg) return '';
return msg
.replace(/This is a test message from Komari\.?/ig, "这是一条来自 Komari 的测试通知。")
.replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=sum/ig, "流量或硬盘总量超限!已使用 $1 (具体数值:$2)")
.replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=cpu/ig, "CPU 负载过高!当前占用 $1 ($2)")
.replace(/used\s+([0-9.]+.*?%)\s+\((.*?)\),\s*type=memory/ig, "内存占用过高!当前已用 $1 ($2)")
.replace(/type=sum/ig, "触发类型:总量累计")
.replace(/used/ig, "已使用");
};
// 发送邮件
const send = async (html, sub) => {
try {
const resp = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${C.key}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: C.from,
to: C.to,
subject: sub,
html: html
})
});
return resp.ok;
} catch (e) { return false; }
};
try {
const c = event.clients || [];
const map = {
'Offline': ['#e11d48', '离线警告'],
'Online': ['#059669', '恢复上线'],
'Alert': ['#d97706', '系统告警'],
'Renew': ['#2563eb', '自动续费'],
'Expire': ['#dc2626', '到期提醒'],
'Test': ['#7c3aed', '测试通知']
};
const [color, title] = map[event.event] || ['#4b5563', '系统通知'];
// 北京时间转换
const d = new Date(new Date(event.time).getTime() + 28800000);
const date = `${d.getUTCFullYear()}/${d.getUTCMonth() + 1}/${d.getUTCDate()} ${d.getUTCHours().toString().padStart(2, '0')}:${d.getUTCMinutes().toString().padStart(2, '0')}`;
const fontBase = "font-family:'MiSans',system-ui,-apple-system,BlinkMacSystemFont,sans-serif";
const translatedMessage = translateMsg(event.message);
// 服务器信息行
const rows = c.map(x => {
const cpu = x.cpu_cores ? x.cpu_cores + 'C' : '?';
const specs = `${cpu} · ${fmt(x.mem_total)} · ${fmt(x.disk_total)}`;
const ip = x.ipv4 ||



bd
确实好看
Email 不能用吗。
帮顶 美观!
@蓝钦 #2 不能直接用SMTP。你可以自建一个转换一下。
推荐项目
可以可以
支持一下
信息不够我的丰富
好看的
mark一下