🐛(mailbox) fix case-sensitive duplicate display names

uniqueness on first name + last name was case-sensitive, which allowed
duplicates
This commit is contained in:
Marie PUPO JEAMMET
2025-10-21 17:24:38 +02:00
committed by Marie
parent 302671bc69
commit 71a7bf688f
5 changed files with 34 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
- 🐛(mailbox) fix case-sensitive duplicates on display names
- ✨(mailbox) synchronize password of newly created mailbox with Dimail's - ✨(mailbox) synchronize password of newly created mailbox with Dimail's
## [1.19.1] - 2025-09-19 ## [1.19.1] - 2025-09-19

View File

@@ -206,7 +206,7 @@ class MailboxAdmin(UserAdmin):
list_display = ("__str__", "domain", "status", "updated_at") list_display = ("__str__", "domain", "status", "updated_at")
list_filter = ("status",) list_filter = ("status",)
search_fields = ("local_part", "domain__name") search_fields = ("local_part", "domain__name", "first_name", "last_name")
readonly_fields = ["updated_at"] readonly_fields = ["updated_at"]
fieldsets = None fieldsets = None

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.7 on 2025-10-21 15:49
import django.db.models.functions.text
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailbox_manager', '0026_alter_mailbox_unique_together_and_more'),
]
operations = [
migrations.RemoveConstraint(
model_name='mailbox',
name='unique_ox_display_name',
),
migrations.AddConstraint(
model_name='mailbox',
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('first_name'), django.db.models.functions.text.Lower('last_name'), models.F('domain'), name='unique_ox_display_name', violation_error_message='Mailbox with this First name, Last name and Domain already exists.'),
),
]

View File

@@ -10,6 +10,7 @@ from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import exceptions, mail, validators from django.core import exceptions, mail, validators
from django.db import models from django.db import models
from django.db.models.functions import Lower
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import get_language, gettext, override from django.utils.translation import get_language, gettext, override
@@ -308,12 +309,17 @@ class Mailbox(AbstractBaseUser, BaseModel):
fields=["local_part", "domain"], name="unique_username" fields=["local_part", "domain"], name="unique_username"
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=["first_name", "last_name", "domain"], Lower("first_name"),
Lower("last_name"),
"domain",
name="unique_ox_display_name", name="unique_ox_display_name",
violation_error_message="Mailbox with this First name, \
Last name and Domain already exists.",
), ),
# Display name in OpenXChange must be unique # Display name in OpenXChange must be unique
# To avoid sending failing requests to dimail, # To avoid sending failing requests to dimail,
# we impose uniqueness here too # we impose uniqueness here too
# And compare to lowercase to enforce case-insensitive uniqueness
] ]
ordering = ["-created_at"] ordering = ["-created_at"]

View File

@@ -77,7 +77,7 @@ def test_api_mailboxes__create_viewer_failure(mailbox_data):
def test_api_mailboxes__create_display_name_must_be_unique(): def test_api_mailboxes__create_display_name_must_be_unique():
"""Primary id on OpenXchange is display name (first_name + last_name). """Primary id on OpenXchange is display name (first_name + last_name).
It needs to be unique on each context. We don't track context info for now It needs to be unique on each context. We don't track context info for now
but can impose uniqueness by domain.""" but can impose case-insensitive uniqueness by domain."""
access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER) access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)
existing_mailbox = factories.MailboxFactory(domain=access.domain) existing_mailbox = factories.MailboxFactory(domain=access.domain)
@@ -86,8 +86,8 @@ def test_api_mailboxes__create_display_name_must_be_unique():
new_mailbox_data = { new_mailbox_data = {
"local_part": "something_else", "local_part": "something_else",
"first_name": existing_mailbox.first_name, "first_name": existing_mailbox.first_name.upper(), # ensure case-insensitivity
"last_name": existing_mailbox.last_name, "last_name": existing_mailbox.last_name.lower(),
} }
response = client.post( response = client.post(
f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/", f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",