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 向け 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 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// メール情報と現在のフィードバックを返す 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'] 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 == 'never_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': 'never_notify'} ) elif scope == 'domain': MailSender.objects.update_or_create( domain=mail_email.sender_domain, defaults={'email': None, 'rule': 'never_notify'} ) 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']