# メールフィルタリング機能 仕様書

> **作成**: 2026-02-21
> **ステータス**: 仕様確定 → 実装フェーズへ

---

## 概要

複数のメールアカウントに届くメールをLLMで重要度判定し、重要なメールだけをLINEで通知する。
フィードバックを積み重ねることで判定が賢くなっていく仕組みを持つ。

---

## システム構成

```
[Xserver/Gmail/Hotmail IMAP]
    ↓ Windmill スケジュール実行（ポーリング）
[メール取得・正規化]
    ↓
[重要度判定エンジン]（Django API経由でDBを参照）
├─ アドレスルールあり → ルールに従う（never_notify → スキップ）
├─ ドメインルールあり → ルールに従う（never_notify → スキップ）
└─ ルールなし → LLM（Claude API）に判定させる
               ↓ 文脈として「過去フィードバック集計」を渡す
    ↓ 重要と判断されたメール
[LINE Messaging API で通知]
「件名: ○○ / 送信者: △△
 👉 https://keinafarm.net/mail/feedback/{token}」
    ↓ タップ
[KeinaシステムのDjangoビュー /mail/feedback/{token}]
    ↓ フィードバック選択
[KeinaシステムのPostgreSQL更新]
    ↓ 次回ポーリング時にWindmillが参照
```

---

## 技術スタック

| 役割 | 技術 |
|------|------|
| ワークフロー実行 | Windmill（既にサーバーで稼働中） |
| メール取得 | IMAP統一（Xserver/Gmail/Hotmail） |
| LLM判定 | Claude API |
| LINE通知 | LINE Messaging API（既に連携済み） |
| フィードバック画面・API | Keinaシステム（Django）に機能追加 |
| DB | KeinaシステムのPostgreSQL |
| Windmill ↔ Keinasystem連携 | Django REST API（APIキー認証） |

---

## メールアカウント

- Xserver のメールサービス（自前ドメイン）→ IMAP
- Gmail → IMAP
- Hotmail（Outlook）→ IMAP

ルールは複数アカウントをまたいで共通（アカウントを区別しない）。

---

## 判定フロー（詳細）

```
新着メール受信
    ↓
0. 宛先補正（To ヘッダー）
   - @keinafarm.com 宛先は xserver1〜xserver6 に正規化（Gmail先行取り込み時の誤ラベル防止）
    ↓
1. mail_senders に一致するアドレスルールがあるか？
   → never_notify: スキップ（LLM呼ばない、記録もしない）
    ↓
2. mail_senders に一致するドメインルールがあるか？
   → never_notify: スキップ（LLM呼ばない、記録もしない）
    ↓
3. LLM判定（Claude API）
   入力:
     - 送信者メールアドレス・ドメイン
     - 件名
     - 本文冒頭（200字程度）
     - 過去フィードバック集計（後述）
   出力:
     - important / not_important
    ↓
4. important → mail_emails に記録 → トークン発行 → LINE通知
   not_important → mail_emails に記録（通知なし）
```

### 優先順位

**アドレスルール > ドメインルール > LLM判断**（具体的なほど優先）

### LLMに渡す過去フィードバック集計

トークン肥大化を防ぐため、生の履歴ではなく集計値を渡す：

```
送信者: promo@example.com (example.com)
過去に通知したメール: 8通
  - 重要だった:           2回
  - 普通のメール:         5回
  - 今後通知しない:       0回
  - フィードバック未提供: 1回

件名: ××のご案内
本文冒頭: ...（200字）
```

---

## データモデル

### `mail_senders`（送信者ルール）

| フィールド | 型 | 説明 |
|---|---|---|
| id | AutoField | |
| email | EmailField (nullable) | アドレス指定 例: info@amazon.co.jp |
| domain | CharField (nullable) | ドメイン指定 例: amazon.co.jp |
| rule | CharField | 'never_notify' のみ（MVP） |
| note | TextField | メモ（「○○の営業メール」など） |
| created_at | DateTimeField | |
| updated_at | DateTimeField | |

- `email` か `domain` のどちらか一方のみ設定する
- アドレスルールとドメインルールが両方存在する場合、アドレスが優先

### `mail_emails`（通知メール記録）

