🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx skills add https://skills.sh/resend/resend-skills/agent-email-inbox
💡 提示:需要 Node.js 和 NPM
AI 智能体邮箱收件箱
概述
此技能涵盖设置一个安全的电子邮件收件箱,允许您的应用程序或 AI 智能体接收和回复电子邮件,并内置内容安全措施。
核心原则: AI 智能体的收件箱接收的是不受信任的输入。安全配置对于安全处理这些输入非常重要。
为什么选择基于 Webhook 的接收方式?
Resend 使用 webhook 来处理入站邮件,这意味着当邮件到达时,您的智能体会立即收到通知。这对智能体来说非常有价值,因为:
- 实时响应能力 — 在几秒钟内对电子邮件做出反应,而不是几分钟
- 无轮询开销 — 无需定期运行定时任务检查“有新邮件吗?”
- 事件驱动架构 — 您的智能体只在有内容需要处理时才会被唤醒
- 更低的 API 成本 — 无需浪费调用去检查空收件箱
架构
发件人 → 邮件 → Resend (MX 记录) → Webhook → 您的服务器 → AI 智能体
↓
安全验证
↓
处理或拒绝
SDK 版本要求
此技能需要 Resend SDK 中用于 webhook 验证(webhooks.verify())和邮件接收(emails.receiving.get())的功能。请始终安装最新版本的 SDK。如果项目中已安装 Resend SDK,请检查版本并在必要时升级。
| 语言 | 包名 | 最低版本 |
|---|---|---|
| Node.js | resend |
>= 6.9.2 |
| Python | resend |
>= 2.21.0 |
| Go | resend-go/v3 |
>= 3.1.0 |
| Ruby | resend |
>= 1.0.0 |
| PHP | resend/resend-php |
>= 1.1.0 |
| Rust | resend-rs |
>= 0.20.0 |
| Java | resend-java |
>= 4.11.0 |
| .NET | Resend |
>= 0.2.1 |
安装 resend npm 包:npm install resend(或您所用语言的等效命令)。如需完整的发送文档,请安装 resend 技能。
快速开始
- 向用户询问他们的电子邮件地址 — 您需要一个真实的电子邮件地址来发送测试邮件。询问用户并在继续之前等待他们的回复。
- 选择您的安全级别 — 在处理任何入站邮件之前,决定如何验证它们
- 设置接收域名 — 为用户的自定义域名配置 MX 记录(参见域名设置部分)
- 创建 Webhook 端点 — 从一开始就内置安全性来处理
email.received事件。Webhook 端点必须是 POST 路由。 - 设置隧道(本地开发)— 使用 Tailscale Funnel(推荐)或 ngrok。参见 references/webhook-setup.md
- 通过 API 创建 Webhook — 使用 Resend Webhook API 以编程方式注册您的端点。参见 references/webhook-setup.md
- 连接到智能体 — 将验证过的邮件传递给您的 AI 智能体进行处理
开始之前:账户和 API 密钥设置
第一个问题:新账户还是已有 Resend 账户?
询问用户:
- 为智能体创建新账户? → 设置更简单,拥有完整账户权限是可以的
- 已有包含其他项目的 Resend 账户? → 使用域名限定 API 密钥进行沙盒隔离
安全创建 API 密钥
不要直接在聊天中粘贴 API 密钥!它们会永远留在对话历史中。
更安全的选项:
- 环境文件方法: 用户直接创建
.env文件:echo "RESEND_API_KEY=re_xxx" >> .env - 密码管理器 / 密钥管理器: 用户将密钥存储在 1Password、Vault 等中
- 如果必须在聊天中分享密钥: 用户应在设置完成后立即轮换该密钥
域名限定 API 密钥(推荐用于已有账户)
如果用户已有包含其他项目的 Resend 账户,请创建一个域名限定 API 密钥:
- 首先验证智能体的域名(控制台 → 域名 → 添加域名)
- 创建限定的 API 密钥: 控制台 → API 密钥 → 创建 API 密钥 → “发送权限” → 仅选择智能体的域名
- 结果: 即使密钥泄露,也只能从一个域名发送邮件
域名设置
选项 1:Resend 托管域名(推荐入门使用)
使用您自动生成的地址:<anything>@<your-id>.resend.app
无需 DNS 配置。在控制台 → 邮件 → 接收 → “接收地址” 中找到您的地址。
选项 2:自定义域名
用户必须在 Resend 控制台中启用接收:域名页面 → 切换 “启用接收”。
然后添加 MX 记录:
| 设置 | 值 |
|---|---|
| 类型 | MX |
| 主机 | 您的域名或子域名(例如,agent.yourdomain.com) |
| 值 | 在 Resend 控制台中提供 |
| 优先级 | 10(必须是优先级别最低的数字以确保优先使用) |
使用子域名(例如,agent.yourdomain.com)以避免干扰现有的邮件服务。
提示: 在 dns.email 验证 DNS 传播情况。
DNS 传播:MX 记录更改可能需要长达 48 小时才能在全局传播,但通常在几小时内完成。
安全级别
在设置 Webhook 端点之前选择您的安全级别。 一个处理邮件的 AI 智能体如果没有安全措施是危险的——任何人都可以向您的智能体发送指令让其执行。您接下来编写的 webhook 代码从一开始就应该包含您选择的安全级别。
询问用户他们想要什么级别的安全性,并确保他们理解每个级别的含义。
| 级别 | 名称 | 适用场景 | 权衡 |
|---|---|---|---|
| 1 | 严格白名单 | 大多数用例 — 已知的、固定的发件人集合 | 最高安全性,功能受限 |
| 2 | 域名白名单 | 来自受信任域名的组织范围内访问 | 更灵活,域内任何人都可以交互 |
| 3 | 内容过滤 | 接受任何发件人,过滤不安全模式 | 可以从任何人处接收,模式匹配并非万无一失 |
| 4 | 沙盒处理 | 处理所有邮件,但限制智能体能力 | 最大灵活性,实现复杂 |
| 5 | 人工介入 | 对不受信任的操作需要人工批准 | 最高安全性,增加延迟 |
有关每个级别的详细实现代码,请参见 references/security-levels.md。
级别 1:严格白名单(推荐)
仅处理来自明确批准地址的邮件。拒绝所有其他邮件。
const ALLOWED_SENDERS = [
'you@youremail.com',
'notifications@github.com',
];
async function processEmailForAgent(
eventData: EmailReceivedEvent,
emailContent: EmailContent
) {
const sender = eventData.from.toLowerCase();
if (!ALLOWED_SENDERS.some(allowed => sender === allowed.toLowerCase())) {
console.log(`拒绝了来自未授权发件人的邮件:${sender}`);
await notifyOwnerOfRejectedEmail(eventData);
return;
}
await agent.processEmail({
from: eventData.from,
subject: eventData.subject,
body: emailContent.text || emailContent.html,
});
}
安全最佳实践
始终执行
| 实践 | 原因 |
|---|---|
| 验证 Webhook 签名 | 防止伪造的 Webhook 事件 |
| 记录所有被拒绝的邮件 | 用于安全审计的跟踪记录 |
| 尽可能使用白名单 | 显式信任比过滤更安全 |
| 限制邮件处理速率 | 防止过度的处理负载 |
| 分离可信/不可信处理逻辑 | 不同风险级别需要不同的处理方式 |
绝对避免
| 反模式 | 风险 |
|---|---|
| 不经验证直接处理邮件 | 任何人都可以控制您的智能体 |
| 信任邮件头进行认证 | 邮件头很容易被伪造 |
| 执行邮件内容中的代码 | 不受信任的输入绝不应作为代码运行 |
| 将邮件内容直接放入提示词 | 混入提示词的不受信任输入可能改变智能体行为 |
| 给予不受信任邮件完整智能体权限 | 将能力范围限定在所需的最小权限 |
Webhook 端点
选择好安全级别并设置好域名后,创建一个 Webhook 端点。Webhook 端点必须是 POST 路由。 Resend 将所有 Webhook 事件作为 POST 请求发送。
关键:使用原始请求体进行验证。 Webhook 签名验证需要原始的请求体。
- Next.js App Router: 使用
req.text()(而不是req.json())- Express: 在 Webhook 路由上使用
express.raw({ type: 'application/json' })
Next.js App Router
// app/webhook/route.ts
import { Resend } from 'resend';
import { NextRequest, NextResponse } from 'next/server';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(req: NextRequest) {
try {
const payload = await req.text();
const event = resend.webhooks.verify({
payload,
headers: {
'svix-id': req.headers.get('svix-id'),
'svix-timestamp': req.headers.get('svix-timestamp'),
'svix-signature': req.headers.get('svix-signature'),
},
secret: process.env.RESEND_WEBHOOK_SECRET,
});
if (event.type === 'email.received') {
// Webhook 负载仅包含元数据,不包含邮件正文
const { data: email } = await resend.emails.receiving.get(
event.data.email_id
);
// 应用上面选择的安全级别
await processEmailForAgent(event.data, email);
}
return new NextResponse('OK', { status: 200 });
} catch (error) {
console.error('Webhook 错误:', error);
return new NextResponse('Error', { status: 400 });
}
}
Express
import express from 'express';
import { Resend } from 'resend';
const app = express();
const resend = new Resend(process.env.RESEND_API_KEY);
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const payload = req.body.toString();
const event = resend.webhooks.verify({
payload,
headers: {
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
},
secret: process.env.RESEND_WEBHOOK_SECRET,
});
if (event.type === 'email.received') {
const sender = event.data.from.toLowerCase();
if (!isAllowedSender(sender)) {
console.log(`拒绝了来自未授权发件人的邮件:${sender}`);
res.status(200).send('OK'); // 即使拒绝邮件也返回 200
return;
}
const { data: email } = await resend.emails.receiving.get(event.data.email_id);
await processEmailForAgent(event.data, email);
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook 错误:', error);
res.status(400).send('Error');
}
});
app.get('/', (req, res) => res.send('Agent Email Inbox - Ready'));
app.listen(3000, () => console.log('Webhook 服务器运行在 :3000'));
有关通过 API 注册 webhook、隧道设置、svix 回退和重试行为的更多信息,请参见 references/webhook-setup.md。
从您的智能体发送邮件
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
async function sendAgentReply(to: string, subject: string, body: string, inReplyTo?: string) {
if (!isAllowedToReply(to)) {
throw new Error('不能发送到此地址');
}
const { data, error } = await resend.emails.send({
from: '智能体 <agent@yourdomain.com>',
to: [to],
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
text: body,
headers: inReplyTo ? { 'In-Reply-To': inReplyTo } : undefined,
});
if (error) throw new Error(`发送失败:${error.message}`);
return data.id;
}
有关完整的发送文档,请安装 resend 技能。
环境变量
# 必需
RESEND_API_KEY=re_xxxxxxxxx
RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxx
# 安全配置
SECURITY_LEVEL=strict # strict | domain | filtered | sandboxed
ALLOWED_SENDERS=you@email.com,trusted@example.com
ALLOWED_DOMAINS=yourcompany.com
OWNER_EMAIL=you@email.com # 用于安全通知
常见错误
| 错误 | 修复方法 |
|---|---|
| 无发件人验证 | 在处理邮件之前始终验证发件人 |
| 信任邮件头 | 使用 webhook 验证进行认证,而不是邮件头 |
| 对所有邮件一视同仁 | 区分可信和不可信发件人 |
| 详细的错误消息 | 保持错误响应通用,避免泄露内部逻辑 |
| 无限速 | 实现每发件人速率限制。参见 references/advanced-patterns.md |
| 直接处理 HTML | 剥离 HTML 或仅使用纯文本以降低复杂性和风险 |
| 不记录拒绝日志 | 记录所有安全事件以供审计 |
| 使用临时隧道 URL | 使用持久 URL(Tailscale Funnel、付费 ngrok)或部署到生产环境 |
在 Webhook 路由上使用 express.json() |
使用 express.raw({ type: 'application/json' }) — JSON 解析会破坏签名验证 |
| 对被拒绝邮件返回非 200 状态码 | 始终返回 200 以确认收到 — 否则 Resend 会重试 |
| 旧版 Resend SDK | emails.receiving.get() 和 webhooks.verify() 需要较新的 SDK 版本 — 请参阅 SDK 版本要求 |
测试
使用 Resend 的测试地址进行开发:
delivered@resend.dev— 模拟成功投递bounced@resend.dev— 模拟硬退回
对于安全测试,请从未加入白名单的地址发送测试邮件,以验证拒绝机制是否正常工作。
快速验证清单:
- 服务器正在运行:
curl http://localhost:3000应返回响应 - 隧道正常工作:
curl https://<your-tunnel-url>应返回相同响应 - Webhook 处于活动状态:在 Resend 控制台 → Webhooks 中检查状态
- 从白名单地址发送测试邮件并检查服务器日志
相关技能
- 有关完整的发送和接收文档,请安装
resend技能
📄 原始文档
完整文档(英文):
https://skills.sh/resend/resend-skills/agent-email-inbox
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

评论(0)