# コードで管理するメール：見える化して、移行して、改善する

正規URL：https://brew.new/ja/blog/emails-in-code-visibility-migration-mcp
執筆：Thomas Park
公開日：2026-10-12
更新日：2026-10-15

コードで管理するメールとは、コードベースで定義され、ほかの機能と同じようにリリースされるプロダクトメールのことです。Resend と React Email で開発しているチームに、製品がどんなメールを送っているのかを尋ねると、一瞬の沈黙が返ってきます。ウェルカムメールは `emails/` の中のコンポーネントです。パスワードリセットは、認証プロバイダーのダッシュボードにあります。トライアル終了のメールは、cron ジョブの中の文字列です。領収書は Stripe が送っています。「またお会いしたいです」というメールを何が送っているのかは、誰も覚えていません。

どのメールも手軽にリリースできたので、全体像に責任を持つ人がいないのです。私たちなら、見える化、移行、改善の順で解決します。そのほとんどは、すでにお使いのコーディングエージェントを、MCP経由で Brew に接続すれば任せられます。

## この記事のポイント

- 新しいツールではなく、棚卸しから始めましょう。すべてのメールについて、トリガー、ファイル、変数を洗い出します。
- コーディングエージェントは、送信の呼び出しを見つけ、各テンプレートをレンダリングし、MCPツールの `import_email` でインポートできます。インポートは決定論的な処理で、AIを使わず、無料です。
- インポートしたメールはグループごとに1つのキャンバスに並ぶので、チームはデスクトップとモバイルの表示で、メールを並べてレビューできます。
- 移行では、Resend 式の送信を、トリガーと公開済みのオートメーションの組み合わせに置き換え、1回のREST呼び出しで発火させます。本番のコードが使うのは、MCPではなくRESTです。
- メールの種類ごとに1つずつ、フラグで切り替えながら移行し、トランザクションメールは最後に移しましょう。

## コードで定義したメールが見えなくなる理由

React Email と Resend のおかげで、開発者にとってのメールは格段に扱いやすくなりました。型付きのコンポーネントを、プルリクエストでレビューできます。代償が表れるのは、あとになってからです。

**メールが散らばっている**。コンポーネント、バックグラウンドジョブの中に直接書かれたHTML、サードパーティのダッシュボードにあるテンプレート、別のサービスが代わりに送っているメール。それらをすべて列挙したファイルはありません。

**プレビューは本番とは違う**。React Email のプレビューサーバーが表示するのは、プレビュー用の props を当てた1つのテンプレートだけです。顧客に実際に届いたものではなく、ほかのテンプレートは何も表示されません。

**エンジニアしか見られない**。プロダクト、デザイン、サポートの担当者は、エンジニアにプロジェクトを動かしてもらうよう頼まなければならないので、たいていは見ないままです。

**リリース後は誰もレビューしない**。2年前のメールには、今も古いロゴと、もう販売していないプランが載っています。メールをまとめて見る人がいないので、誰も気づきません。

## ステップ1：見える化

目標は、すべてのメールを1つの共有ビューで見られるようにすることです。このステップでは、本番環境には一切触れません。

![メールをコードから移す3つのフェーズ：import_email とグループごとのキャンバスによる見える化、ドメイン、コンタクト、トリガー、オートメーションによる移行、そして診断、レンダリングテスト、分析による最適化。](/images/blog/emails-in-code-three-phases.png)

*どのフェーズもそれだけで役に立ち、どの段階で止めてもかまいません。*

### コーディングエージェントを Brew に接続する

Brew のMCPサーバーは `https://brew.new/api/mcp` にあり、OAuthを使います。Brew にログインし、エージェントが作業できるブランドを選ぶだけです。Claude Code では、次のように追加します。

```bash
claude mcp add --transport http brew "https://brew.new/api/mcp"
```

サーバーを追加しても、保存されるのはこのプロジェクトのローカル設定だけです。すべてのプロジェクトで使うなら、`--scope user` を付けます。ツールを使う前に、`claude mcp login brew` を実行するか、`/mcp` を開いて Brew の認証を済ませてください。

