Skip to content

Commit 0014c47

Browse files
committed
✨(recipients) handle MAX_RECIPIENTS_PER_MESSAGE
Define global env var to configure max recipients allowed per message Implement frontend restrictions Add custom_limits on mailbox and maildomain Add playwright tests
1 parent 3b6f0b9 commit 0014c47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2487
-85
lines changed

src/backend/core/api/openapi.json

Lines changed: 273 additions & 7 deletions
Large diffs are not rendered by default.

src/backend/core/api/permissions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,33 @@ def has_permission(self, request, view):
450450
return models.MailboxAccess.objects.filter(
451451
user=request.user, mailbox=view.kwargs.get("mailbox_id")
452452
).exists()
453+
454+
455+
class CanManageSettings(permissions.BasePermission):
456+
"""
457+
Permission class that checks if the user has CAN_MANAGE_SETTINGS ability
458+
for the given MailDomain or Mailbox object.
459+
460+
This permission should be used for actions that modify custom_limits and other settings.
461+
"""
462+
463+
message = "You do not have permission to manage settings for this resource."
464+
465+
def has_object_permission(self, request, view, obj):
466+
"""
467+
Check if user has CAN_MANAGE_SETTINGS ability for the object.
468+
Works with both MailDomain and Mailbox objects.
469+
"""
470+
if not request.user or not request.user.is_authenticated:
471+
return False
472+
473+
# Get abilities for this object
474+
abilities = obj.get_abilities(request.user)
475+
476+
# Check for the specific ability
477+
if isinstance(obj, models.MailDomain):
478+
return abilities[enums.MailDomainAbilities.CAN_MANAGE_SETTINGS]
479+
if isinstance(obj, models.Mailbox):
480+
return abilities[enums.MailboxAbilities.CAN_MANAGE_SETTINGS]
481+
482+
return False

src/backend/core/api/serializers.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66

7+
from django.conf import settings
78
from django.db import transaction
89
from django.db.models import Count, Exists, OuterRef, Q
910
from django.utils.translation import gettext_lazy as _
@@ -216,10 +217,18 @@ class MailboxSerializer(AbilitiesModelSerializer):
216217
role = serializers.SerializerMethodField(read_only=True)
217218
count_unread_messages = serializers.SerializerMethodField(read_only=True)
218219
count_messages = serializers.SerializerMethodField(read_only=True)
220+
max_recipients_per_message = serializers.SerializerMethodField(read_only=True)
219221

220222
class Meta:
221223
model = models.Mailbox
222-
fields = ["id", "email", "role", "count_unread_messages", "count_messages"]
224+
fields = [
225+
"id",
226+
"email",
227+
"role",
228+
"count_unread_messages",
229+
"count_messages",
230+
"max_recipients_per_message",
231+
]
223232

224233
def get_email(self, instance):
225234
"""Return the email of the mailbox."""
@@ -280,6 +289,11 @@ def get_abilities(self, instance):
280289
"""Get abilities for the instance."""
281290
return super().get_abilities(instance)
282291

292+
@extend_schema_field(serializers.IntegerField())
293+
def get_max_recipients_per_message(self, instance):
294+
"""Return the maximum number of recipients per message for the mailbox."""
295+
return instance.get_max_recipients_per_message()
296+
283297

284298
class MailboxLightSerializer(serializers.ModelSerializer):
285299
"""Serializer for mailbox details in thread access."""
@@ -870,7 +884,14 @@ def get_expected_dns_records(self, instance):
870884

871885
class Meta:
872886
model = models.MailDomain
873-
fields = ["id", "name", "created_at", "updated_at", "expected_dns_records"]
887+
fields = [
888+
"id",
889+
"name",
890+
"created_at",
891+
"updated_at",
892+
"expected_dns_records",
893+
"custom_limits",
894+
]
874895
read_only_fields = fields
875896

