resend-inbound-emails について
resend-inbound-emailsは、Resendを使ったメール送受信と受信トレイのワークフローを実装するスキルです。ドメイン管理や受信Webhook、スレッド化に加え、任意でパーソナライズや一括送信にも対応します。
主な機能
- 01送信ドメインの管理と検証
- 02受信メールWebhookの処理
- 03スレッド化メールの送信
- 04受信トレイの更新ポーリング
- 05任意のAIパーソナライズとレビュー
- 06任意のキューによる一括送信
活用シーン
- 01メールスレッドに対応したチーム受信トレイの構築
- 02ResendのWebhookによる返信受信
- 03レビュー後のパーソナライズメール送信
- 04一括メールキャンペーンのキュー送信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ blink-new/ claude --skill resend-inbound-emailsインストール後、Claude Code で/resend-inbound-emailsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/api/available-domains-route.tsdomains-activate-route.tsdomains-route.tsdomains-verify-route.tsinbox-events-sse-route.tsinbox-send-route.tsinbox-updates-route.tsusers-me-route.tswebhook-resend-route.tsassets/components/email-settings-section.tsxinbox-notification-provider.tsxnotifications-section.tsxteam-personalization-section.tsxassets/config/env-additions.txtassets/hooks/use-available-domains.tsuse-inbox-polling.tsuse-inbox-realtime.tsuse-inbox.tsuse-team-domains.tsuse-user.tsassets/lib/redis.tsresend.tsassets/lib/inbox/email-headers.tsinbound-notification.tsreply-parser.tsthreading.tsassets/lib/personalization/process-liquid-tags.tstypes.tsassets/prisma/schema-additions.prisma詳細
このスキルを使用する場面
以下の場合に使用します:
- メールの双方向通信(送信と受信)を設定するとき
- カスタム送信ドメインの管理を実装するとき
- スレッド機能を備えた受信トレイシステムを構築するとき
- AI によるメールのパーソナライズを追加するとき
- Webhook 経由で受信メールを処理するとき
機能の概要
コア機能(必須)
| 機能 | 説明 |
|---|---|
| カスタムドメイン | Resend API を介して送信ドメインを追加、検証、管理します |
| 受信 Webhook | スレッド機能を備えた受信メールを受信して処理します |
| 個別送信 | RFC 5322 のスレッド機能を使用してメールを個別に送信します |
| ユーザー設定 | 通知設定、送信ドメインの選択 |
| リアルタイム更新 | トースト通知を使用して、受信トレイを即時更新するためにポーリングします |
| 通知 | 返信時にチームメンバーへメール通知を送信します |
オプション機能
| 機能 | 説明 |
|---|---|
| AI パーソナライズ | Vercel AI SDK を使用して {{ tag }} を AI で置換します |
| プレビュー/レビュー | 送信前にパーソナライズ済みメールを確認、編集します |
| 一括送信 | pg-boss キューを介してメールをまとめて送信します |
| リッチテキストエディター | 添付ファイルと書式設定に対応した TipTap ベースのエディター |
アーキテクチャ図
┌─────────────────────────────────────────────────────────────────────┐
│ OUTBOUND FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ Composer → Detect {{tags}} → Generate Previews → Review Modal │
│ ↓ │
│ Send via Resend → Store InboxMessage → Update Thread │
├─────────────────────────────────────────────────────────────────────┤
│ INBOUND FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ Resend Webhook → Verify Signature → Parse Headers │
│ ↓ │
│ Match Thread (RFC 5322) → Resolve Creator → Store Message │
│ ↓ │
│ Notify Team → Publish Event → Update UI via Polling/SSE │
└─────────────────────────────────────────────────────────────────────┘
前提条件
- API キーを持つ Resend アカウント
- PostgreSQL データベース
- 一括メールキュー用の pg-boss
- パーソナライズ用の Vercel AI SDK
環境変数
# Resend
RESEND_API_KEY="re_your_api_key"
RESEND_WEBHOOK_SECRET="whsec_your_webhook_secret"
EMAIL_FROM="[email protected]"
# Your default inbound domain (set up in Resend dashboard)
# Format: emails to *@yourdomain.com will be forwarded to your webhook
DEFAULT_EMAIL_DOMAIN="inbox.yourdomain.com"
# App URL for notification links
NEXT_PUBLIC_APP_URL="https://yourdomain.com"
データベーススキーマ
Prisma スキーマに以下のモデルを追加します。
チームドメイン管理
model TeamDomain {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
resendDomainId String @unique // Resend's domain ID
domain String // e.g., "acme.com"
status String @default("not_started") // "not_started" | "pending" | "verified" | "invalid"
isActive Boolean @default(false) // Only one domain can be active per team
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([teamId, domain])
@@index([teamId])
}
受信トレイのスレッド機能
model InboxThread {
id String @id @default(cuid())
teamId String
creatorId String? // Link to your entity (creator, contact, etc.)
creatorEmail String // Primary email for this thread
primaryEmail String? // Original email the thread was created with
isRead Boolean @default(false)
isArchived Boolean @default(false)
lastMessageAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Track all emails that have participated
participantEmails String[] @default([])
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
messages InboxMessage[]
@@index([teamId, lastMessageAt])
@@index([teamId, creatorId])
}
model InboxMessage {
id String @id @default(cuid())
threadId String
direction InboxDirection // INBOUND or OUTBOUND
from String
to String
subject String?
textBody String?
htmlBody String?
resendEmailId String?
messageId String? // RFC 5322 Message-ID
inReplyTo String? // RFC 5322 In-Reply-To
references String[] // RFC 5322 References chain
sentAt DateTime?
receivedAt DateTime?
deliveryStatus InboxDeliveryStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
thread InboxThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
attachments InboxAttachment[]
@@index([resendEmailId])
@@index([messageId])
}
model InboxAttachment {
id String @id @default(cuid())
messageId String
filename String
url String
size Int?
contentType String?
message InboxMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)
}
enum InboxDirection {
INBOUND
OUTBOUND
}
enum InboxDeliveryStatus {
PENDING
SENT
DELIVERED
BOUNCED
FAILED
}
パーソナライズレビュー
model PersonalizedEmailPreview {
id String @id @default(cuid())
teamId String
creatorId String?
createdById String
originalSubject String
originalBody String @db.Text
personalizedSubject String
personalizedBody String @db.Text
explanation String? @db.Text
recipientEmail String
recipientName String?
status EmailReviewStatus @default(PENDING)
editedSubject String?
editedBody String? @db.Text
wasEdited Boolean @default(false)
sentMessageId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime // 7 days from creation
@@index([teamId, status])
}
enum EmailReviewStatus {
PENDING
SENT
EXPIRED
}
コアパターン
1. カスタムドメイン管理
ユーザーは Resend API を介してカスタム送信ドメインを追加できます。
// Add domain
const { data } = await resend.domains.create({ name: "acme.com" });
// Verify DNS records
await resend.domains.verify(domainId);
// Get DNS records for user to configure
const { data: details } = await resend.domains.get(domainId);
// details.records contains MX, TXT, DKIM records
2. From/Reply-To アドレスの構築
// Always use your inbound domain for reply-to to ensure tracking
const fromAddress = buildFromAddress(userEmail, userName, sendingDomain);
// Result: "Kai <[email protected]>"
const replyTo = buildReplyToAddress(userEmail);
// Result: "[email protected]" (your inbound domain)
3. RFC 5322 メールのスレッド機能
標準のメールヘッダーを使用してスレッドの継続性を維持します。
// On send: build headers from previous messages
const lastMessage = await getLastThreadMessage(threadId);
const headers = {
"Message-ID": `<${uuid()}@${EMAIL_DOMAIN}>`,
"In-Reply-To": formatMessageIdHeader(lastMessage?.messageId),
"References": buildReferencesHeader(
mergeReferences(lastMessage?.references ?? [], lastMessage?.messageId)
),
};
// On receive: match thread by headers
const match = await findThreadByHeaders({
teamId,
messageId: parseMessageIdHeader(headers["message-id"]),
inReplyTo: parseMessageIdHeader(headers["in-reply-to"]),
references: parseReferencesHeader(headers["references"]),
});
4. Resend Webhook の処理
// Verify webhook signature using Svix headers
const payload = await req.text(); // Must read as text, not json
const 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 ?? "",
});
// Handle event types
switch (event.type) {
case "email.received":
// Fetch full email content
const { data: email } = await resend.emails.receiving.get(event.data.email_id);
// email.html, email.text, email.headers, email.attachments
break;
case "email.delivered":
case "email.bounced":
// Update delivery status
break;
}
重要:
- 検証前に、ペイロードは
req.json()ではなくreq.text()で読み取ります - Svix ヘッダーは小文字とハイフンを使用します:
svix-id、svix-timestamp、svix-signature - 処理前に必ず
resendEmailIdを使用して重複を排除します
5. {{ タグ }} による AI パーソナライズ(任意)
注: この機能は任意です。AI によるメールのパーソナライズが不要な場合は、省略してください。
// Detect if personalization is needed
if (hasLiquidTags(subject) || hasLiquidTags(body)) {
const result = await generatePersonalizedEmail({
subject,
body,
context: buildPersonalizationContext({ creator, team, sender }),
});
// Verify all tags were replaced
if (hasLiquidTags(result.subject) || hasLiquidTags(result.message)) {
throw new Error("Personalization failed");
}
}
使用可能なタグ:
{{ name }}- クリエイターの名前{{ time_based_greeting }}- 時間帯に応じた挨拶{{ compliment }}- AI が生成する褒め言葉{{ content_fit_pitch }}- コラボレーションが適している理由{{ reply_cta }}- 行動喚起{{ user_name }}- 送信者の名前
パーソナライズに必要なもの:
- Vercel AI SDK(
@ai-sdk/gateway) - チームのパーソナライズ設定 UI
PersonalizedEmailPreviewモデル(レビューワークフロー用)
6. pg-boss による一括メール送信(任意)
注: この機能は任意です。メールを個別に送信するだけでよい場合は、省略してください。
// Queue bulk send job
await boss.send(QUEUES.BULK_SEND_EMAIL, {
teamId,
recipients: [{ creatorId, email, name }],
subject,
body,
fromEmail: session.user.email,
}, DEFAULT_JOB_OPTIONS);
// Worker processes in batches of 100 via Resend batch API
const { data } = await resend.batch.send(emailObjects);
一括送信に必要なもの:
- pg-boss のセットアップ(team-saas スキルを参照)
- 一括送信用 API ルート
BULK_SEND_EMAILジョブ用のワーカーハンドラー
ファイル構成
src/
├── app/api/
│ ├── teams/[teamId]/
│ │ ├── domains/
│ │ │ ├── route.ts # List/add domains
│ │ │ └── [domainId]/
│ │ │ ├── route.ts # Get/delete domain
│ │ │ ├── verify/route.ts # Verify DNS
│ │ │ └── activate/route.ts # Activate domain
│ │ ├── inbox/
│ │ │ ├── send/route.ts # Single email send
│ │ │ ├── bulk-send/route.ts # Bulk send (queued)
│ │ │ ├── threads/route.ts # List threads
│ │ │ ├── personalization/
│ │ │ │ └── preview/route.ts # Generate previews
│ │ │ └── reviews/ # Review management
│ │ └── ...
│ └── webhooks/
│ └── resend/route.ts # Webhook handler
├── lib/
│ ├── resend.ts # Client + address builders
│ ├── inbox/
│ │ ├── threading.ts # Thread matching
│ │ ├── email-headers.ts # RFC 5322 utilities
│ │ ├── reply-parser.ts # Strip quoted content
│ │ ├── resend-webhook.ts # Webhook helpers
│ │ └── inbound-notification.ts # Team notifications
│ ├── personalization/
│ │ ├── types.ts # Context types
│ │ ├── build-context.ts # Build AI context
│ │ ├── generate-personalized-email.ts
│ │ └── process-liquid-tags.ts # Tag detection
│ └── jobs/handlers/
│ └── bulk-email-handler.ts # Bulk send worker
├── hooks/
│ ├── use-inbox.ts # Thread/message hooks
│ ├── use-inbox-polling.ts # Real-time updates
│ ├── use-team-domains.ts # Domain management
│ └── use-email-reviews.ts # Review hooks
└── components/
├── inbox/
│ ├── inbox-editor.tsx # TipTap rich editor
│ ├── inbox-compose-dialog.tsx # Compose modal
│ ├── inbox-message-bubble.tsx # Message display
│ ├── personalization-button.tsx# Tag insertion
│ └── personalization-preview-modal.tsx
└── settings/
├── team-domains-section.tsx # Domain UI
└── domain-dns-records.tsx # DNS records table
含まれるアセットファイル
| アセット | 説明 |
|---|---|
assets/lib/resend.ts | Resend クライアントとアドレスビルダー |
assets/lib/inbox/threading.ts | スレッドの照合ロジック |
assets/lib/inbox/email-headers.ts | RFC 5322 ユーティリティ |
assets/lib/inbox/reply-parser.ts | 引用コンテンツの削除 |
assets/lib/inbox/inbound-notification.ts | 通知メール |
assets/lib/personalization/types.ts | コンテキストの型(任意) |
assets/lib/personalization/process-liquid-tags.ts | タグの検出(任意) |
assets/api/domains-route.ts | ドメイン管理 API |
assets/api/inbox-send-route.ts | 単一送信 API |
assets/api/webhook-resend-route.ts | Webhook ハンドラー |
assets/hooks/use-team-domains.ts | ドメイン用フック |
assets/hooks/use-inbox.ts | 受信トレイ用フック |
assets/prisma/schema-additions.prisma | スキーマモデル |
assets/api/users-me-route.ts | ユーザープロフィールの GET/PATCH |
assets/api/available-domains-route.ts | ユーザーが利用可能なドメインを取得 |
assets/api/inbox-updates-route.ts | 新着メッセージのポーリングエンドポイント |
assets/hooks/use-user.ts | ユーザープロフィール用フック |
assets/hooks/use-available-domains.ts | 利用可能なドメイン用フック |
assets/hooks/use-inbox-polling.ts | トースト通知付き受信トレイポーリング |
assets/components/notifications-section.tsx | 通知切り替え UI |
assets/components/email-settings-section.tsx | ドメイン選択 UI |
assets/components/team-personalization-section.tsx | AI パーソナライズ設定(任意) |
assets/components/inbox-notification-provider.tsx | ポーリングプロバイダーのラッパー |
assets/lib/redis.ts | Redis pub/sub クライアント(任意) |
assets/api/inbox-events-sse-route.ts | SSE エンドポイント(任意) |
assets/hooks/use-inbox-realtime.ts | SSE フック(任意) |
セットアップ手順
1. Resend を設定する
- resend.com でアカウントを作成します
- ダッシュボードから API キーを取得します
- 受信ドメインを設定します:
- Resend Dashboard → Domains に移動します
- 受信ドメイン(例:
inbox.yourdomain.com)を追加します - Resend を指すように DNS MX レコードを設定します
- webhook エンドポイントを設定します
2. Webhook を設定する
Resend Dashboard → Webhooks で設定します:
- URL:
https://yourdomain.com/api/webhooks/resend - イベント:
email.received、email.delivered、email.bounced、email.sent - webhook シークレットを
RESEND_WEBHOOK_SECRETにコピーします
3. 依存関係をインストールする
bun add resend
bun add @tiptap/react @tiptap/starter-kit @tiptap/extension-link @tiptap/extension-placeholder
4. データベースモデルを追加する
assets/prisma/schema-additions.prisma のスキーマ追加分をスキーマにコピーします。
5. アセットファイルをコピーする
assets/ のテンプレートファイルをプロジェクトの構成にコピーします。
受信メールの照合優先順位
メールを受信したとき:
- 重複排除 -
resendEmailIdがすでに存在するか確認します(重複している場合はスキップします) - OUTBOUND の検出 - チームメンバーが外部宛てに送信しているか確認します
- RFC 5322 ヘッダー - Message-ID、In-Reply-To、または References を使って照合します
- クリエイターのメールアドレス - 送信者をメインのクリエイターのメールアドレスと照合します
- クリエイターの連絡先 - 送信者を関連付けられた連絡先メールアドレスと照合します
- ドメイン照合 - 送信者のドメインをクリエイターのメールドメインと照合します
- 自動作成 - 一致するものが見つからない場合、新しいクリエイターを作成します
OUTBOUND の検出: チームメンバーが個人用メールクライアント(Gmail、Outlook)から外部アドレス宛てにメールを送信すると、webhook はそれを email.received として受信します。ハンドラーは送信者がチームメンバーかどうかを確認してこれを検出し、OUTBOUND メッセージレコードを作成します。
ユーザーのドメイン選択フロー
- 管理者が Team Settings → Domains からドメインを追加します
- ユーザーが DNS レコード(MX、SPF、DKIM)を設定します
- 管理者が「Verify DNS」ボタンでドメインを検証します
- 管理者がドメインを有効化します(チームごとに 1 つのみ有効)
- ユーザーが Personal Settings → Email で優先ドメインを選択します
- 送信メールでは、From アドレスにユーザーが選択したドメインが使用されます
- 追跡用の Reply-To には常に受信ドメインが使用されます
ユーザー設定
通知設定
ユーザーは受信した返信のメール通知を切り替えられます:
// PATCH /api/users/me
{ notifyInboundEmail: true }
// Only members with notifyInboundEmail: true receive notifications
// If creator has an assignee, only the assignee is notified
送信ドメインの選択
ユーザーは、検証済みのチームドメインから希望する送信ドメインを選択できます。
// GET /api/users/me/available-domains
// Returns all verified domains from user's teams
// PATCH /api/users/me
{ sendFromDomain: "acme.com" } // or null for default
// Validated: domain must be verified and belong to user's team
チーム設定
AI パーソナライズ設定(任意)
Note: AI パーソナライズが不要な場合は、このセクションをスキップしてください。
チームでは、AI パーソナライズの設定を構成できます。
| フィールド | 説明 |
|---|---|
personalizationAboutUs | AI のコンテキスト向けのチーム説明(最大 2000 文字) |
personalizationModelId | AI モデル:google/gemini-3-flash、anthropic/claude-sonnet-4.5 など |
personalizationInstructions | カスタム AI 指示(最大 2000 文字) |
personalizationPreviewEnabled | パーソナライズされたメールを送信する前にプレビューモーダルを表示 |
// PATCH /api/teams/[teamId]
{
personalizationAboutUs: "We are a marketing agency...",
personalizationModelId: "google/gemini-3-flash",
personalizationInstructions: "Keep tone professional but friendly",
personalizationPreviewEnabled: true
}
リアルタイム更新
2 つの方法を利用できます。シンプルでサーバーレス環境との互換性があるため、ポーリングを推奨します。
方法 1:ポーリング(推奨)
システムでは、サーバーレス環境での信頼性を高めるため、ポーリング(間隔 10 秒)を使用します。
// Wrap dashboard with InboxNotificationProvider
<InboxNotificationProvider>
<DashboardShell>{children}</DashboardShell>
</InboxNotificationProvider>
// Hook configuration
useInboxPolling(teamId, {
interval: 10000, // 10 seconds
enabled: true,
});
機能:
- 新しいメッセージのトースト通知(最大 3 件、それ以降は要約)
- 新しいメッセージが届くと受信トレイのクエリを自動再取得
- メモリのクリーンアップ(直近 100 件のメッセージ ID を保持)
- 「View」をクリックしてメッセージに移動
- Redis は不要
方法 2:Redis を使った SSE(任意)
真のリアルタイム更新を行うには、Redis の pub/sub と Server-Sent Events を使用します。
// Hook usage
useInboxRealtime(teamId, {
enabled: true,
onNewMessage: (data) => console.log("New message:", data),
});
要件:
- Redis インスタンス(例:Railway Redis)
REDIS_URL環境変数ioredisパッケージ
フロー:
- Webhook がメールを受信 →
publishInboxEvent(teamId, event)を呼び出す - Redis がチャネル
inbox:events:{teamId}にパブリッシュする - チャネルを購読している SSE 接続がイベントを即時に受信する
- フロントエンドが
useInboxRealtimeフックで更新する
イベントタイプ:
new_message- 新しい受信メールを受信inbox_update- スレッド/メッセージを更新message_status- 配信ステータスが変更(送信済み、配信済み、バウンス)
利点: 真のリアルタイム更新、より効率的 欠点: Redis が必要、サーバーレス環境では接続数に制限あり
チェックリスト
基本設定(必須)
- Resend API キーを設定
- Resend で受信ドメインをセットアップ
- Webhook エンドポイントをデプロイして検証
- データベーススキーマを更新(TeamDomain、InboxThread、InboxMessage)
- Resend クライアントユーティリティを追加
- スレッド処理ユーティリティを追加
- Webhook ハンドラーを実装
- ドメイン管理 API ルート
- メール単体送信 API ルート
- ユーザー設定(通知の切り替え、ドメインセレクター)
- トースト通知付きの受信トレイのポーリング
オプション機能
- AI パーソナライズシステム(lib/personalization/*、チーム設定 UI)
- PersonalizedEmailPreview モデルとレビュー API ルート
- 一括送信用 pg-boss ワーカー
- TipTap リッチテキストエディター
- 真のリアルタイム更新を実現する Redis を使った SSE(lib/redis.ts、inbox/events ルート)