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

23 KiB
Raw Permalink Blame History

マスタードキュメント - メール通知関連編

最終更新: 2026-03-05 対象バージョン: Phase 1 完了時点(本番稼働中) 目的: このドキュメントだけでメール通知機能の全容を把握できること


目次

  1. システム概要・全体構成
  2. データモデル
  3. API仕様バックエンド
  4. Windmill フロー仕様
  5. 画面仕様(フロントエンド)
  6. 本番環境の設定値
  7. 設計判断と制約
  8. 運用手順
  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 更新日時

制約: emaildomain は必ずどちらか一方のみ設定DB CHECK 制約 mail_sender_email_or_domain

ルール判定の優先順位: アドレスルールが先、次にドメインルール

2.2 MailEmail受信メール記録

テーブル名: mail_mailemail

フィールド 説明
id BigAutoField PK
account CharField(20) gmail / gmail_service / hotmail / xserver1xserver6(旧データは 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.productionMAIL_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"
}

feedbackimportant / not_important / never_notify / always_notify のいずれか。 scopenever_notify / always_notify の場合のみ必要(address / domain)。

never_notify / always_notify + scope の場合、MailSender レコードを自動 upsert。


3.4 フロントエンド向けエンドポイントJWT認証

GET /api/mail/emails/

メール処理履歴を返す最新100件

クエリパラメータ: account(アカウント絞り込み)、verdictLLM判定絞り込み


PATCH /api/mail/emails/<pk>/feedback/

履歴画面から直接フィードバックを更新する。

リクエストボディ: feedback(必須)、scopenever_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.productionMAIL_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_UID0 または未設定の場合、現在の最大 UID を記録して終了(既存メールを遡らない)
  • 通常実行: LAST_UID + 1 以降の UID を検索して処理
  • エラー時: 個別メッセージの処理に失敗しても、成功した最大 UID まで更新する

4.5 LLM 判定Gemini

モデル: gemini-2.0-flashtemperature=0, maxOutputTokens=10

プロンプトに渡す情報:

  • 送信者アドレス・件名・本文冒頭
  • 過去の同一送信者のフィードバック集計(sender-context API から取得)

回答: 1(重要)/ 2重要でないの1文字。1 で始まる場合 important

4.7 宛先補正ロジック

  • 対象: Gmail 側で先に取得された転送メール
  • 方法: To ヘッダーの宛先アドレスを recipient_mapxserver1xserver6 に変換
  • 目的: 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 APIAPIキー認証でのみ連携。

→ Windmill の障害が KeinaSystem のメイン機能(作付け計画等)に影響しない。

APIキー認証の実装

MailAPIKeyPermissionBasePermission サブクラス)で 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 ヘッダー宛先補正により xserver1xserver6 を優先して記録する。


8. 運用手順

新しいメールアカウントを追加する場合

  1. flows/mail_filter.flow.jsonACCOUNTS リストにエントリを追加
  2. 本番 Windmill に Variables を追加USER, PASSWORD, LAST_UID
  3. フローを再デプロイ
# 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