mcpField
A
agent-email-inbox

agent-email-inbox

github.com/resendエージェントワークフロー20713.6k 回インストール2 週間前に更新
リポジトリ

agent-email-inbox について

agent-email-inbox は、Resend を使ってアプリケーションや AI エージェントがメールを受信・返信するための受信箱構築スキルです。Webhook による処理と、信頼できないメールに対する複数の検証方式を重視します。

主な機能

  1. 01Webhook 起点の受信メール処理
  2. 02Resend の受信箱でのスレッド返信
  3. 03メール検証の 5 段階のセキュリティ
  4. 04Webhook 署名検証のガイド
  5. 05ドメイン受信と転送の設定

活用シーン

  1. 01AI エージェントをメール受信箱に接続
  2. 02許可した送信者のみを処理
  3. 03人の承認を伴う受信メール処理
  4. 04既存のメールスレッドで返信

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/resend/resend-skills --skill agent-email-inbox

インストール後、Claude Code で/agent-email-inboxを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
advanced-patterns.md
security-levels.md
webhook-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.jsresend>= 6.9.2
Pythonresend>= 2.21.0
Goresend-go/v4>= 4.0.0
Rubyresend>= 1.0.0
PHPresend/resend-php>= 1.1.0
Rustresend-rs>= 0.20.0
Javaresend-java>= 4.11.0
.NETResend>= 0.2.1

resend npm パッケージをインストールしてください。npm install resend を実行します(または、使用する言語に対応する同等の方法を使います)。メール送信に関するドキュメントをすべて確認するには、resend スキルをインストールしてください。

クイックスタート

  1. ユーザーにメールアドレスを尋ねます。 テストメールの送信先として、実際に使えるメールアドレスが必要です。ユーザーに尋ね、回答を待ってから次に進んでください。
  2. セキュリティレベルを選択します。 メールを処理する前に、受信メールをどのように検証するかを決めます
  3. ドメインを設定します。 送信に使用するユーザーのカスタムドメインを認証します。受信にも使用する場合は、受信トレイで転送を使用する場合を除き、受信用にも認証してください(「ドメインの設定」セクションを参照)
  4. 受信トレイを作成します。 resend.inboxes.create() を使ってエージェントのアドレスを作成します(「受信トレイの作成」セクションを参照)
  5. Webhook エンドポイントを作成します。 最初からセキュリティを組み込んで、inbox.email.received イベントを処理します。Webhook エンドポイントは必ず POST ルートにしてください。
  6. トンネリングを設定します(ローカル開発)。 Tailscale Funnel(推奨)または ngrok を使用します。references/webhook-setup.md を参照してください
  7. API 経由で Webhook を作成します。 Resend Webhook API を使って、エンドポイントをプログラムから登録します。references/webhook-setup.md を参照してください
  8. エージェントに接続します。 検証済みのメールを AI エージェントに渡し、スレッド内で返信します

始める前に:アカウントと API キーの設定

最初の質問:新しい Resend アカウントですか?それとも既存のアカウントですか?

ユーザーに確認します。

  • エージェント専用の新しいアカウントですか? → 設定が簡単で、アカウントへのフルアクセスを許可しても問題ありません
  • ほかのプロジェクトでも使っている既存のアカウントですか? → エージェントには、すべてのプロジェクトにアクセスできるフルアクセスキーが必要です。エージェント専用の新しいアカウントを推奨します(「API キーの権限」を参照)
API キーを安全に作成する

API キーをチャットに貼り付けないでください。会話履歴に永久に残ります。

より安全な方法:

  1. 環境ファイルを使う方法: ユーザーが .env ファイルを直接作成します:echo "RESEND_API_KEY=re_xxx" >> .env
  2. パスワードマネージャー/シークレットマネージャー: ユーザーがキーを 1Password、Vault などに保存します。
  3. チャットでキーを共有する必要がある場合: 設定後、すぐにキーをローテーションするようユーザーに依頼します。
API キーの権限

エージェントのサーバーには、フルアクセスの API キーが必要です。「送信アクセス」キーで可能なのは、返信、転送、下書きの作成、下書きの送信のみです。このキーで受信トレイのメールを読み取ったり、Webhook を登録したりすると、401 restricted_api_key が返されます。1 つのドメインに制限された送信アクセスキーを使って別のドメインの受信トレイにアクセスすると、403 が返されます。

ユーザーがほかのプロジェクトにも使用している既存の Resend アカウントを持っている場合は、リスクを限定してください。

  1. **可能であれば、エージェント専用のアカウントを使用します。**キーが漏えいしても、ほかのプロジェクトにアクセスできません。
  2. **キーはサーバー上だけに保管します。**AI モデルに渡したり、プロンプトに記載したりしないでください。
  3. キーをチャットに貼り付けたり、コミットしたりした場合は、ローテーションします。

ドメインの設定

受信トレイのアドレスには、ユーザーが 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 のテスト用アドレスを使用してください。

セキュリティテストでは、許可リストにないアドレスからテストメールを送信し、正しく拒否されることを確認してください。

簡易確認チェックリスト:

  1. サーバーが稼働している:curl http://localhost:3000 が応答を返すこと
  2. トンネルが動作している:curl https://<your-tunnel-url> が同じ応答を返すこと
  3. Webhook が有効になっている:Resend ダッシュボード → Webhooks でステータスを確認すること
  4. 許可リストにあるアドレスからテストメールを送信し、サーバーログを確認すること

関連スキル

  • 送信、受信、受信トレイのドキュメント一式を利用するには、resend スキルをインストールしてください

コメント