邮件服务 #

邮件服务概述 #

Heroku 应用可以通过邮件服务 Add-ons 发送事务邮件、营销邮件等。

服务对比 #

服务 免费额度 特点
SendGrid 100封/天 功能全面,API 友好
Mailgun 5000封/月 开发者友好,文档详细
Mailtrap 1000封/月 测试友好,邮箱预览

SendGrid #

安装配置 #

bash
# 安装 SendGrid
heroku addons:create sendgrid:starter

# 查看 API Key
heroku config:get SENDGRID_API_KEY

Node.js 集成 #

bash
npm install @sendgrid/mail
javascript
const sgMail = require('@sendgrid/mail');

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

// 发送简单邮件
async function sendEmail() {
  const msg = {
    to: 'recipient@example.com',
    from: 'sender@example.com',
    subject: 'Hello from Heroku',
    text: 'This is a plain text email',
    html: '<p>This is an <strong>HTML</strong> email</p>'
  };
  
  await sgMail.send(msg);
  console.log('Email sent');
}

// 发送带附件的邮件
async function sendEmailWithAttachment() {
  const msg = {
    to: 'recipient@example.com',
    from: 'sender@example.com',
    subject: 'Email with Attachment',
    text: 'Please find the attached file',
    attachments: [
      {
        content: 'base64-encoded-content',
        filename: 'document.pdf',
        type: 'application/pdf',
        disposition: 'attachment'
      }
    ]
  };
  
  await sgMail.send(msg);
}

// 使用模板
async function sendTemplateEmail() {
  const msg = {
    to: 'recipient@example.com',
    from: 'sender@example.com',
    templateId: 'd-1234567890abcdef',
    dynamicTemplateData: {
      name: 'John Doe',
      verification_url: 'https://example.com/verify?token=abc123'
    }
  };
  
  await sgMail.send(msg);
}

Python 集成 #

bash
pip install sendgrid
python
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

def send_email():
    message = Mail(
        from_email='sender@example.com',
        to_emails='recipient@example.com',
        subject='Hello from Heroku',
        html_content='<p>This is an HTML email</p>'
    )
    
    sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
    response = sg.send(message)
    
    print(f'Status: {response.status_code}')

动态模板 #

javascript
// 在 SendGrid Dashboard 创建模板
// 使用动态数据填充

const msg = {
  to: user.email,
  from: 'noreply@example.com',
  templateId: 'd-welcome-email',
  dynamicTemplateData: {
    name: user.name,
    login_url: 'https://example.com/login',
    support_email: 'support@example.com'
  }
};

await sgMail.send(msg);

批量发送 #

javascript
// 批量发送
const messages = users.map(user => ({
  to: user.email,
  from: 'noreply@example.com',
  subject: 'Newsletter',
  html: `<p>Hello ${user.name},</p><p>Here's your newsletter!</p>`
}));

// 分批发送(每批最多 1000 封)
const batchSize = 500;
for (let i = 0; i < messages.length; i += batchSize) {
  const batch = messages.slice(i, i + batchSize);
  await sgMail.send(batch);
}

Mailgun #

安装配置 #

bash
# 安装 Mailgun
heroku addons:create mailgun:starter

# 查看配置
heroku config:get MAILGUN_API_KEY
heroku config:get MAILGUN_DOMAIN

Node.js 集成 #

bash
npm install mailgun.js
javascript
const formData = require('form-data');
const Mailgun = require('mailgun.js');

const mailgun = new Mailgun(formData);
const mg = mailgun.client({
  username: 'api',
  key: process.env.MAILGUN_API_KEY
});

// 发送邮件
async function sendEmail() {
  const data = {
    from: `Excited User <mailgun@${process.env.MAILGUN_DOMAIN}>`,
    to: 'recipient@example.com',
    subject: 'Hello from Mailgun',
    text: 'This is a plain text email',
    html: '<p>This is an HTML email</p>'
  };
  
  const result = await mg.messages.create(process.env.MAILGUN_DOMAIN, data);
  console.log('Email sent:', result.id);
}

// 发送带附件的邮件
async function sendEmailWithAttachment() {
  const data = {
    from: `Sender <mailgun@${process.env.MAILGUN_DOMAIN}>`,
    to: 'recipient@example.com',
    subject: 'Email with Attachment',
    text: 'Please find the attached file',
    attachment: [{
      filename: 'document.pdf',
      data: Buffer.from('...').toString('base64')
    }]
  };
  
  await mg.messages.create(process.env.MAILGUN_DOMAIN, data);
}

