Skip to main content
Glama
robconery

big-mailer

by robconery

big-mailer 📬

ブロードキャスト、ドリップシーケンス、トランザクションメールを1つのCloudflare Workerで。 有料ESPのセルフホスト代替品。リストも送信もエンゲージメントデータも、すべて自分の手元に残ります。

CI License: MIT TypeScript Cloudflare Workers

ステータス: 機能は完成しておりローカルで実行可能ですが、まだデプロイしていません。単一の運用者向けに作られており、マルチテナントではありません。これは省略ではなく設計によるものです。 残っている作業の正直なリストはデプロイ前にを参照してください。

The big-mailer dashboard, showing consent broken out by scope

デモデータ投入後のダッシュボード。スコープ別の同意が重要なパネルです。2人が個別のシリーズを離脱し、それでもリストには残っています。通常のESPではこの2つの数字は同じ数字になります。


アイデア 💡

どのESPも退会を1つのスイッチとして扱います。オンボーディングシリーズを終えた人が「退会」をクリックしてそれだけを止めると、静かにニュースレターからも去っていきます。あなたはそれに気づきません。数字が減るだけです。

ここでは、同意はスコープ単位です。 1つのシーケンスを離脱すると、そのシーケンスだけから外れます。ニュースレターを離脱しても、明示的にオプトインしたシリーズはキャンセルされません。完全にリストから削除されるのは、「すべてから退会」の明示的な操作、ハードバウンス、またはスパム報告だけです。

この非対称性こそがこのプロジェクトが存在する理由であり、コードベース内の他のすべては、それが偶然に壊れないように配置されています。


Related MCP server: Resend MCP Server

実行する 🚀

bun install
bun run db:migrate     # applies migrations to the local D1 database
bun run dev            # http://localhost:8787

http://localhost:8787 を開いてデモデータを投入をクリックしてください。12人、2つの稼働中のシリーズ、送信済みブロードキャストが作成されます。次に送信トレイを開いて、「送信された」メールを読んでください。

あなたのマシンからは何も出て行きません。EMAIL_PROVIDER=console がローカルのデフォルトで、完全にレンダリングされたメールを送信する代わりにアプリ内の送信トレイに書き込みます。APIキーは不要で、いじっている間に誤って実在の人物にメールを送ってしまうこともありません。

このプロジェクトの目的を試す 🎯

  1. 購読者 → 誰かを選ぶ → その人のプリファレンスセンターを開く

  2. そのURLに ?scope=sequence:2 を追加します。これはシーケンスメール内のリンクが実際に持つ形です。

  3. このシリーズだけを停止をクリック

  4. 購読者ページに戻る: ステータスは active のまま、ニュースレターにも残っていて、ちょうど1つのシリーズだけから外れています

  5. 同意パネルには、単一のシリーズを離脱した人と、(空の)完全に削除された人のリストが表示されます

その後ブロードキャストを送信しても、その人はまだ受け取ります。それがこのプロジェクトの主張のすべてです。

シーケンスの遅延は日単位で、最初のステップはデフォルトで0(参加時に届く)、以降のステップはデフォルトで1です。つまり、投入したシリーズは見ている間には終了しないので、ダッシュボードには時計を早送り(ローカルのみ)があります。保留中のすべてのステップを現在に引き寄せてティックを実行します。これを使うと、ステップ2がそのシリーズを離脱した人をスキップする様子が見られます。


中身 🗂

src/
  worker.tsx      fetch + scheduled + queue handlers — the whole entry point, 151 lines
  core/           domain logic: consent, sending, sequences, segments, rendering
  db/             Drizzle schema (24 tables) and the D1 client
  web/            server-rendered admin console (Hono + JSX, no frontend framework)
  api/            transactional send API, signup forms, media upload, bearer-key auth
  mcp/            MCP server — 93 tools, 4 resources, 4 prompts
  providers/      EmailProvider port + console and Resend adapters
  client/         the only browser JS in the project: the TipTap editor bundle
migrations/       drizzle-kit generated, applied by wrangler
docs/             problem brief, architecture, spec, and a decision log

約16,000行のTypeScript。bun run typecheck はWorkerとブラウザバンドルを別々にチェックし、クリーンです。


アーキテクチャ概要 🧱

Cloudflare Workers · Drizzle経由のD1 (SQLite) · 送信ファンアウト用のQueues · スケジューリング用のCron Triggers · メディア用のR2 · Hono + JSXサーバーレンダリング管理画面 · 認証用のCloudflare Access · console とResendアダプタを持つプラグ可能な EmailProvider ポート。

