agent-email-inbox について
agent-email-inbox は、Resend を使ってアプリケーションや AI エージェントがメールを受信・返信するための受信箱構築スキルです。Webhook による処理と、信頼できないメールに対する複数の検証方式を重視します。
主な機能
- 01Webhook 起点の受信メール処理
- 02Resend の受信箱でのスレッド返信
- 03メール検証の 5 段階のセキュリティ
- 04Webhook 署名検証のガイド
- 05ドメイン受信と転送の設定
活用シーン
- 01AI エージェントをメール受信箱に接続
- 02許可した送信者のみを処理
- 03人の承認を伴う受信メール処理
- 04既存のメールスレッドで返信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ resend/ resend-skills --skill agent-email-inboxインストール後、Claude Code で/agent-email-inboxを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-patterns.mdsecurity-levels.mdwebhook-setup.md詳細
AI エージェントのメール受信トレイ
概要
このスキルでは、コンテンツの安全対策を講じたうえで、アプリケーションや AI エージェントがメールを受信し、返信できるセキュアなメール受信トレイの設定方法を説明します。
基本原則: AI エージェントの受信トレイには、信頼できない入力が届きます。安全に取り扱うには、セキュリティ設定が重要です。
このスキルでは、Resend の Inbox を使ってエージェントを構築します。受信トレイではメールがスレッドにまとめられ、エージェントは 1 回の呼び出しでスレッド内に返信できます。宛先とスレッド用ヘッダーは Resend が設定します。ユーザーがカスタムドメインを持っていない場合、またはプランで受信トレイが利用できない場合は、代わりに受信トレイを使わずに受信するを使用してください。どちらの場合もセキュリティレベルは同じです。
Webhook を使った受信の理由
Resend は受信メールに Webhook を使用するため、メールが届くとすぐにエージェントへ通知されます。これは、次のような点でエージェントにとって有益です。
- リアルタイムで応答 — 数分ではなく、数秒以内にメールに対応できます
- ポーリングによるオーバーヘッドがない — 「新着メールはありますか?」と繰り返し確認する cron ジョブは不要です
- イベント駆動型アーキテクチャ — 実際に処理するメールがあるときだけ、エージェントが起動します
- API コストの削減 — 受信トレイが空なのに確認する無駄な呼び出しがなくなります
アーキテクチャ
Sender → Email → Resend Inbox → inbox.email.received webhook → Your Server → AI Agent
↓
Security Validation
↓
Process or Reject → Reply in the thread
SDK のバージョン要件
受信トレイのフローには、resend.inboxes.* と inbox.* Webhook イベントタイプを備えた Node.js SDK resend >= 6.34.1 が必要です。常に最新の SDK バージョンをインストールしてください。プロジェクトに Resend SDK がすでにインストールされている場合は、バージョンを確認し、必要であればアップグレードしてください。
Python SDK resend >= 2.50.0 にも受信トレイのメソッドがあります。他の言語では、SDK に受信トレイのメソッドが追加されるまで、HTTP 経由で Inboxes API を呼び出すか、受信トレイを使わずに受信するを使用してください。
以下の表に、受信フローで必要な最小バージョンを示します。対象となるのは、Webhook の検証(webhooks.verify())とメールの受信(emails.receiving.get())です。resend スキルでは、ドメインクレーム(domains.claims.*)などの新しい機能も扱うため、より高い最小バージョンが記載されています。両方のスキルを読み込んでいる場合は、より高いバージョンを使用してください。
| 言語 | パッケージ | 最小バージョン |
|---|---|---|
| Node.js | resend | >= 6.9.2 |
| Python | resend | >= 2.21.0 |
| Go | resend-go/v4 | >= 4.0.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 スキルをインストールしてください。
クイックスタート
- ユーザーにメールアドレスを尋ねます。 テストメールの送信先として、実際に使えるメールアドレスが必要です。ユーザーに尋ね、回答を待ってから次に進んでください。
- セキュリティレベルを選択します。 メールを処理する前に、受信メールをどのように検証するかを決めます
- ドメインを設定します。 送信に使用するユーザーのカスタムドメインを認証します。受信にも使用する場合は、受信トレイで転送を使用する場合を除き、受信用にも認証してください(「ドメインの設定」セクションを参照)
- 受信トレイを作成します。
resend.inboxes.create()を使ってエージェントのアドレスを作成します(「受信トレイの作成」セクションを参照) - Webhook エンドポイントを作成します。 最初からセキュリティを組み込んで、
inbox.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 キーの権限
エージェントのサーバーには、フルアクセスの API キーが必要です。「送信アクセス」キーで可能なのは、返信、転送、下書きの作成、下書きの送信のみです。このキーで受信トレイのメールを読み取ったり、Webhook を登録したりすると、401 restricted_api_key が返されます。1 つのドメインに制限された送信アクセスキーを使って別のドメインの受信トレイにアクセスすると、403 が返されます。
ユーザーがほかのプロジェクトにも使用している既存の Resend アカウントを持っている場合は、リスクを限定してください。
- **可能であれば、エージェント専用のアカウントを使用します。**キーが漏えいしても、ほかのプロジェクトにアクセスできません。
- **キーはサーバー上だけに保管します。**AI モデルに渡したり、プロンプトに記載したりしないでください。
- キーをチャットに貼り付けたり、コミットしたりした場合は、ローテーションします。
ドメインの設定
受信トレイのアドレスには、ユーザーが Resend に追加したカスタムドメインを使用します。送信するには、ドメインの認証(DKIM と SPF)が必要です。このドメインでメールを受信するには、ドメインで受信を有効にするか、forwarding: true を指定して受信トレイを作成します。
既存のメールサービスに影響を与えないよう、サブドメイン(例:agent.example.com)を使用してください。
オプション 1:ドメインで受信する(推奨)
ユーザーは Resend ダッシュボードで受信を有効にする必要があります。「Domains」ページ →「Enable Receiving」をオンにします。
次に、MX レコードを追加します。
| 設定 | 値 |
|---|---|
| 種類 | MX |
| ホスト | ドメインまたはサブドメイン(例:agent.example.com) |
| 値 | Resend ダッシュボードに表示されます |
| 優先度 | 10(優先されるよう、最も小さい数値にする必要があります) |
ヒント: dns.email で DNS の反映状況を確認できます。
DNS の反映:MX レコードの変更が全世界に反映されるまで、最大 48 時間かかることがあります。通常は数時間で完了します。
オプション 2:転送
アドレスがすでに別の場所(たとえば Google Workspace)でメールを受信しており、ユーザーがその MX レコードを変更できない場合は、forwarding: true を指定して受信トレイを作成します。Resend により、受信トレイに受信用アドレスが割り当てられます。ユーザーはメールボックスからそのアドレスへの転送を設定します。resend.inboxes.get() で receiving_address からアドレスを読み取ります。
受信トレイを作成する
エージェントのアドレス用に受信トレイを 1 つ作成します。
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.inboxes.create({
emailAddress: '[email protected]',
name: 'AI Agent',
fromName: 'Acme Agent',
});
if (error) throw new Error(`Failed to create inbox: ${error.message}`);
console.log('Inbox created:', data.id);
fromName は、エージェントの返信で受信者に表示される名前です。@ より前の部分に使用できるのは、英字、数字、.、_、%、- のみです。そのため、agent+test@ を指定すると 422 が返されます。403 エラーは、プランで作成できる受信トレイの上限に達していることを意味します。ユーザーにアップグレードを依頼するか、受信トレイを使用せずに受信する方法を利用してください。
セキュリティレベル
Webhook エンドポイントを設定する前に、セキュリティレベルを選択してください。 セキュリティ対策なしでメールを処理する AI エージェントは危険です。誰でもエージェントが実行する指示をメールで送信できてしまいます。次に作成する Webhook コードには、最初から選択したセキュリティレベルを含めてください。
ユーザーに希望するセキュリティレベルを尋ね、それぞれのレベルの意味を理解していることを確認してください。
| レベル | 名前 | 使用する場面 | トレードオフ |
|---|---|---|---|
| 1 | 厳格な許可リスト | ほとんどのユースケース。既知の固定された送信者リストがある場合 | 最大限のセキュリティを確保できますが、機能が制限されます |
| 2 | ドメイン許可リスト | 信頼できるドメインから組織全体でアクセスする場合 | より柔軟ですが、そのドメイン内の誰もがやり取りできます |
| 3 | コンテンツフィルタリング | 誰からでも受信し、安全でないパターンを除外する場合 | 誰からでも受信できますが、パターンマッチングでは完全に防げません |
| 4 | サンドボックス処理 | エージェントの機能を制限して、すべてのメールを処理する場合 | 最大限の柔軟性がありますが、実装が複雑です |
| 5 | ヒューマン・イン・ザ・ループ | 信頼できない操作に人間の承認を必要とする場合 | 最大限のセキュリティを確保できますが、遅延が増えます |
各レベルの詳しい実装コードについては、references/security-levels.md を参照してください。
レベル 1:厳格な許可リスト(推奨)
明示的に承認されたアドレスからのメールのみを処理します。それ以外はすべて拒否します。
import type { GetInboxThreadEmailResponseSuccess as InboxEmail } from 'resend';
const ALLOWED_SENDERS = [
'[email protected]',
'[email protected]',
];
// `from` is either `address` or `Name <address>`. Compare the address only:
// a substring check would accept a display name such as "[email protected]".
function senderAddress(from: string): string {
const match = from.match(/<([^<>]+)>\s*$/);
return (match ? match[1] : from).trim().toLowerCase();
}
function isAllowedSender(from: string): boolean {
const sender = senderAddress(from);
return ALLOWED_SENDERS.some(allowed => sender === allowed.toLowerCase());
}
// Identifies the email, so the agent can reply in its thread
type ThreadRef = { inboxId: string; threadId: string; emailId: string };
async function processEmailForAgent(email: InboxEmail, thread: ThreadRef) {
if (!isAllowedSender(email.from)) {
console.log(`Rejected email from unauthorized sender: ${senderAddress(email.from)}`);
await notifyOwnerOfRejectedEmail(email);
return;
}
await agent.processEmail({
from: email.from,
subject: email.subject,
body: email.text || email.html,
thread,
});
}
InboxEmail は resend.inboxes.threads.emails.get() が返すメールです。Webhook ペイロードには html と text を除き、同じフィールドが含まれます。agent と notifyOwnerOfRejectedEmail は、独自のコードを表します。
セキュリティのベストプラクティス
常に実施すること
| プラクティス | 理由 |
|---|---|
| Webhook の署名を検証する | 偽造された Webhook イベントを防ぎます |
| 拒否したメールをすべて記録する | セキュリティレビュー用の監査証跡になります |
| 可能な場合は許可リストを使用する | 明示的な信頼はフィルタリングより安全です |
| メール処理にレート制限を設ける | 過剰な処理負荷を防ぎます |
| 信頼できる処理と信頼できない処理を分ける | リスクレベルに応じて異なる処理が必要です |
絶対に行わないこと
| アンチパターン | リスク |
|---|---|
| 検証せずにメールを処理する | 誰でもエージェントを制御できてしまいます |
| 認証にメールヘッダーを信頼する | ヘッダーは簡単に偽造できます |
| メールの内容からコードを実行する | 信頼できない入力をコードとして実行してはいけません |
| メールの内容をそのままプロンプトに保存する | 信頼できない入力をプロンプトに混ぜると、エージェントの動作が変わるおそれがあります |
| 信頼できないメールにエージェントへの完全なアクセスを許可する | 機能は必要最小限に制限してください |
Webhook エンドポイント
セキュリティレベルを選択し、ドメインを設定したら、Webhook エンドポイントを作成します。Webhook エンドポイントは必ず POST ルートにしてください。 Resend はすべての Webhook イベントを POST リクエストとして送信します。
重要:検証には生のボディを使用してください。 Webhook の署名検証には、生のリクエストボディが必要です。
- Next.js App Router:
req.json()ではなくreq.text()を使用します- 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) {
const payload = await req.text();
let event;
try {
event = resend.webhooks.verify({
payload,
headers: {
id: req.headers.get('svix-id') ?? '',
timestamp: req.headers.get('svix-timestamp') ?? '',
signature: req.headers.get('svix-signature') ?? '',
},
webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
});
} catch {
return new NextResponse('Invalid signature', { status: 400 });
}
if (event.type === 'inbox.email.received') {
const { inbox_id, thread_id, email_id } = event.data;
// The webhook payload has no body. Fetch the email to read it.
const { data: email, error } = await resend.inboxes.threads.emails.get({
inboxId: inbox_id,
threadId: thread_id,
emailId: email_id,
});
if (error) {
// A non-2xx response makes Resend retry the webhook later
return new NextResponse('Error', { status: 500 });
}
// Apply the security level chosen above
await processEmailForAgent(email, {
inboxId: inbox_id,
threadId: thread_id,
emailId: email_id,
});
}
return new NextResponse('OK', { status: 200 });
}
Express
import express from 'express';
import { Resend } from 'resend';
const app = express();
const resend = new Resend(process.env.RESEND_API_KEY);
const ALLOWED_SENDERS = (process.env.ALLOWED_SENDERS || '')
.split(',')
.map((sender) => sender.trim().toLowerCase())
.filter(Boolean);
const senderAddress = (from) => {
const match = from.match(/<([^<>]+)>\s*$/);
return (match ? match[1] : from).trim().toLowerCase();
};
const isAllowedSender = (from) => ALLOWED_SENDERS.includes(senderAddress(from));
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = resend.webhooks.verify({
payload: req.body.toString(),
headers: {
id: req.headers['svix-id'],
timestamp: req.headers['svix-timestamp'],
signature: req.headers['svix-signature'],
},
webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
});
} catch {
res.status(400).send('Invalid signature');
return;
}
if (event.type === 'inbox.email.received') {
const { inbox_id, thread_id, email_id, email } = event.data;
// The payload already has the sender, so reject before fetching the body
if (!email || !isAllowedSender(email.from)) {
console.log(`Rejected email from unauthorized sender: ${email ? senderAddress(email.from) : 'unknown'}`);
res.status(200).send('OK'); // Return 200 even for rejected emails
return;
}
const { data: fullEmail, error } = await resend.inboxes.threads.emails.get({
inboxId: inbox_id,
threadId: thread_id,
emailId: email_id,
});
if (error) {
res.status(500).send('Error'); // Resend retries the webhook later
return;
}
await processEmailForAgent(fullEmail, { inboxId: inbox_id, threadId: thread_id, emailId: email_id });
}
res.status(200).send('OK');
});
app.get('/', (req, res) => res.send('Agent Email Inbox - Ready'));
app.listen(3000, () => console.log('Webhook server running on :3000'));
Resend は、他のすべての Webhook と同じ svix-* ヘッダーを使って、受信トレイの Webhook に署名します。inbox.email.received は、受信トレイがメールを受信したときに発生します。エージェント自身の返信では、代わりに inbox.email.sent が発生するため、inbox.email.received のみを購読してください。
API による Webhook の登録、トンネリングの設定、svix フォールバック、再試行の動作については、references/webhook-setup.md を参照してください。
エージェントから返信する
スレッド内で返信します。Resend は返信先を設定し、In-Reply-To ヘッダーと References ヘッダーを設定するため、送信者のメールクライアントでは同じ会話として表示されます。
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
async function replyInThread(
thread: { inboxId: string; threadId: string; emailId: string },
body: string,
) {
const { data, error } = await resend.inboxes.threads.emails.reply({
inboxId: thread.inboxId,
threadId: thread.threadId,
emailId: thread.emailId,
text: body,
});
if (error) throw new Error(`Failed to reply: ${error.message}`);
return data.id;
}
返信は、メールの Reply-To アドレスに送信されます。Reply-To がない場合は、送信者に送信されます。許可リストにある送信者は、Reply-To を任意のアドレスに設定できます。そのため、エージェントが機密情報を含む返信を送る前に、email.reply_to も許可リストと照合してください。元のメールのすべての宛先にも返信する必要がない限り、replyAll はオフにしてください。
新しい会話を始めるには、受信トレイのアドレスから resend.emails.send() を使用して送信します。送信の詳細については、resend スキルをインストールしてください。
受信トレイなしで受信する
ユーザーがカスタムドメインを持っていない場合や、プランで受信トレイを利用できない場合は、Resend Receiving を直接使用してください。上記のセキュリティレベルはそのまま適用されます。異なる点は次のとおりです。
- アドレス: 自動生成された
<anything>@<your-id>.resend.appアドレス(Dashboard → Emails → Receiving → "Receiving address")を使用するか、受信を有効にしたカスタムドメインを使用します。受信トレイは作成されません。 - Webhook イベント:
inbox.email.receivedではなくemail.receivedを購読します。ペイロードにはemail_id、from、to、subjectが含まれますが、本文は含まれません。 - メールの読み取り:
resend.emails.receiving.get(emailId)で取得します。 - 返信:
resend.emails.send()を使用して送信し、In-Reply-Toヘッダーを自分で設定します。スレッドはありません。
if (event.type === 'email.received') {
if (!isAllowedSender(event.data.from)) {
return new NextResponse('OK', { status: 200 });
}
const { data: email, error } = await resend.emails.receiving.get(event.data.email_id);
if (error) {
return new NextResponse('Error', { status: 500 });
}
await agent.processEmail({ from: email.from, subject: email.subject, body: email.text || email.html });
}
Webhook の検証方法は、受信トレイの例と同じです。
環境変数
# Required
RESEND_API_KEY=re_xxxxxxxxx
RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxx
# Security Configuration
SECURITY_LEVEL=strict # strict | domain | filtered | sandboxed
[email protected],[email protected]
ALLOWED_DOMAINS=example.com
[email protected] # 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 バージョン | resend.inboxes.* には resend >= 6.34.1 が必要です。emails.receiving.get() と webhooks.verify() にも最近のバージョンが必要です。「SDK のバージョン要件」を参照してください |
from を文字列全体で比較する | from は Name <address> の形式の場合があります。許可リストと比較する前にアドレスを抽出してください |
Reply-To を確認せずに返信する | 信頼できる送信者でも、Reply-To を任意の宛先に指定できます。機密情報を含む返信を送る前に、許可リストと照合してください |
| "Sending access" API キーを使用する | 返信、転送、下書きの作成、下書きの送信のみが可能です。受信トレイの読み取りと Webhook の設定には、フルアクセスキーが必要です |
secret または svix-* キーを webhooks.verify() に渡す | webhookSecret を渡し、headers は { id, timestamp, signature } としてください |
テスト
開発には Resend のテスト用アドレスを使用してください。
[email protected]— 配信成功をシミュレートします[email protected]— ハードバウンスをシミュレートします
セキュリティテストでは、許可リストにないアドレスからテストメールを送信し、正しく拒否されることを確認してください。
簡易確認チェックリスト:
- サーバーが稼働している:
curl http://localhost:3000が応答を返すこと - トンネルが動作している:
curl https://<your-tunnel-url>が同じ応答を返すこと - Webhook が有効になっている:Resend ダッシュボード → Webhooks でステータスを確認すること
- 許可リストにあるアドレスからテストメールを送信し、サーバーログを確認すること
関連スキル
- 送信、受信、受信トレイのドキュメント一式を利用するには、
resendスキルをインストールしてください