邮件服务
Resend 邮件集成完整指南
邮件服务
Sistine Starter 使用 Resend 作为邮件服务提供商,支持交易邮件(验证邮件、密码重置、购买确认等)和 Newsletter 订阅功能。本文档详细介绍 Resend 的配置步骤、邮件发送方法、模板展示和最佳实践。
Resend 简介
Resend 是一个现代化的开发者邮件平台,专为应用程序设计,具有以下特点:
- 开发者友好: 简洁的 API 设计,完整的 TypeScript 支持
- 高送达率: 优化的邮件基础设施,确保邮件到达收件箱
- 域名验证: 支持自定义域名,提升品牌可信度
- 免费额度: 每月 3,000 封免费邮件 (足够开发和小规模使用)
- 测试邮箱: 开发环境可使用
onboarding@resend.dev
无需域名验证
配置步骤
1. 创建 Resend 账号
- 访问 Resend 并注册账号
- 登录后进入 Dashboard
2. 获取 API Key
- 在 Resend Dashboard 中导航到 API Keys
- 点击 Create API Key
- 输入名称 (如 "Sistine Starter Production")
- 选择权限: Sending access (发送邮件)
- 复制生成的 API Key (格式:
re_xxxxxxxxxxxxx
)
3. 配置环境变量
将以下环境变量添加到 .env.local
:
# Resend API Key (必需)
RESEND_API_KEY="re_xxxxxxxxxxxxx"
# 开发环境可以不配置发件邮箱,会自动使用测试邮箱
# RESEND_FROM_EMAIL 未设置时,默认使用:
# onboarding@resend.dev (仅开发环境有效)
配置优先级:
- 如果设置了
RESEND_FROM_EMAIL
,直接使用该地址 - 如果是开发环境 (
NODE_ENV=development
),使用onboarding@resend.dev
- 如果是生产环境,使用
noreply@{RESEND_VERIFIED_DOMAIN}
4. 验证域名 (生产环境必需)
为了在生产环境发送邮件,你需要验证自己的域名。
步骤 A: 添加域名
- 在 Resend Dashboard 中导航到 Domains
- 点击 Add Domain
- 输入你的域名 (如
yourdomain.com
) - 选择 Region: 选择离用户最近的区域 (如
us-east-1
)
步骤 B: 配置 DNS 记录
Resend 会提供需要添加的 DNS 记录,包括:
SPF 记录 (Sender Policy Framework):
类型: TXT
名称: @
值: v=spf1 include:_spf.resend.com ~all
DKIM 记录 (DomainKeys Identified Mail):
类型: TXT
名称: resend._domainkey
值: (Resend 提供的长字符串)
DMARC 记录 (可选,推荐):
类型: TXT
名称: _dmarc
值: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
步骤 C: 验证域名
- 在你的 DNS 提供商 (如 Cloudflare、AWS Route53、Namecheap) 中添加上述记录
- 返回 Resend Dashboard,点击 Verify Domain
- 等待验证完成 (通常几分钟内,最多 48 小时)
- 验证成功后,状态会显示为 Verified
DNS 配置示例
# 登录 Cloudflare Dashboard
# 选择你的域名 > DNS > Add record
# SPF 记录
Type: TXT
Name: @
Content: v=spf1 include:_spf.resend.com ~all
Proxy status: DNS only (灰色云朵)
# DKIM 记录
Type: TXT
Name: resend._domainkey
Content: (从 Resend 复制)
Proxy status: DNS only
5. 测试邮件发送
配置完成后,使用 Resend Dashboard 的 Send Test Email 功能测试:
- 导航到 Emails > Send Test Email
- 输入收件地址
- 发送并检查是否收到
或在代码中测试:
import { sendEmail } from "@/lib/email";
await sendEmail({
to: "your-email@example.com",
subject: "Test Email from Sistine AI",
html: "<p>Hello! This is a test email.</p>",
});
邮件发送方法
核心邮件逻辑位于 lib/email.ts
。
通用邮件发送函数
sendEmail
是所有邮件发送的基础函数:
export interface SendEmailOptions {
to: string | string[]; // 收件人 (支持单个或多个)
subject: string; // 邮件主题
react?: React.ReactElement; // React 邮件组件 (推荐)
html?: string; // HTML 内容
text?: string; // 纯文本内容 (可选)
from?: string; // 发件人 (可选,默认使用环境变量)
replyTo?: string; // 回复地址 (可选)
}
export async function sendEmail(options: SendEmailOptions) {
try {
const data = await resend.emails.send({
to: options.to,
subject: options.subject,
react: options.react,
html: options.html,
text: options.text,
from: options.from || DEFAULT_FROM_EMAIL,
replyTo: options.replyTo,
});
return { success: true, data };
} catch (error) {
console.error('Failed to send email:', error);
return { success: false, error };
}
}
使用示例:
// 发送 HTML 邮件
await sendEmail({
to: "user@example.com",
subject: "Welcome to Sistine AI",
html: "<h1>Welcome!</h1><p>Thank you for joining us.</p>",
});
// 发送给多个收件人
await sendEmail({
to: ["user1@example.com", "user2@example.com"],
subject: "System Maintenance Notice",
html: "<p>Our system will be under maintenance...</p>",
});
// 自定义发件人和回复地址
await sendEmail({
to: "support@example.com",
subject: "Support Request from User",
html: "<p>User needs help with...</p>",
from: "Support <support@yourdomain.com>",
replyTo: "user@example.com",
});
邮件模板展示
Sistine Starter 提供了多种预定义的邮件模板,开箱即用。
1. 验证邮件
用户注册后发送邮箱验证链接。
函数: sendVerificationEmail(email: string, token: string)
使用场景:
- 用户注册后验证邮箱
- 修改邮箱地址后重新验证
示例代码:
import { sendVerificationEmail } from "@/lib/email";
// 生成验证 Token
const token = randomUUID();
// 存储 Token 到数据库
await db.insert(verificationToken).values({
userId,
token,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 小时有效
});
// 发送验证邮件
await sendVerificationEmail(user.email, token);
邮件效果:
主题: Verify your email - Sistine AI
内容:
┌─────────────────────────────────────┐
│ Welcome to Sistine AI! │
│ │
│ Please click the link below to │
│ verify your email address: │
│ │
│ [Verify Email] (黑色按钮) │
│ │
│ Or copy this link to your browser: │
│ https://yourdomain.com/verify-email?token=xxx
│ │
│ If you didn't sign up for Sistine │
│ AI, you can safely ignore this │
│ email. │
└─────────────────────────────────────┘
模板代码 (可在 lib/email.ts:72-93
自定义):
export async function sendVerificationEmail(email: string, token: string) {
const verificationUrl = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}`;
return sendEmail({
to: email,
subject: 'Verify your email - Sistine AI',
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Welcome to Sistine AI!</h1>
<p>Please click the link below to verify your email address:</p>
<a href="${verificationUrl}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 6px; margin: 20px 0;">
Verify Email
</a>
<p>Or copy this link to your browser:</p>
<p style="color: #666; word-break: break-all;">${verificationUrl}</p>
<p style="color: #999; font-size: 14px; margin-top: 30px;">
If you didn't sign up for Sistine AI, you can safely ignore this email.
</p>
</div>
`,
});
}
2. 密码重置邮件
用户忘记密码时发送重置链接。
函数: sendPasswordResetEmail(email: string, token: string)
使用场景:
- 用户点击"忘记密码"
- 管理员为用户重置密码
示例代码:
import { sendPasswordResetEmail } from "@/lib/email";
import { randomUUID } from "crypto";
// 生成重置 Token
const token = randomUUID();
// 存储到数据库 (1 小时有效)
await db.insert(passwordResetToken).values({
userId: user.id,
token,
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
});
// 发送邮件
await sendPasswordResetEmail(user.email, token);
邮件效果:
主题: Reset your password - Sistine AI
内容:
┌─────────────────────────────────────┐
│ Password Reset Request │
│ │
│ We received a request to reset │
│ your password. Click the link │
│ below to reset it: │
│ │
│ [Reset Password] (黑色按钮) │
│ │
│ Or copy this link to your browser: │
│ https://yourdomain.com/reset-password?token=xxx
│ │
│ This link will expire in 1 hour. │
│ If you didn't request a password │
│ reset, you can safely ignore this │
│ email. │
└─────────────────────────────────────┘
3. 欢迎邮件
新用户注册后发送欢迎邮件。
函数: sendWelcomeEmail(email: string, name?: string)
使用场景:
- 用户完成注册后
- 邮箱验证成功后
示例代码:
import { sendWelcomeEmail } from "@/lib/email";
// 在用户注册成功后调用
await sendWelcomeEmail(user.email, user.name);
邮件效果:
主题: Welcome to Sistine AI!
内容:
┌─────────────────────────────────────┐
│ Welcome to Sistine AI, John! │
│ │
│ Thank you for joining us! We're │
│ excited to have you on board. │
│ │
│ Here's what you can do next: │
│ • Complete your profile │
│ • Explore our features │
│ • Try the demo │
│ • Check out our documentation │
│ │
│ [Go to Dashboard] (黑色按钮) │
│ │
│ If you have any questions, feel │
│ free to contact our support team. │
└─────────────────────────────────────┘
4. 购买确认邮件
用户完成支付后发送购买详情。
函数: sendPurchaseEmail(email: string, orderDetails: any)
使用场景:
- Creem Webhook 处理支付成功后
- 手动发放积分时
示例代码:
import { sendPurchaseEmail } from "@/lib/email";
await sendPurchaseEmail(user.email, {
orderId: "order_123",
plan: "pro_monthly",
amount: "$99.00 USD",
credits: 10000,
type: "subscription",
});
邮件效果:
主题: Purchase Confirmation - Sistine AI
内容:
┌─────────────────────────────────────┐
│ Purchase Successful! │
│ │
│ Thank you for your purchase. Here │
│ are your order details: │
│ │
│ ┌─────────────────────────────┐ │
│ │ Order ID: order_123 │ │
│ │ Product: pro_monthly │ │
│ │ Amount: $99.00 USD │ │
│ │ Credits Added: 10000 │ │
│ │ Type: Monthly Subscription │ │
│ └─────────────────────────────┘ │
│ │
│ [View Dashboard] (黑色按钮) │
│ │
│ Thank you for choosing Sistine AI! │
└─────────────────────────────────────┘
订单详情对象:
interface OrderDetails {
orderId: string; // 订单 ID
plan: string; // 计划名称或积分包 Key
amount: string; // 金额 (格式化后的字符串)
credits: number; // 发放的积分数
type: "subscription" | "one_time"; // 类型
}
5. 订阅到期提醒
订阅即将到期时提醒用户续费。
函数: sendSubscriptionExpiryReminder(email: string, daysRemaining: number)
使用场景:
- 定时任务检查订阅到期时间
- 提前 7 天、3 天、1 天发送提醒
示例代码:
import { sendSubscriptionExpiryReminder } from "@/lib/email";
// 查询即将到期的订阅
const expiringSoon = await db
.select()
.from(subscription)
.where(
sql`${subscription.currentPeriodEnd} <= NOW() + INTERVAL '7 days'
AND ${subscription.currentPeriodEnd} > NOW()`
);
for (const sub of expiringSoon) {
const daysRemaining = Math.ceil(
(sub.currentPeriodEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
await sendSubscriptionExpiryReminder(sub.userEmail, daysRemaining);
}
邮件效果:
主题: Your subscription expires in 7 days - Sistine AI
内容:
┌─────────────────────────────────────┐
│ Subscription Expiry Reminder │
│ │
│ Your Sistine AI subscription will │
│ expire in 7 days. │
│ │
│ To continue enjoying uninterrupted │
│ access to our services, please │
│ renew your subscription. │
│ │
│ [Renew Subscription] (黑色按钮) │
│ │
│ If you have any questions, please │
│ contact our support team. │
└─────────────────────────────────────┘
6. 积分不足提醒
用户积分余额过低时发送提醒。
函数: sendLowCreditsNotification(email: string, remainingCredits: number)
使用场景:
- 用户积分低于阈值 (如 50 积分)
- 用户尝试使用功能但积分不足
示例代码:
import { sendLowCreditsNotification } from "@/lib/email";
import { getUserCredits } from "@/lib/credits";
// 在 AI API 中检查积分
const credits = await getUserCredits(userId);
if (credits < 50 && credits > 0) {
// 发送提醒 (可设置节流,避免频繁发送)
await sendLowCreditsNotification(user.email, credits);
}
邮件效果:
主题: Low Credits Alert - Sistine AI
内容:
┌─────────────────────────────────────┐
│ Low Credits Alert │
│ │
│ You have only 20 credits remaining │
│ in your account. │
│ │
│ To continue using our AI services │
│ without interruption, consider │
│ purchasing more credits. │
│ │
│ [Buy More Credits] (黑色按钮) │
│ │
│ Need help? Contact our support │
│ team. │
└─────────────────────────────────────┘
Newsletter 订阅功能
Sistine Starter 支持用户订阅 Newsletter,使用 Resend 的 Contacts API。
数据库表结构
Newsletter 订阅依赖 newsletter_subscription
表,建表语句请参考 数据库
文档中的 Newsletter 章节。
订阅流程
前端表单:
// components/newsletter-form.tsx
"use client";
import { useState } from "react";
export function NewsletterForm() {
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch("/api/newsletter/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (res.ok) {
setMessage("Successfully subscribed! Check your email.");
setEmail("");
} else {
setMessage(data.error || "Failed to subscribe");
}
} catch (error) {
setMessage("Something went wrong");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={loading}>
{loading ? "Subscribing..." : "Subscribe"}
</button>
{message && <p>{message}</p>}
</form>
);
}
后端 API (app/api/newsletter/subscribe/route.ts
):
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { newsletterSubscription } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { randomUUID } from "crypto";
export async function POST(req: NextRequest) {
try {
const { email } = await req.json();
if (!email || !email.includes("@")) {
return NextResponse.json(
{ error: "Invalid email" },
{ status: 400 }
);
}
// 检查是否已订阅
const existing = await db
.select()
.from(newsletterSubscription)
.where(eq(newsletterSubscription.email, email));
if (existing.length > 0) {
if (existing[0].status === "active") {
return NextResponse.json(
{ error: "Already subscribed" },
{ status: 400 }
);
} else {
// 重新激活
await db
.update(newsletterSubscription)
.set({ status: "active", unsubscribedAt: null })
.where(eq(newsletterSubscription.email, email));
return NextResponse.json({ message: "Resubscribed successfully" });
}
}
// 插入新订阅
await db.insert(newsletterSubscription).values({
id: randomUUID(),
email,
status: "active",
});
// 发送欢迎邮件
await sendEmail({
to: email,
subject: "Welcome to Sistine AI Newsletter",
html: `
<p>Thank you for subscribing to our newsletter!</p>
<p>You'll receive updates about new features, AI insights, and more.</p>
`,
});
return NextResponse.json({ message: "Subscribed successfully" });
} catch (error) {
console.error("Newsletter subscription error:", error);
return NextResponse.json(
{ error: "Failed to subscribe" },
{ status: 500 }
);
}
}
取消订阅
API 端点 (app/api/newsletter/unsubscribe/route.ts
):
export async function POST(req: NextRequest) {
const { email } = await req.json();
await db
.update(newsletterSubscription)
.set({
status: "unsubscribed",
unsubscribedAt: new Date(),
})
.where(eq(newsletterSubscription.email, email));
return NextResponse.json({ message: "Unsubscribed successfully" });
}
邮件模板中添加取消订阅链接:
<p style="color: #999; font-size: 12px; margin-top: 40px;">
You're receiving this email because you subscribed to Sistine AI Newsletter.
<a href="https://yourdomain.com/unsubscribe?email=${email}">Unsubscribe</a>
</p>
发送 Newsletter
创建管理员接口发送群发邮件:
// app/api/admin/newsletter/send/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { newsletterSubscription } from "@/lib/db/schema";
import { sendEmail } from "@/lib/email";
import { auth } from "@/lib/auth";
import { eq } from "drizzle-orm";
export async function POST(req: NextRequest) {
// 验证管理员权限
const session = await auth.api.getSession({ headers: req.headers });
if (session?.user?.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { subject, html } = await req.json();
// 获取所有活跃订阅者
const subscribers = await db
.select()
.from(newsletterSubscription)
.where(eq(newsletterSubscription.status, "active"));
// 批量发送 (Resend 支持批量发送)
const emailPromises = subscribers.map(sub =>
sendEmail({
to: sub.email,
subject,
html,
})
);
await Promise.all(emailPromises);
return NextResponse.json({
message: `Sent to ${subscribers.length} subscribers`
});
}
国际化支持
邮件内容可以根据用户语言偏好进行国际化。
方法 1: 使用条件渲染
export async function sendWelcomeEmail(
email: string,
name?: string,
locale: string = "en"
) {
const messages = {
en: {
subject: "Welcome to Sistine AI!",
greeting: `Welcome to Sistine AI${name ? ', ' + name : ''}!`,
body: "Thank you for joining us! We're excited to have you on board.",
},
zh: {
subject: "欢迎加入 Sistine AI!",
greeting: `欢迎加入 Sistine AI${name ? ', ' + name : ''}!`,
body: "感谢您的加入!我们很高兴您能成为我们的一员。",
},
};
const msg = messages[locale] || messages.en;
return sendEmail({
to: email,
subject: msg.subject,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1>${msg.greeting}</h1>
<p>${msg.body}</p>
<!-- ... -->
</div>
`,
});
}
方法 2: 使用 next-intl
import { getTranslations } from 'next-intl/server';
export async function sendWelcomeEmail(
email: string,
locale: string = "en"
) {
const t = await getTranslations({ locale, namespace: 'email' });
return sendEmail({
to: email,
subject: t('welcome.subject'),
html: `
<div>
<h1>${t('welcome.greeting')}</h1>
<p>${t('welcome.body')}</p>
</div>
`,
});
}
翻译文件 (messages/en/email.json
):
{
"welcome": {
"subject": "Welcome to Sistine AI!",
"greeting": "Welcome to Sistine AI!",
"body": "Thank you for joining us!"
}
}
最佳实践
1. 使用 React Email 组件 (推荐)
React Email 提供可重用的 React 组件来构建邮件模板:
安装依赖:
pnpm add @react-email/components
创建邮件组件 (emails/welcome.tsx
):
import { Html, Head, Body, Container, Heading, Text, Button } from '@react-email/components';
interface WelcomeEmailProps {
name?: string;
dashboardUrl: string;
}
export function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto' }}>
<Heading style={{ color: '#333' }}>
Welcome to Sistine AI{name ? ', ' + name : ''}!
</Heading>
<Text>
Thank you for joining us! We're excited to have you on board.
</Text>
<Button
href={dashboardUrl}
style={{
backgroundColor: '#000',
color: '#fff',
padding: '12px 24px',
borderRadius: '6px',
textDecoration: 'none',
}}
>
Go to Dashboard
</Button>
</Container>
</Body>
</Html>
);
}
使用组件:
import { render } from '@react-email/render';
import { WelcomeEmail } from '@/emails/welcome';
await sendEmail({
to: email,
subject: 'Welcome to Sistine AI',
react: <WelcomeEmail name={name} dashboardUrl="https://yourdomain.com/dashboard" />,
});
2. 错误处理和重试
邮件发送可能失败,需要适当处理:
export async function sendEmailWithRetry(
options: SendEmailOptions,
maxRetries: number = 3
) {
let lastError: any;
for (let i = 0; i < maxRetries; i++) {
const result = await sendEmail(options);
if (result.success) {
return result;
}
lastError = result.error;
// 等待一段时间后重试 (指数退避)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
console.error('Failed to send email after retries:', lastError);
return { success: false, error: lastError };
}
3. 邮件发送日志
记录所有邮件发送情况:
import { db } from "@/lib/db";
import { emailLog } from "@/lib/db/schema";
import { randomUUID } from "crypto";
export async function sendEmailWithLogging(options: SendEmailOptions) {
const logId = randomUUID();
// 记录发送尝试
await db.insert(emailLog).values({
id: logId,
to: Array.isArray(options.to) ? options.to.join(',') : options.to,
subject: options.subject,
status: "pending",
});
const result = await sendEmail(options);
// 更新日志
await db
.update(emailLog)
.set({
status: result.success ? "sent" : "failed",
error: result.success ? null : JSON.stringify(result.error),
sentAt: result.success ? new Date() : null,
})
.where(eq(emailLog.id, logId));
return result;
}
邮件日志表结构
建表语句请参考 数据库
文档中的 email_log
表定义。
4. 避免被标记为垃圾邮件
- 使用已验证的域名: 确保 SPF、DKIM、DMARC 记录正确配置
- 添加取消订阅链接: 所有营销邮件必须包含取消订阅功能
- 避免垃圾词汇: 不要在主题中使用 "Free", "Act Now", "Guaranteed" 等词汇
- 保持良好的发送频率: 不要短时间内大量发送
- 提供纯文本版本: 除了 HTML,也提供
text
参数
await sendEmail({
to: email,
subject: "Welcome to Sistine AI",
html: "<h1>Welcome!</h1><p>Thank you for joining us.</p>",
text: "Welcome!\n\nThank you for joining us.", // 纯文本版本
});
5. 邮件模板测试
在发送给真实用户前,先进行测试:
创建测试端点 (app/api/test/email/route.ts
):
import { NextRequest, NextResponse } from "next/server";
import { sendWelcomeEmail } from "@/lib/email";
export async function GET(req: NextRequest) {
// 仅在开发环境启用
if (process.env.NODE_ENV !== "development") {
return NextResponse.json({ error: "Not available" }, { status: 404 });
}
const email = req.nextUrl.searchParams.get("email") || "test@example.com";
await sendWelcomeEmail(email, "Test User");
return NextResponse.json({ message: `Email sent to ${email}` });
}
访问 http://localhost:3000/api/test/email?email=your-email@example.com
进行测试。
6. 邮件发送限流
避免短时间内向同一用户发送大量邮件:
import { rateLimiter } from "@/lib/rate-limiter";
export async function sendEmailWithRateLimit(
userId: string,
options: SendEmailOptions
) {
// 限制每用户每小时最多 10 封邮件
const allowed = await rateLimiter.checkLimit(
`email:${userId}`,
10,
60 * 60 * 1000
);
if (!allowed) {
console.warn(`Rate limit exceeded for user ${userId}`);
return { success: false, error: "Rate limit exceeded" };
}
return sendEmail(options);
}
7. 监控邮件送达率
定期检查 Resend Dashboard 中的送达率指标:
- Delivered: 成功送达的邮件数
- Bounced: 被退回的邮件 (邮箱不存在等)
- Opened: 邮件打开率 (需要启用追踪)
- Clicked: 链接点击率
如果送达率低于 95%,需要检查:
- DNS 配置是否正确
- 邮件内容是否触发垃圾邮件过滤器
- 收件人列表是否包含无效邮箱
常见问题
Q: 开发环境无法发送邮件?
解决方法:
- 确认
RESEND_API_KEY
已正确配置 - 使用 Resend 的测试邮箱
onboarding@resend.dev
(无需域名验证) - 检查 Resend Dashboard 的 Logs 查看错误信息
Q: 生产环境邮件进入垃圾箱?
解决方法:
- 确认域名已验证,并正确配置 SPF/DKIM/DMARC 记录
- 在邮件中添加取消订阅链接
- 避免使用垃圾词汇
- 提供纯文本版本
- 使用真实的
from
地址 (不要使用noreply@gmail.com
)
Q: 如何批量发送邮件?
Resend 支持批量发送 API:
import { resend } from "@/lib/email";
await resend.batch.send([
{
from: "Sistine AI <noreply@yourdomain.com>",
to: ["user1@example.com"],
subject: "Hello User 1",
html: "<p>Hello!</p>",
},
{
from: "Sistine AI <noreply@yourdomain.com>",
to: ["user2@example.com"],
subject: "Hello User 2",
html: "<p>Hello!</p>",
},
]);
限制: 单次批量最多 100 封邮件。
Q: 如何追踪邮件打开和点击?
Resend 支持自动追踪:
await sendEmail({
to: email,
subject: "Newsletter",
html: "<p>Hello!</p>",
tags: [
{ name: "category", value: "newsletter" },
],
});
在 Resend Dashboard 中查看打开率和点击率。
Q: 如何处理退信 (Bounce)?
监听 Resend 的 Webhook 事件:
配置 Webhook:
- 在 Resend Dashboard 中导航到 Webhooks
- 添加 Webhook URL:
https://yourdomain.com/api/webhooks/resend
- 选择事件:
email.bounced
处理 Webhook (app/api/webhooks/resend/route.ts
):
export async function POST(req: NextRequest) {
const event = await req.json();
if (event.type === "email.bounced") {
const email = event.data.to;
// 标记邮箱为无效
await db
.update(user)
.set({ emailValid: false })
.where(eq(user.email, email));
console.log(`Marked email as invalid: ${email}`);
}
return NextResponse.json({ received: true });
}
Q: 如何测试邮件模板?
pnpm add @react-email/render --dev
# 启动预览服务器
pnpm email dev
访问 http://localhost:3000
查看所有邮件模板的实时预览。
相关文档
总结
Resend 为 Sistine Starter 提供了可靠的邮件服务,通过:
- 简单配置: 最少环境变量即可开始使用
- 预定义模板: 开箱即用的常用邮件模板
- 国际化支持: 多语言邮件内容
- 最佳实践: 错误处理、重试机制、送达率优化
遵循本文档的配置步骤和最佳实践,你可以快速为你的 SaaS 应用集成完整的邮件功能。