| フィールド | 型 | 説明 |
|---|---|---|
| id | AutoField | |
| account | CharField | 'gmail' / 'gmail_service' / 'hotmail' / 'xserver1'〜'xserver6'（旧データは 'xserver'） |
| message_id | CharField (unique) | メールのMessage-ID（重複防止） |
| sender_email | EmailField | |
| sender_domain | CharField | |
| subject | CharField | |
| body_preview | TextField | 本文冒頭200字程度 |
| received_at | DateTimeField | |
| llm_verdict | CharField | 'important' / 'not_important' |
| notified_at | DateTimeField (nullable) | LINE通知日時 |
| feedback | CharField (nullable) | 'important' / 'not_important' / 'never_notify' |
| feedback_at | DateTimeField (nullable) | フィードバック日時 |

### `mail_notification_tokens`（フィードバック用トークン）

| フィールド | 型 | 説明 |
|---|---|---|
| id | AutoField | |
| email | OneToOneField(MailEmail) | 1メール1トークン |
| token | UUIDField (unique) | 推測不可能なランダムUUID |
| created_at | DateTimeField | |

- 有効期限なし
- フィードバック変更可能（同じURLで何度でも再選択できる）

---

## API エンドポイント（WindmillからDjangoへ）

認証: リクエストヘッダー `X-API-Key: <secret>` で行う。

| メソッド | URL | 用途 |
|---|---|---|
| GET | `/api/mail/sender-rule/` | 送信者ルール確認 |
| GET | `/api/mail/sender-context/` | LLM用フィードバック集計取得 |
| POST | `/api/mail/emails/` | メール記録＋トークン発行 |

### GET `/api/mail/sender-rule/`

クエリパラメータ: `?email=promo@example.com&domain=example.com`

レスポンス例:
```json
{
  "matched": true,
  "rule": "never_notify",
  "match_type": "domain"
}
```

### GET `/api/mail/sender-context/`

クエリパラメータ: `?email=promo@example.com&domain=example.com`

レスポンス例:
```json
{
  "total_notified": 8,
  "important": 2,
  "not_important": 5,
  "never_notify": 0,
  "no_feedback": 1
}
```

### POST `/api/mail/emails/`

リクエストボディ:
```json
{
  "account": "gmail",
  "message_id": "<xxx@gmail.com>",
  "sender_email": "promo@example.com",
  "sender_domain": "example.com",
  "subject": "○○のご案内",
  "body_preview": "...",
  "received_at": "2026-02-21T10:30:00Z",
  "llm_verdict": "important"
}
```

レスポンス（important の場合のみトークンを返す）:
```json
{
  "id": 42,
  "feedback_url": "https://keinafarm.net/mail/feedback/550e8400-e29b-41d4-a716-446655440000"
}
```

---

## フロントエンド画面

### 1. フィードバックページ（`/mail/feedback/{token}`）

- 認証不要（LINEからタップ一発でアクセス）
- 表示内容: 送信者、件名、受信日時、本文冒頭
- 現在のフィードバック状態を表示（変更済みの場合、どれを選んでいるか分かるように）
- フィードバック選択肢:
  1. ✅ 重要だった
  2. 📧 普通のメール
  3. 🔇 今後通知しない

「今後通知しない」を選ぶと展開：
```
○ このアドレスだけ（promo@example.com）
○ このドメインごと（example.com）
[確定する]
```

- 送信後: 「受け付けました」と表示
- 再選択可能（同じURLを再度開いて変更できる）

「今後通知しない」でフィードバック確定時、`mail_senders` にルールを自動追加する。

### 2. ルール管理ページ（`/mail/rules/`）

ナビゲーションにメニュー追加。

表示内容:
- ルール一覧テーブル（種別、値、メモ、設定日、削除ボタン）
- 手動でルール追加フォーム（アドレスまたはドメイン、メモを入力）

---

## 実装順序

1. **DBテーブル定義・マイグレーション**（Django apps/mail/）
2. **Windmill向けDjango API**（sender-rule, sender-context, emails POST）
3. **フィードバックページ**（/mail/feedback/{token}）
4. **ルール管理ページ**（/mail/rules/）
5. **WindmillのIMAPポーリングスクリプト**（まず1アカウントから）
6. **LLM判定ロジック**（Claude API呼び出し、集計文脈付き）
7. **LINE通知**（フィードバックURL付き）
8. **残りのメールアカウントを追加**



