api/views/emails.py (196 lines of code) (raw):

"""API views for emails""" from logging import getLogger from typing import Generic, TypeVar from django.apps import apps from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet from django.template.loader import render_to_string import django_ftl from django_filters import rest_framework as filters from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import api_view, permission_classes, throttle_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.serializers import BaseSerializer from rest_framework.status import ( HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND, ) from rest_framework.throttling import UserRateThrottle from rest_framework.viewsets import ModelViewSet from waffle import flag_is_active from emails.apps import EmailsConfig from emails.models import DomainAddress, RelayAddress from emails.utils import generate_from_header, ses_message_props from emails.views import _get_address, wrap_html_email from privaterelay.ftl_bundles import main as ftl_bundle from privaterelay.utils import glean_logger from ..permissions import IsOwner from ..serializers.emails import ( DomainAddressSerializer, FirstForwardedEmailSerializer, RelayAddressSerializer, ) from . import SaveToRequestUser logger = getLogger("events") class RelayAddressFilter(filters.FilterSet): used_on = filters.CharFilter(field_name="used_on", lookup_expr="icontains") class Meta: model = RelayAddress fields = [ "enabled", "description", "generated_for", "block_list_emails", "used_on", # read-only "id", "address", "domain", "created_at", "last_modified_at", "last_used_at", "num_forwarded", "num_blocked", "num_spam", ] _Address = TypeVar("_Address", RelayAddress, DomainAddress) class AddressViewSet(Generic[_Address], SaveToRequestUser, ModelViewSet): def perform_create(self, serializer: BaseSerializer[_Address]) -> None: super().perform_create(serializer) if not serializer.instance: raise ValueError("serializer.instance must be truthy value.") glean_logger().log_email_mask_created( request=self.request, mask=serializer.instance, created_by_api=True, ) def perform_update(self, serializer: BaseSerializer[_Address]) -> None: if not serializer.instance: raise ValueError("serializer.instance must be truthy value.") old_description = serializer.instance.description super().perform_update(serializer) new_description = serializer.instance.description if old_description != new_description: glean_logger().log_email_mask_label_updated( request=self.request, mask=serializer.instance ) def perform_destroy(self, instance: _Address) -> None: user = instance.user is_random_mask = isinstance(instance, RelayAddress) super().perform_destroy(instance) glean_logger().log_email_mask_deleted( request=self.request, user=user, is_random_mask=is_random_mask, ) @extend_schema(tags=["emails"]) class RelayAddressViewSet(AddressViewSet[RelayAddress]): """An email address with a random name provided by Relay.""" serializer_class = RelayAddressSerializer permission_classes = [IsAuthenticated, IsOwner] filterset_class = RelayAddressFilter def get_queryset(self) -> QuerySet[RelayAddress]: if isinstance(self.request.user, User): return RelayAddress.objects.filter(user=self.request.user) return RelayAddress.objects.none() class DomainAddressFilter(filters.FilterSet): used_on = filters.CharFilter(field_name="used_on", lookup_expr="icontains") class Meta: model = DomainAddress fields = [ "enabled", "description", "block_list_emails", "used_on", # read-only "id", "address", "domain", "created_at", "last_modified_at", "last_used_at", "num_forwarded", "num_blocked", "num_spam", ] @extend_schema(tags=["emails"]) class DomainAddressViewSet(AddressViewSet[DomainAddress]): """An email address with subdomain chosen by a Relay user.""" serializer_class = DomainAddressSerializer permission_classes = [IsAuthenticated, IsOwner] filterset_class = DomainAddressFilter def get_queryset(self) -> QuerySet[DomainAddress]: if isinstance(self.request.user, User): return DomainAddress.objects.filter( user=self.request.user ).prefetch_related("user", "user__profile") return DomainAddress.objects.none() class FirstForwardedEmailRateThrottle(UserRateThrottle): rate = settings.FIRST_EMAIL_RATE_LIMIT @permission_classes([IsAuthenticated]) @extend_schema( tags=["emails"], request=FirstForwardedEmailSerializer, responses={ 201: OpenApiResponse(description="Email sent to user."), 400: OpenApiResponse(description="Invalid mask."), 401: OpenApiResponse(description="Authentication required."), 403: OpenApiResponse(description="Flag 'free_user_onboarding' is required."), 404: OpenApiResponse(description="Unable to find the mask."), }, ) @api_view(["POST"]) @throttle_classes([FirstForwardedEmailRateThrottle]) def first_forwarded_email(request): """ Requires `free_user_onboarding` flag to be active for the user. Send the `first_forwarded_email.html` email to the user via a mask. See [/emails/first_forwarded_email](/emails/first_forwarded_email). Note: `mask` value must be a `RelayAddress` that belongs to the authenticated user. A `DomainAddress` will not work. """ if not flag_is_active(request, "free_user_onboarding"): # Return Permission Denied error return Response( {"detail": "Requires free_user_onboarding waffle flag."}, status=403 ) serializer = FirstForwardedEmailSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) mask = str(serializer.data.get("mask")) user = request.user try: address = _get_address(mask) RelayAddress.objects.get(user=user, address=address) except ObjectDoesNotExist: return Response(f"{mask} does not exist for user.", status=HTTP_404_NOT_FOUND) profile = user.profile app_config = apps.get_app_config("emails") if not isinstance(app_config, EmailsConfig): raise TypeError("app_config must be type EmailsConfig") ses_client = app_config.ses_client if not ses_client: raise ValueError("ses_client must be truthy value.") if not settings.RELAY_FROM_ADDRESS: raise ValueError("settings.RELAY_FROM_ADDRESS must have a value.") with django_ftl.override(profile.language): translated_subject = ftl_bundle.format("forwarded-email-hero-header") first_forwarded_email_html = render_to_string( "emails/first_forwarded_email.html", { "SITE_ORIGIN": settings.SITE_ORIGIN, }, ) from_address = generate_from_header(settings.RELAY_FROM_ADDRESS, mask) wrapped_email = wrap_html_email( first_forwarded_email_html, profile.language, profile.has_premium, from_address, 0, ) ses_client.send_email( Destination={ "ToAddresses": [user.email], }, Source=from_address, Message={ "Subject": ses_message_props(translated_subject), "Body": { "Html": ses_message_props(wrapped_email), }, }, ) logger.info(f"Sent first_forwarded_email to user ID: {user.id}") return Response(status=HTTP_201_CREATED)