知っておく価値のある決定とその理由:

すべての messages 行は、1通の送信が行われる前に具体化されます。 ブロードキャストは事前に完全な受信者リストを解決し、意図した送信ごとに行を書き込み、その後にのみキューにファンアウトします。これにより、ブロードキャストはクラッシュ後に再開可能になり、キューの再試行全体で冪等になり、後から監査可能になります。受信者を送信時に遅延解決する方が安価ですが、ブロードキャスト途中の障害は回復不能な混乱に変わります。

同意はエンキュー時ではなく、プロバイダ呼び出しの直前に再チェックされます。 キューはメッセージ作成から数分後に配信される可能性があり、その間に誰かがオプトアウトできます。エンキュー時にチェックすると、その人にもメールが送信されてしまいます。

キューの並行性は6に固定されています。 1バッチが1つのプロバイダリクエストなので、バッチの並行性はそのままリクエストレートです。未設定のままにすると、Cloudflare Queuesは250の同時コンシューマに自動スケールし、Resendの10 req/s制限を429で埋め尽くし、3回の再試行をすべて消費し、完全に正常なメールをデッドレターにしてしまいます。100件のバッチを6つ並行させると、制限内に収まりながら毎秒約600通の余裕が生まれます。

抑制は購読者ではなくメールアドレスをキーにしています。 トランザクション受信者やバウンスしたアドレスには購読者行がまったくないことが多いため、subscribers.status フラグでは静かに見逃してしまいます。

観測可能なものはすべてD1の行であり、ログ行ではありません。 Workersのログは3〜7日で失効します。1週間の保持期間の監査証跡は監査証跡ではありません。mcp_calls は拒否を含むすべてのエージェントアクションを記録し、sync_runs はすべてのStripeプルを記録します。

管理コンソールにはパスワードがありません。 Cloudflare AccessがエッジでIDを終端し、src/web/auth.ts が転送されたJWTを適切に検証します。チームのライブJWKSに対する署名(アイソレートごとにキャッシュされ、未知のキーIDでは強制再取得)、alg は RS256 に固定、さらにaudience、issuer、exp、nbf を検証します。ヘッダーの存在は何も証明せず、証明として扱われることは決してありません。設定を誤るとミドルウェアはフェイルクローズし、あなたを含む全員をロックアウトします。それが正しい失敗の方向です。

メールHTMLレンダラーは手書きです(core/render-doc.ts)。@tiptap/html はサーバーエントリポイントに happy-dom が必要で、workerd 内では動作しないためです。結局これがより良い答えでした。ウォーカーはすべてのスタイルをインライン化し(Gmailは <style> を削除します)、ボタンにはネストしたテーブルを出力します(Outlookは <a> のパディングを無視します)。これは汎用のHTMLシリアライズではできません。

拒否された代替案とその理由を含む完全な決定ログは、docs/MEMORY.md にあります。


同意モデル 🔐

3つの独立したスコープ。狭い選択が広い選択にエスカレートすることは決してありません。

スコープ

保存場所

効果

シーケンス

sequence_optouts 行

その1つのシリーズだけから外れる。他はすべて継続。

ブロードキャスト

subscribers.status

ニュースレターから外れる。シリーズは継続。

グローバル

suppressions 行

すべてから外れる。法的な逃げ道。

グローバル抑制が書き込まれるのは、「すべてから退会」の明示的な操作、ハードバウンス、またはスパム報告だけです。

シーケンス送信は意図的に status = 'unsubscribed' を無視します。そのフラグはブロードキャストスコープだからです。ニュースレターを離脱した人でも、依頼したオンボーディングシリーズは受け取ります。トランザクションメール(領収書、ダウンロード)はマーケティングの同意を完全に無視し、無効なアドレスまたはスパム報告によってのみブロックされます。領収書はマーケティングではなく、退会した顧客にもダウンロードは必要です。


エディター ✍️

ブロック型リッチテキスト、TipTap v3、バニラ(Reactなし)。投入済みの下書き**「下書き: エディターでできるすべて」**を開くと、すべてを見られます。

  • 行で / → ブロックメニュー: 見出し、リスト、チェックリスト、引用、コード、テーブル、トグル、区切り線、画像、YouTube、CTAボタン

  • @ → パーソナライゼーションフィールドが実ノードとして挿入されるので、first_name のスペルミスは不可能

  • 左マージンのハンドルをドラッグして並べ替え。shift-selectで複数ブロックを選択

  • どこにでも画像をドロップまたはペースト → R2にアップロードされ、URLが返ると挿入される

  • コードブロックはシンタックスハイライト対応(15言語、Ruby、Elixir、TS、SQLを含む)

  • テキストを選択するとバブルメニューが表示。ボタンを選択すると、バブルメニューがそのURLとカラーピッカーに変わる

