メール通知スパム問題をAIと一緒に3時間で解決した話

メール通知スパム問題をAIと一緒に3時間で解決した話

「めっちゃ通知来る」

深夜22時、Telegramに大量のメール再通知が届き始めた。

1分間に10件、20件、30件…。過去に処理済みのメールが次々と再通知されている。完全にスパム状態だった。

慌てて「Gmail Watcher」のcronを無効化した。これ以上の通知を止めるために。

でも、問題の原因はわかっていた。数時間前、メールフィルターを調整したときに、何かがおかしくなったんだ。

問題の経緯

この日の夜、私は「Gmail Watcher V2」という自作のメール監視システムを改善していた。

重要なメールを逃さないための再通知機能を実装したのだが、「通知が来なくなった」という問題が発生していた。

原因は明確だった:
– メールフィルターが強すぎて、全メールを「IGNORE(無視)」判定していた
– 重要な送信元(岩野圭佑さん、久保さん、松本さんなど)からのメールも見逃していた

フィルターを緩めた。除外キーワードを35個から8個に減らし、ブラックリストも13個から4個に削減した。

結果、再通知スパムが発生した。

何が起きたのか

問題の本質は「既存メールの再処理」だった。

フィルター変更後、過去に「IGNORE」判定されたメールが再分類され、一気に「HIGH(重要)」判定に変わった。それらすべてが再通知対象になってしまった。

コードを確認すると、原因は is_already_processed() 関数にあった:

def is_already_processed(self, message_id):
    # メールが既に存在 AND 通知済み
    c.execute('SELECT notification_status FROM emails WHERE message_id = ?', (message_id,))
    result = c.fetchone()

    if result:
        # 既に通知済みならスキップ
        if result[0] in ['notified', 'sent', 'skipped']:
            return True
        # レコードはあるが未通知なら再処理
        return False

    return False

notification_status = 'pending' のメールは「未通知」扱いで再処理されていた。

IGNOREメールは 'pending' のまま保存されていたため、フィルター変更後に再処理 → HIGH判定 → 大量再通知、という流れになっていた。

AIと一緒に解決した手順

深夜22時、私はAIアシスタント(OpenClaw)にメッセージを送った。

「Gmail Watcherが再通知スパム状態。緊急停止した。原因は既存メールの再処理。修正してくれ」

AIは即座に状況を理解し、修正方針を提示してきた:

修正1: is_already_processed() の変更

def is_already_processed(self, message_id):
    # 既存メールは無条件でスキップ(再処理防止)
    c.execute('SELECT id, notification_status FROM emails WHERE message_id = ?', (message_id,))
    result = c.fetchone()

    if result:
        # 既存メールは無条件でスキップ
        return True

    return False

シンプルな変更だが、効果は絶大だった。「既存メールは二度と処理しない」という鉄則を徹底した。

修正2: save_email_v2() の改善

def save_email_v2(self, email_data, classification_result):
    classification = classification_result['classification']

    # notification_status を判定結果に応じて設定
    if classification == 'ignore':
        notification_status = 'ignore'
        handled_status = 'ignored'
    elif classification == 'digest_only':
        notification_status = 'digest'
        handled_status = 'unhandled'
    else:
        notification_status = 'pending'
        handled_status = 'unhandled'

IGNOREメールは notification_status = 'ignore' で保存し、再処理対象から完全に除外する。

テストと再開

修正完了後、テスト実行した:

python3 gmail_watcher.py --check

結果:

📧 crosslink: 新着メールなし
📧 programming_school: 新着メールなし
✅ チェック完了: 新規 0件 / 重要 0件

再通知スパムなし。完璧に動いた。

cronを再開した。5分ごとのメール監視が復活した。

学んだこと

1. 「既存データの再処理」は危険

フィルターやロジックを変更するとき、既存データにどう影響するかを必ず考える必要がある。

今回の場合、フィルター変更が「過去のIGNOREメールを再処理」という予期しない動作を引き起こした。

教訓:「既存メールは二度と処理しない」というルールを徹底する。

2. ステータス管理は明確に

notification_status = 'pending' が「未通知」を意味するのか「処理中」を意味するのか、曖昧だった。

修正後:
'ignore' → 完全無視
'digest' → ダイジェスト対象
'pending' → 通知予定
'sent' → 通知済み

各ステータスの意味を明確にすることで、バグを防げる。

3. AIと一緒に解決する速さ

今回、問題発生から修正完了まで約3時間だった。

その内訳:
– 問題発生・緊急停止:5分
– 原因特定:10分
– 修正方針検討:5分
– 実装・テスト:2時間30分
– 再開・動作確認:10分

AIに修正を任せたことで、自分は「方針を決める」ことに集中できた。コード書きはすべてAIが担当した。

深夜のデバッグは焦らない

深夜22時、大量の通知が来たとき、最初は焦った。

でも、冷静に考えれば対処法は明確だった:
1. まず止める(cron無効化)
2. 原因を特定する
3. 修正する
4. テストする
5. 再開する

AIがいることで、「自分が全部やらなきゃ」というプレッシャーがなかった。「AIに任せて、自分は判断だけする」というスタンスで進められた。

まとめ

今回の教訓:

  • フィルター変更は既存データに影響する → 再処理防止ロジックを徹底
  • ステータス管理は明確に → 曖昧な状態を残さない
  • AIと一緒なら、深夜のデバッグも怖くない

「めっちゃ通知来る」状態から、3時間で完全復旧した。

AIを使いこなせば、トラブルシューティングも楽になる。


関連記事:
岩盤浴で寝落ちしてる間にAIが全自動でシステム構築した話
Gmail Watcher V2 実装の全記録
個人開発でハマったデバッグの話