実装完了

作成・変更したファイル
バックエンド(新規):

apps/mail/models.py — MailSender, MailEmail, MailNotificationToken
apps/mail/serializers.py
apps/mail/views.py — Windmill用API、フィードバック、ルール管理
apps/mail/urls.py
apps/mail/admin.py
マイグレーション(自動生成・適用済み)
バックエンド(変更):

settings.py — apps.mail 追加、MAIL_API_KEY/FRONTEND_URL 環境変数
urls.py — /api/mail/ 追加
フロントエンド(新規):

mail/feedback/[token]/page.tsx — 認証不要、フィードバック3択+スコープ選択
mail/rules/page.tsx — ルール管理(一覧・追加・削除)
フロントエンド(変更):

Navbar.tsx — 「メールルール」メニュー追加
types/index.ts — MailSender, MailEmailFeedback 型追加
次のステップ(Windmill側)
Keinaシステム側の実装は完了しています。次はWindmillにIMAPポーリングスクリプトを書く必要があります。Windmillのスクリプトが必要になったタイミングでお声がけください。
This commit is contained in:
Akira
2026-02-22 09:27:27 +09:00
parent 24fa9b4e64
commit 7a1aa81f9f
17 changed files with 1367 additions and 1 deletions

View File

@@ -0,0 +1,99 @@
import uuid
from django.db import models
class MailSender(models.Model):
"""送信者ルールnever_notify: 通知しない)"""
email = models.EmailField(null=True, blank=True, verbose_name="メールアドレス")
domain = models.CharField(max_length=255, null=True, blank=True, verbose_name="ドメイン")
rule = models.CharField(
max_length=20,
choices=[('never_notify', '通知しない')],
default='never_notify',
verbose_name="ルール"
)
note = models.TextField(blank=True, verbose_name="メモ")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "送信者ルール"
verbose_name_plural = "送信者ルール"
constraints = [
models.CheckConstraint(
check=(
models.Q(email__isnull=False, domain__isnull=True) |
models.Q(email__isnull=True, domain__isnull=False)
),
name='mail_sender_email_or_domain'
)
]
def __str__(self):
value = self.email or self.domain
kind = "アドレス" if self.email else "ドメイン"
return f"[{kind}] {value}"
ACCOUNT_CHOICES = [
('xserver', 'Xserver'),
('gmail', 'Gmail'),
('hotmail', 'Hotmail'),
]
FEEDBACK_CHOICES = [
('important', '重要だった'),
('not_important', '普通のメール'),
('never_notify', '今後通知しない'),
]
class MailEmail(models.Model):
"""受信メール記録LLMに渡したメール"""
account = models.CharField(max_length=20, choices=ACCOUNT_CHOICES, verbose_name="アカウント")
message_id = models.CharField(max_length=500, unique=True, verbose_name="Message-ID")
sender_email = models.EmailField(verbose_name="送信者アドレス")
sender_domain = models.CharField(max_length=255, verbose_name="送信者ドメイン")
subject = models.CharField(max_length=500, verbose_name="件名")
body_preview = models.TextField(verbose_name="本文冒頭")
received_at = models.DateTimeField(verbose_name="受信日時")
llm_verdict = models.CharField(
max_length=20,
choices=[('important', '重要'), ('not_important', '重要でない')],
verbose_name="LLM判定"
)
notified_at = models.DateTimeField(null=True, blank=True, verbose_name="LINE通知日時")
feedback = models.CharField(
max_length=20,
choices=FEEDBACK_CHOICES,
null=True, blank=True,
verbose_name="フィードバック"
)
feedback_at = models.DateTimeField(null=True, blank=True, verbose_name="フィードバック日時")
class Meta:
verbose_name = "受信メール"
verbose_name_plural = "受信メール"
ordering = ['-received_at']
def __str__(self):
return f"{self.subject} ({self.sender_email})"
class MailNotificationToken(models.Model):
"""LINEフィードバックURL用トークン有効期限なし"""
email = models.OneToOneField(
MailEmail,
on_delete=models.CASCADE,
related_name='notification_token',
verbose_name="メール"
)
token = models.UUIDField(default=uuid.uuid4, unique=True, verbose_name="トークン")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "通知トークン"
verbose_name_plural = "通知トークン"
def __str__(self):
return str(self.token)