// 使用模板
async function sendTemplateEmail() {
  const data = {
    from: `Sender <mailgun@${process.env.MAILGUN_DOMAIN}>`,
    to: 'recipient@example.com',
    subject: 'Welcome',
    template: 'welcome-email',
    'h:X-Mailgun-Variables': JSON.stringify({
      name: 'John Doe',
      verification_url: 'https://example.com/verify'
    })
  };
  
  await mg.messages.create(process.env.MAILGUN_DOMAIN, data);
}

Python 集成 #

bash
pip install requests
python
import os
import requests

def send_email():
    return requests.post(
        f"https://api.mailgun.net/v3/{os.environ['MAILGUN_DOMAIN']}/messages",
        auth=("api", os.environ["MAILGUN_API_KEY"]),
        data={
            "from": f"Excited User <mailgun@{os.environ['MAILGUN_DOMAIN']}>",
            "to": ["recipient@example.com"],
            "subject": "Hello from Mailgun",
            "text": "This is a plain text email"
        }
    )

Webhook 处理 #

javascript
// 处理邮件事件 Webhook
app.post('/webhooks/mailgun', express.urlencoded({ extended: false }), (req, res) => {
  const { event, recipient, timestamp, token, signature } = req.body;
  
  // 验证签名
  const expectedSignature = crypto
    .createHmac('sha256', process.env.MAILGUN_API_KEY)
    .update(timestamp + token)
    .digest('hex');
  
  if (signature !== expectedSignature) {
    return res.status(401).send('Invalid signature');
  }
  
  // 处理事件
  switch (event) {
    case 'delivered':
      console.log(`Email delivered to ${recipient}`);
      break;
    case 'opened':
      console.log(`Email opened by ${recipient}`);
      break;
    case 'clicked':
      console.log(`Link clicked by ${recipient}`);
      break;
    case 'bounced':
      console.log(`Email bounced for ${recipient}`);
      break;
    case 'complained':
      console.log(`Spam complaint from ${recipient}`);
      break;
  }
  
  res.status(200).send('OK');
});

邮件模板 #

HTML 模板最佳实践 #

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Welcome</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0">
    <tr>
      <td align="center" style="padding: 20px;">
        <table width="600" cellpadding="0" cellspacing="0">
          <!-- Header -->
          <tr>
            <td style="background-color: #4A90D9; padding: 20px; text-align: center;">
              <h1 style="color: white; margin: 0;">Welcome!</h1>
            </td>
          </tr>
          
          <!-- Content -->
          <tr>
            <td style="padding: 20px; background-color: #f9f9f9;">
              <p>Hello {{name}},</p>
              <p>Thank you for signing up!</p>
              <p>
                <a href="{{verification_url}}" 
                   style="display: inline-block; padding: 10px 20px; 
                          background-color: #4A90D9; color: white; 
                          text-decoration: none; border-radius: 5px;">
                  Verify Email
                </a>
              </p>
            </td>
          </tr>
          
          <!-- Footer -->
          <tr>
            <td style="padding: 20px; text-align: center; color: #666;">
              <p>&copy; 2024 My App. All rights reserved.</p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>

模板变量 #

javascript
// SendGrid 动态模板数据
const templateData = {
  name: 'John Doe',
  email: 'john@example.com',
  verification_url: 'https://example.com/verify?token=abc',
  support_url: 'https://example.com/support',
  unsubscribe_url: 'https://example.com/unsubscribe?email=john@example.com'
};

// Mailgun 模板变量
const templateVariables = {
  name: 'John Doe',
  verification_url: 'https://example.com/verify'
};

邮件队列 #

使用 BullMQ #

javascript
const { Queue, Worker } = require('bullmq');
const sgMail = require('@sendgrid/mail');

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

// 创建邮件队列
const emailQueue = new Queue('emails', {
  connection: { host: 'localhost', port: 6379 }
});

// 添加邮件任务
async function queueEmail(to, subject, content) {
  await emailQueue.add('send', {
    to,
    subject,
    content,
    createdAt: new Date().toISOString()
  });
}

