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