agent-email-inbox
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAI Agent Email Inbox
AI Agent 邮件收件箱
Overview
概述
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.
本技能介绍如何设置一个安全的邮件收件箱,让你的应用或AI Agent能够接收和回复邮件,同时具备内容安全防护措施。
**核心原则:**AI Agent的收件箱会接收不可信输入,安全配置对于安全处理这类输入至关重要。
Why Webhook-Based Receiving?
为什么选择基于Webhook的接收方式?
Resend uses webhooks for inbound email, meaning your agent is notified instantly when an email arrives. This is valuable for agents because:
- Real-time responsiveness — React to emails within seconds, not minutes
- No polling overhead — No cron jobs checking "any new mail?" repeatedly
- Event-driven architecture — Your agent only wakes up when there's actually something to process
- Lower API costs — No wasted calls checking empty inboxes
Resend使用Webhook处理入站邮件,这意味着当邮件到达时,你的Agent会立即收到通知。这对Agent来说非常有价值,因为:
- 实时响应——在数秒内而非数分钟内回复邮件
- 无轮询开销——无需定时任务反复检查“是否有新邮件?”
- 事件驱动架构——只有当确实有需要处理的内容时,你的Agent才会启动
- 更低的API成本——不会因检查空收件箱而产生无效调用
Architecture
架构
Sender → Email → Resend (MX) → Webhook → Your Server → AI Agent
↓
Security Validation
↓
Process or Reject发件人 → 邮件 → Resend (MX) → Webhook → 你的服务器 → AI Agent
↓
安全验证
↓
处理或拒绝SDK Version Requirements
SDK版本要求
This skill requires Resend SDK features for webhook verification () and email receiving (). Always install the latest SDK version. If the project already has a Resend SDK installed, check the version and upgrade if needed.
webhooks.verify()emails.receiving.get()| Language | Package | Min Version |
|---|---|---|
| Node.js | | >= 6.9.2 |
| Python | | >= 2.21.0 |
| Go | | >= 3.1.0 |
| Ruby | | >= 1.0.0 |
| PHP | | >= 1.1.0 |
| Rust | | >= 0.20.0 |
| Java | | >= 4.11.0 |
| .NET | | >= 0.2.1 |
Install the npm package: (or the equivalent for your language). For full sending docs, install the skill.
resendnpm install resendresend本技能需要Resend SDK的Webhook验证()和邮件接收()功能。请始终安装最新版本的SDK。如果项目中已安装Resend SDK,请检查版本并在需要时升级。
webhooks.verify()emails.receiving.get()| 语言 | 包 | 最低版本 |
|---|---|---|
| Node.js | | >= 6.9.2 |
| Python | | >= 2.21.0 |
| Go | | >= 3.1.0 |
| Ruby | | >= 1.0.0 |
| PHP | | >= 1.1.0 |
| Rust | | >= 0.20.0 |
| Java | | >= 4.11.0 |
| .NET | | >= 0.2.1 |
安装 npm包:(或对应语言的等效命令)。如需完整的发送文档,请安装技能。
resendnpm install resendresendQuick Start
快速开始
- Ask the user for their email address — You need a real email address to send test emails to. Ask the user and wait for their response before proceeding.
- Choose your security level — Decide how to validate incoming emails before any are processed
- Set up receiving domain — Configure MX records for the user's custom domain (see Domain Setup section)
- Create webhook endpoint — Handle events with security built in from the start. The webhook endpoint MUST be a POST route.
email.received - Set up tunneling (local dev) — Use Tailscale Funnel (recommended) or ngrok. See references/webhook-setup.md
- Create webhook via API — Use the Resend Webhook API to register your endpoint programmatically. See references/webhook-setup.md
- Connect to agent — Pass validated emails to your AI agent for processing
- 向用户索要邮箱地址——你需要一个真实邮箱地址来发送测试邮件。在继续操作前,请询问用户并等待回复。
- 选择安全级别——在处理任何邮件之前,确定如何验证入站邮件
- 设置接收域名——为用户的自定义域名配置MX记录(请参阅域名设置部分)
- 创建Webhook端点——从一开始就内置安全机制,处理事件。Webhook端点必须是POST路由。
email.received - 设置隧道(本地开发)——推荐使用Tailscale Funnel或ngrok。请参阅references/webhook-setup.md
- 通过API创建Webhook——使用Resend Webhook API以编程方式注册你的端点。请参阅references/webhook-setup.md
- 连接到Agent——将验证后的邮件传递给你的AI Agent进行处理
Before You Start: Account & API Key Setup
开始之前:账户与API密钥设置
First Question: New or Existing Resend Account?
第一个问题:新的还是已有的Resend账户?
Ask your human:
- New account just for the agent? → Simpler setup, full account access is fine
- Existing account with other projects? → Use domain-scoped API keys for sandboxing
询问用户:
- 只为Agent创建新账户? → 设置更简单,使用完整账户权限即可
- 已有包含其他项目的账户? → 使用域范围的API密钥进行沙箱隔离
Creating API Keys Securely
安全创建API密钥
Don't paste API keys in chat! They'll be in conversation history forever.
Safer options:
- Environment file method: Human creates file directly:
.envecho "RESEND_API_KEY=re_xxx" >> .env - Password manager / secrets manager: Human stores key in 1Password, Vault, etc.
- If key must be shared in chat: Human should rotate the key immediately after setup
不要在聊天中粘贴API密钥!它们会永久保存在对话历史中。
更安全的选项:
- **环境文件法:**用户直接创建文件:
.envecho "RESEND_API_KEY=re_xxx" >> .env - **密码管理器/密钥管理器:**用户将密钥存储在1Password、Vault等工具中
- **如果必须在聊天中共享密钥:**用户应在设置完成后立即轮换密钥
Domain-Scoped API Keys (Recommended for Existing Accounts)
域范围API密钥(已有账户推荐)
If your human has an existing Resend account with other projects, create a domain-scoped API key:
- Verify the agent's domain first (Dashboard → Domains → Add Domain)
- Create a scoped API key: Dashboard → API Keys → Create API Key → "Sending access" → select only the agent's domain
- Result: Even if the key leaks, it can only send from one domain
如果用户已有包含其他项目的Resend账户,请创建域范围API密钥:
- 先验证Agent的域名(控制台 → 域名 → 添加域名)
- **创建范围化API密钥:**控制台 → API密钥 → 创建API密钥 → “发送权限” → 仅选择Agent的域名
- **结果:**即使密钥泄露,也只能从该域名发送邮件
Domain Setup
域名设置
Option 1: Resend-Managed Domain (Recommended for Getting Started)
选项1:Resend托管域名(入门推荐)
Use your auto-generated address:
<anything>@<your-id>.resend.appNo DNS configuration needed. Find your address in Dashboard → Emails → Receiving → "Receiving address".
使用自动生成的地址:
<anything>@<your-id>.resend.app无需DNS配置。在控制台 → 邮件 → 接收 → “接收地址”中查找你的地址。
Option 2: Custom Domain
选项2:自定义域名
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., |
| Value | Provided in Resend dashboard |
| Priority | 10 (must be lowest number to take precedence) |
Use a subdomain (e.g., ) to avoid disrupting existing email services.
agent.yourdomain.comTip: 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.
用户必须在Resend控制台中启用接收功能:域名页面 → 开启“启用接收”。
然后添加MX记录:
| 设置 | 值 |
|---|---|
| 类型 | MX |
| 主机 | 你的域名或子域名(例如 |
| 值 | Resend控制台中提供的内容 |
| 优先级 | 10(必须是最低数字以确保优先级) |
使用子域名(例如),避免影响现有邮件服务。
agent.yourdomain.com**提示:**在dns.email验证DNS传播情况。
DNS传播:MX记录更改可能需要长达48小时才能在全球生效,但通常几小时内即可完成。
Security Levels
安全级别
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.
**在设置Webhook端点之前,请选择安全级别。**未配置安全机制的邮件处理AI Agent非常危险——任何人都可以发送邮件指令,让你的Agent执行。接下来编写的Webhook代码应从一开始就包含你选择的安全级别。
询问用户想要的安全级别,并确保他们理解每个级别的含义。
| 级别 | 名称 | 使用场景 | 权衡 |
|---|---|---|---|
| 1 | 严格白名单 | 大多数使用场景——已知的固定发件人集合 | 安全性最高,功能受限 |
| 2 | 域名白名单 | 来自可信域名的组织范围内访问 | 灵活性更高,域名内任何人都可交互 |
| 3 | 内容过滤 | 接收来自任何人的邮件,过滤不安全模式 | 可接收任何人的邮件,但模式匹配并非万无一失 |
| 4 | 沙箱处理 | 以受限的Agent能力处理所有邮件 | 灵活性最高,实现复杂 |
| 5 | 人工介入 | 不可信操作需要人工批准 | 安全性最高,增加延迟 |
每个级别的详细实现代码,请参阅references/security-levels.md。
Level 1: Strict Allowlist (Recommended)
级别1:严格白名单(推荐)
Only process emails from explicitly approved addresses. Reject everything else.
typescript
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,
});
}仅处理来自明确批准的地址的邮件,拒绝所有其他邮件。
typescript
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,
});
}Security Best Practices
安全最佳实践
Always Do
必须执行
| 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 |
| 实践 | 原因 |
|---|---|
| 验证Webhook签名 | 防止伪造的Webhook事件 |
| 记录所有被拒绝的邮件 | 为安全审查提供审计跟踪 |
| 尽可能使用白名单 | 明确的信任比过滤更安全 |
| 对邮件处理进行速率限制 | 防止过度的处理负载 |
| 区分可信/不可信处理 | 不同风险级别需要不同的处理方式 |
Never Do
切勿执行
| 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 |
| 反模式 | 风险 |
|---|---|
| 未验证就处理邮件 | 任何人都可以控制你的Agent |
| 信任邮件头 | 邮件头很容易被伪造 |
| 执行邮件内容中的代码 | 不可信输入绝不能作为代码运行 |
| 直接在提示词中存储邮件内容 | 不可信输入混入提示词可能会改变Agent的行为 |
| 给予不可信邮件完整的Agent权限 | 将能力范围限制在最小必要范围内 |
Webhook Endpoint
Webhook端点
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
(notreq.text())req.json()- Express: Use
on the webhook routeexpress.raw({ type: 'application/json' })
选择安全级别并设置域名后,创建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
Next.js App Router
typescript
// 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 });
}
}typescript
// 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
Express
javascript
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.
javascript
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。
Sending Emails from Your Agent
从Agent发送邮件
typescript
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 skill.
resendtypescript
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;
}如需完整的发送文档,请安装技能。
resendEnvironment Variables
环境变量
bash
undefinedbash
undefinedRequired
必填
RESEND_API_KEY=re_xxxxxxxxx
RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxx
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
undefinedSECURITY_LEVEL=strict # strict | domain | filtered | sandboxed
ALLOWED_SENDERS=you@email.com,trusted@example.com
ALLOWED_DOMAINS=yourcompany.com
OWNER_EMAIL=you@email.com # 用于安全通知
undefinedCommon Mistakes
常见错误
| 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 | Use |
| Returning non-200 for rejected emails | Always return 200 to acknowledge receipt — otherwise Resend retries |
| Old Resend SDK version | |
| 错误 | 修复方案 |
|---|---|
| 无发件人验证 | 处理前始终验证邮件发送者 |
| 信任邮件头 | 使用Webhook验证,而非邮件头进行身份验证 |
| 所有邮件同等处理 | 区分可信与不可信发件人 |
| 冗长的错误消息 | 保持错误响应通用,避免泄露内部逻辑 |
| 无速率限制 | 实现按发件人速率限制。请参阅references/advanced-patterns.md |
| 直接处理HTML | 剥离HTML或仅使用文本格式,降低复杂度和风险 |
| 未记录被拒绝的邮件 | 记录所有安全事件以便审计 |
| 使用临时隧道URL | 使用持久URL(Tailscale Funnel、付费ngrok)或部署到生产环境 |
在Webhook路由上使用 | 使用 |
| 被拒绝的邮件返回非200状态码 | 始终返回200以确认接收——否则Resend会重试 |
| Resend SDK版本过旧 | |
Testing
测试
Use Resend's test addresses for development:
- — Simulates successful delivery
delivered@resend.dev - — Simulates hard bounce
bounced@resend.dev
For security testing, send test emails from non-allowlisted addresses to verify rejection works correctly.
Quick verification checklist:
- Server is running: should return a response
curl http://localhost:3000 - Tunnel is working: should return the same response
curl https://<your-tunnel-url> - Webhook is active: Check status in Resend dashboard → Webhooks
- Send a test email from an allowlisted address and check server logs
使用Resend的测试地址进行开发:
- — 模拟成功投递
delivered@resend.dev - — 模拟硬退信
bounced@resend.dev
为了安全测试,请从非白名单地址发送测试邮件,验证拒绝功能是否正常工作。
快速验证清单:
- 服务器正在运行:应返回响应
curl http://localhost:3000 - 隧道正常工作:应返回相同响应
curl https://<your-tunnel-url> - Webhook处于活动状态:在Resend控制台 → Webhooks中检查状态
- 从白名单地址发送测试邮件并检查服务器日志
Related Skills
相关技能
- For full sending and receiving docs, install the skill
resend
- 如需完整的发送和接收文档,请安装技能
resend