當您建立任何需要透過電子郵件內容觸發動作的系統時使用——AI 代理收件匣、自動化支援處理器、郵件轉任務管道,或任何處理不受信任入站郵件的工作流程。每當使用者想要以程式化方式接收郵件並對其採取動作時,請務必使用此技能,即使他們沒有提到「代理」——此技能包含關鍵的安全模式(寄件者白名單、內容過濾、沙箱處理),可防止不受信任的郵件控制您的系統。
AI 代理電子郵件收件匣
概述
此技能涵蓋如何設定一個安全的電子郵件收件匣,讓您的應用程式或 AI 代理能夠接收並回覆電子郵件,同時具備內容安全措施。
核心原則: AI 代理的收件匣會接收不受信任的輸入。安全設定對於安全處理這類輸入非常重要。
為什麼使用 Webhook 接收?
Resend 使用 webhook 處理入站郵件,這表示您的代理會在郵件到達時立即收到通知。這對代理來說很有價值,因為:
- 即時回應 — 在數秒內(而非數分鐘)對郵件做出反應
- 無輪詢開銷 — 無需 cron 工作反覆檢查「有新郵件嗎?」
- 事件驅動架構 — 您的代理只在有事情需要處理時才啟動
- 較低的 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 會以 POST 請求發送所有 webhook 事件。
關鍵:使用原始請求主體進行驗證。 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 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 });
}
}
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'); // 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'));
如需透過 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 技能。
環境變數
# 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=example.com
OWNER_EMAIL=you@email.com # For security notifications
常見錯誤
| 錯誤 | 修正方式 |
|---|---|
| 沒有寄件者驗證 | 在處理郵件前務必驗證寄件者身分 |
| 信任郵件標頭 | 使用 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技能






