受信トレイのメールを LLM で読み、どれくらい急いで対応すべきかを判定して Gmail のラベル(P1〜P3)を付けるツールです。
件名・本文・送信者・Gmail のカテゴリ・自分で付けたラベルを見て判断します。判断の基準はプロンプトに書いてあり、自由に書き換えられます(「大学からのメールは常に P1」など)。
OpenAI・Amazon Bedrock・ローカルの OpenAI 互換サーバ(LM Studio 等)を切り替えられます。クラウドの高性能モデルを使うことも、メールを一切外に出さずに手元のモデルだけで完結させることもできます。
受信トレイの整理は絞り込みではなく順位付けの問題です。
Gmail のフィルタが答えるのは「この条件に一致するか?」で、気にしたい送信者やキーワードをあらかじめ全部列挙しておく必要があります。このツールが答えるのは別の質問です。
「今日メールを数通しか読む時間がないとしたら、どれを読むべきか?」
答えはスレッドごとに1つのラベルとして表現されます。何も隠さず、アーカイブせず、削除しません。受信トレイはそのままで、並べ替えや検索やフィルタの材料になる情報が1次元増えるだけです。
判断そのものは LLM に任せますが、判断の基準はあなたが所有するプロンプトの中にあります。請求書をどれだけ強く扱うか、特定の顧客をどう扱うかを変えるのは、コードの変更ではなくプロンプトの変更です。
1回の起動が1バッチです。起動して、見つけたものを仕分けて、ラベルを付けて、終了します。常駐しません。
config.yml + .env
│
▼
┌──────────────────┐
│ 設定の読み込み │ どのラベルが P1/P2/P3 か、何件まで、どの速さで、
│ バックエンド選択 │ どの LLM を使うか
└────────┬─────────┘
▼
┌──────────────────┐
│ ラベルIDの解決 │ 設定の「名前」からメールボックスの ID を引く。
│ │ 存在しない名前なら、ここで候補付きで落ちる
└────────┬─────────┘
▼
┌──────────────────┐
│ スレッド一覧 │ in:inbox から、既に P1/P2/P3 が付いたものを除外。
│ │ 前回の続きがあれば「何が変わったか」だけ訊く
└────────┬─────────┘
▼
┌──────────────────┐
│ 取得とパース │ MIME を再帰的に辿り、charset を見て復号し、
│ │ HTML からテキストを取り出し、ヘッダを抽出
└────────┬─────────┘
▼
┌──────────────────┐
│ 無害化と予算配分 │ 隠し文字・制御文字を除去し、スレッド内に既にある引用だけを落とし、
│ │ 新しいメッセージ優先でトークン予算に収める
└────────┬─────────┘
▼
┌──────────────────┐
│ 分類(並行) │ → LLM → {"priority": "P1", "reason": "..."}
│ │ レート制限を守りながら並行実行
└────────┬─────────┘
▼
┌──────────────────┐
│ アクション計画 │ 優先度と設定から「何を変えるか」を決める
└────────┬─────────┘
▼
┌──────────────────┐
│ 適用 │ ラベルを書き込む。あるいはファイルに出す(dry-run)。
│ │ あるいはキューに入れて別プロセスに任せる
└──────────────────┘
分類の単位は個々のメッセージではなくスレッドです。返信が来れば会話全体の緊急度が変わるので、会話をまとめて評価してスレッドにラベルを付けます。
分類状態をローカルに持ちません。スレッドに優先度ラベルが付いていること自体が「処理済み」の記録です。
おかげでツールはステートレスで、いつ中断しても安全で、結果はスマホでも Web でもデスクトップでも Gmail が見える場所ならどこでも見えます。
既にラベルが付いたスレッドは Gmail の検索クエリ自体で除外し、取得後にもう一度確認します(実行中にラベルが付く場合があるため)。
落ちても失うのは処理中の分だけで、次の実行は続きから始まります。同じスレッドに2回課金することはありません。
既にラベルが付いたスレッドは、数えるだけで再判定も再計画もしません。
あなたが手でラベルを直したかもしれないからです。ラベルを外す操作ができるようになった以上、既存ラベル付きスレッドを再計画すると、実行のたびにあなたの修正を黙って戻すことになります。
既定では、ラベルを追加するだけです。
設定を書けばスターを付けたりメインタブへ移したりもできますが、受信トレイから出す操作(アーカイブ)は allow_destructive を有効にしない限り実行されません。
「どのドメインを常に急ぎとするか」「ニュースレターをどう扱うか」「判断がつかないときどうするか」は、自然言語でプロンプトに書きます。
一方でその結果として何をするか(スターを付ける、タブを移す)は設定が決めます。プロンプトではありません。
この分離が効くのは、メール本文が世界中の誰でも書ける入力だからです。P1 だと言いくるめることができたとしても、設定がそう言っていなければスターは付きません。
分類器のインターフェースは1つだけです。スレッドが入って、優先度が出る。
OpenAI・Bedrock・ローカルサーバの切り替えは設定1行で、他のどこもその違いを知りません。
受信トレイは大きく、LLM 呼び出しは無料ではありません。
スレッドは送る前にトークン予算に収め、スレッド内の前のメッセージと重複する引用は落とし、並行数と毎分リクエスト数を設定でき、1回の実行で処理する件数に上限があります。毎回の検索は優先度ラベルの付いたスレッドを除外するので、返ってくるのはまだラベルのないスレッドだけです。新着のない定期実行は、一覧を1回取得するだけで終わります。
| 優先度 | 意味 | 典型例 |
|---|---|---|
| P1 | 今すぐ対応が要る | 48時間以内の締切、障害、セキュリティや支払いの問題、重要な相手からの緊急の依頼 |
| P2 | 返信は要るが今日でなくていい | 通常の業務連絡、日程調整、質問、急ぎでない私信 |
| P3 | 後回しでよい | プロモーション、ニュースレター、SNS通知、返信不要の自動通知 |
同梱のプロンプトは迷ったら P2 に倒します。重要なメールを P3 にしてしまう損失は、ニュースレターを P2 にしてしまう損失よりはるかに大きいためです。
また下限を1つ設けています。人が個別に書いたと見えるメールは P3 になりません。
これらはプロンプトの取り決めであって、コードの制約ではありません。コードが要求するのは「3つのうちどれかを返すこと」だけです。
- Python 3.14(pyenv 推奨)
- uv
- Gmail API を有効にした Google Cloud プロジェクトと、デスクトップアプリ種別の OAuth クライアント
- 以下のいずれか
- OpenAI の API キー
- Bedrock にアクセスできる AWS 認証情報
- ローカルの OpenAI 互換サーバ(LM Studio 等)
-
(pyenv を使っていて Python 3.14 が未導入なら)
pyenv install 3.14.5
-
クローン
git clone https://github.com/KO1231/EmailPriorityClassifier.git cd EmailPriorityClassifier -
インストール
make install
-
設定ファイルを用意する
config.yml.exampleをconfig.ymlにコピーして埋める- 自分だけのルールがあれば
policy.yml.exampleをpolicy.ymlにコピーする。このファイルは git 管理外で、それによってprompts/の中身を公開できる状態に保っています - Gmail の OAuth クライアント認証情報(デスクトップアプリ)を
secrets/client_secrets.jsonに置く - モデルの認証情報を
.envに置く(.env.example参照)
-
Gmail に3つのラベルを作る(例:
#/P1#/P2#/P3)。config.ymlには名前だけ書きます。内部 ID は起動時にメールボックスから解決するので、手で管理するものはありません。 -
認証
uv run epc login uv run epc labels # 設定した名前が正しく解決できるか確認 uv run epc labels --create # まだないラベルを作る場合
config.yml にはポリシーだけを書き、秘密情報は書きません。そのため読んで diff を取ってレビューできます。注釈付きの全体像は config.yml.example にあります。
labels: # 表示名だけ。ID は起動時に解決する
p1: "#/P1"
p2: "#/P2"
p3: "#/P3"
gmail:
query: "in:inbox"
extra_query: "" # 期間制限は付けない(下の「運用上の注意」参照)
max_threads: 1500 # 1回に分類する上限。残りは次の実行が続ける
llm:
backend: openai # openai | bedrock | local
model: gpt-5.4-mini
reasoning_effort: low
concurrency: 15
requests_per_min: 120
actions: # 優先度が決まった後に何をするか
rules:
- when: { priority: P1 }
unless: { any_label: [SPAM, TRASH] } # 迷惑メール判定なら何もしない
do: [add_star, move_to_primary, mark_important]
dry_run: false # dispatch より優先される。Gmail に一切書かないどの値も EPC__ を前置した環境変数で上書きできます。階層は __ で区切ります(EPC__GMAIL__MAX_THREADS=50)。優先順位はコマンドライン → 環境変数 → ファイル → 既定値です。
知らないキーは無視せずエラーにします。書いたのに何も起きない設定を作らないためです。
config.yml には絶対に書きません。OPENAI_API_KEY などは環境変数から(.env.example 参照)、Gmail の認証情報は認証バックエンドが保持します(ローカルでは JSON ファイル、AWS ではパラメータストア)。
OpenAI のキーは api.responses.write と api.responses.read があれば足ります。より広い model.request は不要です。
優先度の判断基準はプロンプトそのものなので、レビューと diff ができるようリポジトリに置いてあります。
一方であなた固有のルールは公開リポジトリに置くべきものではありません。そちらは policy.yml(git 管理外)に書き、実行時にシステムプロンプトへ差し込まれます。
guidance:
- "大学・官公庁ドメインからのメールは P3 にしない。"
- "example-vendor.com のニュースレターは、件名が URGENT でも P3。"epc prompt render で、実際に送られる内容をそのまま確認できます。
まず、何も触らずに「何をするつもりか」を出します。
uv run --env-file .env epc run --dry-run意図した変更が全部 log/mutations.jsonl に書かれ、Gmail には何も適用されません。読んで納得したら、読んだものをそのまま適用します。
uv run epc apply log/mutations.jsonl設定を信用できるようになったら、直接実行します。
uv run --env-file .env epc run途中で使うもの:
uv run epc config validate # 設定を検証して表示する。通信なし
uv run epc prompt render # 実際に送られるプロンプト(policy 込み)
uv run epc run --limit 20 # 試しているあいだ件数を絞る
uv run epc failures list # 失敗が続いてスキップしているスレッド
uv run epc failures clear # その記録を消して、次の実行で再試行させる終了コードは 0 正常、1 致命的(設定・認証情報)、2 部分的(一部のスレッドを落としたが実行は完走)。常駐しないバッチなので、cron / launchd / systemd timer から回してください。
config.yml 実行時の設定(git 管理外)
policy.yml 自分の判断ルール(git 管理外)
prompts/ 汎用のプロンプト(コミット対象)
secrets/ OAuth クライアント認証情報とトークン(git 管理外)
src/epc/
├── cli.py エントリポイント
├── settings.py 設定スキーマと階層
├── pipeline.py 実行本体(一覧 → 取得 → 分類 → 計画 → 適用)
├── ratelimit.py リクエストの整流
├── state.py 失敗が続くスレッドの記録
├── report.py 分類履歴
├── gmail/ auth · client · query · mime · labels · models
├── classify/ budget · prompt · base · openai/bedrock/local
├── security/ sanitize · detect
├── actions/ model · rules · planner
└── dispatch/ sink · applier · sqs
environments/ Terraform(sample のみコミット)
modules/ Terraform モジュール(aws_*)
tests/ unit · integration · injection · fixtures(全て合成データ)
外に出るもの。 backend: openai の場合、予算内に収めたスレッドの抜粋(ヘッダと、重複する引用を落とした本文)が OpenAI API に送られます。store: false を付けているので、プロバイダ側に保持を依頼しません。backend: bedrock なら送り先が AWS になります。backend: local なら何も外に出ません。
保存されるもの。 OAuth トークン(secrets/token.json、本人のみ読み取り可、永続フィールドのみで短命なアクセストークンは含まない)、アプリケーションログ、そして明示的に有効にした場合のみ分類履歴。
分類履歴が記録するのは件名のハッシュと送信者のドメインで、本文は保存しません。このファイルは実行より長生きし、バックアップに入る種類のものだからです。
要求する Gmail の権限。 gmail.modify のみです。読み取りとラベル管理を包含します。このスコープは技術的にはアーカイブや削除も許可しますが、このツールがそれを行うのはあなたがそういうルールを書いたときだけで、allow_destructive は既定で無効です。
- 初回のコストは、受信トレイにあるラベルなしスレッドの数で決まります。 大きな受信トレイでの初回は、古いメールも含めて
max_threadsまで分類し、残りは次の実行が続けます。最初の数回はmax_threadsを下げて実際のコストを見てから広げるのが安全です。一巡した後の定期実行は新着の分だけです。 - ラベルを外せば分類し直します。 判断基準やプロンプトを変えた後、Gmail で該当スレッドの優先度ラベルを外すと、何年前のメールでも次の実行で新しい基準で分類されます。
extra_queryにnewer_than:14dのような期間制限を付けると、それより古いメールではこれが効かなくなります。 - 中断しても壊れません。 分類済みの結果は停止要求を受けても書き出され、処理できなかった分はラベルがないままなので次回また来ます。
- 失敗はスレッド単位です。 失敗したスレッドは次の実行で再試行され、残りは続行します。プロバイダが内容を拒否したなどそのスレッド自体が原因の失敗が2回続き、その間に他のメールが正常に分類されていれば、以降はスキップします。スレッドに変化があったとき、モデルやプロンプトを変えたとき、30日経ったときにまた試します。障害や通信エラーによる失敗では、スキップは起きません。
- 本文は削りすぎないことを優先しています。 引用を落とすのは、同じ文面がスレッド内の前のメッセージに実際にあると確かめられたときだけです。途中から CC に入ったメールや、元のメッセージが削除された返信では、引用もそのまま送ります。署名は落としません(
--は区切り線にも使われるため)。1通あたりの文字数の上限を超えたときは、引用行から先に削ります。 - 手で直した判定は残ります。 手でラベルを変えたスレッドは、以降の実行対象から外れます。
- ラベル付きスレッドに後から届いた返信には、そのスレッドのラベルを付けます。 Gmail のラベルはメッセージ単位なので、付けないと返信のたびにそのスレッドが検索に戻ってきてしまうためです。付けるのはすでに付いている優先度ラベルだけで、優先度の判定し直しやアクションは行いません。優先度ラベルが2つ付いているスレッドには何もしません。
ローカルでもコンテナでも動きます。AWS 側は Terraform に記述してあります(ECR のイメージ、定期実行される Fargate タスク、FIFO キュー、ラベル変更を適用する Lambda)。
AWS では config.yml と policy.yml をイメージに入れず、パラメータストアに置きます。ECS が起動時に環境変数 EPC_CONFIG_YAML / EPC_POLICY_YAML として注入するので、同じイメージをどの環境でも使えます。パラメータの中身は Terraform を通さず、terraform output next_steps に出る aws ssm put-parameter で入れてください(Terraform を通すと state ファイルに平文で残るため)。Gmail の認証情報はパラメータストアのほか、credentials.backend: secrets_manager で Secrets Manager にも置けます。
未実装: 評価(eval)の仕組み。プロンプトの変更が良くなったのか悪くなったのかを測れるようにするものです。判断基準をこれから調整していく段階になったら作る価値があります。
ブランチ運用:
main— リリース状態。develop/hotfix/hotfix/*以外からの Pull Request は自動的にクローズされますdevelop— 統合ブランチ。機能開発はここから分岐してここへ戻しますmainからの Pull Request も自動的にクローズされます
いずれも .github/workflows/ の GitHub Actions で強制しています。
コミットメッセージは type: summary 形式です(add: fix: update: refactor: remove: change:)。
make check # lint + format + 型検査 + テスト
make audit # 依存の既知脆弱性チェック
make docker-build
make tf-validate