Files
keinasystem/document/11_マスタードキュメント_メール通知関連編.md

601 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# マスタードキュメント - メール通知関連編
> **最終更新**: 2026-03-05
> **対象バージョン**: Phase 1 完了時点(本番稼働中)
> **目的**: このドキュメントだけでメール通知機能の全容を把握できること
---
## 目次
1. [システム概要・全体構成](#1-システム概要全体構成)
2. [データモデル](#2-データモデル)
3. [API仕様バックエンド](#3-api仕様バックエンド)
4. [Windmill フロー仕様](#4-windmill-フロー仕様)
5. [画面仕様(フロントエンド)](#5-画面仕様フロントエンド)
6. [本番環境の設定値](#6-本番環境の設定値)
7. [設計判断と制約](#7-設計判断と制約)
8. [運用手順](#8-運用手順)
9. [ソースファイル索引](#9-ソースファイル索引)
---
## 1. システム概要・全体構成
### 機能の目的
複数のメールアカウントGmail × 2 + Xserver × 6 = 計8アカウントに届く大量のメールを自動でフィルタリングし、農家にとって重要なメールだけを LINE で通知する。
### システム構成図
```
[メールサーバー群] [Windmill] [KeinaSystem]
Gmail (2口) ─IMAP─▶ mail_filter フロー ──API──▶ Django バックエンド
Xserver (6口) ↓ (DB記録・ルール参照)
[Gemini API] ↓
LLM判定 フロントエンド
↓ ・メール履歴画面
LINE Messaging API ・ルール管理画面
(重要と判定した場合のみ通知) ・フィードバックページ
LINE通知文にフィードバックURL ────────┘
を含め、ユーザーがタップして
フィードバックを送信
```
### 処理フロー1メールあたり
```
1. IMAP 接続 → 前回処理済み UID 以降の新着メールを取得
2. 送信者ルール確認GET /api/mail/sender-rule/
├── never_notify → スキップ(記録しない)
├── always_notify → LLMスキップ、即 LINE 通知
└── ルールなし → 3へ
3. 過去フィードバック集計取得GET /api/mail/sender-context/
4. Gemini API で重要度判定LLM
5. KeinaSystem に記録POST /api/mail/emails/
├── not_important → 記録のみ、通知なし
└── important → フィードバックURLを発行、LINE 通知
6. 処理済み最終 UID を Windmill Variable に保存
```
### 10分ごとの定期実行
Windmill スケジュール `0 */10 * * * *` で自動実行。サーバー上の production Windmill で稼働。
---
## 2. データモデル
### 2.1 MailSender送信者ルール
**テーブル名**: `mail_mailsender`
| フィールド | 型 | 説明 |
|---|---|---|
| `id` | BigAutoField | PK |
| `email` | EmailField (null可) | アドレス指定ルールの場合 |
| `domain` | CharField(255, null可) | ドメイン指定ルールの場合 |
| `rule` | CharField(20) | `never_notify` / `always_notify` |
| `note` | TextField | メモ(任意)|
| `created_at` | DateTimeField | 作成日時 |
| `updated_at` | DateTimeField | 更新日時 |
**制約**: `email``domain` は必ずどちらか一方のみ設定DB CHECK 制約 `mail_sender_email_or_domain`
**ルール判定の優先順位**: アドレスルールが先、次にドメインルール
### 2.2 MailEmail受信メール記録
**テーブル名**: `mail_mailemail`
| フィールド | 型 | 説明 |
|---|---|---|
| `id` | BigAutoField | PK |
| `account` | CharField(20) | `gmail` / `gmail_service` / `hotmail` / `xserver1``xserver6`(旧データは `xserver` |
| `message_id` | CharField(500, unique) | メールの Message-ID ヘッダー(重複防止に使用)|
| `sender_email` | EmailField | 送信者メールアドレス |
| `sender_domain` | CharField(255) | 送信者ドメイン |
| `subject` | CharField(500) | 件名 |
| `body_preview` | TextField | 本文冒頭最大500文字|
| `received_at` | DateTimeField | 受信日時 |
| `llm_verdict` | CharField(20) | `important` / `not_important` |
| `notified_at` | DateTimeField (null可) | LINE 通知日時(通知済みの場合のみ)|
| `feedback` | CharField(20, null可) | `important` / `not_important` / `never_notify` / `always_notify` |
| `feedback_at` | DateTimeField (null可) | フィードバック日時 |
**ordering**: `-received_at`(新しい順)
**重複防止**: `message_id` の unique 制約。同じメールが複数アカウントで受信された場合は 2件目以降を「重複メール、スキップ」として処理400エラーを無視
### 2.3 MailNotificationTokenフィードバック用トークン
**テーブル名**: `mail_mailnotificationtoken`
| フィールド | 型 | 説明 |
|---|---|---|
| `id` | BigAutoField | PK |
| `email` | OneToOneField → MailEmail | |
| `token` | UUIDField (unique) | フィードバック URL 用 UUID |
| `created_at` | DateTimeField | |
**用途**: `important` と判定されたメールに対して作成。`/mail/feedback/<token>/` の URL をLINE通知文に含める。有効期限なし。
---
## 3. API仕様バックエンド
ベース URL: `https://main.keinafarm.net/api/mail/`
### 3.1 認証方式
| 認証方式 | 対象エンドポイント | ヘッダー |
|---|---|---|
| APIキー認証Windmill用 | sender-rule, sender-context, POST emails/ | `X-API-Key: <MAIL_API_KEY>` |
| JWT認証フロントエンド用 | GET emails/, stats/, senders/, PATCH emails/<pk>/feedback/ | `Authorization: Bearer <token>` |
| 認証不要 | GET/POST feedback/<token>/ | なし |
**MAIL_API_KEY**: `.env.production``MAIL_API_KEY` と一致している必要がある。Windmill Variable `u/admin/KEINASYSTEM_API_KEY` に設定。
### 3.2 Windmill向けエンドポイント
#### GET /api/mail/sender-rule/
送信者ルールを確認する。
**リクエスト**: クエリパラメータ `email` `domain`
**レスポンス例**:
```json
{"matched": true, "rule": "never_notify", "match_type": "address"}
{"matched": false}
```
**判定順序**: アドレス一致 → ドメイン一致 → マッチなし
---
#### GET /api/mail/sender-context/
過去フィードバックの集計を返すLLMへのコンテキスト用
**リクエスト**: クエリパラメータ `email` `domain`
**レスポンス例**:
```json
{
"total_notified": 8,
"important": 2,
"not_important": 5,
"never_notify": 0,
"no_feedback": 1
}
```
---
#### POST /api/mail/emails/
メールを記録し、`important` の場合はフィードバックURLを発行する。
**リクエストボディ**:
```json
{
"account": "gmail",
"message_id": "<unique-message-id>",
"sender_email": "sender@example.com",
"sender_domain": "example.com",
"subject": "件名",
"body_preview": "本文冒頭...",
"received_at": "2026-02-25T15:46:00+09:00",
"llm_verdict": "important"
}
```
**レスポンス例**:
```json
{"id": 69, "feedback_url": "https://main.keinafarm.net/mail/feedback/<uuid>"}
```
`not_important` の場合: `{"id": 68}`feedback_url なし)
**重複処理**: `message_id` が既存の場合 400 を返す。Windmill 側で「重複メール、スキップ」として処理。
---
### 3.3 フィードバックエンドポイント(認証不要)
#### GET /api/mail/feedback/\<token\>/
フィードバックページ表示用にメール情報を返す。
**レスポンス例**:
```json
{
"id": 69,
"sender_email": "sender@example.com",
"sender_domain": "example.com",
"subject": "件名",
"body_preview": "本文...",
"received_at": "2026-02-25T15:46:00+09:00",
"feedback": null
}
```
---
#### POST /api/mail/feedback/\<token\>/
フィードバックを保存する。
**リクエストボディ**:
```json
{
"feedback": "never_notify",
"scope": "address"
}
```
`feedback``important` / `not_important` / `never_notify` / `always_notify` のいずれか。
`scope``never_notify` / `always_notify` の場合のみ必要(`address` / `domain`)。
`never_notify` / `always_notify` + scope の場合、`MailSender` レコードを自動 upsert。
---
### 3.4 フロントエンド向けエンドポイントJWT認証
#### GET /api/mail/emails/
メール処理履歴を返す最新100件
**クエリパラメータ**: `account`(アカウント絞り込み)、`verdict`LLM判定絞り込み
---
#### PATCH /api/mail/emails/\<pk\>/feedback/
履歴画面から直接フィードバックを更新する。
**リクエストボディ**: `feedback`(必須)、`scope``never_notify`/`always_notify` 時のみ)
---
#### GET /api/mail/stats/
ダッシュボード用統計。
**レスポンス例**:
```json
{
"today_processed": 12,
"today_notified": 3,
"feedback_pending": 1,
"total_rules": 5
}
```
---
#### GET /api/mail/senders/
送信者ルール一覧。
#### POST /api/mail/senders/
送信者ルール追加。`email` または `domain` のどちらか一方を指定。
#### DELETE /api/mail/senders/\<id\>/
送信者ルール削除。
---
## 4. Windmill フロー仕様
### 4.1 基本情報
| 項目 | 値 |
|---|---|
| フローパス | `f/mail/mail_filter` |
| スクリプト言語 | Python 3 |
| スケジュール | `0 */10 * * * *`10分ごと|
| スケジュールパス | `f/mail/mail_filter_schedule` |
| Windmill URL | `https://windmill.keinafarm.net` |
| ワークスペース | `admins` |
### 4.2 処理対象アカウント
| 変数名 | メールアドレス | サーバー |
|---|---|---|
| `XSERVER1` | `akira@keinafarm.com` | `sv579.xserver.jp:993` |
| `XSERVER2` | `service@keinafarm.com` | `sv579.xserver.jp:993` |
| `XSERVER3` | `midori@keinafarm.com` | `sv579.xserver.jp:993` |
| `XSERVER4` | `kouseiren@keinafarm.com` | `sv579.xserver.jp:993` |
| `XSERVER5` | `post@keinafarm.com` | `sv579.xserver.jp:993` |
| `XSERVER6` | `sales@keinafarm.com` | `sv579.xserver.jp:993` |
| `GMAIL` | `akiracraftwork@gmail.com` | `imap.gmail.com:993`、All Mail |
| `GMAIL2` | `akiranoushi@gmail.com` | `imap.gmail.com:993`、All Mail |
Hotmail は定義済みだがコメントアウト(未有効化)。
### 4.3 Windmill Variables 一覧
本番 Windmill (`windmill.keinafarm.net`、ワークスペース `admins`) に設定。
| Variable パス | 内容 | Secret |
|---|---|---|
| `u/admin/GMAIL_IMAP_USER` | Gmail ユーザー | ✓ |
| `u/admin/GMAIL_IMAP_PASSWORD` | Gmail アプリパスワード | ✓ |
| `u/admin/GMAIL2_IMAP_USER` | Gmail2 ユーザー | ✓ |
| `u/admin/GMAIL2_IMAP_PASSWORD` | Gmail2 アプリパスワード | ✓ |
| `u/admin/XSERVER1_IMAP_USER` | `akira@keinafarm.com` | — |
| `u/admin/XSERVER1_IMAP_PASSWORD` | Xserver IMAP パスワード | ✓ |
| `u/admin/XSERVER2_IMAP_USER` | `service@keinafarm.com` | — |
| `u/admin/XSERVER2_IMAP_PASSWORD` | Xserver IMAP パスワード | ✓ |
| `u/admin/XSERVER3_IMAP_USER` | `midori@keinafarm.com` | — |
| `u/admin/XSERVER3_IMAP_PASSWORD` | Xserver IMAP パスワード | ✓ |
| `u/admin/XSERVER4_IMAP_USER` | `kouseiren@keinafarm.com` | — |
| `u/admin/XSERVER4_IMAP_PASSWORD` | Xserver IMAP パスワード | ✓ |
| `u/admin/XSERVER5_IMAP_USER` | `post@keinafarm.com` | — |
| `u/admin/XSERVER5_IMAP_PASSWORD` | Xserver IMAP パスワード | ✓ |
| `u/admin/XSERVER6_IMAP_USER` | `sales@keinafarm.com` | — |
| `u/admin/XSERVER6_IMAP_PASSWORD` | Xserver IMAP パスワード | ✓ |
| `u/admin/GEMINI_API_KEY` | Gemini API キー | ✓ |
| `u/admin/LINE_CHANNEL_ACCESS_TOKEN` | LINE Messaging API トークン | ✓ |
| `u/admin/LINE_TO` | LINE 通知先ユーザー ID | ✓ |
| `u/admin/KEINASYSTEM_API_KEY` | KeinaSystem API キー(`.env.production``MAIL_API_KEY` と同値)| ✓ |
| `u/admin/KEINASYSTEM_API_URL` | `https://main.keinafarm.net` | — |
| `u/admin/MAIL_FILTER_GMAIL_LAST_UID` | Gmail 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_GMAIL2_LAST_UID` | Gmail2 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_XSERVER1_LAST_UID` | Xserver1 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_XSERVER2_LAST_UID` | Xserver2 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_XSERVER3_LAST_UID` | Xserver3 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_XSERVER4_LAST_UID` | Xserver4 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_XSERVER5_LAST_UID` | Xserver5 最終処理済み UID | — |
| `u/admin/MAIL_FILTER_XSERVER6_LAST_UID` | Xserver6 最終処理済み UID | — |
### 4.4 LAST_UID の仕組み
- **初回実行**: `LAST_UID``0` または未設定の場合、現在の最大 UID を記録して終了(既存メールを遡らない)
- **通常実行**: `LAST_UID + 1` 以降の UID を検索して処理
- **エラー時**: 個別メッセージの処理に失敗しても、成功した最大 UID まで更新する
### 4.5 LLM 判定Gemini
モデル: `gemini-2.0-flash``temperature=0`, `maxOutputTokens=10`
プロンプトに渡す情報:
- 送信者アドレス・件名・本文冒頭
- 過去の同一送信者のフィードバック集計(`sender-context` API から取得)
回答: `1`(重要)/ `2`重要でないの1文字。`1` で始まる場合 `important`
### 4.6 LINE 通知文フォーマット
```
📧 重要なメールが届きました
宛先: Gmail (メイン)
差出人: sender@example.com
件名: 件名テキスト
フィードバック:
https://main.keinafarm.net/mail/feedback/<uuid>
```
---
## 5. 画面仕様(フロントエンド)
### 5.1 フィードバックページ(認証不要)
**URL**: `/mail/feedback/[token]`
**ファイル**: `frontend/src/app/mail/feedback/[token]/page.tsx`
LINE 通知のリンクから直接アクセス。JWT 認証不要のため、`api` インスタンスJWT自動付与ではなく素の `fetch` を使用。
**表示内容**:
- 送信者アドレス・件名・受信日時・本文冒頭
- フィードバックボタン: 重要だった / 普通のメール / 今後通知しない / 常に通知してほしい
- `今後通知しない` / `常に通知` 選択時: アドレス / ドメイン の適用範囲を選択
**状態**:
- 既にフィードバック済みの場合は現在の値をハイライト表示(再選択可能)
- 送信完了後「受け付けました」画面に切り替わる
---
### 5.2 メール処理履歴JWT認証
**URL**: `/mail/history`
**Navbar**: 「メール履歴」History アイコン)
**ファイル**: `frontend/src/app/mail/history/page.tsx`
**表示内容**: 処理したメール最新100件
| カラム | 内容 |
|---|---|
| 受信日時 | 日時 + アカウント名 |
| 送信者 | メールアドレス |
| 件名 | テキストtruncate|
| LLM判定 | 重要(赤)/ 通常(灰)バッジ |
| フィードバック | 状態バッジ + 編集ボタン |
**フィルター**: アカウント別・LLM判定別セレクトボックス
**フィードバックモーダル**: 履歴画面からも4択でフィードバックを設定・変更できる。`never_notify` / `always_notify` 選択時はアドレス/ドメインの適用範囲を表示。
---
### 5.3 メール通知ルール管理JWT認証
**URL**: `/mail/rules`
**Navbar**: 「メールルール」Shield アイコン)
**ファイル**: `frontend/src/app/mail/rules/page.tsx`
**追加フォーム**:
- 種別(アドレス / ドメイン)
- ルール(通知しない / 常に通知)
- 値(メールアドレスまたはドメイン名)
- メモ(任意)
**一覧表示**: 種別バッジ・ルールバッジ・値・メモ・設定日・削除ボタン
**ルールが自動追加される場面**: フィードバックで `never_notify` / `always_notify` を選択してスコープを指定すると自動登録される。
---
## 6. 本番環境の設定値
### バックエンド(`.env.production`
```
MAIL_API_KEY=<Windmill の KEINASYSTEM_API_KEY と同じ値>
```
`settings.py` での参照:
```python
MAIL_API_KEY = os.environ.get('MAIL_API_KEY', '')
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')
```
`FRONTEND_URL` はフィードバック URL の生成に使用(本番値: `https://main.keinafarm.net`)。
### デプロイコマンド
```bash
# サーバー上で実行(--env-file を必ず指定)
cd /home/keinasystem/keinasystem_t02
git pull
docker compose -f docker-compose.prod.yml --env-file .env.production build
docker compose -f docker-compose.prod.yml --env-file .env.production up -d
```
**注意**: `--env-file .env.production` を省略すると SECRET_KEY 等が空になりバックエンドが起動しない。
### Windmill フローの更新手順
ローカルの `flows/mail_filter.flow.json` を編集後:
```bash
cd C:/Users/akira/Develop/windmill_workflow
# サーバーに転送
scp flows/mail_filter.flow.json keinafarm-claude:/tmp/mail_filter.flow.json
# サーバー上でデプロイWindmill API 経由)
ssh keinafarm-claude 'TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh; WM=http://172.18.0.15:8000/api/w/admins
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$WM/flows/delete/f/mail/mail_filter"
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @/tmp/mail_filter.flow.json "$WM/flows/create"'
```
---
## 7. 設計判断と制約
### Windmill と KeinaSystem の疎結合
Windmill はスケジューラ・LLM呼び出し担当、KeinaSystem は DB・UI 担当として明確に分離。両者は REST APIAPIキー認証でのみ連携。
→ Windmill の障害が KeinaSystem のメイン機能(作付け計画等)に影響しない。
### APIキー認証の実装
`MailAPIKeyPermission``BasePermission` サブクラス)で `X-API-Key` ヘッダーを検証。`secrets.compare_digest` でタイミング攻撃を防止。
`authentication_classes = []` を明示的に設定したビューとしていないビューがある:
- `SenderRuleView`, `SenderContextView`: `authentication_classes = []` を設定済み → 403
- `MailEmailView` の POST: 設定なし(デフォルトの JWTAuthentication が動く)→ キー不一致時は 401
**統一するなら** `MailEmailView` にも `authentication_classes = []` を追加すべき(現状は動作上問題なし)。
### フィードバック URL のセキュリティ
UUID v4 のランダムトークンのみで認証。有効期限なし。LINE に送信された URL を知っている人なら誰でもフィードバックを送れる(悪用リスクは低いため許容)。
### 重複メール処理
同じメールが複数アカウントで受信される場合(転送設定等)、`message_id` の unique 制約で2件目以降を自動スキップ。最初に処理したアカウントの `account_code` でDBに記録される。
---
## 8. 運用手順
### 新しいメールアカウントを追加する場合
1. `flows/mail_filter.flow.json``ACCOUNTS` リストにエントリを追加
2. 本番 Windmill に Variables を追加USER, PASSWORD, LAST_UID
3. フローを再デプロイ
```bash
# Variable 追加例(サーバー上で実行)
ssh keinafarm-claude 'TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh; WM=http://172.18.0.15:8000/api/w/admins
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"path\":\"u/admin/XSERVER7_IMAP_USER\",\"value\":\"newuser@keinafarm.com\",\"is_secret\":false,\"description\":\"\"}" \
"$WM/variables/create"'
```
### LINE トークンが期限切れになった場合
LINE Developers Console でトークンを再発行し、本番 Windmill の Variable を更新:
```bash
ssh keinafarm-claude 'TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh; WM=http://172.18.0.15:8000/api/w/admins
# 古いものを削除してから再作成
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$WM/variables/delete/u/admin/LINE_CHANNEL_ACCESS_TOKEN"
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"path\":\"u/admin/LINE_CHANNEL_ACCESS_TOKEN\",\"value\":\"<新トークン>\",\"is_secret\":true,\"description\":\"LINE Messaging APIトークン\"}" \
"$WM/variables/create"'
```
### フローのログを確認する
```bash
# 最近のジョブ一覧
ssh keinafarm-claude 'TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh
curl -s -H "Authorization: Bearer $TOKEN" "http://172.18.0.15:8000/api/w/admins/jobs/completed/list?per_page=10" \
| grep -o "\"id\":\"[^\"]*\"\|\"started_at\":\"[^\"]*\"\|\"script_path\":\"[^\"]*\"" \
| paste - - - | grep mail_filter'
# ステップジョブのログ取得
ssh keinafarm-claude 'TOKEN=qLJ3VPZ61kTDiIwaUPUu1dXszGrsN1Dh; JOB_ID=<ステップジョブID>
curl -s -H "Authorization: Bearer $TOKEN" \
"http://172.18.0.15:8000/api/w/admins/jobs/completed/get/$JOB_ID" \
| grep -o "\"logs\":\"[^\"]*\"" | sed "s/\\\\n/\n/g"'
```
---
## 9. ソースファイル索引
### バックエンド
| ファイル | 内容 |
|---|---|
| `backend/apps/mail/models.py` | MailSender, MailEmail, MailNotificationToken |
| `backend/apps/mail/serializers.py` | MailSenderSerializer, MailEmailCreateSerializer, MailEmailListSerializer, FeedbackDetailSerializer |
| `backend/apps/mail/views.py` | 全ビューSenderRuleView, SenderContextView, MailEmailView, MailStatsView, FeedbackView, MailEmailFeedbackView, MailSenderViewSet+ MailAPIKeyPermission |
| `backend/apps/mail/urls.py` | URL ルーティング |
| `backend/apps/mail/admin.py` | Django 管理画面登録 |
| `backend/apps/mail/migrations/` | マイグレーション |
| `backend/keinasystem/settings.py` | `MAIL_API_KEY`, `FRONTEND_URL` 設定L161-162|
| `backend/keinasystem/urls.py` | `path('api/mail/', include('apps.mail.urls'))` |
### フロントエンド
| ファイル | 内容 |
|---|---|
| `frontend/src/app/mail/feedback/[token]/page.tsx` | フィードバックページ(認証不要)|
| `frontend/src/app/mail/history/page.tsx` | メール処理履歴画面 |
| `frontend/src/app/mail/rules/page.tsx` | 送信者ルール管理画面 |
| `frontend/src/components/Navbar.tsx` | 「メール履歴」「メールルール」リンク追加済み |
### Windmill フロー
| ファイル | 内容 |
|---|---|
| `C:/Users/akira/Develop/windmill_workflow/flows/mail_filter.flow.json` | フロー定義Python スクリプト本体を含む)|
本番 Windmill でのパス: `f/mail/mail_filter`
スケジュール: `f/mail/mail_filter_schedule`