ボタンとマージタグは、メール専用に作られたカスタムノードです。 CTAはネストしたテーブルとしてレンダリングされ、すべてのスタイルがインライン化されます。マージタグは生の {{first_name}} テキストではなくノードです。生テキストのタイプミスは「こんにちは {{frist_name}}」をリスト全体に送信してしまうからです。

本文はTipTap JSONとして body_json に保存されます。body_md のレガシーマークダウンは引き続きレンダリングされ、エディターで開いた瞬間に変換されます。一括マイグレーションは行われません。一括マイグレーションが失敗するとアーカイブも道連れになるからです。

クライアントバンドルは約226KB(gzip)で、メールを作成する2つの画面でのみ読み込まれます。管理コンソールの他のすべては、JavaScriptゼロのサーバーレンダリングです。

ブラウザスモークテスト(bun run smoke、33チェック)があり、実際のChromiumを駆動します。名前を変更した拡張オプションはブラウザで静かに失敗し、本文フィールドが保存されなくなるからです。サーバー側ではそれを検出できません。


Claude Codeから操作する 🤖

Workerは POST /mcp/<secret> でMCPサーバーを提供します。93のツールがメーラー全体をカバーしており、エージェントはセグメントの作成、ブロードキャストの下書きと送信、シーケンスの構築、キャンペーン実績の閲覧、Stripeの調整ができます。

# 1. a path secret (this is what makes the endpoint exist at all)
openssl rand -hex 24                      # → put in .dev.vars as MCP_PATH_SECRET

# 2. an admin-scoped key — POST /seed prints one, or use apikey_create

# 3. point Claude Code at it
claude mcp add --transport http --scope local \
  --header "Authorization: Bearer $BIG_MAILER_KEY" \
  big-mailer "http://localhost:8787/mcp/$MCP_PATH_SECRET"

3つのゲート、最も安いものから順に。 定数時間で比較される推測不能なパスシークレット(ミスは 403 ではなく 404 を返します。誰も推測しないURLは何もないように見えるべきだからです)、次に admin スコープのベアラーキー(トランザクション用の send キーは到達できません)、その後にツールごとのガード。拒否を含むすべての呼び出しが mcp_calls に記録されます。

取り消し不能な送信にはプレフライトが必要です。 broadcast_send は broadcast_preflight からのトークンなしでは拒否されます。単回使用、10分の有効期限、コンテンツまたはオーディエンスへの編集で無効化されます。sequence_activate も同様です。さらに、MCP_ALLOW_SEND は本番環境では "false" なので、MCPはすべてを読み取り・下書きできますが、意図的にフリップするまでメールを送信できません。フリップして戻すことが即時のオフスイッチです。

エージェントは同意に触れる前に bigmailer://conventions を読むべきです。スコープ付き退会は、通常のESPでトレーニングされたものの想定する形ではありません。そしてそれを間違えることは、このプロジェクトが作られたまさにその失敗です。


Stripe → キャンペーン帰属 💳

STRIPE_SECRET_KEY を設定します(制限付き、charges/refunds/customersの読み取り専用)。UTC 09:17の毎日のcronが新しいチャージを取得し、各チャージを購入者の最後の帰属タッチに、またはチャージに metadata.campaign がある場合はそれに割り当てます。StripeのチャージIDで冪等なので、再実行や重複するバックフィルは無害です。

stripe_sync_preview はドライラン、sales_unattributed はヒューリスティックが配置できなかったもののワークリスト、sync_runs_list は夜間ジョブが実際に実行されていることを証明します。


コマンド ▶️

bun run dev

クライアントバンドルをビルドして、:8787でサーブ

bun run watch:client

変更時にエディターバンドルを再ビルド(dev と併用)

bun run smoke

エディターのブラウザスモークテスト。dev の実行が必要

bun run db:migrate

ローカルD1にマイグレーションを適用

bun run db:generate

src/db/schema.ts 編集後にマイグレーションを生成

bun run db:studio

ローカルデータベースに対するDrizzle Studio

bun run typecheck

Workerとブラウザバンドルを別々に型チェック

bun run deploy

ビルドしてから wrangler deploy --env production。先に以下のセクションを読むこと


実際に送信する 📮

