Skip to content

Commit db6fcac

Browse files
committed
!drop when backend will be fixed
#303
1 parent 8bc60b5 commit db6fcac

17 files changed

+2006
-46
lines changed

src/backend/core/api/openapi.json

Lines changed: 544 additions & 0 deletions
Large diffs are not rendered by default.

src/backend/core/api/serializers.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,8 +922,9 @@ class ReadOnlyMessageTemplateSerializer(AbilitiesModelSerializer):
922922

923923
kind = IntegerChoicesField(choices_class=models.MessageTemplateKindChoices)
924924
is_default = serializers.SerializerMethodField()
925+
raw_blob = serializers.SerializerMethodField()
925926

926-
def get_is_default(self, obj):
927+
def get_is_default(self, obj) -> bool:
927928
"""Get is_default information."""
928929
maildomain_id = self.context.get("maildomain_id")
929930
mailbox_id = self.context.get("mailbox_id")
@@ -936,6 +937,10 @@ def get_is_default(self, obj):
936937
)
937938
return template.is_default if template else False
938939

940+
def get_raw_blob(self, obj) -> str | None:
941+
"""Get raw blob."""
942+
return obj.raw_blob.get_content().decode("utf-8") if obj.raw_blob else None
943+
939944
class Meta:
940945
model = models.MessageTemplate
941946
fields = [
@@ -951,7 +956,7 @@ class Meta:
951956
"created_at",
952957
"updated_at",
953958
]
954-
read_only_fields = ["id", "created_at", "updated_at"]
959+
read_only_fields = ["id", "created_at", "updated_at", "raw_blob"]
955960

956961

957962
class MessageTemplateSerializer(AbilitiesModelSerializer):
@@ -969,6 +974,7 @@ class MessageTemplateSerializer(AbilitiesModelSerializer):
969974
is_default = serializers.BooleanField(
970975
required=False, default=False, help_text="Set as default template"
971976
)
977+
raw_blob = serializers.SerializerMethodField()
972978

973979
class Meta:
974980
model = models.MessageTemplate
@@ -987,7 +993,11 @@ class Meta:
987993
"maildomain_id",
988994
"is_default",
989995
]
990-
read_only_fields = ["id", "created_at", "updated_at"]
996+
read_only_fields = ["id", "created_at", "updated_at", "raw_blob"]
997+
998+
def get_raw_blob(self, obj) -> str | None:
999+
"""Get raw blob."""
1000+
return obj.raw_blob.get_content().decode("utf-8") if obj.raw_blob else None
9911001

9921002
def validate(self, attrs):
9931003
"""Validate template data."""
@@ -1021,8 +1031,17 @@ def create(self, validated_data):
10211031
maildomain_id = validated_data.pop("maildomain_id", None)
10221032
is_default = validated_data.pop("is_default", False)
10231033

1034+
# Create raw_blob relationship
1035+
if self.initial_data.get("raw_blob"):
1036+
blob = models.Blob.objects.create_blob(
1037+
content=self.initial_data.get("raw_blob", "").encode("utf-8"),
1038+
content_type="application/json",
1039+
)
1040+
validated_data["raw_blob"] = blob
1041+
10241042
template = super().create(validated_data)
10251043

1044+
10261045
# Create mailbox relationship
10271046
if mailbox_id:
10281047
mailbox = models.Mailbox.objects.get(id=mailbox_id)
@@ -1058,8 +1077,21 @@ def update(self, instance, validated_data):
10581077
maildomain_id = validated_data.pop("maildomain_id", None)
10591078
is_default = validated_data.pop("is_default", None)
10601079

1080+
if self.initial_data.get("raw_blob"):
1081+
try:
1082+
if instance.raw_blob:
1083+
instance.raw_blob.delete()
1084+
except models.Blob.DoesNotExist:
1085+
pass
1086+
blob = models.Blob.objects.create_blob(
1087+
content=self.initial_data.get("raw_blob", "").encode("utf-8"),
1088+
content_type="application/json",
1089+
)
1090+
validated_data["raw_blob"] = blob
1091+
10611092
template = super().update(instance, validated_data)
10621093

