邮件通知
NEXTY.DEV 已经内置了邮件发送抽象层,支持 Resend 和 Cloudflare Email Sending 两个服务商,并封装好了常用工具函数和邮件模板。你无需从零配置,只需要按照本文步骤,就可以:
- 新增自己的邮件模板(例如订单通知、会员到期提醒等)
- 在服务端 Action 中发送邮件
- 保证发件人信息、退订链接等符合最佳实践
前置步骤
使用邮件通知功能前,请先完成任一邮件服务商配置:
配置完成后,通过环境变量 DEFAULT_EMAIL_PROVIDER 切换默认服务商,业务代码无需改动。
一、整体架构概览
在 NEXTY.DEV 中,邮件相关的核心文件有:
- 统一邮件入口:
lib/mail/index.ts - 邮件服务商实现:
lib/mail/providers/resend.ts、lib/mail/providers/cloudflare.ts - React 邮件模板渲染:
lib/mail/render.tsx - 联系人管理:
lib/mail/contacts.ts - 具体业务场景 Action 示例:
actions/newsletter/index.ts、actions/stripe/index.ts - React 邮件模板:
emails/*.tsx
推荐你把所有邮件发送调用都统一走 lib/mail 的 sendEmail,不要在业务里自己 new Resend() 或实例化 Cloudflare SDK,这样可以:
- 统一管理 API Key、发件人邮箱、退订逻辑
- 自动处理 React 模板渲染和纯文本 fallback
- 支持在 Resend 和 Cloudflare 之间无缝切换
- 自动把收件人加入联系人列表(Resend 会同步到 Contacts,Cloudflare 会记录到 Redis)
二、环境变量与基础配置
在开始写代码之前,请确保以下环境变量已经配置:
| 环境变量 | 说明 |
|---|---|
DEFAULT_EMAIL_PROVIDER | 默认邮件服务商:resend 或 cloudflare,默认 resend |
DEFAULT_ADMIN_NAME | 发件人名称(例如:NEXTY.DEV Team) |
DEFAULT_ADMIN_EMAIL | 发件人邮箱(例如:[email protected]) |
NEXT_PUBLIC_SITE_URL | 站点完整域名,用于生成邮件中的绝对链接 |
如果你使用 Resend,还需要:
RESEND_API_KEY:你的 Resend API Key
如果你使用 Cloudflare Email Sending,还需要:
CLOUDFLARE_EMAIL_API_TOKEN:Cloudflare API TokenCLOUDFLARE_ACCOUNT_ID:Cloudflare 账号 ID
在本地开发环境中,你可以在 .env.local 中这样配置(以 Cloudflare 为例):
DEFAULT_EMAIL_PROVIDER=cloudflare
DEFAULT_ADMIN_NAME="NEXTY.DEV Team"
DEFAULT_ADMIN_EMAIL="[email protected]"
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
CLOUDFLARE_EMAIL_API_TOKEN=your_cf_email_api_token
CLOUDFLARE_ACCOUNT_ID=your_cf_account_id三、核心发送工具:sendEmail
lib/mail 提供了统一的 sendEmail 方法,参数定义如下(已简化说明):
interface SendEmailOptions {
to: string | string[]; // 收件人邮箱
from: string | { email: string; name?: string }; // 发件人
subject: string; // 邮件标题
html?: string; // HTML 内容(与 react 二选一)
text?: string; // 纯文本内容(可选)
replyTo?: string | { email: string; name?: string }; // 回复邮箱
cc?: string | string[]; // 抄送
bcc?: string | string[]; // 密送
headers?: Record<string, string>; // 自定义邮件头
attachments?: EmailAttachment[]; // 附件
react?: React.ComponentType<any> | React.ReactElement; // React 邮件模板
reactProps?: Record<string, any>; // 传给模板的 props
addToContacts?: boolean; // 发送成功后是否加入联系人列表
}内部会帮你做这些事情:
- 如果
to为空,直接返回错误 - 如果环境变量没配置好,返回错误
- 如果使用 React 模板,自动渲染为 HTML,并生成纯文本 fallback
- 使用
DEFAULT_ADMIN_NAME+DEFAULT_ADMIN_EMAIL组合出发件人(你也可以在调用时覆盖) - 根据
DEFAULT_EMAIL_PROVIDER路由到对应的服务商 - 发送失败时按 HTTP 状态码自动重试一次
显式指定服务商
sendEmail 的第二个参数可以显式指定服务商,常用于 A/B 测试或需要同时走不同渠道的场景:
import { sendEmail } from "@/lib/mail";
// 使用默认服务商
await sendEmail({ ...options });
// 显式使用 Resend
await sendEmail({ ...options }, "resend");
// 显式使用 Cloudflare
await sendEmail({ ...options }, "cloudflare");四、内置示例:订阅 Newsletter 的欢迎邮件
actions/newsletter/index.ts 展示了一个完整的示例,流程大致为:
- 对用户输入的邮箱做归一化和校验
- 做频率限制(防止恶意刷接口)
- 准备好邮件标题和退订链接
- 调用
sendEmail发送NewsletterWelcomeEmail模板
片段逻辑说明(伪代码级别):
await sendEmail({
to: normalizedEmail,
from: {
email: process.env.DEFAULT_ADMIN_EMAIL!,
name: process.env.DEFAULT_ADMIN_NAME || siteConfig.name,
},
subject: `Welcome to ${siteConfig.name} Newsletter!`,
react: NewsletterWelcomeEmail,
reactProps: {
email: normalizedEmail,
unsubscribeLink,
},
headers: {
"List-Unsubscribe": `<${unsubscribeLink}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
},
addToContacts: true,
});你在自己业务中可以完全参考这套写法,只需要替换:
- 模板组件(
react字段) - 模板 props(
reactProps字段) - subject 文案
五、联系人管理
lib/mail/contacts.ts 提供了统一的联系人管理接口:
addContact(email, provider):发送成功后调用,Resend 会同步到 Resend Contacts,Cloudflare 会记录到 Redis 集合。removeContact(email):退订时调用,会同时清理 Resend Contacts 和 Redis 记录,保证切换服务商后退订仍然有效。isContact(email):判断邮箱是否在 Redis 联系人集合中。
业务代码一般不需要直接调用这些方法,sendEmail 的 addToContacts: true 会自动处理添加联系人。
六、注意事项
- 新建邮件模板时,需保持不同邮件模板风格一致,这样对你的品牌形象最有利。
- 邮件模板内品牌信息使用了
siteConfig.name和siteConfig.url,你需要确保config/site.ts中的配置正确。 - 邮件模板无法使用
lucide-react,请使用 SVG 图标或者图片绝对链接。 - 邮件模板无法使用 Tailwind CSS,请使用内联样式。
- 邮件模板内不能使用相对链接,请使用绝对链接。
- Resend 免费计划可以保存 1000 个联系人,每个月可以发送 3000 封,如果无法满足你的需求,你需要升级计划。
- Cloudflare Email Sending 要求发件人域名已经通过 DNS 验证,且当前 main 分支通过 REST API 发送,适用于服务器或 Vercel 部署;Cloudflare Workers 部署请使用 cf-pg 分支。
新建邮件模板时,让 AI 参照已有邮件模板新建模版即可,无需人工检查各个细节。