Skip to content

Commit 4bb94eb

Browse files
committed
feat(account): Store emails lower case, treat case insensitive
1 parent 5b3a8c4 commit 4bb94eb

File tree

6 files changed

+70
-21
lines changed

6 files changed

+70
-21
lines changed

ChangeLog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
- New providers: TikTok, Lichess.
66

7+
- Starting since version 0.62.0, new email addresses are always stored as lower
8+
case. In this version, we take the final step and also convert existing data
9+
to lower case, alter the database indices and perform lookups
10+
accordingly. Migrations are in place. For rationale, see the note about email
11+
case sensitivity in the documentation.
12+
713

814
0.62.1 (2024-04-24)
915
*******************

allauth/account/managers.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import functools
21
from datetime import timedelta
32

43
from django.db import models
@@ -53,7 +52,7 @@ def add_new_email(self, request, user, email):
5352
def add_email(self, request, user, email, confirm=False, signup=False):
5453
email = email.lower()
5554
email_address, created = self.get_or_create(
56-
user=user, email__iexact=email, defaults={"email": email}
55+
user=user, email=email, defaults={"email": email}
5756
)
5857

5958
if created and confirm:
@@ -84,7 +83,7 @@ def get_users_for(self, email):
8483
# this is a list rather than a generator because we probably want to
8584
# do a len() on it right away
8685
return [
87-
address.user for address in self.filter(verified=True, email__iexact=email)
86+
address.user for address in self.filter(verified=True, email=email.lower())
8887
]
8988

9089
def fill_cache_for_user(self, user, addresses):
@@ -101,26 +100,22 @@ def get_for_user(self, user, email):
101100
addresses = getattr(user, cache_key, None)
102101
email = email.lower()
103102
if addresses is None:
104-
ret = self.get(user=user, email__iexact=email)
103+
ret = self.get(user=user, email=email)
105104
# To avoid additional lookups when e.g.
106105
# EmailAddress.set_as_primary() starts touching self.user
107106
ret.user = user
108107
return ret
109108
else:
110109
for address in addresses:
111-
if address.email.lower() == email.lower():
110+
if address.email == email:
112111
return address
113112
raise self.model.DoesNotExist()
114113

115114
def is_verified(self, email):
116-
return self.filter(email__iexact=email, verified=True).exists()
115+
return self.filter(email=email.lower(), verified=True).exists()
117116

118117
def lookup(self, emails):
119-
q_list = [Q(email__iexact=e) for e in emails]
120-
if not q_list:
121-
return self.none()
122-
q = functools.reduce(lambda a, b: a | b, q_list)
123-
return self.filter(q)
118+
return self.filter(email__in=[e.lower() for e in emails])
124119

125120

126121
class EmailConfirmationManager(models.Manager):
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django.conf import settings
2+
from django.db import migrations
3+
from django.db.models.functions import Lower
4+
5+
from allauth.account import app_settings
6+
7+
8+
def forwards(apps, schema_editor):
9+
EmailAddress = apps.get_model("account.EmailAddress")
10+
User = apps.get_model(settings.AUTH_USER_MODEL)
11+
EmailAddress.objects.all().exclude(email=Lower("email")).update(
12+
email=Lower("email")
13+
)
14+
email_field = app_settings.USER_MODEL_EMAIL_FIELD
15+
if email_field:
16+
User.objects.all().exclude(**{email_field: Lower(email_field)}).update(
17+
**{email_field: Lower(email_field)}
18+
)
19+
20+
21+
class Migration(migrations.Migration):
22+
dependencies = [
23+
("account", "0005_emailaddress_idx_upper_email"),
24+
]
25+
26+
operations = [migrations.RunPython(forwards)]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.conf import settings
2+
from django.db import migrations, models
3+
4+
5+
EMAIL_MAX_LENGTH = getattr(settings, "ACCOUNT_EMAIL_MAX_LENGTH", 254)
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("account", "0006_emailaddress_lower"),
11+
]
12+
13+
operations = [
14+
migrations.RemoveIndex(
15+
model_name="emailaddress",
16+
name="account_emailaddress_upper",
17+
),
18+
migrations.AlterField(
19+
model_name="emailaddress",
20+
name="email",
21+
field=models.EmailField(
22+
db_index=True, max_length=EMAIL_MAX_LENGTH, verbose_name="email address"
23+
),
24+
),
25+
]

allauth/account/models.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
from django.contrib.auth import get_user_model
55
from django.core import signing
66
from django.db import models
7-
from django.db.models import Index, Q
7+
from django.db.models import Q
88
from django.db.models.constraints import UniqueConstraint
9-
from django.db.models.functions import Upper
109
from django.utils import timezone
1110
from django.utils.translation import gettext_lazy as _
1211

@@ -22,6 +21,7 @@ class EmailAddress(models.Model):
2221
on_delete=models.CASCADE,
2322
)
2423
email = models.EmailField(
24+
db_index=True,
2525
max_length=app_settings.EMAIL_MAX_LENGTH,
2626
verbose_name=_("email address"),
2727
)
@@ -42,7 +42,6 @@ class Meta:
4242
condition=Q(verified=True),
4343
)
4444
]
45-
indexes = [Index(Upper("email"), name="account_emailaddress_upper")]
4645

4746
def __str__(self):
4847
return self.email
@@ -58,7 +57,7 @@ def can_set_verified(self):
5857
if app_settings.UNIQUE_EMAIL:
5958
conflict = (
6059
EmailAddress.objects.exclude(pk=self.pk)
61-
.filter(verified=True, email__iexact=self.email)
60+
.filter(verified=True, email=self.email)
6261
.exists()
6362
)
6463
return not conflict

allauth/account/utils.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,7 @@ def sync_user_email_addresses(user):
385385
from .models import EmailAddress
386386

387387
email = user_email(user)
388-
if (
389-
email
390-
and not EmailAddress.objects.filter(user=user, email__iexact=email).exists()
391-
):
388+
if email and not EmailAddress.objects.filter(user=user, email=email).exists():
392389
# get_or_create() to gracefully handle races
393390
EmailAddress.objects.get_or_create(
394391
user=user, email=email, defaults={"primary": False, "verified": False}
@@ -431,7 +428,8 @@ def filter_users_by_email(email, is_active=None, prefer_verified=False):
431428
from .models import EmailAddress
432429

433430
User = get_user_model()
434-
mails = EmailAddress.objects.filter(email__iexact=email).select_related("user")
431+
email = email.lower()
432+
mails = EmailAddress.objects.filter(email=email).select_related("user")
435433
mails = list(mails)
436434
is_verified = False
437435
if prefer_verified:
@@ -444,7 +442,7 @@ def filter_users_by_email(email, is_active=None, prefer_verified=False):
444442
if _unicode_ci_compare(e.email, email):
445443
users.append(e.user)
446444
if app_settings.USER_MODEL_EMAIL_FIELD and not is_verified:
447-
q_dict = {app_settings.USER_MODEL_EMAIL_FIELD + "__iexact": email}
445+
q_dict = {app_settings.USER_MODEL_EMAIL_FIELD: email}
448446
user_qs = User.objects.filter(**q_dict)
449447
for user in user_qs.iterator():
450448
user_email = getattr(user, app_settings.USER_MODEL_EMAIL_FIELD)

0 commit comments

Comments
 (0)