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);