关于 resend
resend 是有状态的 Resend Email API 本地模拟器,面向测试邮件工作流的开发者。它不会发送真实邮件,而是保存发送内容,供通过收件箱界面或 REST API 检查。
主要功能
- 01在内存中保存发送的邮件
- 02通过收件箱界面或 REST 查看邮件
- 03模拟域名、API 密钥、受众和联系人
- 04支持幂等发送请求
- 05测试收件人允许列表和发送尝试
适用场景
- 01不发送真实邮件地测试邮件流程
- 02在测试中提取验证码或魔法链接
- 03检查收件人拒绝与重试行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill resend安装后在 Claude Code 中输入/resend确认已加载。
详细介绍
Resend 邮件 API 模拟器
完整模拟 Resend API 的状态管理。邮件、域名、API 密钥、受众群体和联系人都会保存在内存中。已发送的邮件会被捕获,并可通过收件箱 UI 或 REST API 查看。
不会发送真实邮件。对 POST /emails 的调用成功后,邮件会保存在本地,方便你通过程序或浏览器检查。通过验证的发送请求也会记录尝试情况,包括收件人被拒绝的情况。
启动
本地监听器默认绑定到 127.0.0.1。如需从容器或其他机器访问,请在 CLI 中传入 --host 0.0.0.0,或在 createEmulator 中设置 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置可供这些客户端访问的对外 URL。
# Resend only
npx emulate --service resend
# Default port (when run alone)
# http://localhost:4000
或者以编程方式启动:
import { createEmulator } from 'emulate'
const resend = await createEmulator({ service: 'resend', port: 4000 })
// resend.url === 'http://localhost:4000'
身份验证
通过 Authorization: Bearer <token> 传递令牌。任何带有 re_ 前缀的令牌都可以使用。
curl http://localhost:4000/emails \
-H "Authorization: Bearer re_test_key"
未提供令牌时,请求会回退到默认用户。
将应用指向模拟器
环境变量(Resend SDK)
官方 Resend Node.js SDK 会在模块加载时读取 RESEND_BASE_URL。将其设置为模拟器 URL,SDK 即可在无需更改代码的情况下正常工作:
RESEND_BASE_URL=http://localhost:4000
import { Resend } from 'resend'
// No baseUrl argument needed; the SDK reads RESEND_BASE_URL automatically.
const resend = new Resend('re_test_key')
await resend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Hello',
html: '<p>It works!</p>',
})
嵌入 Next.js(adapter-next)
使用 @emulators/adapter-next 时,模拟器会在 Next.js 应用内的 /emulate/resend 路径下运行。通过 next.config.ts 设置 RESEND_BASE_URL:
// next.config.ts
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
env: {
RESEND_BASE_URL: `http://localhost:${process.env.PORT ?? '3000'}/emulate/resend`,
},
})
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as resend from '@emulators/resend'
export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
services: {
resend: {
emulator: resend,
seed: {
domains: [{ name: 'example.com' }],
},
},
},
})
直接使用 fetch
如果无法使用 SDK 或环境变量,请直接调用模拟器:
await fetch('http://localhost:4000/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer re_test_key',
},
body: JSON.stringify({
from: '[email protected]',
to: '[email protected]',
subject: 'Hello',
html: '<p>It works!</p>',
}),
})
种子配置
resend:
domains:
- name: example.com
region: us-east-1
contacts:
- email: [email protected]
first_name: Test
last_name: User
audience: Default
获取已发送的邮件
这是模拟器的主要特点:通过 POST /emails 发送的每封邮件都会被存储并可供查询。
收件箱 UI
在浏览器中查看已发送的邮件:
http://localhost:4000/inbox
REST API
# List all sent emails
curl http://localhost:4000/emails \
-H "Authorization: Bearer re_test_key"
# Get a single email by ID
curl http://localhost:4000/emails/<id> \
-H "Authorization: Bearer re_test_key"
从邮件中提取数据(测试、Agent)
可用于以编程方式完成魔法链接、验证码或密码重置流程:
# Get the latest email ID
EMAIL_ID=$(curl -s http://localhost:4000/emails \
-H "Authorization: Bearer re_test_key" | jq -r '.data[0].id')
# Extract a 6-digit code from the HTML body
CODE=$(curl -s http://localhost:4000/emails/$EMAIL_ID \
-H "Authorization: Bearer re_test_key" | jq -r '.html' | grep -oE '[0-9]{6}')
echo "Verification code: $CODE"
API 端点
邮件
# Send an email
curl -X POST http://localhost:4000/emails \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"from": "[email protected]", "to": "[email protected]", "subject": "Hello", "html": "<p>Hi</p>"}'
# Send batch (up to 100)
curl -X POST http://localhost:4000/emails/batch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[{"from": "[email protected]", "to": "[email protected]", "subject": "One", "html": "<p>1</p>"}]'
# List all emails
curl http://localhost:4000/emails \
-H "Authorization: Bearer $TOKEN"
# Get email by ID
curl http://localhost:4000/emails/<id> \
-H "Authorization: Bearer $TOKEN"
# Cancel a scheduled email
curl -X POST http://localhost:4000/emails/<id>/cancel \
-H "Authorization: Bearer $TOKEN"
支持的字段:from、to、subject、html、text、cc、bcc、reply_to、headers、tags、scheduled_at。
幂等发送
POST /emails 和 POST /emails/batch 接受不区分大小写的 Idempotency-Key 标头。密钥长度必须为 1 到 256 个字符,有效期为 24 小时。使用相同密钥和通过验证的负载重试时,会返回原始响应、邮件 ID 和状态,不会创建邮件,也不会重复派发 email.sent 和 email.delivered Webhook。使用同一密钥但负载或端点不同,会返回 409 invalid_idempotent_request;密钥长度无效时,会返回 400 invalid_idempotency_key。省略该标头则保持正常行为。
curl -X POST http://localhost:4000/emails \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-email-123" \
-d '{"from": "[email protected]", "to": "[email protected]", "subject": "Hello"}'
收件人限制和发送尝试
在种子配置中设置 resend.allowlist,以测试收件人拒绝行为:
resend:
allowlist:
- [email protected]
- [email protected]
省略 allowlist 时接受所有收件人;allowlist: [] 则拒绝所有收件人。to、cc 和 bcc 中的每个地址都必须列在允许列表中。匹配时会忽略首尾空格和字母大小写;列表项必须是单个地址,不支持通配符或域名匹配。这是一项本地测试控制,与 Resend 的域名验证和账户限制相互独立。
发送被拒绝时,会返回 403 validation_error,并指出被拒绝的地址和配置的允许列表。不会创建邮件 ID、邮件记录、幂等记录或 webhook。如果批次中有任何收件人不被允许,整个批次都会被拒绝。重试被拒绝的请求(即使使用相同的幂等键),也会创建新的尝试。重放已接受的幂等请求时,即使允许列表之后发生了变化,也会返回原始响应。
通过验证的发送会在 resend.send_attempts 存储集合中为每封邮件追加一条记录,包括被拒绝批次中的邮件。每条记录包含 from、to、cc、bcc、subject、idempotency_key(或 null),以及存储中的 id、created_at 和 updated_at。不会将邮件正文复制到尝试记录中。格式错误的请求、无效或冲突的幂等键以及幂等请求重放,都不会追加尝试记录。
使用 @emulators/resend 中的 getResendStore(store).sendAttempts.all() 读取尝试记录,或检查持久化状态。共享适配器快照使用键 resend:resend.send_attempts。尝试记录和收件人限制都会在恢复已保存状态后保留,包括被拒绝的发送。Resend 没有用于查询尝试记录的额外 API 端点。被拒绝的发送有一条尝试记录,但没有邮件;从未发生的发送则两者都没有。
域名
# Create domain
curl -X POST http://localhost:4000/domains \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "example.com", "region": "us-east-1"}'
# List domains
curl http://localhost:4000/domains \
-H "Authorization: Bearer $TOKEN"
# Get domain
curl http://localhost:4000/domains/<id> \
-H "Authorization: Bearer $TOKEN"
# Verify domain (instantly marks all records as verified)
curl -X POST http://localhost:4000/domains/<id>/verify \
-H "Authorization: Bearer $TOKEN"
# Delete domain
curl -X DELETE http://localhost:4000/domains/<id> \
-H "Authorization: Bearer $TOKEN"
API 密钥
# Create API key
curl -X POST http://localhost:4000/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Production"}'
# List API keys
curl http://localhost:4000/api-keys \
-H "Authorization: Bearer $TOKEN"
# Delete API key
curl -X DELETE http://localhost:4000/api-keys/<id> \
-H "Authorization: Bearer $TOKEN"
受众
# Create audience
curl -X POST http://localhost:4000/audiences \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Newsletter"}'
# List audiences
curl http://localhost:4000/audiences \
-H "Authorization: Bearer $TOKEN"
# Delete audience
curl -X DELETE http://localhost:4000/audiences/<id> \
-H "Authorization: Bearer $TOKEN"
联系人
# Create contact in an audience
curl -X POST http://localhost:4000/audiences/<audience_id>/contacts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "first_name": "Jane", "last_name": "Doe"}'
# List contacts in an audience
curl http://localhost:4000/audiences/<audience_id>/contacts \
-H "Authorization: Bearer $TOKEN"
# Delete contact
curl -X DELETE http://localhost:4000/audiences/<audience_id>/contacts/<id> \
-H "Authorization: Bearer $TOKEN"
Webhook
状态变更时,模拟器会分发 webhook 事件:
- 对
POST /emails分发email.sent和email.delivered - 对域名操作分发
domain.created和domain.deleted - 对联系人操作分发
contact.created和contact.deleted
常见模式
魔法链接 / 验证码流程
TOKEN="re_test_key"
BASE="http://localhost:4000"
# 1. Send verification email
curl -X POST $BASE/emails \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"from": "[email protected]", "to": "[email protected]", "subject": "Your code", "html": "<p>Code: <strong>482910</strong></p>"}'
# 2. Retrieve the email
EMAIL_ID=$(curl -s $BASE/emails -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')
# 3. Read the HTML body
curl -s $BASE/emails/$EMAIL_ID -H "Authorization: Bearer $TOKEN" | jq -r '.html'
在测试中发送和验证
import { createEmulator } from 'emulate'
import { Resend } from 'resend'
const emu = await createEmulator({ service: 'resend', port: 4000 })
process.env.RESEND_BASE_URL = emu.url
const resend = new Resend('re_test_key')
// Send
await resend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Verify',
html: '<p>Code: <strong>123456</strong></p>',
})
// Retrieve
const res = await fetch(`${emu.url}/emails`, {
headers: { Authorization: 'Bearer re_test_key' },
})
const { data: emails } = await res.json()
console.log(emails[0].html) // contains "123456"
与内置模拟器一起使用自定义模拟器
使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。使用 emulate 中的 defineConfig,将其注册到 emulate.config.ts,与 { emulator: "resend" } 等内置条目并列。运行 npx emulate start --watch 以重新加载导入内容,并在打印出的 /_emulate URL 中检查自定义状态。成功重新加载后,会将本次运行重置为种子状态。现有的扁平种子配置仍可使用;--config 可指定显式配置文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。