// 处理邮件任务
const worker = new Worker('emails', async job => {
  const { to, subject, content } = job.data;
  
  try {
    await sgMail.send({
      to,
      from: 'noreply@example.com',
      subject,
      html: content
    });
    
    console.log(`Email sent to ${to}`);
    return { success: true };
  } catch (error) {
    console.error(`Failed to send email: ${error.message}`);
    throw error;
  }
}, {
  connection: { host: 'localhost', port: 6379 },
  attempts: 3,
  backoff: {
    type: 'exponential',
    delay: 1000
  }
});

重试策略 #

javascript
const worker = new Worker('emails', processEmail, {
  attempts: 5,
  backoff: {
    type: 'exponential',
    delay: 2000
  }
});

worker.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed after ${job.attemptsMade} attempts:`, err);
  
  if (job.attemptsMade >= job.opts.attempts) {
    // 发送失败通知
    notifyFailedEmail(job.data);
  }
});

测试邮件 #

使用 Mailtrap #

bash
# 安装 Mailtrap
heroku addons:create mailtrap:free

# 查看配置
heroku config | grep MAILTRAP
javascript
const nodemailer = require('nodemailer');

// Mailtrap 配置
const transporter = nodemailer.createTransport({
  host: 'smtp.mailtrap.io',
  port: 2525,
  auth: {
    user: process.env.MAILTRAP_USER,
    pass: process.env.MAILTRAP_PASSWORD
  }
});

// 发送测试邮件
async function sendTestEmail() {
  await transporter.sendMail({
    from: 'test@example.com',
    to: 'recipient@example.com',
    subject: 'Test Email',
    html: '<p>This is a test email</p>'
  });
}

本地开发配置 #

javascript
// 开发环境使用 Mailtrap
const transporter = nodemailer.createTransport(
  process.env.NODE_ENV === 'production'
    ? {
        service: 'SendGrid',
        auth: {
          user: 'apikey',
          pass: process.env.SENDGRID_API_KEY
        }
      }
    : {
        host: 'smtp.mailtrap.io',
        port: 2525,
        auth: {
          user: process.env.MAILTRAP_USER,
          pass: process.env.MAILTRAP_PASSWORD
        }
      }
);

最佳实践 #

1. 发件人设置 #

javascript
// 使用一致的发件人
const FROM_EMAIL = process.env.FROM_EMAIL || 'noreply@example.com';
const FROM_NAME = process.env.FROM_NAME || 'My App';

const msg = {
  from: `${FROM_NAME} <${FROM_EMAIL}>`,
  // ...
};

2. 退订链接 #

html
<p style="font-size: 12px; color: #666;">
  You received this email because you signed up for My App.
  <a href="{{unsubscribe_url}}">Unsubscribe</a>
</p>

3. 邮件验证 #

javascript
// 验证邮箱格式
function validateEmail(email) {
  const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return re.test(email);
}

// 发送前验证
if (!validateEmail(recipient)) {
  throw new Error('Invalid email address');
}

4. 速率限制 #

javascript
// 使用 Redis 限流
const redis = new Redis(process.env.REDIS_URL);

async function checkRateLimit(userId, limit = 100) {
  const key = `email:rate:${userId}`;
  const count = await redis.incr(key);
  
  if (count === 1) {
    await redis.expire(key, 3600); // 1小时
  }
  
  return count <= limit;
}

// 发送前检查
if (!await checkRateLimit(userId)) {
  throw new Error('Rate limit exceeded');
}

故障排查 #

邮件未发送 #

bash
# 检查配置
heroku config | grep SENDGRID

# 检查日志
heroku logs --tail | grep -i email

# 测试 API 连接
heroku run "curl -X POST https://api.sendgrid.com/v3/mail/send \
  -H 'Authorization: Bearer $SENDGRID_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{\"personalizations\":[{\"to\":[{\"email\":\"test@example.com\"}]}],\"from\":{\"email\":\"sender@example.com\"},\"subject\":\"Test\",\"content\":[{\"type\":\"text/plain\",\"value\":\"Test\"}]}'"

邮件进入垃圾箱 #

markdown
## 避免垃圾箱检查清单
- [ ] 设置 SPF 记录
- [ ] 设置 DKIM 签名
- [ ] 设置 DMARC 记录
- [ ] 使用一致的发件人
- [ ] 避免垃圾邮件关键词
- [ ] 包含退订链接
- [ ] 使用纯文本版本

下一步 #

邮件服务掌握后,接下来学习 搜索服务 了解全文搜索配置!

最后更新:2026-03-28