mcpField
R
resend-inbound-emails

resend-inbound-emails

github.com/blink-new生産性53 回インストール
リポジトリ

resend-inbound-emails について

resend-inbound-emailsは、Resendを使ったメール送受信と受信トレイのワークフローを実装するスキルです。ドメイン管理や受信Webhook、スレッド化に加え、任意でパーソナライズや一括送信にも対応します。

主な機能

  1. 01送信ドメインの管理と検証
  2. 02受信メールWebhookの処理
  3. 03スレッド化メールの送信
  4. 04受信トレイの更新ポーリング
  5. 05任意のAIパーソナライズとレビュー
  6. 06任意のキューによる一括送信

活用シーン

  1. 01メールスレッドに対応したチーム受信トレイの構築
  2. 02ResendのWebhookによる返信受信
  3. 03レビュー後のパーソナライズメール送信
  4. 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.ts
domains-activate-route.ts
domains-route.ts
domains-verify-route.ts
inbox-events-sse-route.ts
inbox-send-route.ts
inbox-updates-route.ts
users-me-route.ts
webhook-resend-route.ts
assets/components/
email-settings-section.tsx
inbox-notification-provider.tsx
notifications-section.tsx
team-personalization-section.tsx
assets/config/
env-additions.txt
assets/hooks/
use-available-domains.ts
use-inbox-polling.ts
use-inbox-realtime.ts
use-inbox.ts
use-team-domains.ts
use-user.ts
assets/lib/
redis.ts
resend.ts
assets/lib/inbox/
email-headers.ts
inbound-notification.ts
reply-parser.ts
threading.ts
assets/lib/personalization/
process-liquid-tags.ts
types.ts
assets/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.tsResend クライアントとアドレスビルダー
assets/lib/inbox/threading.tsスレッドの照合ロジック
assets/lib/inbox/email-headers.tsRFC 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.tsWebhook ハンドラー
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.tsxAI パーソナライズ設定(任意)
assets/components/inbox-notification-provider.tsxポーリングプロバイダーのラッパー
assets/lib/redis.tsRedis pub/sub クライアント(任意)
assets/api/inbox-events-sse-route.tsSSE エンドポイント(任意)
assets/hooks/use-inbox-realtime.tsSSE フック(任意)

セットアップ手順

1. Resend を設定する
  1. resend.com でアカウントを作成します
  2. ダッシュボードから API キーを取得します
  3. 受信ドメインを設定します:
    • 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/ のテンプレートファイルをプロジェクトの構成にコピーします。

受信メールの照合優先順位

メールを受信したとき:

  1. 重複排除 - resendEmailId がすでに存在するか確認します(重複している場合はスキップします)
  2. OUTBOUND の検出 - チームメンバーが外部宛てに送信しているか確認します
  3. RFC 5322 ヘッダー - Message-ID、In-Reply-To、または References を使って照合します
  4. クリエイターのメールアドレス - 送信者をメインのクリエイターのメールアドレスと照合します
  5. クリエイターの連絡先 - 送信者を関連付けられた連絡先メールアドレスと照合します
  6. ドメイン照合 - 送信者のドメインをクリエイターのメールドメインと照合します
  7. 自動作成 - 一致するものが見つからない場合、新しいクリエイターを作成します

OUTBOUND の検出: チームメンバーが個人用メールクライアント(Gmail、Outlook)から外部アドレス宛てにメールを送信すると、webhook はそれを email.received として受信します。ハンドラーは送信者がチームメンバーかどうかを確認してこれを検出し、OUTBOUND メッセージレコードを作成します。

ユーザーのドメイン選択フロー

  1. 管理者が Team Settings → Domains からドメインを追加します
  2. ユーザーが DNS レコード(MX、SPF、DKIM)を設定します
  3. 管理者が「Verify DNS」ボタンでドメインを検証します
  4. 管理者がドメインを有効化します(チームごとに 1 つのみ有効)
  5. ユーザーが Personal Settings → Email で優先ドメインを選択します
  6. 送信メールでは、From アドレスにユーザーが選択したドメインが使用されます
  7. 追跡用の 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 パーソナライズの設定を構成できます。

フィールド説明
personalizationAboutUsAI のコンテキスト向けのチーム説明(最大 2000 文字)
personalizationModelIdAI モデル: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 パッケージ

フロー:

  1. Webhook がメールを受信 → publishInboxEvent(teamId, event) を呼び出す
  2. Redis がチャネル inbox:events:{teamId} にパブリッシュする
  3. チャネルを購読している SSE 接続がイベントを即時に受信する
  4. フロントエンドが 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 ルート)

コメント