From aaa9b27c6112a8c20f7c378655e783fbcfd79197 Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Tue, 10 Feb 2026 22:45:15 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8(email)=20we=20should=20ignore=20ca?= =?UTF-8?q?se=20when=20looking=20for=20existing=20emails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our products mostly rely on email regardless of their case. Next step would be to normalize email to lower case when storing them in database (or sending them to dimail if not done yet). --- CHANGELOG.md | 1 + src/backend/core/models.py | 2 +- .../core/tests/test_models_invitations.py | 13 +++++++ .../mailbox_manager/api/client/viewsets.py | 2 +- .../test_api_domain_invitations_create.py | 37 +++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd0c2d..61b180a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to ### Changed +- 🚸(email) we should ignore case when looking for existing emails #1056 - 🏗️(core) migrate from pip to uv - ✨(front) add show invitations mails domains access #1040 diff --git a/src/backend/core/models.py b/src/backend/core/models.py index cf8ccf1..33a25af 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -998,7 +998,7 @@ class BaseInvitation(BaseModel): super().clean() # Check if a user already exists for the provided email - if User.objects.filter(email=self.email).exists(): + if User.objects.filter(email__iexact=self.email).exists(): raise EmailAlreadyKnownException @property diff --git a/src/backend/core/tests/test_models_invitations.py b/src/backend/core/tests/test_models_invitations.py index 936f3c8..73fe414 100644 --- a/src/backend/core/tests/test_models_invitations.py +++ b/src/backend/core/tests/test_models_invitations.py @@ -17,6 +17,8 @@ from freezegun import freeze_time from core import factories, models +from mailbox_manager.exceptions import EmailAlreadyKnownException + pytestmark = pytest.mark.django_db @@ -48,6 +50,17 @@ def test_models_invitations_team_required(): factories.InvitationFactory(team=None) +def test_models_invitations_email_case_insensitive_duplicate_check(): + """The email validation should be case-insensitive when checking for existing users.""" + # Create a user with a lowercase email + factories.UserFactory(email="john.doe@example.com") + + # Try to create an invitation with different case + # This should raise the same exception as if the email was exactly the same + with pytest.raises(EmailAlreadyKnownException): + factories.InvitationFactory(email="John.Doe@Example.COM") + + def test_models_invitations_team_should_be_team_instance(): """The "team" field should be a team instance.""" with pytest.raises(ValueError, match='Invitation.team" must be a "Team" instance'): diff --git a/src/backend/mailbox_manager/api/client/viewsets.py b/src/backend/mailbox_manager/api/client/viewsets.py index be30d71..f032dc5 100644 --- a/src/backend/mailbox_manager/api/client/viewsets.py +++ b/src/backend/mailbox_manager/api/client/viewsets.py @@ -419,7 +419,7 @@ class MailDomainInvitationViewset( try: return super().create(request, *args, **kwargs) except EmailAlreadyKnownException as exc: - user = models.User.objects.get(email=email) + user = models.User.objects.get(email__iexact=email) models.MailDomainAccess.objects.create( user=user, diff --git a/src/backend/mailbox_manager/tests/api/invitations/test_api_domain_invitations_create.py b/src/backend/mailbox_manager/tests/api/invitations/test_api_domain_invitations_create.py index 9799599..8514d17 100644 --- a/src/backend/mailbox_manager/tests/api/invitations/test_api_domain_invitations_create.py +++ b/src/backend/mailbox_manager/tests/api/invitations/test_api_domain_invitations_create.py @@ -158,3 +158,40 @@ def test_api_domain_invitations__inviting_known_email_should_create_access(): assert not models.MailDomainInvitation.objects.exists() assert models.MailDomainAccess.objects.filter(user=existing_user).exists() + + +def test_api_domain_invitations__inviting_known_email_case_insensitive(): + """Email matching should be case-insensitive when creating access for existing users.""" + # Create a user with lowercase email + existing_user = core_factories.UserFactory(email="john.doe@example.com") + access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER) + + client = APIClient() + client.force_login(access.user) + + # Try to invite with same email different case - should create access for existing user + response = client.post( + f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/", + { + "email": "John.Doe@Example.COM", + "role": "owner", + }, + format="json", + ) + assert response.status_code == status.HTTP_201_CREATED + assert ( + response.json()["detail"] + == "Email already known. Invitation not sent but access created instead." + ) + + # No invitation should be created + assert not models.MailDomainInvitation.objects.exists() + + # Access should be created for the existing user (not a new user) + assert models.MailDomainAccess.objects.filter(user=existing_user).exists() + assert models.MailDomainAccess.objects.filter( + user=existing_user, domain=access.domain + ).exists() + + # Ensure only one user exists (no duplicate user created) + assert models.User.objects.filter(email__iexact="john.doe@example.com").count() == 1