# マスタードキュメント - メール通知関連編 > **最終更新**: 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. 宛先補正(To ヘッダー) └── @keinafarm.com 宛先は xserver1〜xserver6 に正規化(Gmail先行取り込み時の誤表示防止) 3. 送信者ルール確認(GET /api/mail/sender-rule/) ├── never_notify → スキップ(記録しない) ├── always_notify → LLMスキップ、即 LINE 通知 └── ルールなし → 4へ 4. 過去フィードバック集計取得(GET /api/mail/sender-context/) 5. Gemini API で重要度判定(LLM) 6. KeinaSystem に記録(POST /api/mail/emails/) ├── not_important → 記録のみ、通知なし └── important → フィードバックURLを発行、LINE 通知 7. 処理済み最終 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//` の URL をLINE通知文に含める。有効期限なし。 --- ## 3. API仕様(バックエンド) ベース URL: `https://main.keinafarm.net/api/mail/` ### 3.1 認証方式 | 認証方式 | 対象エンドポイント | ヘッダー | |---|---|---| | APIキー認証(Windmill用) | sender-rule, sender-context, POST emails/ | `X-API-Key: ` | | JWT認証(フロントエンド用) | GET emails/, stats/, senders/, PATCH emails//feedback/ | `Authorization: Bearer ` | | 認証不要 | GET/POST feedback// | なし | **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": "", "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/"} ``` `not_important` の場合: `{"id": 68}`(feedback_url なし) **重複処理**: `message_id` が既存の場合 400 を返す。Windmill 側で「重複メール、スキップ」として処理。 --- ### 3.3 フィードバックエンドポイント(認証不要) #### GET /api/mail/feedback/\/ フィードバックページ表示用にメール情報を返す。 **レスポンス例**: ```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/\/ フィードバックを保存する。 **リクエストボディ**: ```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/\/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/\/ 送信者ルール削除。 --- ## 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.7 宛先補正ロジック - 対象: Gmail 側で先に取得された転送メール - 方法: `To` ヘッダーの宛先アドレスを `recipient_map` で `xserver1`〜`xserver6` に変換 - 目的: message_id 重複時に Gmail で先着しても、実際の受信メールボックス(Xserver側)を通知文・履歴で保持する ### 4.6 LINE 通知文フォーマット ``` 📧 重要なメールが届きました 宛先: Gmail (メイン) 差出人: sender@example.com 件名: 件名テキスト フィードバック: https://main.keinafarm.net/mail/feedback/ ``` --- ## 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= ``` `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 API(APIキー認証)でのみ連携。 → 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件目以降を自動スキップ。先着レコードを採用するが、Gmail先行時でも `To` ヘッダー宛先補正により `xserver1`〜`xserver6` を優先して記録する。 --- ## 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`