Skip to content

Commit 80f2cea

Browse files
authored
Make email address lookups case-insensitive (#4763)
2 parents 6661d73 + 0405e95 commit 80f2cea

6 files changed

+38
-14
lines changed

crates/storage-pg/.sqlx/query-f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec.json renamed to crates/storage-pg/.sqlx/query-5eea2f4c3e82ae606b09b8a81332594c97ba0afe972f0fee145b6094789fb6c7.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/storage-pg/.sqlx/query-f7d26de1d380e3e52f47f2b89ed7506e1e4cca72682bc7737e6508dc4015b8d5.json renamed to crates/storage-pg/.sqlx/query-ca093cab5143bb3dded2eda9e82473215f4d3c549ea2c5a4f860a102cc46a667.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- no-transaction
2+
-- Copyright 2025 New Vector Ltd.
3+
--
4+
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
-- Please see LICENSE in the repository root for full details.
6+
7+
-- When we're looking up an email address, we want to be able to do a case-insensitive
8+
-- lookup, so we index the email address lowercase and request it like that
9+
CREATE INDEX CONCURRENTLY
10+
user_emails_lower_email_idx
11+
ON user_emails (LOWER(email));

crates/storage-pg/src/user/email.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use mas_storage::{
1515
user::{UserEmailFilter, UserEmailRepository},
1616
};
1717
use rand::RngCore;
18-
use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def};
18+
use sea_query::{Expr, Func, PostgresQueryBuilder, Query, SimpleExpr, enum_def};
1919
use sea_query_binder::SqlxBinder;
2020
use sqlx::PgConnection;
2121
use ulid::Ulid;
@@ -110,10 +110,13 @@ impl Filter for UserEmailFilter<'_> {
110110
.add_option(self.user().map(|user| {
111111
Expr::col((UserEmails::Table, UserEmails::UserId)).eq(Uuid::from(user.id))
112112
}))
113-
.add_option(
114-
self.email()
115-
.map(|email| Expr::col((UserEmails::Table, UserEmails::Email)).eq(email)),
116-
)
113+
.add_option(self.email().map(|email| {
114+
SimpleExpr::from(Func::lower(Expr::col((
115+
UserEmails::Table,
116+
UserEmails::Email,
117+
))))
118+
.eq(Func::lower(email))
119+
}))
117120
}
118121
}
119122

@@ -175,7 +178,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
175178
, created_at
176179
FROM user_emails
177180
178-
WHERE user_id = $1 AND email = $2
181+
WHERE user_id = $1 AND LOWER(email) = LOWER($2)
179182
"#,
180183
Uuid::from(user.id),
181184
email,
@@ -209,7 +212,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
209212
, email
210213
, created_at
211214
FROM user_emails
212-
WHERE email = $1
215+
WHERE LOWER(email) = LOWER($1)
213216
"#,
214217
email,
215218
)

crates/storage-pg/src/user/tests.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,10 @@ async fn test_user_repo_find_by_username(pool: PgPool) {
268268
async fn test_user_email_repo(pool: PgPool) {
269269
const USERNAME: &str = "john";
270270
const EMAIL: &str = "john@example.com";
271+
// This is what is stored in the database, making sure that:
272+
// 1. we don't normalize the email address when storing it
273+
// 2. looking it up is case-incensitive
274+
const UPPERCASE_EMAIL: &str = "JOHN@EXAMPLE.COM";
271275

272276
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
273277
let mut rng = ChaChaRng::seed_from_u64(42);
@@ -295,12 +299,12 @@ async fn test_user_email_repo(pool: PgPool) {
295299

296300
let user_email = repo
297301
.user_email()
298-
.add(&mut rng, &clock, &user, EMAIL.to_owned())
302+
.add(&mut rng, &clock, &user, UPPERCASE_EMAIL.to_owned())
299303
.await
300304
.unwrap();
301305

302306
assert_eq!(user_email.user_id, user.id);
303-
assert_eq!(user_email.email, EMAIL);
307+
assert_eq!(user_email.email, UPPERCASE_EMAIL);
304308

305309
// Check the counts
306310
assert_eq!(repo.user_email().count(all).await.unwrap(), 1);
@@ -321,7 +325,7 @@ async fn test_user_email_repo(pool: PgPool) {
321325
.expect("user email was not found");
322326

323327
assert_eq!(user_email.user_id, user.id);
324-
assert_eq!(user_email.email, EMAIL);
328+
assert_eq!(user_email.email, UPPERCASE_EMAIL);
325329

326330
// Listing the user emails should work
327331
let emails = repo

crates/storage/src/user/email.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ impl<'a> UserEmailFilter<'a> {
3636
}
3737

3838
/// Filter for emails matching a specific email address
39+
///
40+
/// The email address is case-insensitive
3941
#[must_use]
4042
pub fn for_email(mut self, email: &'a str) -> Self {
4143
self.email = Some(email);
@@ -81,6 +83,8 @@ pub trait UserEmailRepository: Send + Sync {
8183

8284
/// Lookup an [`UserEmail`] by its email address for a [`User`]
8385
///
86+
/// The email address is case-insensitive
87+
///
8488
/// Returns `None` if no matching [`UserEmail`] was found
8589
///
8690
/// # Parameters
@@ -95,6 +99,8 @@ pub trait UserEmailRepository: Send + Sync {
9599

96100
/// Lookup an [`UserEmail`] by its email address
97101
///
102+
/// The email address is case-insensitive
103+
///
98104
/// Returns `None` if no matching [`UserEmail`] was found or if multiple
99105
/// [`UserEmail`] are found
100106
///

0 commit comments

Comments
 (0)