Files
keinasystem/backend/apps/mail/views.py
Akira df16ab1ee0 変更内容まとめ
バックエンド
models.py — MailSender.rule に always_notify 追加、MailEmail.feedback にも追加、マイグレーション適用済み
views.py — FeedbackView.post が always_notify を受け取ったら MailSender ルールを作成(never_notify と同じ仕組み)
フロントエンド
feedback/[token]/page.tsx — 4択目「🔔 常に通知してほしい」を追加。スコープ選択(アドレス/ドメイン)もあり。色はteal系で区別
mail/rules/page.tsx — 追加フォームにルール種別セレクタを追加、一覧に「常に通知」バッジ(teal)を表示
Windmill側の使い方(メモ)
GET /api/mail/sender-rule/ のレスポンスに "rule": "always_notify" が返ってきたら、LLMをスキップして llm_verdict: "important" で直接 POST /api/mail/emails/ を呼べばOKです。
2026-02-22 09:49:28 +09:00

205 lines
7.4 KiB
Python
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.
import secrets
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import viewsets, permissions, status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import BasePermission, AllowAny, IsAuthenticated
from django.db.models import Count, Q
from .models import MailSender, MailEmail, MailNotificationToken
from .serializers import (
MailSenderSerializer,
MailEmailCreateSerializer,
FeedbackDetailSerializer,
)
class MailAPIKeyPermission(BasePermission):
"""X-API-Key ヘッダーで認証Windmill向け"""
def has_permission(self, request, view):
key = request.headers.get('X-API-Key', '')
expected = getattr(settings, 'MAIL_API_KEY', '')
if not key or not expected:
return False
return secrets.compare_digest(key, expected)
# ---------------------------------------------------------------------------
# Windmill 向け APIAPIキー認証
# ---------------------------------------------------------------------------
class SenderRuleView(APIView):
"""
GET /api/mail/sender-rule/?email=...&domain=...
送信者ルールを確認する(アドレス優先 > ドメイン優先)
"""
permission_classes = [MailAPIKeyPermission]
authentication_classes = []
def get(self, request):
email = request.query_params.get('email', '')
domain = request.query_params.get('domain', '')
# アドレスルールを先に確認(具体的なほど優先)
if email:
sender = MailSender.objects.filter(email=email).first()
if sender:
return Response({
'matched': True,
'rule': sender.rule,
'match_type': 'address',
})
# ドメインルールを確認
if domain:
sender = MailSender.objects.filter(domain=domain).first()
if sender:
return Response({
'matched': True,
'rule': sender.rule,
'match_type': 'domain',
})
return Response({'matched': False})
class SenderContextView(APIView):
"""
GET /api/mail/sender-context/?email=...&domain=...
LLM用フィードバック集計を返すトークン肥大化防止のため集計値のみ
"""
permission_classes = [MailAPIKeyPermission]
authentication_classes = []
def get(self, request):
email = request.query_params.get('email', '')
domain = request.query_params.get('domain', '')
# アドレスで絞り込み(なければドメインで絞り込み)
if email:
qs = MailEmail.objects.filter(sender_email=email)
elif domain:
qs = MailEmail.objects.filter(sender_domain=domain)
else:
return Response({
'total_notified': 0,
'important': 0,
'not_important': 0,
'never_notify': 0,
'no_feedback': 0,
})
total = qs.count()
important = qs.filter(feedback='important').count()
not_important = qs.filter(feedback='not_important').count()
never_notify = qs.filter(feedback='never_notify').count()
no_feedback = qs.filter(feedback__isnull=True).count()
return Response({
'total_notified': total,
'important': important,
'not_important': not_important,
'never_notify': never_notify,
'no_feedback': no_feedback,
})
class MailEmailCreateView(APIView):
"""
POST /api/mail/emails/
メールを記録する。llm_verdict == 'important' の場合はトークンも発行する。
"""
permission_classes = [MailAPIKeyPermission]
authentication_classes = []
def post(self, request):
serializer = MailEmailCreateSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
mail_email = serializer.save()
response_data = {'id': mail_email.id}
if mail_email.llm_verdict == 'important':
token_obj = MailNotificationToken.objects.create(email=mail_email)
mail_email.notified_at = timezone.now()
mail_email.save(update_fields=['notified_at'])
frontend_url = getattr(settings, 'FRONTEND_URL', 'http://localhost:3000')
response_data['feedback_url'] = f"{frontend_url}/mail/feedback/{token_obj.token}"
return Response(response_data, status=status.HTTP_201_CREATED)
# ---------------------------------------------------------------------------
# フィードバックビュー(認証不要)
# ---------------------------------------------------------------------------
class FeedbackView(APIView):
"""
GET /api/mail/feedback/<token>/ メール情報と現在のフィードバックを返す
POST /api/mail/feedback/<token>/ フィードバックを保存する
"""
permission_classes = [AllowAny]
authentication_classes = []
def _get_mail_email(self, token):
token_obj = get_object_or_404(MailNotificationToken, token=token)
return token_obj.email
def get(self, request, token):
mail_email = self._get_mail_email(token)
serializer = FeedbackDetailSerializer(mail_email)
return Response(serializer.data)
def post(self, request, token):
mail_email = self._get_mail_email(token)
feedback = request.data.get('feedback')
valid_feedbacks = ['important', 'not_important', 'never_notify', 'always_notify']
if feedback not in valid_feedbacks:
return Response(
{'error': f'feedback は {valid_feedbacks} のいずれかを指定してください'},
status=status.HTTP_400_BAD_REQUEST
)
# フィードバックを更新(再選択も可能)
mail_email.feedback = feedback
mail_email.feedback_at = timezone.now()
mail_email.save(update_fields=['feedback', 'feedback_at'])
# 送信者ルールを伴うフィードバックの処理
if feedback in ('never_notify', 'always_notify'):
scope = request.data.get('scope') # 'address' or 'domain'
if scope == 'address':
MailSender.objects.update_or_create(
email=mail_email.sender_email,
defaults={'domain': None, 'rule': feedback}
)
elif scope == 'domain':
MailSender.objects.update_or_create(
domain=mail_email.sender_domain,
defaults={'email': None, 'rule': feedback}
)
return Response({'status': 'ok'})
# ---------------------------------------------------------------------------
# ルール管理JWT認証
# ---------------------------------------------------------------------------
class MailSenderViewSet(viewsets.ModelViewSet):
"""
GET /api/mail/senders/ ルール一覧
POST /api/mail/senders/ ルール追加
DELETE /api/mail/senders/{id}/ ルール削除
"""
queryset = MailSender.objects.all().order_by('-created_at')
serializer_class = MailSenderSerializer
permission_classes = [IsAuthenticated]
http_method_names = ['get', 'post', 'delete', 'head', 'options']