876897
@extend_schema_field(
@@ -937,7 +958,7 @@ def validate(self, attrs):
937958

938959

939960
class MailDomainAdminWriteSerializer(serializers.ModelSerializer):
940-
"""Serialize mail domains for creating / editing admin view."""
961+
"""Serialize mail domains for creating admin view."""
941962

942963
class Meta:
943964
model = models.MailDomain
@@ -953,6 +974,49 @@ class Meta:
953974
read_only_fields = ["id", "created_at", "updated_at"]
954975

955976

977+
class CustomLimitsUpdateMixin:
978+
"""Mixin for validating custom_limits field."""
979+
980+
def validate_custom_limits(self, value):
981+
"""Validate that custom_limits does not exceed global maximum."""
982+
if value and isinstance(value, dict):
983+
max_recipients = value.get("max_recipients_per_message")
984+
if max_recipients is not None and isinstance(max_recipients, int):
985+
if max_recipients > settings.MAX_RECIPIENTS_PER_MESSAGE:
986+
raise serializers.ValidationError(
987+
_(
988+
"The limit cannot exceed the global maximum of %(max)s recipients."
989+
)
990+
% {"max": settings.MAX_RECIPIENTS_PER_MESSAGE}
991+
)
992+
return value
993+
994+
995+
class MailDomainAdminUpdateSerializer(
996+
CustomLimitsUpdateMixin, serializers.ModelSerializer
997+
):
998+
"""Serialize mail domains for updating custom_limits only."""
999+
1000+
class Meta:
1001+
model = models.MailDomain
1002+
fields = [
1003+
"id",
1004+
"custom_limits",
1005+
]
1006+
read_only_fields = ["id"]
1007+
1008+
1009+
class MailboxSettingsUpdateSerializer(
1010+
CustomLimitsUpdateMixin, serializers.ModelSerializer
1011+
):
1012+
"""Serialize mailbox settings (custom_limits) for update operations."""
1013+
1014+
class Meta:
1015+
model = models.Mailbox
1016+
fields = ["id", "custom_limits"]
1017+
read_only_fields = ["id"]
1018+
1019+
9561020
class MailboxAccessNestedUserSerializer(serializers.ModelSerializer):
9571021
"""
9581022
Serialize MailboxAccess for nesting within MailboxAdminSerializer.
@@ -996,6 +1060,7 @@ class Meta:
9961060
"updated_at",
9971061
"can_reset_password",
9981062
"contact",
1063+
"custom_limits",
9991064
]
10001065
read_only_fields = [
10011066
"id",
@@ -1152,6 +1217,9 @@ class MailboxAdminCreateSerializer(MailboxAdminSerializer):
11521217
"""
11531218
Serialize Mailbox details for create admin endpoint, including users with access and
11541219
metadata.
1220+
1221+
Note: custom_limits is excluded from the response as it cannot be set during creation.
1222+
It can only be modified via the dedicated /settings/ endpoint.
11551223
"""
11561224

11571225
one_time_password = serializers.SerializerMethodField(
@@ -1165,7 +1233,12 @@ def get_one_time_password(self, instance) -> str | None:
11651233

11661234
class Meta:
11671235
model = models.Mailbox
1168-
fields = MailboxAdminSerializer.Meta.fields + ["one_time_password"]
1236+
# Exclude custom_limits from creation response - it can only be set via /settings/
1237+
fields = [
1238+
field
1239+
for field in MailboxAdminSerializer.Meta.fields
1240+
if field != "custom_limits"
1241+
] + ["one_time_password"]
11691242
read_only_fields = fields
11701243

11711244

src/backend/core/api/viewsets/config.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from django.conf import settings
44

55
import rest_framework as drf
6-
from drf_spectacular.utils import OpenApiResponse, extend_schema
6+
from drf_spectacular.utils import (
7+
OpenApiResponse,
8+
extend_schema,
9+
)
710
from rest_framework.permissions import AllowAny
811

912
from core.ai.utils import is_ai_enabled, is_ai_summary_enabled, is_auto_labels_enabled
@@ -82,7 +85,28 @@ class ConfigView(drf.views.APIView):
8285
},
8386
"MAX_INCOMING_EMAIL_SIZE": {
8487
"type": "integer",
85-
"description": "Maximum size in bytes for incoming email (including attachments and body)",
88+
"description": (
89+
"Maximum size in bytes for incoming email "
90+
"(including attachments and body)"
91+
),
92+
"readOnly": True,
93+
},
94+
"MAX_RECIPIENTS_PER_MESSAGE": {
95+
"type": "integer",
96+
"description": (
97+
"Maximum number of recipients per message "
98+
"(to + cc + bcc) for the entire system. "
99+
"Cannot be exceeded."
100+
),
101+
"readOnly": True,
102+
},
103+
"MAX_DEFAULT_RECIPIENTS_PER_MESSAGE": {
104+
"type": "integer",
105+
"description": (
106+
"Default maximum number of recipients per message "
107+
"(to + cc + bcc) for a mailbox or maildomain "
108+
"if no custom limit is set."
109+
),
86110
"readOnly": True,
87111
},
88112
},
@@ -98,6 +122,8 @@ class ConfigView(drf.views.APIView):
98122
"MAX_OUTGOING_ATTACHMENT_SIZE",
99123
"MAX_OUTGOING_BODY_SIZE",
100124
"MAX_INCOMING_EMAIL_SIZE",
125+
"MAX_RECIPIENTS_PER_MESSAGE",
126+
"MAX_DEFAULT_RECIPIENTS_PER_MESSAGE",
101127
],
102128
},
103129
)
@@ -132,6 +158,12 @@ def get(self, request):
132158
)
133159
dict_settings["MAX_OUTGOING_BODY_SIZE"] = settings.MAX_OUTGOING_BODY_SIZE
134160
dict_settings["MAX_INCOMING_EMAIL_SIZE"] = settings.MAX_INCOMING_EMAIL_SIZE
161+
dict_settings["MAX_RECIPIENTS_PER_MESSAGE"] = (
162+
settings.MAX_RECIPIENTS_PER_MESSAGE
163+
)
164+
dict_settings["MAX_DEFAULT_RECIPIENTS_PER_MESSAGE"] = (
165+
settings.MAX_DEFAULT_RECIPIENTS_PER_MESSAGE
166+
)
135167

136168
# Drive service
137169
if base_url := settings.DRIVE_CONFIG.get("base_url"):

src/backend/core/api/viewsets/mailbox.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""API ViewSet for Mailbox model."""
22

3-
from django.db.models import OuterRef, Q, Subquery
3+
from django.db.models import Exists, OuterRef, Q, Subquery
44

55
from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
66
from rest_framework import mixins, viewsets
@@ -24,7 +24,7 @@ class MailboxViewSet(
2424
def get_queryset(self):
2525
"""Restrict results to the current user's mailboxes."""
2626
user = self.request.user
27-
# For regular users, annotate with their actual role
27+
# For regular users, annotate with their actual role and domain admin status
2828
return (
2929
models.Mailbox.objects.filter(accesses__user=user)
3030
.prefetch_related("accesses__user", "domain")
@@ -33,7 +33,15 @@ def get_queryset(self):
3333
models.MailboxAccess.objects.filter(
3434
mailbox=OuterRef("pk"), user=user
3535
).values("role")[:1]
36-
)
36+
),
37+
# Annotate domain admin status to avoid N+1 queries in get_abilities()
38+
is_domain_admin=Exists(
39+
models.MailDomainAccess.objects.filter(
40+
user=user,
41+
maildomain=OuterRef("domain"),
42+
role=models.MailDomainAccessRoleChoices.ADMIN,
43+
)
44+
),
3745
)
3846
.order_by("-created_at")
3947
)

0 commit comments

Comments
 (0)