Skip to content

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 } }];
}

小结

邮件与通知系统是自动化工作流的重要组成部分:

  1. 多渠道支持:邮件、Slack、微信、短信等多种通知方式
  2. 模板系统:统一的消息格式和样式管理
  3. 智能分发:基于规则的通知路由和分发
  4. 性能优化:批量发送、去重、频率控制
  5. 监控告警:及时响应系统异常和业务事件

这是我们 n8n 核心功能系列的最后一篇文章。接下来我们将进入进阶应用和实战项目部分,学习如何将这些技术组合起来解决实际问题。

记住,好的通知系统要在及时性和干扰性之间找到平衡。过多的通知会让人忽视重要信息,过少的通知可能错过关键事件。合理的规则设计和智能分发是关键。