23 KiB
マスタードキュメント - メール通知関連編
最終更新: 2026-03-05 対象バージョン: Phase 1 完了時点(本番稼働中) 目的: このドキュメントだけでメール通知機能の全容を把握できること
目次
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/<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//feedback/ | Authorization: Bearer <token> |
| 認証不要 | 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
レスポンス例:
{"matched": true, "rule": "never_notify", "match_type": "address"}
{"matched": false}
判定順序: アドレス一致 → ドメイン一致 → マッチなし
GET /api/mail/sender-context/
過去フィードバックの集計を返す(LLMへのコンテキスト用)。
リクエスト: クエリパラメータ email domain
レスポンス例:
{
"total_notified": 8,
"important": 2,
"not_important": 5,
"never_notify": 0,
"no_feedback": 1
}
POST /api/mail/emails/
メールを記録し、important の場合はフィードバックURLを発行する。
リクエストボディ:
{
"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"
}
レスポンス例:
{"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>/
フィードバックページ表示用にメール情報を返す。
レスポンス例:
{
"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>/
フィードバックを保存する。
リクエストボディ:
{
"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/
ダッシュボード用統計。
レスポンス例:
{
"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-contextAPI から取得)
回答: 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/<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 での参照:
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)。
デプロイコマンド
# サーバー上で実行(--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 を編集後:
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 = []を設定済み → 403MailEmailViewの POST: 設定なし(デフォルトの JWTAuthentication が動く)→ キー不一致時は 401
→ 統一するなら MailEmailView にも authentication_classes = [] を追加すべき(現状は動作上問題なし)。
フィードバック URL のセキュリティ
UUID v4 のランダムトークンのみで認証。有効期限なし。LINE に送信された URL を知っている人なら誰でもフィードバックを送れる(悪用リスクは低いため許容)。
重複メール処理
同じメールが複数アカウントで受信される場合(転送設定等)、message_id の unique 制約で2件目以降を自動スキップ。先着レコードを採用するが、Gmail先行時でも To ヘッダー宛先補正により xserver1〜xserver6 を優先して記録する。
8. 運用手順
新しいメールアカウントを追加する場合
flows/mail_filter.flow.jsonのACCOUNTSリストにエントリを追加- 本番 Windmill に Variables を追加(USER, PASSWORD, LAST_UID)
- フローを再デプロイ
# 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 を更新:
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"'
フローのログを確認する
# 最近のジョブ一覧
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