1094+
10631095
# Update relationships if provided
10641096
if mailbox_id is not None or maildomain_id is not None:
10651097
# Handle mailbox relationship

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""API ViewSet for message templates."""
22

33
from django.db.models import Q
4+
from django.conf import settings
45

56
from drf_spectacular.utils import OpenApiResponse, extend_schema
67
from rest_framework import status, viewsets
@@ -71,7 +72,7 @@ def get_queryset_for_detail(self):
7172

7273
# Filter by user permissions for regular users
7374
accessible_mailboxes = Mailbox.objects.filter(accesses__user=user)
74-
accessible_maildomains = MailDomain.objects.filter(accesses__user=user)
75+
accessible_maildomains = MailDomain.objects.filter(mailbox__accesses__user=user)
7576

7677
return MessageTemplate.objects.filter(
7778
Q(template_mailboxes__mailbox__in=accessible_mailboxes)
@@ -93,8 +94,10 @@ def get_queryset_for_list(self):
9394
queryset = MessageTemplate.objects.all()
9495
elif mailbox_id:
9596
accessible_mailboxes = Mailbox.objects.filter(accesses__user=user)
97+
accessible_maildomains = MailDomain.objects.filter(mailbox__accesses__user=user)
9698
queryset = MessageTemplate.objects.filter(
97-
template_mailboxes__mailbox_id__in=accessible_mailboxes
99+
Q(template_mailboxes__mailbox_id__in=accessible_mailboxes)
100+
| Q(template_maildomains__maildomain_id__in=accessible_maildomains)
98101
)
99102
elif maildomain_id:
100103
accessible_maildomains = MailDomain.objects.filter(accesses__user=user)
@@ -105,7 +108,11 @@ def get_queryset_for_list(self):
105108
is_default = self.request.query_params.get("is_default")
106109
# filter by mailbox_id or maildomain_id
107110
if mailbox_id:
108-
queryset = queryset.filter(template_mailboxes__mailbox_id=mailbox_id)
111+
mailbox = Mailbox.objects.get(id=mailbox_id)
112+
queryset = queryset.filter(
113+
Q(template_mailboxes__mailbox_id=mailbox_id)
114+
| Q(template_maildomains__maildomain_id=mailbox.domain_id)
115+
)
109116
if is_default is not None:
110117
is_default_bool = is_default.lower() in ("true", "1", "yes")
111118
queryset = queryset.filter(
@@ -173,8 +180,14 @@ def render_template(self, request, pk=None): # pylint: disable=unused-argument
173180

174181
context = {
175182
"mailbox": str(mailbox),
176-
"fullname": user.full_name,
183+
"full_name": user.full_name,
177184
}
185+
schema = settings.SCHEMA_CUSTOM_ATTRIBUTES_USER
186+
schema_properties = schema.get("properties", {})
187+
188+
for field_key in schema_properties.keys():
189+
context[field_key] = user.custom_attributes.get(field_key, "")
190+
178191
try:
179192
rendered = template.render_template(context)
180193
return Response(rendered)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.8 on 2025-08-20 14:07
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('core', '0007_messagetemplate_raw_blob'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='blob',
16+
name='mailbox',
17+
field=models.ForeignKey(help_text='Mailbox that owns this blob', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blobs', to='core.mailbox'),
18+
),
19+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.8 on 2025-08-20 14:07
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('core', '0008_alter_blob_mailbox'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='blob',
16+
name='mailbox',
17+
field=models.ForeignKey(blank=True, help_text='Mailbox that owns this blob', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blobs', to='core.mailbox'),
18+
),
19+
]

src/backend/core/models.py

Lines changed: 76 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -482,52 +482,15 @@ def create_blob(
482482
Raises:
483483
ValueError: If content is empty
484484
"""
485-
if not content:
486-
raise ValueError("Content cannot be empty")
487-
488-
# Calculate SHA256 hash of the original content
489-
sha256_hash = hashlib.sha256(content).digest()
490-
491-
# Store the original size
492-
original_size = len(content)
493-
494-
# Apply compression if requested
495-
compressed_content = content
496-
if compression == CompressionTypeChoices.ZSTD:
497-
compressed_content = pyzstd.compress(
498-
content, level_or_option=settings.MESSAGES_BLOB_ZSTD_LEVEL
499-
)
500-
logger.debug(
501-
"Compressed blob from %d bytes to %d bytes (%.1f%% reduction)",
502-
original_size,
503-
len(compressed_content),
504-
(1 - len(compressed_content) / original_size) * 100,
505-
)
506-
elif compression == CompressionTypeChoices.NONE:
507-
compressed_content = content
508-
else:
509-
raise ValueError(f"Unsupported compression type: {compression}")
510485

