邮件服务 #
邮件服务概述 #
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>© 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