[クライアントのセットアップのドキュメント](https://docs.brew.new/api-reference/mcp/connect-your-client)には、Cursor と Codex の手順も載っています。OAuthに対応していないクライアントでは、ブランド単位のAPIキーを使えます。接続したら、エージェントに `get_brew_capabilities` を呼び出させましょう。そうすれば、エージェントは推測に頼らず、稼働中のツールの一覧を読み取ります。

### 棚卸しのリストを作る

エージェントに、リストの作成を依頼します。

```text
Find every email this codebase sends. Search for provider calls
(resend.emails.send, postmark, sendgrid, nodemailer, SES), React Email
components and render() calls, and any email templates referenced from
config or a third-party dashboard.

For each email, record: a name, what triggers it, the file and line that
sends it, the template file, the provider, and the variables it uses.
Write the result as a table in EMAILS.md. List anything you could not
resolve under "Unknown" instead of guessing.
```

「Unknown」のセクションは残しておきましょう。認証の確認メールや Stripe の領収書は、コードの中の送信の呼び出しではありませんが、リストには載せるべきものです。`EMAILS.md` をコミットします。ここで止めたとしても、昨日まではなかったリストが手元に残ります。

### 各テンプレートを現実的なデータでレンダリングする

インポートに最適な入力は、コードがすでに出力しているHTMLです。React Email の [render ユーティリティ](https://react.email/docs/utilities/render)を使うと、そのHTMLを文字列として取得できます。

```tsx
import { render } from 'react-email'
import WelcomeEmail from './emails/welcome'

const html = await render(
  <WelcomeEmail firstName="Ana" planName="Pro" trialDays={14} />
)
```

古いプロジェクトでは、`render` を `@react-email/render` からインポートします。チームがレビューすることになるので、`"test"` のような値ではなく、現実的なテストデータを使いましょう。すべてのテンプレートをレンダリングしてファイルに書き出すスクリプトは、エージェントに書いてもらいます。

Brew は、`format: "jsx"` を指定すれば、React Email のモジュールを直接インポートすることもできます。その場合、モジュールは実行されず、静的に読み取られます。props はデフォルト値か `PreviewProps` の値になり、ほかのローカルファイルからインポートしたコンポーネントは除外されるか拒否され、`await` などは受け付けられません。単体で完結した1ファイルに収まらない場合は、レンダリングしたHTMLを使いましょう。

### すべてを1か所にインポートする

次に、レンダリングした各メールを `import_email` でインポートします。`groupName` は、プロダクトの領域ごとに1つずつ割り当てます。

```text
For each rendered email in ./email-renders, call import_email with
format "html", the title and subject line from EMAILS.md, and a
groupName by area: "Auth", "Onboarding", "Billing" or "Lifecycle".
Report the emailId for each one and any warnings in its assetReport.
```

インポートによってメールが書き換えられるのではないか、と開発者が気にするのはもっともです。書き換えられることはありません。

- AIモデルではなく決定論的なコンパイラーであり、クレジットを消費しません。
- 編集可能な Brew のデザインを生成しますが、配信では、インポートした行について元のソースのHTMLがそのまま使われます。
- 公開されている画像は Brew 側でホストし直し、取得できなかった画像は `assetReport` に一覧で表示されます。
- スクリプト、iframe、フォーム、イベントハンドラーを取り除き、既存の開封トラッキングピクセルを無効化します。
- ソースは1つあたり最大5MBです。URLでの入力には対応していません。ツールが受け取るのはマークアップそのもので、`baseUrl` は画像の相対パスを解決するためだけに使われます。

### チームでメールをレビューする

グループごとにキャンバスが作られます。`brew.new/emails/onboarding` を開くと、シーケンス全体が横に並び、デスクトップとモバイルの表示の切り替えや、各デザインへのコメントも使えます。キャンバスのリンクは、あなたの Brew ワークスペースにログインしている人なら開けます。公開用の共有リンクはないので、レビューする人を招待しましょう。

ここで問題が表面化します。ロゴが2種類ある、ボタンのリンクが切れている、フッターが3種類ある、といった具合です。何かを直す前に、`audit_email` を実行しましょう。リンク、画像、コンプライアンス、読み込みサイズ、クライアントの対応状況、アクセシビリティ、コピーをチェックし、完全な診断1回につき5クレジットを消費します。保存済みの `emailId` だけでなく、生のHTMLも受け付けるので、インポートしていないメールも診断できます。各チェックとしきい値については、[送信前のQAチェックリスト](/ja/blog/pre-send-email-qa-checklist)で説明しています。

これで、棚卸しのリスト、キャンバス上のすべてのメール、問題の一覧がそろいました。しかも、コードは以前と変わらず、すべてのメールを送り続けています。

## ステップ2：移行

ここからは、一部のメールをコードの代わりに Brew が送ります。すべてのメールを移す必要はありません。また、ある種類のメールを送るシステムは、どの時点でも常に1つだけにします。

### 概念を対応づける

| 現在のコード | Brew |
| --- | --- |
| `resend.emails.send({ react: <WelcomeEmail /> })` | トリガーイベントと、「メール送信」ステップを1つ持つ公開済みのオートメーション |
| コンポーネントに渡す props | トリガーのペイロードのフィールド。`{{ firstName \| there }}` や `{{ trigger.planName }}` のような差し込みタグとして使う |
| `from` のドメイン | Brew で認証済みの送信ドメイン |
| ユーザーのテーブル | REST APIや連携サービスで作成したコンタクト |
| シーケンスを送る cron ジョブ | 「待機」「フィルター」「メール送信」のステップを持つオートメーション |

### 1. 送信ドメインを設定する

エージェントは `create_domain` を呼び出してDNSレコードを取得し、レコードが反映されたら `verify_domain` を呼び出します。MCPやAPIで作成したドメインは、マーケティング用のドメインになります。パスワードリセットや領収書には、Brew のアプリで、用途を「トランザクション」に設定したドメインを用意しましょう。このドメインでは配信停止のフッターが省かれ、配信停止済みのコンタクトにもメールが届きます。レコードについては、[送信ドメインの認証](https://docs.brew.new/get-started/verify-your-sending-domain)で説明しています。

### 2. コンタクトを取り込む

MCPはコンタクトを更新できますが、作成はできません。コンタクトは、バックエンドから `POST /v1/contacts` で作成するか、アプリでCSVをアップロードします。このエンドポイントは、1件、または最大1,000件のバッチでコンタクトを作成・更新し、カスタムフィールドも自動で作成します。トリガーを発火させた場合も、ペイロードに含まれるコンタクトが作成または更新されます。インポートするのは、メールの受信に同意した人だけにしましょう。Brew が求める内容は、[コンタクトの追加](https://docs.brew.new/audience/add-contacts)で説明しています。

### 3. プレビュー用の値を差し込みタグに置き換える

見落としやすい点があります。インポートしたウェルカムメールには、文字どおり「Hi Ana」と書かれています。テストデータの値を、それぞれ `{{ firstName | there }}` のような差し込みタグに置き換えましょう。縦棒のあとのテキストが代替値です。Brew のエディターを使うか、置き換えだけに限定して、エージェントに `edit_email` を実行させます。これはクレジットを消費するAIによる編集なので、プレビューを確認してください。構文の一覧は、[差し込みタグ](https://docs.brew.new/create-emails/merge-tags)のドキュメントにあります。

### 4. トリガーとオートメーションを作成する

エージェントは `save_trigger` でトリガーを作成します。ペイロードのスキーマには、受信者の `email` を含める必要があります。型付きのペイロード定義を付け（`infer_payload_contract` がペイロードの例から下書きを作り、`set_trigger_contract` がそれを保存します）、`get_trigger_contract` を使って `ts` 形式か `zod` 形式で取得しましょう。そうすれば、バックエンドと Brew でペイロードの形がそろいます。

次に `save_automation` が、トリガーと、メール、ドメイン、件名を設定した「メール送信」ステップを組み立てます。公開するまでは下書きのままです。`dryRun` はグラフを検証し、`test_automation` は実際のメールをあなたのアドレスに送ります。`check_trigger` は、本番で何かを発火させる前に、公開済みのオートメーションがトリガーを待ち受けていることを確認します。

### 5. 呼び出し箇所を差し替える

アプリケーションのコードで変更するのは、ここだけです。変更前（Resend）：

```ts
await resend.emails.send({
  from: 'Acme <hello@acme.com>',
  to: user.email,
  subject: 'Welcome to Acme',
  react: <WelcomeEmail firstName={user.firstName} planName={plan.name} />,
})
```

変更後（Brew）：

```ts
const res = await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_WELCOME_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `welcome-${user.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, firstName: user.firstName, planName: plan.name },
    }),
  }
)

if (!res.ok) {
  // 422 means the automation is still a draft; 400 means the payload failed the schema.
  throw new Error(`Brew trigger failed: ${res.status} ${await res.text()}`)
}
```

`fetch` はリクエストが拒否されても resolve されるので、`res.ok` をチェックし、再試行やアラートはジョブランナーに任せましょう。エンドポイントは、イベントを受け付けると `202` を、トリガーを待ち受ける公開済みのオートメーションがない場合は `422` を返します。

`Idempotency-Key` は、ユーザーや注文など、変わらないIDから作ります。同じキーを繰り返し使っても2回送信されることはなく、元の実行が返されます。異なるリクエストボディで同じキーを再利用すると、`409 IDEMPOTENCY_CONFLICT` が返されます。Brew は、発火に使われたキーを、トリガーの受信記録と同じ期間、つまり90日間保持します。多くのAPIが採用している24時間ではありません。キーは恒久的なものとして扱い、1つの操作に1つのキーを使いましょう。タイムスタンプのキーは再試行のたびに変わってしまいますし、数週間後に別の操作で同じキーを再利用すると、送信されずに、以前のリクエストの再実行として扱われることがあります。

本番のコードは、MCPではなく、APIキーを使ってこのエンドポイントを呼び出します。OAuth接続では、MCPは本番の送信、トリガーの発火、公開のたびに、人に確認を求めます。チャットでは適切な挙動ですが、リクエストハンドラーには向きません。使い分けについては、[MCPとREST APIの比較ガイド](/ja/blog/mcp-vs-rest-api-for-email-marketing-which-workflow-should-you-use)で説明しています。

### 6. 1通ずつ切り替える

1. 新しい呼び出しを古い呼び出しと並べて置き、フィーチャーフラグで切り替えられるようにします。そうすれば、元に戻すのは設定の変更だけで済みます。
2. ライフサイクルで利用を促すメールのような、リスクの低いメールから始め、数日間、配信とクリックを見守ります。
3. トリガーが正しいペイロードで発火するようになったら、次にシーケンスを移します。
4. トランザクションメールは最後に移します。パスワードリセットやマジックリンクが壊れると、ユーザーは真っ先に気づきます。
5. 問題のない1週間が過ぎたら古い経路を削除し、`EMAILS.md` を更新します。

[Loops 移行ガイド](https://docs.brew.new/migrations/loops)でも、同じパターンを採用しています。

### コードに残すべきもの

Supabase の確認メールとマジックリンクのメールは、引き続き Supabase から送られます。Brew の Supabase 連携は、ユーザーのイベントを受け取ってオートメーションを開始するもので、Supabase Auth のメールを置き換えるものではありません。送信時に複雑なデータから組み立てる明細付きの請求書も、コードのままのほうがシンプルかもしれません。まずは、差し込みタグに何が必要かを確認しましょう。これらのメールも見える状態を保てるよう、インポートしたコピーをキャンバスに置いておきます。

## ステップ3：最適化

**一式をまとめて診断する**。大規模な送信の前に、グループ全体に `audit_email` を実行し、修正して、もう一度実行します。

**実際のクライアントで確認する**。`test_email_rendering` は、ダークモードを含め、特定のメールクライアントやデバイスでのスクリーンショットを返します。1回の実行につき10クレジットです。`create_inbox_placement_test` は、実際のメールをシード受信トレイに送り、どこに届いたかを報告します。こちらも10クレジットです。

**一式に統一感を持たせる**。不統一な部分はエディターで直すか、新しいフッターのような1つの変更を、エージェントに `edit_email` でグループ全体に適用させます。

**結果を読む**。`get_email_analytics` は、送信とエンゲージメントを報告します。`list_insights` は、Brew がアカウントで検出した所見を、それぞれの根拠となる指標とあわせて返します。

**パターンを意図的にテストする**。キャンバスでパターンを作り、クリックしたかどうかなどの行動にもとづいて、「分岐」ステップでコンタクトを振り分けます。Brew がランダム分割の勝者を選んでくれるわけではないので、パターンの比較は自分で行いましょう。

**新しいメールも見える状態に保つ**。`AGENTS.md` などに、次のルールを1行追加しましょう。新しいプロダクトメールは Brew で作成し、トリガーで発火させ、同じプルリクエストで `EMAILS.md` に追記する、というルールです。

## よくある質問

### Brew は React Email のテンプレートをインポートできますか？

はい。`import_email` は、レンダリング済みのHTML、MJML、保存した `.eml` メッセージ、React Email のモジュールを受け付けます。最も確実なのは、レンダリング済みのHTMLです。モジュールは静的に読み取られるため、props はデフォルト値かプレビュー用の値になり、ほかのローカルファイルからインポートしたコンポーネントは除外されるからです。

### Brew は Resend のアカウントと連携しますか？

いいえ。Resend との連携、Resend からのインポート、Resend へのエクスポートはありません。コードがレンダリングしたHTMLをインポートし、移行する場合は、コードから Resend を呼び出す代わりに、Brew のトリガーを発火させます。

### バックエンドからは、MCP経由で Brew を呼び出すべきですか？

いいえ。MCPは、コーディングエージェントからセットアップ、インポート、診断、テストを行うために使いましょう。本番のコードは、APIキーを使ってREST APIを呼び出すべきです。OAuth接続では、本番の送信やトリガーの発火のたびに、MCPが人に確認を求めるからです。

### メールをインポートすると、見た目は変わりますか？

インポートは、メールを編集可能な Brew のデザインに変換し、配信には元のソースのHTMLを使い続けます。スクリプト、iframe、フォーム、イベントハンドラーを取り除き、既存のトラッキングピクセルを無効化し、公開されている画像をホストし直します。Brew から送信する前に、キャンバス上で、デスクトップとモバイルの両方の表示で1通ずつ確認してください。