511486
# Create the blob
512-
blob = Blob.objects.create(
513-
sha256=sha256_hash,
514-
size=original_size,
487+
return Blob.objects.create_blob(
488+
content=content,
515489
content_type=content_type,
516490
compression=compression,
517-
raw_content=compressed_content,
518491
mailbox=self,
519492
)
520493

521-
logger.info(
522-
"Created blob %s: %d bytes, %s compression, %s content type",
523-
blob.id,
524-
original_size,
525-
compression.label,
526-
content_type,
527-
)
528-
529-
return blob
530-
531494
def get_abilities(self, user):
532495
"""
533496
Compute and return abilities for a given user on the mailbox.
@@ -1192,6 +1155,76 @@ def get_tokens_count(self) -> int:
11921155
counted_text = f"{subject} {body}"
11931156
return len(counted_text.split())
11941157

1158+
class BlobManager(models.Manager):
1159+
"""Custom Manager for Blob model."""
1160+
1161+
def create_blob(
1162+
self,
1163+
content: bytes,
1164+
content_type: str,
1165+
compression: Optional[CompressionTypeChoices] = CompressionTypeChoices.ZSTD,
1166+
**kwargs,
1167+
) -> "Blob":
1168+
"""
1169+
Create a new blob with automatic SHA256 calculation and compression.
1170+
1171+
Args:
1172+
content: Raw binary content to store
1173+
content_type: MIME type of the content
1174+
compression: Compression type to use (defaults to ZSTD)
1175+
1176+
Returns:
1177+
The created Blob instance
1178+
1179+
Raises:
1180+
ValueError: If content is empty
1181+
"""
1182+
if not content:
1183+
raise ValueError("Content cannot be empty")
1184+
1185+
# Calculate SHA256 hash of the original content
1186+
sha256_hash = hashlib.sha256(content).digest()
1187+
1188+
# Store the original size
1189+
original_size = len(content)
1190+
1191+
# Apply compression if requested
1192+
compressed_content = content
1193+
if compression == CompressionTypeChoices.ZSTD:
1194+
compressed_content = pyzstd.compress(
1195+
content, level_or_option=settings.MESSAGES_BLOB_ZSTD_LEVEL
1196+
)
1197+
logger.debug(
1198+
"Compressed blob from %d bytes to %d bytes (%.1f%% reduction)",
1199+
original_size,
1200+
len(compressed_content),
1201+
(1 - len(compressed_content) / original_size) * 100,
1202+
)
1203+
elif compression == CompressionTypeChoices.NONE:
1204+
compressed_content = content
1205+
else:
1206+
raise ValueError(f"Unsupported compression type: {compression}")
1207+
1208+
# Create the blob
1209+
blob = Blob.objects.create(
1210+
sha256=sha256_hash,
1211+
size=original_size,
1212+
content_type=content_type,
1213+
compression=compression,
1214+
raw_content=compressed_content,
1215+
**kwargs,
1216+
)
1217+
1218+
logger.info(
1219+
"Created blob %s: %d bytes, %s compression, %s content type",
1220+
blob.id,
1221+
original_size,
1222+
compression.label,
1223+
content_type,
1224+
)
1225+
1226+
return blob
1227+
11951228

11961229
class Blob(BaseModel):
11971230
"""
@@ -1231,11 +1264,15 @@ class Blob(BaseModel):
12311264

12321265
mailbox = models.ForeignKey(
12331266
"Mailbox",
1267+
blank=True,
1268+
null=True,
12341269
on_delete=models.CASCADE,
12351270
related_name="blobs",
12361271
help_text=_("Mailbox that owns this blob"),
12371272
)
12381273

1274+
objects = BlobManager()
1275+
12391276
class Meta:
12401277
db_table = "messages_blob"
12411278
verbose_name = _("blob")

src/frontend/src/features/api/gen/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from "./mailboxes/mailboxes";
99
export * from "./mailbox-accesses/mailbox-accesses";
1010
export * from "./maildomains/maildomains";
1111
export * from "./admin-maildomain-user/admin-maildomain-user";
12+
export * from "./message-templates/message-templates";
1213
export * from "./mta/mta";
1314
export * from "./placeholders/placeholders";
1415
export * from "./tasks/tasks";

0 commit comments

Comments
 (0)