Skip to content

Commit 16fdbfb

Browse files
committed
Job to send the new email authentication codes
1 parent 83a3529 commit 16fdbfb

File tree

10 files changed

+166
-56
lines changed

10 files changed

+166
-56
lines changed

crates/email/src/mailer.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ impl Mailer {
110110
fields(
111111
email.to = %to,
112112
email.language = %context.language(),
113-
user.id = %context.user().id,
114113
),
115114
err,
116115
)]

crates/handlers/src/views/password_register.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use mas_matrix::BoxHomeserverConnection;
2424
use mas_policy::Policy;
2525
use mas_router::UrlBuilder;
2626
use mas_storage::{
27-
queue::{ProvisionUserJob, QueueJobRepositoryExt as _, VerifyEmailJob},
27+
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
2828
user::{BrowserSessionRepository, UserEmailRepository, UserPasswordRepository, UserRepository},
2929
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
3030
};
@@ -327,14 +327,6 @@ pub(crate) async fn post(
327327
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
328328
.await?;
329329

330-
repo.queue_job()
331-
.schedule_job(
332-
&mut rng,
333-
&clock,
334-
VerifyEmailJob::new(&user_email).with_language(locale.to_string()),
335-
)
336-
.await?;
337-
338330
repo.queue_job()
339331
.schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user))
340332
.await?;

crates/storage/src/queue/tasks.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// SPDX-License-Identifier: AGPL-3.0-only
44
// Please see LICENSE in the repository root for full details.
55

6-
use mas_data_model::{Device, User, UserEmail, UserRecoverySession};
6+
use mas_data_model::{Device, User, UserEmailAuthentication, UserRecoverySession};
77
use serde::{Deserialize, Serialize};
88
use ulid::Ulid;
99

@@ -17,37 +17,49 @@ pub struct VerifyEmailJob {
1717
}
1818

