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 向け API(APIキー認証) # --------------------------------------------------------------------------- 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// メール情報と現在のフィードバックを返す POST /api/mail/feedback// フィードバックを保存する """ 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']