在构建任何由邮件内容触发动作的系统时使用——AI代理收件箱、自动支持处理程序、邮件转任务管道,或任何处理不可信入站邮件的工作流。当用户希望以编程方式接收邮件并对其执行操作时,始终使用此技能,即使他们未提及“代理”——该技能包含关键安全模式(发件人白名单、内容过滤、沙箱处理),可防止不可信邮件控制您的系统。
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 账户?
询问您的用户:
- 仅为代理创建新账户? → 设置更简单,完全账户访问权限即可
- 已有其他项目的账户? → 使用域名范围的 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.example.com) |
| 值 | Resend 仪表盘中提供 |
| 优先级 | 10(必须是最低数字以优先) |
使用子域名(例如 agent.example.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(`Rejected email from unauthorized sender: ${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:', 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(`Rejected email from unauthorized sender: ${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:', error);
res.status(400).send('Error');
}
});
app.get('/', (req, res) => res.send('Agent Email Inbox - Ready'));
app.listen(3000, () => console.log('Webhook server running on :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('Cannot send to this address');
}
const { data, error } = await resend.emails.send({
from: 'Agent <agent@example.com>',
to: [to],
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
text: body,
headers: inReplyTo ? { 'In-Reply-To': inReplyTo } : undefined,
});
if (error) throw new Error(`Failed to send: ${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=example.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技能






