Files
keinasystem/backend/apps/mail/views.py
Akira b19e08a8dd メール処理履歴画面にフィードバック機能を追加
- 全メール(重要・通常問わず)に対してフィードバックボタンを追加
- PATCH /api/mail/emails/<pk>/feedback/ エンドポイントを追加(JWT認証)
- フィードバックモーダル: 重要/普通/今後通知しない/常に通知 の4択
- never_notify/always_notify 選択時はアドレス/ドメインの適用範囲を選択可能
- gmail_service アカウントのフィルタオプションを追加

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:52:21 +09:00

288 lines
10 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,
MailEmailListSerializer,
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 MailEmailView(APIView):
"""
GET /api/mail/emails/ メール処理履歴を取得JWT認証
POST /api/mail/emails/ メールを記録するAPIキー認証、Windmill向け
"""
def get_permissions(self):
if self.request.method == 'POST':
return [MailAPIKeyPermission()]
return [IsAuthenticated()]
def get(self, request):
qs = MailEmail.objects.select_related('notification_token').order_by('-received_at')
account = request.query_params.get('account')
if account:
qs = qs.filter(account=account)
verdict = request.query_params.get('verdict')
if verdict:
qs = qs.filter(llm_verdict=verdict)
serializer = MailEmailListSerializer(qs[:100], many=True)
return Response(serializer.data)
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 MailStatsView(APIView):
"""
GET /api/mail/stats/ ダッシュボード用統計
"""
permission_classes = [IsAuthenticated]
def get(self, request):
today = timezone.now().date()
today_processed = MailEmail.objects.filter(received_at__date=today).count()
today_notified = MailEmail.objects.filter(notified_at__date=today).count()
feedback_pending = MailEmail.objects.filter(
llm_verdict='important',
feedback__isnull=True
).count()
total_rules = MailSender.objects.count()
return Response({
'today_processed': today_processed,
'today_notified': today_notified,
'feedback_pending': feedback_pending,
'total_rules': total_rules,
})
# ---------------------------------------------------------------------------
# フィードバックビュー(認証不要)
# ---------------------------------------------------------------------------
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 MailEmailFeedbackView(APIView):
"""
PATCH /api/mail/emails/<pk>/feedback/ 履歴画面から直接フィードバックを更新JWT認証
"""
permission_classes = [IsAuthenticated]
def patch(self, request, pk):
mail_email = get_object_or_404(MailEmail, pk=pk)
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')
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', 'feedback': feedback})
# ---------------------------------------------------------------------------
# ルール管理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']