Skip to main content

headless_lms_server/domain/
email_ownership_verification.rs

1//! Minting and mailing email-ownership verification codes.
2//!
3//! Signup and every writer of `user_details.email` goes through here, so the code, the mail and the
4//! resend cap cannot drift between them.
5
6use chrono::Duration;
7use headless_lms_models::{
8    email_deliveries,
9    email_templates::{self, EmailTemplateType},
10    user_details, user_email_codes,
11    user_email_codes::UserEmailCodePurpose,
12};
13
14use crate::prelude::*;
15
16/// Language for the automatic sends, which have no UI language to work from.
17pub const FALLBACK_EMAIL_LANGUAGE: &str = "en";
18
19/// Mail-bomb guard, not a quota: how soon after mailing a code we refuse another.
20const MIN_RESEND_INTERVAL_MINUTES: i64 = 2;
21
22/// Wrong guesses one code tolerates before it is retired. Six digits are only safe with a cap.
23pub const MAX_CODE_ATTEMPTS: i32 = 5;
24
25const PURPOSE: UserEmailCodePurpose = UserEmailCodePurpose::EmailOwnershipVerification;
26
27#[derive(Debug, PartialEq, Eq, Clone, Copy)]
28pub enum VerificationEmailOutcome {
29    Queued,
30    AlreadyVerified,
31    /// A code was mailed to this address less than the resend interval ago.
32    RecentlySent,
33}
34
35/// Mails a fresh verification code to the address the account holds now.
36///
37/// Errors when the deployment has no `verify_email_address` template: with the feature switched on by
38/// env var, a missing template is a misconfiguration rather than a dormant state.
39pub async fn queue_verification_email(
40    conn: &mut PgConnection,
41    user_id: Uuid,
42    language: &str,
43) -> anyhow::Result<VerificationEmailOutcome> {
44    if user_details::get_email_verification(conn, user_id)
45        .await?
46        .is_some()
47    {
48        return Ok(VerificationEmailOutcome::AlreadyVerified);
49    }
50
51    // The live code is the record of the last send to the current address: an address change retires
52    // it in the database trigger, so a genuine change can be mailed immediately.
53    let live_code =
54        user_email_codes::get_unused_user_email_code_with_user_id(conn, user_id, PURPOSE).await?;
55    if let Some(live_code) = live_code
56        && Utc::now() - live_code.created_at < Duration::minutes(MIN_RESEND_INTERVAL_MINUTES)
57    {
58        return Ok(VerificationEmailOutcome::RecentlySent);
59    }
60
61    // The lookup falls back to English, so no row means the deployment has no template at all.
62    let template = email_templates::get_generic_email_template_by_type_and_language(
63        conn,
64        EmailTemplateType::VerifyEmailAddress,
65        language,
66    )
67    .await
68    .map_err(|e| {
69        anyhow::anyhow!(
70            "Email ownership verification is enabled but the verify_email_address email template is missing: {}",
71            e.message()
72        )
73    })?;
74
75    let mut tx = conn.begin().await?;
76    let code = user_email_codes::generate_code();
77    user_email_codes::insert_user_email_code(&mut tx, user_id, PURPOSE, &code).await?;
78    // To the account, not to a stored address: the sender resolves the address and looks the code up
79    // at send time, so nothing here holds a copy of either.
80    email_deliveries::insert_email_delivery(&mut tx, user_id, template.id).await?;
81    tx.commit().await?;
82
83    Ok(VerificationEmailOutcome::Queued)
84}
85
86/// Queues the mail without letting a failure propagate: no signup or email change may be rolled back
87/// because the mail queue or the template is unavailable.
88///
89/// `enabled` is [`ApplicationConfiguration::enable_email_ownership_verification`], passed in because
90/// `sync_tmc_users` has no `ApplicationConfiguration` to read it from.
91pub async fn queue_verification_email_best_effort(
92    conn: &mut PgConnection,
93    enabled: bool,
94    user_id: Uuid,
95) {
96    if !enabled {
97        return;
98    }
99    match queue_verification_email(conn, user_id, FALLBACK_EMAIL_LANGUAGE).await {
100        Ok(outcome) => {
101            info!("Email ownership verification mail for {user_id}: {outcome:?}");
102        }
103        Err(e) => {
104            error!("Failed to queue email ownership verification mail for {user_id}: {e}");
105        }
106    }
107}