agent-email-inbox

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

AI 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 (
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.
LanguagePackageMin 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
resend-rs
>= 0.20.0
Java
resend-java
>= 4.11.0
.NET
Resend
>= 0.2.1
Install the
resend
npm package:
npm install resend
(or the equivalent for your language). For full sending docs, install the
resend
skill.
本技能需要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
技能。

Quick Start

快速开始

  1. 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.
  2. Choose your security level — Decide how to validate incoming emails before any are processed
  3. Set up receiving domain — Configure MX records for the user's custom domain (see Domain Setup section)
  4. Create webhook endpoint — Handle
    email.received
    events with security built in from the start. The webhook endpoint MUST be a POST route.
  5. Set up tunneling (local dev) — Use Tailscale Funnel (recommended) or ngrok. See references/webhook-setup.md
  6. Create webhook via API — Use the Resend Webhook API to register your endpoint programmatically. See references/webhook-setup.md
  7. Connect to agent — Pass validated emails to your AI agent for processing
  1. 向用户索要邮箱地址——你需要一个真实邮箱地址来发送测试邮件。在继续操作前,请询问用户并等待回复。
  2. 选择安全级别——在处理任何邮件之前,确定如何验证入站邮件
  3. 设置接收域名——为用户的自定义域名配置MX记录(请参阅域名设置部分)
  4. 创建Webhook端点——从一开始就内置安全机制,处理
    email.received
    事件。Webhook端点必须是POST路由。
  5. 设置隧道(本地开发)——推荐使用Tailscale Funnel或ngrok。请参阅references/webhook-setup.md
  6. 通过API创建Webhook——使用Resend Webhook API以编程方式注册你的端点。请参阅references/webhook-setup.md
  7. 连接到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:
  1. Environment file method: Human creates
    .env
    file directly:
    echo "RESEND_API_KEY=re_xxx" >> .env
  2. Password manager / secrets manager: Human stores key in 1Password, Vault, etc.
  3. If key must be shared in chat: Human should rotate the key immediately after setup
不要在聊天中粘贴API密钥!它们会永久保存在对话历史中。
更安全的选项:
  1. **环境文件法:**用户直接创建
    .env
    文件:
    echo "RESEND_API_KEY=re_xxx" >> .env
  2. **密码管理器/密钥管理器:**用户将密钥存储在1Password、Vault等工具中
  3. **如果必须在聊天中共享密钥:**用户应在设置完成后立即轮换密钥

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:
  1. Verify the agent's domain first (Dashboard → Domains → Add Domain)
  2. Create a scoped API key: Dashboard → API Keys → Create API Key → "Sending access" → select only the agent's domain
  3. Result: Even if the key leaks, it can only send from one domain
如果用户已有包含其他项目的Resend账户,请创建域范围API密钥
  1. 先验证Agent的域名(控制台 → 域名 → 添加域名)
  2. **创建范围化API密钥:**控制台 → API密钥 → 创建API密钥 → “发送权限” → 仅选择Agent的域名
  3. **结果:**即使密钥泄露,也只能从该域名发送邮件

Domain Setup

域名设置

Option 1: Resend-Managed Domain (Recommended for Getting Started)

选项1:Resend托管域名(入门推荐)

Use your auto-generated address:
<anything>@<your-id>.resend.app
No 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:
SettingValue
TypeMX
HostYour domain or subdomain (e.g.,
agent.yourdomain.com
)
ValueProvided in Resend dashboard
Priority10 (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.
用户必须在Resend控制台中启用接收功能:域名页面 → 开启“启用接收”。
然后添加MX记录:
设置
类型MX
主机你的域名或子域名(例如
agent.yourdomain.com
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.
LevelNameWhen to UseTrade-off
1Strict AllowlistMost use cases — known, fixed set of sendersMaximum security, limited functionality
2Domain AllowlistOrganization-wide access from trusted domainsMore flexible, anyone at domain can interact
3Content FilteringAccept from anyone, filter unsafe patternsCan receive from anyone, pattern matching not foolproof
4Sandboxed ProcessingProcess all emails with restricted agent capabilitiesMaximum flexibility, complex to implement
5Human-in-the-LoopRequire human approval for untrusted actionsMaximum 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

必须执行

PracticeWhy
Verify webhook signaturesPrevents spoofed webhook events
Log all rejected emailsAudit trail for security review
Use allowlists where possibleExplicit trust is safer than filtering
Rate limit email processingPrevents excessive processing load
Separate trusted/untrusted handlingDifferent risk levels need different treatment
实践原因
验证Webhook签名防止伪造的Webhook事件
记录所有被拒绝的邮件为安全审查提供审计跟踪
尽可能使用白名单明确的信任比过滤更安全
对邮件处理进行速率限制防止过度的处理负载
区分可信/不可信处理不同风险级别需要不同的处理方式

Never Do

切勿执行

Anti-PatternRisk
Process emails without validationAnyone can control your agent
Trust email headers for authenticationHeaders are trivially spoofed
Execute code from email contentUntrusted input should never run as code
Store email content in prompts verbatimUntrusted input mixed into prompts can alter agent behavior
Give untrusted emails full agent accessScope 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
    req.text()
    (not
    req.json()
    )
  • Express: Use
    express.raw({ type: 'application/json' })
    on the webhook route
选择安全级别并设置域名后,创建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
resend
skill.
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;
}
如需完整的发送文档,请安装
resend
技能。

Environment Variables

环境变量

bash
undefined
bash
undefined

Required

必填

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
undefined
SECURITY_LEVEL=strict # strict | domain | filtered | sandboxed ALLOWED_SENDERS=you@email.com,trusted@example.com ALLOWED_DOMAINS=yourcompany.com OWNER_EMAIL=you@email.com # 用于安全通知
undefined

Common Mistakes

常见错误

MistakeFix
No sender verificationAlways validate who sent the email before processing
Trusting email headersUse webhook verification, not email headers for auth
Same treatment for all emailsDifferentiate trusted vs untrusted senders
Verbose error messagesKeep error responses generic to avoid leaking internal logic
No rate limitingImplement per-sender rate limits. See references/advanced-patterns.md
Processing HTML directlyStrip HTML or use text-only to reduce complexity and risk
No logging of rejectionsLog all security events for audit
Using ephemeral tunnel URLsUse 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
Returning non-200 for rejected emailsAlways 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
错误修复方案
无发件人验证处理前始终验证邮件发送者
信任邮件头使用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版本要求

Testing

测试

Use Resend's test addresses for development:
  • delivered@resend.dev
    — Simulates successful delivery
  • bounced@resend.dev
    — Simulates hard bounce
For security testing, send test emails from non-allowlisted addresses to verify rejection works correctly.
Quick verification checklist:
  1. Server is running:
    curl http://localhost:3000
    should return a response
  2. Tunnel is working:
    curl https://<your-tunnel-url>
    should return the same response
  3. Webhook is active: Check status in Resend dashboard → Webhooks
  4. Send a test email from an allowlisted address and check server logs
使用Resend的测试地址进行开发:
  • delivered@resend.dev
    — 模拟成功投递
  • bounced@resend.dev
    — 模拟硬退信
为了安全测试,请从非白名单地址发送测试邮件,验证拒绝功能是否正常工作。
快速验证清单:
  1. 服务器正在运行:
    curl http://localhost:3000
    应返回响应
  2. 隧道正常工作:
    curl https://<your-tunnel-url>
    应返回相同响应
  3. Webhook处于活动状态:在Resend控制台 → Webhooks中检查状态
  4. 从白名单地址发送测试邮件并检查服务器日志

Related Skills

相关技能

  • For full sending and receiving docs, install the
    resend
    skill
  • 如需完整的发送和接收文档,请安装
    resend
    技能