1919
impl VerifyEmailJob {
20-
/// Create a new job to verify an email address.
20+
/// The ID of the email address to verify.
2121
#[must_use]
22-
pub fn new(user_email: &UserEmail) -> Self {
23-
Self {
24-
user_email_id: user_email.id,
25-
language: None,
26-
}
22+
pub fn user_email_id(&self) -> Ulid {
23+
self.user_email_id
2724
}
25+
}
2826

29-
/// Set the language to use for the email.
27+
impl InsertableJob for VerifyEmailJob {
28+
const QUEUE_NAME: &'static str = "verify-email";
29+
}
30+
31+
/// A job to send an email authentication code to a user.
32+
#[derive(Serialize, Deserialize, Debug, Clone)]
33+
pub struct SendEmailAuthenticationCodeJob {
34+
user_email_authentication_id: Ulid,
35+
language: String,
36+
}
37+
38+
impl SendEmailAuthenticationCodeJob {
39+
/// Create a new job to send an email authentication code to a user.
3040
#[must_use]
31-
pub fn with_language(mut self, language: String) -> Self {
32-
self.language = Some(language);
33-
self
41+
pub fn new(user_email_authentication: &UserEmailAuthentication, language: String) -> Self {
42+
Self {
43+
user_email_authentication_id: user_email_authentication.id,
44+
language,
45+
}
3446
}
3547

3648
/// The language to use for the email.
3749
#[must_use]
38-
pub fn language(&self) -> Option<&str> {
39-
self.language.as_deref()
50+
pub fn language(&self) -> &str {
51+
&self.language
4052
}
4153

42-
/// The ID of the email address to verify.
54+
/// The ID of the email authentication to send the code for.
4355
#[must_use]
44-
pub fn user_email_id(&self) -> Ulid {
45-
self.user_email_id
56+
pub fn user_email_authentication_id(&self) -> Ulid {
57+
self.user_email_authentication_id
4658
}
4759
}
4860

49-
impl InsertableJob for VerifyEmailJob {
50-
const QUEUE_NAME: &'static str = "verify-email";
61+
impl InsertableJob for SendEmailAuthenticationCodeJob {
62+
const QUEUE_NAME: &'static str = "send-email-authentication-code";
5163
}
5264

5365
/// A job to provision the user on the homeserver.

crates/tasks/src/email.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
// Please see LICENSE in the repository root for full details.
66

77
use async_trait::async_trait;
8-
use mas_storage::queue::VerifyEmailJob;
8+
use chrono::Duration;
9+
use mas_email::{Address, EmailVerificationContext, Mailbox};
10+
use mas_storage::queue::{SendEmailAuthenticationCodeJob, VerifyEmailJob};
11+
use mas_templates::TemplateContext as _;
12+
use rand::{distributions::Uniform, Rng};
13+
use tracing::info;
914

1015
use crate::{
1116
new_queue::{JobContext, JobError, RunnableJob},
@@ -27,3 +32,87 @@ impl RunnableJob for VerifyEmailJob {
2732
Err(JobError::fail(anyhow::anyhow!("Not implemented")))
2833
}
2934
}
35+
36+
#[async_trait]
37+
impl RunnableJob for SendEmailAuthenticationCodeJob {
38+
#[tracing::instrument(
39+
name = "job.send_email_authentication_code",
40+
fields(user_email_authentication.id = %self.user_email_authentication_id()),
41+
skip_all,
42+
err,
43+
)]
44+
async fn run(&self, state: &State, _context: JobContext) -> Result<(), JobError> {
45+
let clock = state.clock();
46+
let mailer = state.mailer();
47+
let mut rng = state.rng();
48+
let mut repo = state.repository().await.map_err(JobError::retry)?;
49+
50+
let user_email_authentication = repo
51+
.user_email()
52+
.lookup_authentication(self.user_email_authentication_id())
53+
.await
54+
.map_err(JobError::retry)?
55+
.ok_or(JobError::fail(anyhow::anyhow!(
56+
"User email authentication not found"
57+
)))?;
58+
59+
if user_email_authentication.completed_at.is_some() {
60+
return Err(JobError::fail(anyhow::anyhow!(
61+
"User email authentication already completed"
62+
)));
63+
}
64+
65+
// Load the browser session, if any
66+
let browser_session =
67+
if let Some(browser_session) = user_email_authentication.user_session_id {
68+
Some(
69+
repo.browser_session()
70+
.lookup(browser_session)
71+
.await
72+
.map_err(JobError::retry)?
73+
.ok_or(JobError::fail(anyhow::anyhow!(
74+
"Failed to load browser session"
75+
)))?,
76+
)
77+
} else {
78+
None
79+
};
80+
81+
// Generate a new 6-digit authentication code
82+
let range = Uniform::<u32>::from(0..1_000_000);
83+
let code = rng.sample(range);
84+
let code = format!("{code:06}");
85+
let code = repo
86+
.user_email()
87+
.add_authentication_code(
88+
&mut rng,
89+
&clock,
90+
Duration::minutes(5), // TODO: make this configurable
91+
&user_email_authentication,
92+
code,
93+
)
94+
.await
95+
.map_err(JobError::retry)?;
96+
97+
let address: Address = user_email_authentication
98+
.email
99+
.parse()
100+
.map_err(JobError::fail)?;
101+
let username = browser_session.as_ref().map(|s| s.user.username.clone());
102+
let mailbox = Mailbox::new(username, address);
103+
104+
info!("Sending email verification code to {}", mailbox);
105+
106+
let language = self.language().parse().map_err(JobError::fail)?;
107+
108+
let context = EmailVerificationContext::new(code, browser_session).with_language(language);
109+
mailer
110+
.send_verification_email(mailbox, &context)
111+
.await
112+
.map_err(JobError::fail)?;
113+
114+
repo.save().await.map_err(JobError::fail)?;
115+
116+
Ok(())
117+
}
118+
}

crates/tasks/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 New Vector Ltd.
1+
// Copyright 2024, 2025 New Vector Ltd.
22
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
33
//
44
// SPDX-License-Identifier: AGPL-3.0-only
@@ -125,6 +125,7 @@ pub async fn init(
125125
.register_handler::<mas_storage::queue::ProvisionUserJob>()
126126
.register_handler::<mas_storage::queue::ReactivateUserJob>()
127127
.register_handler::<mas_storage::queue::SendAccountRecoveryEmailsJob>()
128+
.register_handler::<mas_storage::queue::SendEmailAuthenticationCodeJob>()
128129
.register_handler::<mas_storage::queue::SyncDevicesJob>()
129130
.register_handler::<mas_storage::queue::VerifyEmailJob>()
130131
.add_schedule(

crates/templates/src/context.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 New Vector Ltd.
1+
// Copyright 2024, 2025 New Vector Ltd.
22
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
33
//
44
// SPDX-License-Identifier: AGPL-3.0-only
@@ -22,7 +22,8 @@ use mas_data_model::{
2222
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
2323
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
2424
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
25-
UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmail, UserRecoverySession,
25+
UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmail, UserEmailAuthenticationCode,
26+
UserRecoverySession,
2627
};
2728
use mas_i18n::DataLocale;
2829
use mas_iana::jose::JsonWebSignatureAlg;
@@ -877,27 +878,33 @@ impl TemplateContext for EmailRecoveryContext {
877878
/// Context used by the `emails/verification.{txt,html,subject}` templates
878879
#[derive(Serialize)]
879880
pub struct EmailVerificationContext {
880-
user: User,
881-
verification: serde_json::Value,
881+
browser_session: Option<BrowserSession>,
882+
authentication_code: UserEmailAuthenticationCode,
882883
}
883884

884885
impl EmailVerificationContext {
885886
/// Constructs a context for the verification email
886887
#[must_use]
887-
pub fn new(user: User, verification: serde_json::Value) -> Self {
888-
Self { user, verification }
888+
pub fn new(
889+
authentication_code: UserEmailAuthenticationCode,
890+
browser_session: Option<BrowserSession>,
891+
) -> Self {
892+
Self {
893+
browser_session,
894+
authentication_code,
895+
}
889896
}
890897

891898
/// Get the user to which this email is being sent
892899
#[must_use]
893-
pub fn user(&self) -> &User {
894-
&self.user
900+
pub fn user(&self) -> Option<&User> {
901+
self.browser_session.as_ref().map(|s| &s.user)
895902
}
896903

897904
/// Get the verification code being sent
898905
#[must_use]
899-
pub fn verification(&self) -> &serde_json::Value {
900-
&self.verification
906+
pub fn code(&self) -> &str {
907+
&self.authentication_code.code
901908
}
902909
}
903910

@@ -906,11 +913,21 @@ impl TemplateContext for EmailVerificationContext {
906913
where
907914
Self: Sized,
908915
{
909-
User::samples(now, rng)
916+
BrowserSession::samples(now, rng)
910917
.into_iter()
911-
.map(|user| {
912-
let verification = serde_json::json!({"code": "123456"});
913-
Self { user, verification }
918+
.map(|browser_session| {
919+
let authentication_code = UserEmailAuthenticationCode {
920+
id: Ulid::from_datetime_with_source(now.into(), rng),
921+
user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
922+
code: "123456".to_owned(),
923+
created_at: now - Duration::try_minutes(5).unwrap(),
924+
expires_at: now + Duration::try_minutes(25).unwrap(),
925+
};
926+
927+
Self {
928+
browser_session: Some(browser_session),
929+
authentication_code,
930+
}
914931
})
915932
.collect()
916933
}

templates/emails/verification.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{#
2-
Copyright 2024 New Vector Ltd.
2+
Copyright 2024, 2025 New Vector Ltd.
33
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
44

55
SPDX-License-Identifier: AGPL-3.0-only
@@ -8,6 +8,6 @@
88

99
{%- set _ = translator(lang) -%}
1010

11-
{{ _("mas.emails.greeting", username=user.username) }}<br />
11+
{{ _("mas.emails.greeting", username=browser_session.user.username | default("user")) }}<br />
1212
<br />
13-
{{ _("mas.emails.verify.body_html", code=verification.code) }}<br />
13+
{{ _("mas.emails.verify.body_html", code=authentication_code.code) }}<br />
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{#
2-
Copyright 2024 New Vector Ltd.
2+
Copyright 2024, 2025 New Vector Ltd.
33
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
44

55
SPDX-License-Identifier: AGPL-3.0-only
@@ -8,4 +8,4 @@ Please see LICENSE in the repository root for full details.
88

99
{%- set _ = translator(lang) -%}
1010

11-
{{ _("mas.emails.verify.subject", code=verification.code) }}
11+
{{ _("mas.emails.verify.subject", code=authentication_code.code) }}

templates/emails/verification.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{#
2-
Copyright 2024 New Vector Ltd.
2+
Copyright 2024, 2025 New Vector Ltd.
33
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
44

55
SPDX-License-Identifier: AGPL-3.0-only
@@ -8,6 +8,6 @@ Please see LICENSE in the repository root for full details.
88

99
{%- set _ = translator(lang) -%}
1010

11-
{{ _("mas.emails.greeting", username=user.username) }}
11+
{{ _("mas.emails.greeting", username=browser_session.user.username | default("user")) }}
1212

13-
{{ _("mas.emails.verify.body_text", code=verification.code) }}
13+
{{ _("mas.emails.verify.body_text", code=authentication_code.code) }}

translations/en.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@
230230
"emails": {
231231
"greeting": "Hello %(username)s,",
232232
"@greeting": {
233-
"context": "emails/verification.html:11:3-51, emails/verification.txt:11:3-51",
233+
"context": "emails/verification.html:11:3-85, emails/verification.txt:11:3-85",
234234
"description": "Greeting at the top of emails sent to the user"
235235
},
236236
"recovery": {
@@ -262,17 +262,17 @@
262262
"verify": {
263263
"body_html": "Your verification code to confirm this email address is: <strong>%(code)s</strong>",
264264
"@body_html": {
265-
"context": "emails/verification.html:13:3-59",
265+
"context": "emails/verification.html:13:3-66",
266266
"description": "The body of the email sent to verify an email address (HTML)"
267267
},
268268
"body_text": "Your verification code to confirm this email address is: %(code)s",
269269
"@body_text": {
270-
"context": "emails/verification.txt:13:3-59",
270+
"context": "emails/verification.txt:13:3-66",
271271
"description": "The body of the email sent to verify an email address (text)"
272272
},
273273
"subject": "Your email verification code is: %(code)s",
274274
"@subject": {
275-
"context": "emails/verification.subject:11:3-57",
275+
"context": "emails/verification.subject:11:3-64",
276276
"description": "The subject line of the email sent to verify an email address"
277277
}
278278
}

0 commit comments

Comments
 (0)