agent-email-inbox by resend/resend-skills
npx skills add https://github.com/resend/resend-skills --skill agent-email-inbox此技能涵盖如何设置一个安全的电子邮件收件箱,使您的应用程序或 AI 智能体能够接收和回复电子邮件,并内置内容安全措施。
核心原则: AI 智能体的收件箱接收的是不受信任的输入。安全配置对于安全处理这些输入至关重要。
Resend 使用 webhook 来处理入站邮件,这意味着当邮件到达时,您的智能体会立即收到通知。这对智能体来说很有价值,因为:
Sender → Email → Resend (MX) → Webhook → Your Server → AI Agent
↓
Security Validation
↓
Process or Reject
此技能需要 Resend SDK 的 webhook 验证(webhooks.verify())和邮件接收(emails.receiving.get())功能。请始终安装最新的 SDK 版本。如果项目中已安装 Resend SDK,请检查版本并在需要时升级。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 语言 | 包 | 最低版本 |
|---|---|---|
| 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 技能。
email.received 事件,从一开始就内置安全性。Webhook 端点必须是 POST 路由。询问您的用户:
不要在聊天中粘贴 API 密钥!它们将永久保存在对话历史记录中。
更安全的选项:
.env 文件:echo "RESEND_API_KEY=re_xxx" >> .env如果您的用户已有一个包含其他项目的现有 Resend 账户,请创建一个域名范围的 API 密钥:
使用您自动生成的地址:<anything>@<your-id>.resend.app
无需 DNS 配置。在控制面板 → 邮件 → 接收 → "接收地址" 中找到您的地址。
用户必须在 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。
仅处理来自明确批准的地址的邮件。拒绝其他所有邮件。
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 端点必须是 POST 路由。 Resend 将所有 Webhook 事件作为 POST 请求发送。
关键:使用原始请求体进行验证。 Webhook 签名验证需要原始请求体。
- Next.js App Router: 使用
req.text()(而不是req.json())- Express: 在 Webhook 路由上使用
express.raw({ type: 'application/json' })
// 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 });
}
}
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@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(`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=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> 应返回相同响应resend 技能每周安装数
339
仓库
GitHub 星标数
92
首次出现
10 天前
安全审计
安装于
codex329
gemini-cli327
github-copilot326
opencode326
cursor326
kimi-cli325
This skill covers setting up a secure email inbox that allows your application or AI agent to receive and respond to emails, with content safety measures in place.
Core principle: An AI agent's inbox receives untrusted input. Security configuration is important to handle this safely.
Resend uses webhooks for inbound email, meaning your agent is notified instantly when an email arrives. This is valuable for agents because:
Sender → Email → Resend (MX) → Webhook → Your Server → AI Agent
↓
Security Validation
↓
Process or Reject
This skill requires Resend SDK features for webhook verification (webhooks.verify()) and email receiving (emails.receiving.get()). Always install the latest SDK version. If the project already has a Resend SDK installed, check the version and upgrade if needed.
| Language | Package | Min Version |
|---|---|---|
| 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 |
Install the resend npm package: npm install resend (or the equivalent for your language). For full sending docs, install the resend skill.
email.received events with security built in from the start. The webhook endpoint MUST be a POST route.Ask your human:
Don't paste API keys in chat! They'll be in conversation history forever.
Safer options:
.env file directly: echo "RESEND_API_KEY=re_xxx" >> .envIf your human has an existing Resend account with other projects, create a domain-scoped API key :
Use your auto-generated address: <anything>@<your-id>.resend.app
No DNS configuration needed. Find your address in Dashboard → Emails → Receiving → "Receiving address".
The user must enable receiving in the Resend dashboard: Domains page → toggle on "Enable Receiving".
Then add an MX record:
| Setting | Value |
|---|---|
| Type | MX |
| Host | Your domain or subdomain (e.g., agent.yourdomain.com) |
| Value | Provided in Resend dashboard |
| Priority | 10 (must be lowest number to take precedence) |
Use a subdomain (e.g., agent.yourdomain.com) to avoid disrupting existing email services.
Tip: Verify DNS propagation at dns.email.
DNS Propagation: MX record changes can take up to 48 hours to propagate globally, though often complete within a few hours.
Choose your security level before setting up the webhook endpoint. An AI agent that processes emails without security is dangerous — anyone can email instructions that your agent will execute. The webhook code you write next should include your chosen security level from the start.
Ask the user what level of security they want, and ensure that they understand what each level means.
| Level | Name | When to Use | Trade-off |
|---|---|---|---|
| 1 | Strict Allowlist | Most use cases — known, fixed set of senders | Maximum security, limited functionality |
| 2 | Domain Allowlist | Organization-wide access from trusted domains | More flexible, anyone at domain can interact |
| 3 | Content Filtering | Accept from anyone, filter unsafe patterns | Can receive from anyone, pattern matching not foolproof |
| 4 | Sandboxed Processing | Process all emails with restricted agent capabilities | Maximum flexibility, complex to implement |
| 5 | Human-in-the-Loop | Require human approval for untrusted actions | Maximum security, adds latency |
For detailed implementation code for each level, see references/security-levels.md.
Only process emails from explicitly approved addresses. Reject everything else.
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,
});
}
| Practice | Why |
|---|---|
| Verify webhook signatures | Prevents spoofed webhook events |
| Log all rejected emails | Audit trail for security review |
| Use allowlists where possible | Explicit trust is safer than filtering |
| Rate limit email processing | Prevents excessive processing load |
| Separate trusted/untrusted handling | Different risk levels need different treatment |
| Anti-Pattern | Risk |
|---|---|
| Process emails without validation | Anyone can control your agent |
| Trust email headers for authentication | Headers are trivially spoofed |
| Execute code from email content | Untrusted input should never run as code |
| Store email content in prompts verbatim | Untrusted input mixed into prompts can alter agent behavior |
| Give untrusted emails full agent access | Scope capabilities to the minimum needed |
After choosing your security level and setting up your domain, create a webhook endpoint. The webhook endpoint MUST be a POST route. Resend sends all webhook events as POST requests.
Critical: Use raw body for verification. Webhook signature verification requires the raw request body.
- Next.js App Router: Use
req.text()(notreq.json())- Express: Use
express.raw({ type: 'application/json' })on the webhook route
// 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 payload only includes metadata, not email body
const { data: email } = await resend.emails.receiving.get(
event.data.email_id
);
// Apply the security level chosen above
await processEmailForAgent(event.data, email);
}
return new NextResponse('OK', { status: 200 });
} catch (error) {
console.error('Webhook error:', error);
return new NextResponse('Error', { status: 400 });
}
}
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'); // Return 200 even for rejected emails
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'));
For webhook registration via API, tunneling setup, svix fallback, and retry behavior, see 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@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(`Failed to send: ${error.message}`);
return data.id;
}
For full sending docs, install the resend skill.
# Required
RESEND_API_KEY=re_xxxxxxxxx
RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxx
# Security Configuration
SECURITY_LEVEL=strict # strict | domain | filtered | sandboxed
ALLOWED_SENDERS=you@email.com,trusted@example.com
ALLOWED_DOMAINS=yourcompany.com
OWNER_EMAIL=you@email.com # For security notifications
| Mistake | Fix |
|---|---|
| No sender verification | Always validate who sent the email before processing |
| Trusting email headers | Use webhook verification, not email headers for auth |
| Same treatment for all emails | Differentiate trusted vs untrusted senders |
| Verbose error messages | Keep error responses generic to avoid leaking internal logic |
| No rate limiting | Implement per-sender rate limits. See references/advanced-patterns.md |
| Processing HTML directly | Strip HTML or use text-only to reduce complexity and risk |
| No logging of rejections | Log all security events for audit |
| Using ephemeral tunnel URLs | Use persistent URLs (Tailscale Funnel, paid ngrok) or deploy to production |
Using express.json() on webhook route | Use express.raw({ type: 'application/json' }) — JSON parsing breaks signature verification |
Use Resend's test addresses for development:
delivered@resend.dev — Simulates successful deliverybounced@resend.dev — Simulates hard bounceFor security testing, send test emails from non-allowlisted addresses to verify rejection works correctly.
Quick verification checklist:
curl http://localhost:3000 should return a responsecurl https://<your-tunnel-url> should return the same responseresend skillWeekly Installs
339
Repository
GitHub Stars
92
First Seen
10 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex329
gemini-cli327
github-copilot326
opencode326
cursor326
kimi-cli325
Azure Data Explorer (Kusto) 查询技能:KQL数据分析、日志遥测与时间序列处理
100,500 周安装
SEO基础指南:E-E-A-T框架、核心网页指标与AI内容优化策略
330 周安装
iOS性能监控与崩溃诊断:MetricKit框架完整使用指南
330 周安装
App Store推荐策略指南:如何提高应用被苹果编辑推荐的机会 | ASO优化技巧
330 周安装
产品经理工具包:RICE优先级排序、客户访谈分析与PRD模板,提升产品管理效率
330 周安装
iOS CoreNFC 教程:Swift 6.2 读取写入 NFC 标签,支持后台读取与 NDEF 消息
330 周安装
AWS SDK Java 2.x Amazon Bedrock 开发指南:集成Claude、Llama、Titan AI模型
330 周安装
resend-rs |
| >= 0.20.0 |
| Java | resend-java | >= 4.11.0 |
| .NET | Resend | >= 0.2.1 |
| Returning non-200 for rejected emails | Always return 200 to acknowledge receipt — otherwise Resend retries |
| Old Resend SDK version | emails.receiving.get() and webhooks.verify() require recent SDK versions — see SDK Version Requirements |