邮件系统(React Email + Resend)
邮件是 SaaS 与用户沟通的关键通道。React Email 用组件写邮件模板,Resend 提供高送达率发送——这是 Next.js 生态的最佳邮件方案。
1. 邮件系统架构
1.1 SaaS 邮件分类
| 类型 | 触发方式 | 示例 | 时效 |
|---|---|---|---|
| 事务邮件 | 用户操作触发 | 验证邮箱、密码重置、邀请 | 秒级 |
| 通知邮件 | 系统事件触发 | 支付成功、订阅到期 | 分钟级 |
| 营销邮件 | 批量发送 | 产品更新、Newsletter | 小时级 |
| 摘要邮件 | 定时发送 | 每周活动摘要 | 定时 |
1.2 服务商选型
| 服务 | 免费额度 | 特点 | 推荐 |
|---|---|---|---|
| Resend | 3000 封/月 | React Email 原生、API 简洁 | ✅ 首选 |
| SendGrid | 100 封/天 | 老牌、功能全 | 备选 |
| Postmark | 100 封/月 | 送达率最高 | 事务邮件 |
| AWS SES | 62000 封/月 | 最便宜 | 大量发送 |
2. React Email 模板
2.1 传统方式 vs React Email
| 传统方式 | React Email |
|---|---|
| 手写 HTML + 内联 CSS | React 组件 + TypeScript |
| 无法复用 | 组件化复用 |
| 无预览 | 本地实时预览 |
| 兼容性靠猜 | 内置兼容性处理 |
2.2 邀请邮件模板
// emails/invitation.tsx
import {
Body, Container, Head, Heading, Html,
Preview, Section, Text, Button, Hr,
} from '@react-email/components'
interface InvitationEmailProps {
orgName: string
inviterName: string
inviteUrl: string
}
export default function InvitationEmail({
orgName, inviterName, inviteUrl,
}: InvitationEmailProps) {
return (
<Html>
<Head />
<Preview>{inviterName} invited you to join {orgName}</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' }}>
<Container style={{ maxWidth: '480px', margin: '0 auto', padding: '20px' }}>
<Heading style={{ fontSize: '24px', color: '#1a1a1a' }}>
Join {orgName}
</Heading>
<Text style={{ color: '#4a4a4a', lineHeight: '1.6' }}>
{inviterName} has invited you to collaborate on {orgName}.
</Text>
<Section style={{ textAlign: 'center', margin: '32px 0' }}>
<Button
href={inviteUrl}
style={{
backgroundColor: '#000', color: '#fff',
padding: '12px 24px', borderRadius: '6px',
fontSize: '14px',
}}
>
Accept Invitation
</Button>
</Section>
<Hr style={{ borderColor: '#e6e6e6' }} />
<Text style={{ color: '#999', fontSize: '12px' }}>
This invite expires in 72 hours.
</Text>
</Container>
</Body>
</Html>
)
}2.3 核心组件
| 组件 | 用途 |
|---|---|
<Html> | 根元素,自动添加 doctype |
<Preview> | 收件箱预览文本 |
<Container> | 限制最大宽度 |
<Button> | CTA 按钮,自动兼容处理 |
<Img> | 图片,需用绝对 URL |
2.4 本地预览
npx react-email dev --dir emails --port 30013. Resend 发送集成
3.1 基础封装
// lib/email/index.ts
import { Resend } from 'resend'
import type { ReactElement } from 'react'
const resend = new Resend(process.env.RESEND_API_KEY!)
interface SendEmailOptions {
to: string | string[]
subject: string
react: ReactElement
tags?: { name: string; value: string }[]
}
export async function sendEmail({ to, subject, react, tags }: SendEmailOptions) {
const { data, error } = await resend.emails.send({
from: 'YourApp <[email protected]>',
to: Array.isArray(to) ? to : [to],
subject,
react,
tags,
})
if (error) throw new Error(`Email send failed: ${error.message}`)
return data
}3.2 在 Server Action 中使用
import { sendEmail } from '@/lib/email'
import InvitationEmail from '@/emails/invitation'
await sendEmail({
to: email,
subject: `Join ${orgName}`,
react: InvitationEmail({ orgName, inviterName, inviteUrl }),
tags: [{ name: 'type', value: 'invitation' }],
})4. 事务邮件模板体系
4.1 常用模板清单
| 模板 | 触发场景 | 关键内容 |
|---|---|---|
welcome | 注册成功 | 欢迎语 + 引导链接 |
verification | 邮箱验证 | 验证链接(6h 过期) |
password-reset | 忘记密码 | 重置链接(1h 过期) |
invitation | 邀请成员 | 邀请链接(72h 过期) |
payment-success | 支付成功 | 套餐信息 + 发票链接 |
payment-failed | 续费失败 | 更新支付方式链接 |
trial-ending | 试用到期 | 升级 CTA |
4.2 模板工厂
// lib/email/templates.ts
import WelcomeEmail from '@/emails/welcome'
import InvitationEmail from '@/emails/invitation'
import PaymentSuccessEmail from '@/emails/payment-success'
const TEMPLATES = {
welcome: { component: WelcomeEmail, subject: 'Welcome to YourApp' },
invitation: {
component: InvitationEmail,
subject: (p: any) => `Join ${p.orgName}`,
},
'payment-success': {
component: PaymentSuccessEmail,
subject: 'Payment confirmed',
},
} as const
export async function sendTemplateEmail<T extends keyof typeof TEMPLATES>(
to: string, template: T,
props: Parameters<(typeof TEMPLATES)[T]['component']>[0]
) {
const { component: Component, subject } = TEMPLATES[template]
const resolvedSubject = typeof subject === 'function' ? subject(props) : subject
return sendEmail({
to,
subject: resolvedSubject,
react: Component(props as any),
tags: [{ name: 'template', value: template }],
})
}5. 异步发送与队列
5.1 为什么需要队列
| 场景 | 问题 | 方案 |
|---|---|---|
| Webhook 中发邮件 | 超时导致 Webhook 重试 | 入队异步 |
| 批量邀请 | 阻塞用户操作 | 入队逐个发送 |
| 发送失败 | 丢失邮件 | 队列自动重试 |
5.2 用 Inngest 异步发送
// lib/inngest/functions/send-email.ts
import { inngest } from '../client'
import { sendTemplateEmail } from '@/lib/email/templates'
export const sendEmailFunction = inngest.createFunction(
{ id: 'send-email', retries: 3 },
{ event: 'email/send' },
async ({ event }) => {
const { to, template, props } = event.data
await sendTemplateEmail(to, template, props)
}
)
// 触发:在 Server Action 或 Webhook 中
await inngest.send({
name: 'email/send',
data: { to: '[email protected]', template: 'welcome', props: { name: 'Alice' } },
})6. 送达率与监控
6.1 域名认证(必须)
| 记录 | 用途 |
|---|---|
| SPF | 声明允许发件的 IP |
| DKIM | 邮件内容签名 |
| DMARC | 认证失败处理策略 |
6.2 送达率因素
| 因素 | 对策 |
|---|---|
| 域名声誉 | SPF/DKIM/DMARC 全配 |
| 退信率 | 注册时验证邮箱 |
| 投诉率 | 提供退订链接 |
| 内容质量 | 避免营销词汇 |
6.3 Resend Webhook 监控
// app/api/webhooks/resend/route.ts
export async function POST(request: Request) {
const event = await request.json()
switch (event.type) {
case 'email.delivered':
console.log('Delivered:', event.data.email_id)
break
case 'email.bounced':
// 标记无效邮箱,避免再次发送
await markEmailInvalid(event.data.to)
break
case 'email.complained':
// 用户标记垃圾邮件,自动退订
await unsubscribeUser(event.data.to)
break
}
return Response.json({ received: true })
}本章小结
- React Email:用 React 组件写邮件模板,TypeScript 类型安全,本地实时预览
- Resend:API 简洁,3000 封/月免费,React Email 原生支持
- 模板工厂:集中管理模板和 subject,
sendTemplateEmail一行发送 - 异步队列:Webhook/批量场景用 Inngest 入队,自动重试 3 次
- 送达率:SPF/DKIM/DMARC 必须配置,监控退信率和投诉率