diff --git a/CHANGELOG.md b/CHANGELOG.md index 9666e40..756d06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to ## [Unreleased] +- 🐛(mailbox) fix case-sensitive duplicates on display names - ✨(mailbox) synchronize password of newly created mailbox with Dimail's ## [1.19.1] - 2025-09-19 diff --git a/src/backend/mailbox_manager/admin.py b/src/backend/mailbox_manager/admin.py index 1ccfef2..c9f9a93 100644 --- a/src/backend/mailbox_manager/admin.py +++ b/src/backend/mailbox_manager/admin.py @@ -206,7 +206,7 @@ class MailboxAdmin(UserAdmin): list_display = ("__str__", "domain", "status", "updated_at") list_filter = ("status",) - search_fields = ("local_part", "domain__name") + search_fields = ("local_part", "domain__name", "first_name", "last_name") readonly_fields = ["updated_at"] fieldsets = None diff --git a/src/backend/mailbox_manager/migrations/0027_remove_mailbox_unique_ox_display_name_and_more.py b/src/backend/mailbox_manager/migrations/0027_remove_mailbox_unique_ox_display_name_and_more.py new file mode 100644 index 0000000..6a15e1a --- /dev/null +++ b/src/backend/mailbox_manager/migrations/0027_remove_mailbox_unique_ox_display_name_and_more.py @@ -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.'), + ), + ] diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index 3ba754e..86710f4 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -10,6 +10,7 @@ from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.sites.models import Site from django.core import exceptions, mail, validators from django.db import models +from django.db.models.functions import Lower from django.template.loader import render_to_string from django.utils.text import slugify from django.utils.translation import get_language, gettext, override @@ -308,12 +309,17 @@ class Mailbox(AbstractBaseUser, BaseModel): fields=["local_part", "domain"], name="unique_username" ), models.UniqueConstraint( - fields=["first_name", "last_name", "domain"], + Lower("first_name"), + Lower("last_name"), + "domain", 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 # To avoid sending failing requests to dimail, # we impose uniqueness here too + # And compare to lowercase to enforce case-insensitive uniqueness ] ordering = ["-created_at"] diff --git a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py index f2f419f..e9bbc80 100644 --- a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py +++ b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py @@ -77,7 +77,7 @@ def test_api_mailboxes__create_viewer_failure(mailbox_data): def test_api_mailboxes__create_display_name_must_be_unique(): """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 - but can impose uniqueness by domain.""" + but can impose case-insensitive uniqueness by domain.""" access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER) existing_mailbox = factories.MailboxFactory(domain=access.domain) @@ -86,8 +86,8 @@ def test_api_mailboxes__create_display_name_must_be_unique(): new_mailbox_data = { "local_part": "something_else", - "first_name": existing_mailbox.first_name, - "last_name": existing_mailbox.last_name, + "first_name": existing_mailbox.first_name.upper(), # ensure case-insensitivity + "last_name": existing_mailbox.last_name.lower(), } response = client.post( f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",