outbound の見込み顧客収集は、inbound フォーム(add-leads)とは 別系統の閉域ローカル専用ツールとして作る。このツールは絶対に本番アプリに載せない(scraping コード・ DB・収集前の候補を公開リポに含めない)。集めた候補は人手で精査し、承認分だけを run-newsletter-ops の取込器へ渡す。このツール自体は 一切メールを送らない。
0. 法令と設計の非交渉条件
特定電子メール法では、Web で公表された「営業用メールアドレス」への送信は一定条件下でオプトイン例外。 その趣旨に沿い、収集器に以下を機械的に強制する(変更禁止):
| 条件 | 実装 |
|---|---|
| robots.txt 尊重 | ドメインごとに取得・解釈、Disallow は取得しない。root Disallow は丸ごとスキップ |
| 礼儀あるレート制限 | 同一ドメイン ≥3s / 同時 ≤2 ドメイン / 15s タイムアウト / ≤6 ページ・ドメイン / 身元入り UA |
| 企業窓口のみ | メールの登録可能ドメイン == サイト自身のドメインのみ採用。フリーメール・キャリアメール除外 |
| 個人アドレス除外 | firstname.lastname@ / 単名 は personal フラグで書き出し対象外。役職 (info/contact/sales/pr/recruit…) のみ |
| 収集元の記録 | メール 1 件ごとに source_url + fetched_at を保存 |
| do-not-contact | donotcontact.txt のドメイン/メールはハード除外 |
| 送信経路なし | 収集器にメール送信コードを持たせない。送信は別系統・人手承認後のみ |
1. 閉域ディレクトリを用意
app リポ直下に _prospect/ を作り、.gitignore に /_prospect/ を追加する(ツールも DB も候補も追跡しない):
echo "/_prospect/" >> .gitignore
mkdir -p _prospect/lib _prospect/data
保存は Node 22.5+ 内蔵の node:sqlite(外部依存ゼロ)。実行は node --experimental-sqlite。
2. スコアリング設定(ニーズ精査の真実源)
_prospect/config.json に、収集の全ノブと重み付きキーワードを置く(ハードコード禁止・ここだけ編集で調整):
{
"rateLimitMs": 3000, "maxPagesPerDomain": 6, "concurrency": 2, "timeoutMs": 15000,
"userAgent": "YOURBOT-research-bot (+https://your.site; contact info@your.site)",
"contactPathHints": ["/contact","/company","/about","/inquiry","/お問い合わせ","/recruit"],
"freemailDomains": ["gmail.com","yahoo.co.jp","outlook.com","icloud.com", "..."],
"roleLocalParts": ["info","contact","inquiry","otoiawase","sales","pr","press","recruit"],
"scoring": {
"keywords": { "ブランディング": 15, "資金調達": 14, "採用": 12, "新商品": 10, "プロダクト": 7, "EC": 7 },
"sizeSignals": {}, "industryWeights": {}
}
}
スコアは clamp(Σ 一致キーワード重み + シグナル, 0, 100) の透明なルールベース。LLM 判定を足す場合も一次はこのルールで絞る。
3. 収集する
collect.mjs(種 CSV: url[必須] / industry / note)を実行:
node --experimental-sqlite _prospect/collect.mjs _prospect/seeds.csv
各ドメインで: robots 確認 → homepage 取得(レート制限)→ 会社名・メール・シグナル抽出 → robots が許す contact 系パスを最大 N ページ → スコア → SQLite に upsert。 7 日以内クロール済みはスキップ(--refresh で再取得)、1 ドメインの失敗で全体を止めない。
4. ニーズ精査(人手レビュー)
review.mjs は 127.0.0.1 のみにバインド(非ループバックは 403)のローカル UI を立てる:
node --experimental-sqlite _prospect/review.mjs # → http://127.0.0.1:4599
スコア降順の一覧で、企業 / ドメイン / 役職メール / シグナル / status を見て 承認 / 却下。 これが「見込みあり」を人が最終判断する関門。localhost 専用なので外部に露出しない。
5. 承認分を書き出す → 取込器へ
node --experimental-sqlite _prospect/export-approved.mjs
# → _prospect/data/approved-export.csv (email,name,source_url) ※役職メールのみ・個人除外
書き出した CSV を run-newsletter-ops の取込器へ:
node scripts/import-inquiries.mjs _prospect/data/approved-export.csv --source generic --apply
取込器側で PII は AES-256-GCM 暗号化・source 記録され、以後は unsubscribe / suppression の管轄下に入る。
6. 送信は別工程(無差別送信を構造的に不可能にする)
承認 → 取込 で連絡先化しても、送信は必ず run-newsletter-ops のキャンペーン作成を経る。 初回コンタクトは送信者表示+配信解除リンクを必須にし、セグメントは cold-prospect として分離運用する。
検証(acid test)
collect.mjsを実在の企業サイト 1–2 社で走らせ、robots 尊重・会社名/シグナル抽出・スコア・
crawl_log ok=1・行が書かれることを確認(メール 0 でも form 運用なら正常)。
review.mjsが 127.0.0.1 で起動し承認/却下が round-trip、非ループバックが 403。export-approved.mjsが役職メールのみ・個人除外で CSV を書き、次コマンドを表示。git check-ignore _prospect/が効いており、git statusに一切現れないこと。