邮件系统(React Email + Resend)

邮件是 SaaS 与用户沟通的关键通道。React Email 用组件写邮件模板,Resend 提供高送达率发送——这是 Next.js 生态的最佳邮件方案。

1. 邮件系统架构

1.1 SaaS 邮件分类

类型触发方式示例时效
事务邮件用户操作触发验证邮箱、密码重置、邀请秒级
通知邮件系统事件触发支付成功、订阅到期分钟级
营销邮件批量发送产品更新、Newsletter小时级
摘要邮件定时发送每周活动摘要定时

1.2 服务商选型

服务免费额度特点推荐
Resend3000 封/月React Email 原生、API 简洁✅ 首选
SendGrid100 封/天老牌、功能全备选
Postmark100 封/月送达率最高事务邮件
AWS SES62000 封/月最便宜大量发送

2. React Email 模板

2.1 传统方式 vs React Email

传统方式React Email
手写 HTML + 内联 CSSReact 组件 + 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 核心组件

组件用途
&lt;Html&gt;根元素,自动添加 doctype
&lt;Preview&gt;收件箱预览文本
&lt;Container&gt;限制最大宽度
&lt;Button&gt;CTA 按钮,自动兼容处理
&lt;Img&gt;图片,需用绝对 URL

2.4 本地预览

npx react-email dev --dir emails --port 3001

3. 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 必须配置,监控退信率和投诉率