n8n 邮件与通知系统 - 构建完善的沟通机制
通知系统是自动化工作流的重要组成部分,它让我们能够及时了解系统状态、处理结果和异常情况。今天我们来学习如何在 n8n 中构建完善的邮件和通知系统。
邮件发送基础
SMTP 配置
json
{
"host": "smtp.gmail.com",
"port": 587,
"secure": false,
"auth": {
"user": "your-email@gmail.com",
"pass": "your-app-password"
},
"tls": {
"rejectUnauthorized": false
}
}
常用邮件服务配置
Gmail
json
{
"host": "smtp.gmail.com",
"port": 587,
"secure": false,
"auth": {
"user": "your-email@gmail.com",
"pass": "your-app-password"
}
}
Outlook/Hotmail
json
{
"host": "smtp-mail.outlook.com",
"port": 587,
"secure": false,
"auth": {
"user": "your-email@outlook.com",
"pass": "your-password"
}
}
企业邮箱
json
{
"host": "smtp.company.com",
"port": 465,
"secure": true,
"auth": {
"user": "username@company.com",
"pass": "password"
}
}
邮件模板系统
HTML 邮件模板
javascript
// 创建邮件模板
function createEmailTemplate(templateName, data) {
const templates = {
welcome: {
subject: '欢迎加入 {{ companyName }}!',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; text-align: center;">
<h1 style="color: white; margin: 0;">欢迎加入我们!</h1>
</div>
<div style="padding: 40px; background: #f8f9fa;">
<h2 style="color: #333;">你好,{{ userName }}!</h2>
<p style="color: #666; line-height: 1.6;">
感谢您注册 {{ companyName }}。我们很高兴您能加入我们的社区。
</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ activationLink }}"
style="background: #667eea; color: white; padding: 12px 30px;
text-decoration: none; border-radius: 5px; display: inline-block;">
激活账户
</a>
</div>
<p style="color: #999; font-size: 12px;">
如果您没有注册此账户,请忽略此邮件。
</p>
</div>
</div>
`
},
notification: {
subject: '{{ title }} - 系统通知',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: {{ headerColor }}; padding: 20px; text-align: center;">
<h2 style="color: white; margin: 0;">{{ title }}</h2>
</div>
<div style="padding: 30px; background: white; border: 1px solid #ddd;">
<div style="margin-bottom: 20px;">
<strong>时间:</strong> {{ timestamp }}
</div>
<div style="margin-bottom: 20px;">
<strong>类型:</strong>
<span style="background: {{ typeColor }}; color: white; padding: 4px 8px;
border-radius: 3px; font-size: 12px;">{{ type }}</span>
</div>
<div style="margin-bottom: 20px;">
<strong>内容:</strong>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px;
border-left: 4px solid {{ typeColor }};">
{{ content }}
</div>
{{#if details}}
<div style="margin-top: 20px;">
<strong>详细信息:</strong>
<pre style="background: #f1f1f1; padding: 10px; border-radius: 3px;
overflow-x: auto; font-size: 12px;">{{ details }}</pre>
</div>
{{/if}}
</div>
</div>
`
},
report: {
subject: '{{ reportTitle }} - {{ date }}',
html: `
<div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
<div style="background: #2c3e50; padding: 30px; text-align: center;">
<h1 style="color: white; margin: 0;">{{ reportTitle }}</h1>
<p style="color: #bdc3c7; margin: 10px 0 0 0;">{{ date }}</p>
</div>
<div style="padding: 30px; background: white;">
<!-- 摘要部分 -->
<div style="margin-bottom: 30px;">
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
执行摘要
</h2>
<div style="display: flex; justify-content: space-between; margin: 20px 0;">
{{#each summary}}
<div style="text-align: center; flex: 1;">
<div style="font-size: 24px; font-weight: bold; color: {{ color }};">
{{ value }}
</div>
<div style="color: #7f8c8d; font-size: 14px;">{{ label }}</div>
</div>
{{/each}}
</div>
</div>
<!-- 详细数据 -->
{{#if tableData}}
<div style="margin-bottom: 30px;">
<h3 style="color: #2c3e50;">详细数据</h3>
<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">
<thead>
<tr style="background: #ecf0f1;">
{{#each tableHeaders}}
<th style="padding: 12px; text-align: left; border: 1px solid #bdc3c7;">
{{ this }}
</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each tableData}}
<tr>
{{#each this}}
<td style="padding: 10px; border: 1px solid #bdc3c7;">{{ this }}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
</div>
</div>
`
}
};
const template = templates[templateName];
if (!template) {
throw new Error(`Template '${templateName}' not found`);
}
// 简单的模板引擎
let subject = template.subject;
let html = template.html;
Object.entries(data).forEach(([key, value]) => {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
subject = subject.replace(regex, value);
html = html.replace(regex, value);
});
return { subject, html };
}
// 使用邮件模板
const emailData = {
userName: items[0].json.name,
companyName: 'TechCorp',
activationLink: `https://app.techcorp.com/activate?token=${items[0].json.token}`
};
const email = createEmailTemplate('welcome', emailData);
return [{
json: {
to: items[0].json.email,
subject: email.subject,
html: email.html
}
}];
动态内容生成
javascript
// 生成报告邮件内容
function generateReportEmail(reportData) {
const { metrics, charts, period } = reportData;
// 计算摘要数据
const summary = [
{
label: '总订单数',
value: metrics.totalOrders.toLocaleString(),
color: '#3498db'
},
{
label: '总收入',
value: `¥${metrics.totalRevenue.toLocaleString()}`,
color: '#27ae60'
},
{
label: '新用户',
value: metrics.newUsers.toLocaleString(),
color: '#e74c3c'
},
{
label: '转化率',
value: `${(metrics.conversionRate * 100).toFixed(1)}%`,
color: '#f39c12'
}
];
// 生成表格数据
const tableHeaders = ['产品', '销量', '收入', '增长率'];
const tableData = metrics.topProducts.map(product => [
product.name,
product.sales.toLocaleString(),
`¥${product.revenue.toLocaleString()}`,
`${product.growth > 0 ? '+' : ''}${product.growth.toFixed(1)}%`
]);
const emailData = {
reportTitle: '销售业绩报告',
date: period,
summary: summary,
tableHeaders: tableHeaders,
tableData: tableData
};
return createEmailTemplate('report', emailData);
}
// 使用示例
const reportData = items[0].json;
const reportEmail = generateReportEmail(reportData);
return [{
json: {
to: 'manager@company.com',
cc: ['sales@company.com', 'marketing@company.com'],
subject: reportEmail.subject,
html: reportEmail.html,
attachments: reportData.charts.map(chart => ({
filename: `${chart.name}.png`,
content: chart.data,
encoding: 'base64'
}))
}
}];
邮件接收和处理
IMAP 邮件监控
javascript
// 邮件监控和处理
class EmailProcessor {
constructor(imapConfig) {
this.config = imapConfig;
this.processedEmails = new Set();
}
async processNewEmails() {
const Imap = require('imap');
const { simpleParser } = require('mailparser');
return new Promise((resolve, reject) => {
const imap = new Imap(this.config);
const results = [];
imap.once('ready', () => {
imap.openBox('INBOX', false, (err, box) => {
if (err) return reject(err);
// 搜索未读邮件
imap.search(['UNSEEN'], (err, results) => {
if (err) return reject(err);
if (results.length === 0) {
imap.end();
return resolve([]);
}
const fetch = imap.fetch(results, { bodies: '' });
fetch.on('message', (msg, seqno) => {
msg.on('body', (stream, info) => {
simpleParser(stream, (err, parsed) => {
if (err) return;
const emailData = this.extractEmailData(parsed);
if (this.shouldProcessEmail(emailData)) {
results.push(emailData);
}
});
});
msg.once('attributes', (attrs) => {
// 标记为已读
imap.addFlags(attrs.uid, '\\Seen', (err) => {
if (err) console.error('Failed to mark as read:', err);
});
});
});
fetch.once('end', () => {
imap.end();
resolve(results);
});
});
});
});
imap.once('error', reject);
imap.connect();
});
}
extractEmailData(parsed) {
return {
messageId: parsed.messageId,
from: parsed.from.text,
to: parsed.to ? parsed.to.text : '',
subject: parsed.subject,
date: parsed.date,
text: parsed.text,
html: parsed.html,
attachments: parsed.attachments || []
};
}
shouldProcessEmail(emailData) {
// 避免重复处理
if (this.processedEmails.has(emailData.messageId)) {
return false;
}
// 过滤规则
const filters = [
// 只处理特定发件人
email => email.from.includes('support@') || email.from.includes('orders@'),
// 只处理包含特定关键词的邮件
email => email.subject.includes('订单') || email.subject.includes('退款'),
// 排除自动回复
email => !email.subject.toLowerCase().includes('auto-reply')
];
const shouldProcess = filters.some(filter => filter(emailData));
if (shouldProcess) {
this.processedEmails.add(emailData.messageId);
}
return shouldProcess;
}
}
// 使用邮件处理器
const emailProcessor = new EmailProcessor({
user: process.env.EMAIL_USER,
password: process.env.EMAIL_PASSWORD,
host: 'imap.gmail.com',
port: 993,
tls: true
});
const newEmails = await emailProcessor.processNewEmails();
return newEmails.map(email => ({ json: email }));
邮件内容解析
javascript
// 解析订单确认邮件
function parseOrderEmail(emailContent) {
const orderPatterns = {
orderId: /订单号[::]\s*([A-Z0-9]+)/i,
amount: /金额[::]\s*[¥¥]?(\d+(?:\.\d{2})?)/i,
customerName: /客户[::]\s*([^\n\r]+)/i,
orderDate: /下单时间[::]\s*(\d{4}-\d{2}-\d{2})/i
};
const extractedData = {};
Object.entries(orderPatterns).forEach(([key, pattern]) => {
const match = emailContent.match(pattern);
if (match) {
extractedData[key] = match[1].trim();
}
});
// 解析商品列表
const itemPattern = /商品[::]([^]+?)(?=\n\n|\n[^\s]|$)/i;
const itemMatch = emailContent.match(itemPattern);
if (itemMatch) {
const itemsText = itemMatch[1];
const items = itemsText.split('\n')
.filter(line => line.trim())
.map(line => {
const itemMatch = line.match(/(.+?)\s+x(\d+)\s+[¥¥]?(\d+(?:\.\d{2})?)/);
if (itemMatch) {
return {
name: itemMatch[1].trim(),
quantity: parseInt(itemMatch[2]),
price: parseFloat(itemMatch[3])
};
}
return null;
})
.filter(item => item !== null);
extractedData.items = items;
}
return extractedData;
}
// 处理邮件内容
const emailText = items[0].json.text;
const orderData = parseOrderEmail(emailText);
if (orderData.orderId) {
return [{
json: {
type: 'order_confirmation',
...orderData,
processed: true,
processedAt: new Date().toISOString()
}
}];
} else {
return [{
json: {
type: 'unknown',
originalContent: emailText,
processed: false
}
}];
}
多渠道通知系统
Slack 通知
javascript
// Slack 通知系统
class SlackNotifier {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}
async sendMessage(message) {
const payload = {
text: message.text,
channel: message.channel || '#general',
username: message.username || 'n8n Bot',
icon_emoji: message.icon || ':robot_face:',
attachments: message.attachments || []
};
const response = await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Slack notification failed: ${response.statusText}`);
}
return await response.text();
}
createRichMessage(data) {
const { title, message, level, details } = data;
const colors = {
success: 'good',
warning: 'warning',
error: 'danger',
info: '#36a64f'
};
const attachment = {
color: colors[level] || colors.info,
title: title,
text: message,
fields: [],
footer: 'n8n Automation',
ts: Math.floor(Date.now() / 1000)
};
if (details) {
Object.entries(details).forEach(([key, value]) => {
attachment.fields.push({
title: key,
value: value,
short: true
});
});
}
return {
text: title,
attachments: [attachment]
};
}
}
// 使用 Slack 通知
const slack = new SlackNotifier(process.env.SLACK_WEBHOOK_URL);
const notificationData = {
title: '工作流执行完成',
message: '数据处理任务已成功完成',
level: 'success',
details: {
'处理记录数': items.length,
'执行时间': '2分30秒',
'成功率': '100%'
}
};
const slackMessage = slack.createRichMessage(notificationData);
await slack.sendMessage(slackMessage);
return [{ json: { notificationSent: true, channel: 'slack' } }];
微信通知
javascript
// 企业微信通知
async function sendWeChatNotification(message, options = {}) {
const accessToken = await getWeChatAccessToken();
const payload = {
touser: options.touser || '@all',
msgtype: 'text',
agentid: process.env.WECHAT_AGENT_ID,
text: {
content: message
}
};
const response = await fetch(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
const result = await response.json();
if (result.errcode !== 0) {
throw new Error(`WeChat notification failed: ${result.errmsg}`);
}
return result;
}
async function getWeChatAccessToken() {
const response = await fetch(
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${process.env.WECHAT_CORP_ID}&corpsecret=${process.env.WECHAT_SECRET}`
);
const result = await response.json();
if (result.errcode !== 0) {
throw new Error(`Failed to get WeChat access token: ${result.errmsg}`);
}
return result.access_token;
}
短信通知
javascript
// 短信通知系统
class SMSNotifier {
constructor(config) {
this.config = config;
}
async sendSMS(phoneNumber, message, options = {}) {
// 阿里云短信服务示例
const params = {
PhoneNumbers: phoneNumber,
SignName: this.config.signName,
TemplateCode: options.templateCode || this.config.defaultTemplate,
TemplateParam: JSON.stringify(options.templateParams || { message })
};
const response = await this.makeAPICall('SendSms', params);
if (response.Code !== 'OK') {
throw new Error(`SMS sending failed: ${response.Message}`);
}
return response;
}
async makeAPICall(action, params) {
// 实现阿里云 API 签名和调用逻辑
// 这里简化处理
const response = await fetch('https://dysmsapi.aliyuncs.com/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
Action: action,
...params,
AccessKeyId: this.config.accessKeyId,
Timestamp: new Date().toISOString(),
SignatureMethod: 'HMAC-SHA1',
SignatureVersion: '1.0'
})
});
return await response.json();
}
}
// 紧急通知短信
const smsNotifier = new SMSNotifier({
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
signName: '系统通知',
defaultTemplate: 'SMS_123456789'
});
const urgentMessage = `系统异常:${items[0].json.error},请立即处理!`;
await smsNotifier.sendSMS('13800138000', urgentMessage, {
templateCode: 'SMS_URGENT_ALERT',
templateParams: {
error: items[0].json.error,
time: new Date().toLocaleString('zh-CN')
}
});
通知规则引擎
智能通知分发
javascript
// 通知规则引擎
class NotificationRuleEngine {
constructor() {
this.rules = [];
this.channels = new Map();
}
addChannel(name, handler) {
this.channels.set(name, handler);
}
addRule(rule) {
this.rules.push(rule);
}
async processNotification(event) {
const applicableRules = this.rules.filter(rule =>
this.evaluateCondition(rule.condition, event)
);
const notifications = [];
for (const rule of applicableRules) {
for (const channelConfig of rule.channels) {
const channel = this.channels.get(channelConfig.type);
if (channel) {
try {
const message = this.formatMessage(channelConfig.template, event);
await channel.send(message, channelConfig.options);
notifications.push({
channel: channelConfig.type,
status: 'sent',
message: message.text || message.subject
});
} catch (error) {
notifications.push({
channel: channelConfig.type,
status: 'failed',
error: error.message
});
}
}
}
}
return notifications;
}
evaluateCondition(condition, event) {
// 简单的条件评估
if (condition.severity) {
if (event.severity !== condition.severity) return false;
}
if (condition.type) {
if (event.type !== condition.type) return false;
}
if (condition.source) {
if (!event.source || !event.source.includes(condition.source)) return false;
}
return true;
}
formatMessage(template, event) {
let formatted = template;
Object.entries(event).forEach(([key, value]) => {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
formatted = formatted.replace(regex, value);
});
return { text: formatted };
}
}
// 配置通知规则
const notificationEngine = new NotificationRuleEngine();
// 添加通知渠道
notificationEngine.addChannel('email', {
send: async (message, options) => {
// 发送邮件逻辑
console.log('Sending email:', message.text);
}
});
notificationEngine.addChannel('slack', {
send: async (message, options) => {
// 发送 Slack 消息逻辑
console.log('Sending Slack message:', message.text);
}
});
// 添加通知规则
notificationEngine.addRule({
condition: { severity: 'critical' },
channels: [
{
type: 'email',
template: '紧急:{{ title }} - {{ message }}',
options: { to: 'admin@company.com' }
},
{
type: 'slack',
template: '🚨 {{ title }}: {{ message }}',
options: { channel: '#alerts' }
}
]
});
notificationEngine.addRule({
condition: { type: 'order' },
channels: [
{
type: 'email',
template: '新订单通知:订单号 {{ orderId }},金额 {{ amount }}',
options: { to: 'sales@company.com' }
}
]
});
// 处理事件
const event = items[0].json;
const notifications = await notificationEngine.processNotification(event);
return [{ json: { event, notifications } }];
通知性能优化
批量通知
javascript
// 批量邮件发送
async function sendBatchEmails(emails, batchSize = 10) {
const results = [];
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
const batchPromises = batch.map(async (email) => {
try {
await sendEmail(email);
return { email: email.to, status: 'sent' };
} catch (error) {
return { email: email.to, status: 'failed', error: error.message };
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// 批次间延迟,避免触发频率限制
if (i + batchSize < emails.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
// 使用批量发送
const emails = items.map(item => ({
to: item.json.email,
subject: '系统维护通知',
html: `<p>亲爱的 ${item.json.name},系统将于今晚进行维护...</p>`
}));
const sendResults = await sendBatchEmails(emails, 5);
return [{
json: {
totalEmails: emails.length,
sent: sendResults.filter(r => r.status === 'sent').length,
failed: sendResults.filter(r => r.status === 'failed').length,
details: sendResults
}
}];
通知去重
javascript
// 通知去重机制
class NotificationDeduplicator {
constructor(ttl = 3600000) { // 1小时
this.cache = new Map();
this.ttl = ttl;
}
generateKey(notification) {
return `${notification.type}_${notification.recipient}_${notification.content}`;
}
shouldSend(notification) {
const key = this.generateKey(notification);
const now = Date.now();
if (this.cache.has(key)) {
const lastSent = this.cache.get(key);
if (now - lastSent < this.ttl) {
return false; // 重复通知,不发送
}
}
this.cache.set(key, now);
return true;
}
cleanup() {
const now = Date.now();
for (const [key, timestamp] of this.cache.entries()) {
if (now - timestamp > this.ttl) {
this.cache.delete(key);
}
}
}
}
const deduplicator = new NotificationDeduplicator();
const notification = {
type: 'error',
recipient: 'admin@company.com',
content: items[0].json.error
};
if (deduplicator.shouldSend(notification)) {
// 发送通知
await sendNotification(notification);
return [{ json: { sent: true, notification } }];
} else {
return [{ json: { sent: false, reason: 'duplicate', notification } }];
}
小结
邮件与通知系统是自动化工作流的重要组成部分:
- 多渠道支持:邮件、Slack、微信、短信等多种通知方式
- 模板系统:统一的消息格式和样式管理
- 智能分发:基于规则的通知路由和分发
- 性能优化:批量发送、去重、频率控制
- 监控告警:及时响应系统异常和业务事件
这是我们 n8n 核心功能系列的最后一篇文章。接下来我们将进入进阶应用和实战项目部分,学习如何将这些技术组合起来解决实际问题。
记住,好的通知系统要在及时性和干扰性之间找到平衡。过多的通知会让人忽视重要信息,过少的通知可能错过关键事件。合理的规则设计和智能分发是关键。