.dev.vars.example を .dev.vars にコピーし、Resend キーを追加して、EMAIL_PROVIDER=resend を設定します。Resend のウェブフックを /webhooks/resend に向けて、バウンスと苦情が適切に抑制されるようにします。そのウェブフックがないと、無効なアドレスが抑制されることはなく、送信レピュテーションが静かに低下し、ドメイン全体の配信可能性を失うという緩やかな道をたどることになります。

シークレットは .dev.vars(gitignore 対象)または wrangler secret put に置きます。wrangler.jsonc には絶対に置かず、.dev.vars.example にも絶対に置かないでください。

⚠️ デプロイ前の注意

まだデプロイされていません。ここから本番稼働までの間には、実際のセットアップが必要です:

  • DEV_AUTH_BYPASS=true はトップレベルの wrangler.jsonc の vars にあり、アプリをローカルで実行可能にしています。--env production はこれを false に設定します。素の wrangler deploy は認証なしの管理コンソールを公開します。そのため bun run deploy は --env production をハードコードしています。これを回避しないでください。

  • database_id はプレースホルダーです。 wrangler d1 create で実際の D1 データベースを作成してください。

  • R2 バケット(big-mailer-media)と 2 つのキュー(big-mailer-send、big-mailer-dlq)を作成してください。キューには月額 5 ドルの Workers 有料プランが必要です。

  • PUBLIC_URL を実際のホストに設定してください。 これは 送信時に トラッキングリンクと画像 URL に埋め込まれるため、誤った値はすでに配信されたメールに恒久的に壊れたリンクを送り込むことになります。後から修正することはできません。

  • MCP_PATH_SECRET は実際のシークレット(wrangler secret put)である必要があり、var ではありません。未設定の場合、MCP エンドポイントは 404 を返します。これは安全なデフォルトです。意図的にオンにしてください。

  • Cloudflare Access には 1 つの Allow アプリといくつかの Bypass アプリが必要です。 単一の Allow ポリシーでホスト名全体を保護すると、トラッキングピクセル、サインアップフォーム、プリファレンスセンター、ウェブフック、MCP も保護されることになり、送信したすべてのメールのトラッキングピクセルが、すでに配信されたメールについて、恒久的にログイン画面へリダイレクトされることになります。Access は最も具体的なパスを最初に照合するため、/t、/f、/p、/api、/webhooks、/mcp にはそれぞれ独自の Bypass アプリが必要です。これらの各パスは独自の認証を備えているか、設計上公開されています。

  • サーバーサイドのテストスイートはありません。 bun run smoke がエディタをカバーしています。docs/SPEC.md は番号付きのテスト可能な要件として書かれており、実行可能な形に整えられています。


ドキュメント 📚

docs/PROJECT.md

問題、対象者、そして明示的にスコープ外とされているもの

docs/ARCHITECTURE.md

システム設計、データモデル、送信パイプライン

docs/SPEC.md

番号付きの行動要件。意図された動作のリファレンス

docs/MEMORY.md

決定ログ:選ばれたもの、却下されたもの、そしてその理由


コントリビューション 🤝

バグ報告、正確性の修正、メールクライアントのレンダリング修正は大歓迎です。マルチテナンシー、ドラッグ&ドロップビルダー、自前 SMTP は意図的にスコープ外です。PR を開く前に CONTRIBUTING.md を参照し、脆弱性を見つけた場合は SECURITY.md を参照してください(issue ではなく非公開で報告してください)。

フォークは心から推奨されています。このプロジェクトは十分に小さいので、最初から最後まで読んで自分用にカスタマイズできます。参加は 行動規範 に従います。

ライセンス 📄

MIT © Rob Conery

Related MCP Connectors

Related MCP Servers

  • A
    license
    C
    quality
    D
    maintenance
    MCP server that exposes the complete Libredesk REST API (54 endpoints) as tools, enabling natural language management of conversations, contacts, agents, teams, and more for the open-source customer support desk.
    54
    8 npm
    4
    MIT
  • A
    license
    A
    quality
    F
    maintenance
    An MCP server for the Resend email API, enabling AI assistants to send emails, manage contacts, audiences, and domains through natural language.
    18
    21 npm
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Remote MCP server for the Transmit email platform, enabling email sending, contact management, template and campaign operations via natural language.
    -
  • A
    license
    C
    quality
    D
    maintenance
    Comprehensive MCP server for Mailchimp Marketing API v3.0 with over 104 tools and 15+ React UI apps, enabling management of campaigns, audiences, ecommerce, automations, reports, and more via natural language.
    100
    1
    MIT