mas_storage/user/email.rs
1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use async_trait::async_trait;
9use mas_data_model::{
10 BrowserSession, Clock, UpstreamOAuthAuthorizationSession, User, UserEmail,
11 UserEmailAuthentication, UserEmailAuthenticationCode, UserRegistration,
12};
13use rand_core::RngCore;
14use ulid::Ulid;
15
16use crate::{Pagination, pagination::Page, repository_impl};
17
18/// Filter parameters for listing user emails
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
20pub struct UserEmailFilter<'a> {
21 user: Option<&'a User>,
22 email: Option<&'a str>,
23}
24
25impl<'a> UserEmailFilter<'a> {
26 /// Create a new [`UserEmailFilter`] with default values
27 #[must_use]
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 /// Filter for emails of a specific user
33 #[must_use]
34 pub fn for_user(mut self, user: &'a User) -> Self {
35 self.user = Some(user);
36 self
37 }
38
39 /// Filter for emails matching a specific email address
40 ///
41 /// The email address is case-insensitive
42 #[must_use]
43 pub fn for_email(mut self, email: &'a str) -> Self {
44 self.email = Some(email);
45 self
46 }
47
48 /// Get the user filter
49 ///
50 /// Returns [`None`] if no user filter is set
51 #[must_use]
52 pub fn user(&self) -> Option<&User> {
53 self.user
54 }
55
56 /// Get the email filter
57 ///
58 /// Returns [`None`] if no email filter is set
59 #[must_use]
60 pub fn email(&self) -> Option<&str> {
61 self.email
62 }
63}
64
65/// A [`UserEmailRepository`] helps interacting with [`UserEmail`] saved in the
66/// storage backend
67#[async_trait]
68pub trait UserEmailRepository: Send + Sync {
69 /// The error type returned by the repository
70 type Error;
71
72 /// Lookup an [`UserEmail`] by its ID
73 ///
74 /// Returns `None` if no [`UserEmail`] was found
75 ///
76 /// # Parameters
77 ///
78 /// * `id`: The ID of the [`UserEmail`] to lookup
79 ///
80 /// # Errors
81 ///
82 /// Returns [`Self::Error`] if the underlying repository fails
83 async fn lookup(&mut self, id: Ulid) -> Result<Option<UserEmail>, Self::Error>;
84
85 /// Lookup an [`UserEmail`] by its email address for a [`User`]
86 ///
87 /// The email address is case-insensitive
88 ///
89 /// Returns `None` if no matching [`UserEmail`] was found
90 ///
91 /// # Parameters
92 ///
93 /// * `user`: The [`User`] for whom to lookup the [`UserEmail`]
94 /// * `email`: The email address to lookup
95 ///
96 /// # Errors
97 ///
98 /// Returns [`Self::Error`] if the underlying repository fails
99 async fn find(&mut self, user: &User, email: &str) -> Result<Option<UserEmail>, Self::Error>;
100
101 /// Lookup an [`UserEmail`] by its email address
102 ///
103 /// The email address is case-insensitive
104 ///
105 /// Returns `None` if no matching [`UserEmail`] was found or if multiple
106 /// [`UserEmail`] are found
107 ///
108 /// # Parameters
109 /// * `email`: The email address to lookup
110 ///
111 /// # Errors
112 ///
113 /// Returns [`Self::Error`] if the underlying repository fails
114 async fn find_by_email(&mut self, email: &str) -> Result<Option<UserEmail>, Self::Error>;
115
116 /// Get all [`UserEmail`] of a [`User`]
117 ///
118 /// # Parameters
119 ///
120 /// * `user`: The [`User`] for whom to lookup the [`UserEmail`]
121 ///
122 /// # Errors
123 ///
124 /// Returns [`Self::Error`] if the underlying repository fails
125 async fn all(&mut self, user: &User) -> Result<Vec<UserEmail>, Self::Error>;
126
127 /// List [`UserEmail`] with the given filter and pagination
128 ///
129 /// # Parameters
130 ///
131 /// * `filter`: The filter parameters
132 /// * `pagination`: The pagination parameters
133 ///
134 /// # Errors
135 ///
136 /// Returns [`Self::Error`] if the underlying repository fails
137 async fn list(
138 &mut self,
139 filter: UserEmailFilter<'_>,
140 pagination: Pagination,
141 ) -> Result<Page<UserEmail>, Self::Error>;
142
143 /// Count the [`UserEmail`] with the given filter
144 ///
145 /// # Parameters
146 ///
147 /// * `filter`: The filter parameters
148 ///
149 /// # Errors
150 ///
151 /// Returns [`Self::Error`] if the underlying repository fails
152 async fn count(&mut self, filter: UserEmailFilter<'_>) -> Result<usize, Self::Error>;
153
154 /// Create a new [`UserEmail`] for a [`User`]
155 ///
156 /// Returns the newly created [`UserEmail`]
157 ///
158 /// # Parameters
159 ///
160 /// * `rng`: The random number generator to use
161 /// * `clock`: The clock to use
162 /// * `user`: The [`User`] for whom to create the [`UserEmail`]
163 /// * `email`: The email address of the [`UserEmail`]
164 ///
165 /// # Errors
166 ///
167 /// Returns [`Self::Error`] if the underlying repository fails
168 async fn add(
169 &mut self,
170 rng: &mut (dyn RngCore + Send),
171 clock: &dyn Clock,
172 user: &User,
173 email: String,
174 ) -> Result<UserEmail, Self::Error>;
175
176 /// Delete a [`UserEmail`]
177 ///
178 /// # Parameters
179 ///
180 /// * `user_email`: The [`UserEmail`] to delete
181 ///
182 /// # Errors
183 ///
184 /// Returns [`Self::Error`] if the underlying repository fails
185 async fn remove(&mut self, user_email: UserEmail) -> Result<(), Self::Error>;
186
187 /// Delete all [`UserEmail`] with the given filter
188 ///
189 /// Returns the number of deleted [`UserEmail`]s
190 ///
191 /// # Parameters
192 ///
193 /// * `filter`: The filter parameters
194 ///
195 /// # Errors
196 ///
197 /// Returns [`Self::Error`] if the underlying repository fails
198 async fn remove_bulk(&mut self, filter: UserEmailFilter<'_>) -> Result<usize, Self::Error>;
199
200 /// Add a new [`UserEmailAuthentication`] for a [`BrowserSession`]
201 ///
202 /// # Parameters
203 ///
204 /// * `rng`: The random number generator to use
205 /// * `clock`: The clock to use
206 /// * `email`: The email address to add
207 /// * `session`: The [`BrowserSession`] for which to add the
208 /// [`UserEmailAuthentication`]
209 ///
210 /// # Errors
211 ///
212 /// Returns an error if the underlying repository fails
213 async fn add_authentication_for_session(
214 &mut self,
215 rng: &mut (dyn RngCore + Send),
216 clock: &dyn Clock,
217 email: String,
218 session: &BrowserSession,
219 ) -> Result<UserEmailAuthentication, Self::Error>;
220
221 /// Add a new [`UserEmailAuthentication`] for a [`UserRegistration`]
222 ///
223 /// # Parameters
224 ///
225 /// * `rng`: The random number generator to use
226 /// * `clock`: The clock to use
227 /// * `email`: The email address to add
228 /// * `registration`: The [`UserRegistration`] for which to add the
229 /// [`UserEmailAuthentication`]
230 ///
231 /// # Errors
232 ///
233 /// Returns an error if the underlying repository fails
234 async fn add_authentication_for_registration(
235 &mut self,
236 rng: &mut (dyn RngCore + Send),
237 clock: &dyn Clock,
238 email: String,
239 registration: &UserRegistration,
240 ) -> Result<UserEmailAuthentication, Self::Error>;
241
242 /// Add a new [`UserEmailAuthenticationCode`] for a
243 /// [`UserEmailAuthentication`]
244 ///
245 /// # Parameters
246 ///
247 /// * `rng`: The random number generator to use
248 /// * `clock`: The clock to use
249 /// * `duration`: The duration for which the code is valid
250 /// * `authentication`: The [`UserEmailAuthentication`] for which to add the
251 /// [`UserEmailAuthenticationCode`]
252 /// * `code`: The code to add
253 ///
254 /// # Errors
255 ///
256 /// Returns an error if the underlying repository fails or if the code
257 /// already exists for this session
258 async fn add_authentication_code(
259 &mut self,
260 rng: &mut (dyn RngCore + Send),
261 clock: &dyn Clock,
262 duration: chrono::Duration,
263 authentication: &UserEmailAuthentication,
264 code: String,
265 ) -> Result<UserEmailAuthenticationCode, Self::Error>;
266
267 /// Lookup a [`UserEmailAuthentication`]
268 ///
269 /// # Parameters
270 ///
271 /// * `id`: The ID of the [`UserEmailAuthentication`] to lookup
272 ///
273 /// # Errors
274 ///
275 /// Returns an error if the underlying repository fails
276 async fn lookup_authentication(
277 &mut self,
278 id: Ulid,
279 ) -> Result<Option<UserEmailAuthentication>, Self::Error>;
280
281 /// Find a [`UserEmailAuthenticationCode`] by its code and session
282 ///
283 /// # Parameters
284 ///
285 /// * `authentication`: The [`UserEmailAuthentication`] to find the code for
286 /// * `code`: The code of the [`UserEmailAuthentication`] to lookup
287 ///
288 /// # Errors
289 ///
290 /// Returns an error if the underlying repository fails
291 async fn find_authentication_code(
292 &mut self,
293 authentication: &UserEmailAuthentication,
294 code: &str,
295 ) -> Result<Option<UserEmailAuthenticationCode>, Self::Error>;
296
297 /// Complete a [`UserEmailAuthentication`] by using the given code
298 ///
299 /// Returns the completed [`UserEmailAuthentication`]
300 ///
301 /// # Parameters
302 ///
303 /// * `clock`: The clock to use to generate timestamps
304 /// * `authentication`: The [`UserEmailAuthentication`] to complete
305 /// * `code`: The [`UserEmailAuthenticationCode`] to use
306 ///
307 /// # Errors
308 ///
309 /// Returns an error if the underlying repository fails
310 async fn complete_authentication_with_code(
311 &mut self,
312 clock: &dyn Clock,
313 authentication: UserEmailAuthentication,
314 code: &UserEmailAuthenticationCode,
315 ) -> Result<UserEmailAuthentication, Self::Error>;
316
317 /// Complete a [`UserEmailAuthentication`] by using the given upstream oauth
318 /// authorization session
319 ///
320 /// Returns the completed [`UserEmailAuthentication`]
321 ///
322 /// # Parameters
323 ///
324 /// * `clock`: The clock to use to generate timestamps
325 /// * `authentication`: The [`UserEmailAuthentication`] to complete
326 /// * `upstream_oauth_authorization_session`: The
327 /// [`UpstreamOAuthAuthorizationSession`] to use
328 ///
329 /// # Errors
330 ///
331 /// Returns an error if the underlying repository fails
332 async fn complete_authentication_with_upstream(
333 &mut self,
334 clock: &dyn Clock,
335 authentication: UserEmailAuthentication,
336 upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
337 ) -> Result<UserEmailAuthentication, Self::Error>;
338
339 /// Cleanup old email authentications
340 ///
341 /// This will delete email authentications with IDs up to and including
342 /// `until`. Uses ULID cursor-based pagination for efficiency.
343 /// Authentication codes will cascade-delete automatically.
344 ///
345 /// Returns the number of authentications deleted and the cursor for the
346 /// next batch
347 ///
348 /// # Parameters
349 ///
350 /// * `since`: The cursor to start from (exclusive), or `None` to start from
351 /// the beginning
352 /// * `until`: The maximum ULID to delete (inclusive upper bound)
353 /// * `limit`: The maximum number of authentications to delete in this batch
354 ///
355 /// # Errors
356 ///
357 /// Returns [`Self::Error`] if the underlying repository fails
358 async fn cleanup_authentications(
359 &mut self,
360 since: Option<Ulid>,
361 until: Ulid,
362 limit: usize,
363 ) -> Result<(usize, Option<Ulid>), Self::Error>;
364}
365
366repository_impl!(UserEmailRepository:
367 async fn lookup(&mut self, id: Ulid) -> Result<Option<UserEmail>, Self::Error>;
368 async fn find(&mut self, user: &User, email: &str) -> Result<Option<UserEmail>, Self::Error>;
369 async fn find_by_email(&mut self, email: &str) -> Result<Option<UserEmail>, Self::Error>;
370
371 async fn all(&mut self, user: &User) -> Result<Vec<UserEmail>, Self::Error>;
372 async fn list(
373 &mut self,
374 filter: UserEmailFilter<'_>,
375 pagination: Pagination,
376 ) -> Result<Page<UserEmail>, Self::Error>;
377 async fn count(&mut self, filter: UserEmailFilter<'_>) -> Result<usize, Self::Error>;
378
379 async fn add(
380 &mut self,
381 rng: &mut (dyn RngCore + Send),
382 clock: &dyn Clock,
383 user: &User,
384 email: String,
385 ) -> Result<UserEmail, Self::Error>;
386 async fn remove(&mut self, user_email: UserEmail) -> Result<(), Self::Error>;
387
388 async fn remove_bulk(&mut self, filter: UserEmailFilter<'_>) -> Result<usize, Self::Error>;
389
390 async fn add_authentication_for_session(
391 &mut self,
392 rng: &mut (dyn RngCore + Send),
393 clock: &dyn Clock,
394 email: String,
395 session: &BrowserSession,
396 ) -> Result<UserEmailAuthentication, Self::Error>;
397
398 async fn add_authentication_for_registration(
399 &mut self,
400 rng: &mut (dyn RngCore + Send),
401 clock: &dyn Clock,
402 email: String,
403 registration: &UserRegistration,
404 ) -> Result<UserEmailAuthentication, Self::Error>;
405
406 async fn add_authentication_code(
407 &mut self,
408 rng: &mut (dyn RngCore + Send),
409 clock: &dyn Clock,
410 duration: chrono::Duration,
411 authentication: &UserEmailAuthentication,
412 code: String,
413 ) -> Result<UserEmailAuthenticationCode, Self::Error>;
414
415 async fn lookup_authentication(
416 &mut self,
417 id: Ulid,
418 ) -> Result<Option<UserEmailAuthentication>, Self::Error>;
419
420 async fn find_authentication_code(
421 &mut self,
422 authentication: &UserEmailAuthentication,
423 code: &str,
424 ) -> Result<Option<UserEmailAuthenticationCode>, Self::Error>;
425
426 async fn complete_authentication_with_code(
427 &mut self,
428 clock: &dyn Clock,
429 authentication: UserEmailAuthentication,
430 code: &UserEmailAuthenticationCode,
431 ) -> Result<UserEmailAuthentication, Self::Error>;
432
433 async fn complete_authentication_with_upstream(
434 &mut self,
435 clock: &dyn Clock,
436 authentication: UserEmailAuthentication,
437 upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
438 ) -> Result<UserEmailAuthentication, Self::Error>;
439
440 async fn cleanup_authentications(
441 &mut self,
442 since: Option<Ulid>,
443 until: Ulid,
444 limit: usize,
445 ) -> Result<(usize, Option<Ulid>), Self::Error>;
446);