diff --git a/Cargo.lock b/Cargo.lock index fcdc270ece..a74a10a621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,18 +558,18 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfields" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d866f92dc1574aa8da443eacb06ad8fbe4056dbc1b7c3aae508cbccd46c7e706" +checksum = "ef6e59298da389bc0649c7463856b34c6e17fe542f88939426ede4436c6b1195" dependencies = [ "bitfields-impl", ] [[package]] name = "bitfields-impl" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09459e6af3016ea58af8332e31d5da117d33a621bad7019355eefccc4a567d4" +checksum = "f2c044f98f86f15414668d6c8187c7e4fadab1ad2b31680f648703e0fe07c555" dependencies = [ "proc-macro2", "quote", @@ -6451,18 +6451,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" dependencies = [ "indexmap 2.13.0", "toml_datetime", @@ -6472,9 +6472,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ "winnow", ] diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index c83048f292..8a7175d839 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -867,7 +867,7 @@ Sent by Defguard {{ defguard_version }} Star us on GitHub! https://github.com/defguard/defguard\ "; - pub static WELCOME_EMAIL_SUBJECT: &str = "[defguard] Welcome message after enrollment"; + pub static WELCOME_EMAIL_SUBJECT: &str = "Defguard: Welcome message after enrollment"; } #[cfg(test)] diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index 6509b9e5f7..4dc04df95c 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -585,14 +585,17 @@ impl User { /// Select all users without sensitive data. // FIXME: Remove it when Model macro will support SecretString - pub async fn all_without_sensitive_data(pool: &PgPool) -> sqlx::Result> { + pub async fn all_without_sensitive_data<'e, E>(executor: E) -> sqlx::Result> + where + E: PgExecutor<'e>, + { let users = query!( "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, \ mfa_method \"mfa_method: MFAMethod\", password_hash, is_active, openid_sub, \ from_ldap, ldap_pass_randomized, ldap_rdn \ FROM \"user\"" ) - .fetch_all(pool) + .fetch_all(executor) .await?; let res = users .iter() diff --git a/crates/defguard_core/src/db/models/enrollment.rs b/crates/defguard_core/src/db/models/enrollment.rs index 76b5b1c0f0..083f89da62 100644 --- a/crates/defguard_core/src/db/models/enrollment.rs +++ b/crates/defguard_core/src/db/models/enrollment.rs @@ -3,16 +3,13 @@ use defguard_common::{ VERSION, db::{ Id, - models::{Settings, settings::defaults::WELCOME_EMAIL_SUBJECT, user::User}, + models::{Settings, user::User}, }, random::gen_alphanumeric, types::UrlParseError, }; -use defguard_mail::{ - Mail, - templates::{self, TemplateError, safe_tera}, -}; -use sqlx::{PgConnection, PgExecutor, PgPool, Transaction, query, query_as}; +use defguard_mail::templates; +use sqlx::{PgConnection, PgExecutor, PgPool, query, query_as}; use tera::Context; use thiserror::Error; use tonic::{Code, Status}; @@ -49,7 +46,7 @@ pub enum TokenError { #[error(transparent)] TemplateErrorInternal(#[from] tera::Error), #[error(transparent)] - TemplateError(#[from] TemplateError), + TemplateError(#[from] templates::TemplateError), #[error(transparent)] UrlParseError(#[from] UrlParseError), } @@ -84,7 +81,7 @@ impl From for Status { pub struct Token { pub id: String, pub user_id: Id, - pub admin_id: Option, + pub admin_id: Option, pub email: Option, pub created_at: NaiveDateTime, pub expires_at: NaiveDateTime, @@ -121,7 +118,8 @@ impl Token { E: PgExecutor<'e>, { query!( - "INSERT INTO token (id, user_id, admin_id, email, created_at, expires_at, used_at, token_type, device_id) \ + "INSERT INTO token (id, user_id, admin_id, email, created_at, expires_at, used_at, \ + token_type, device_id) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", self.id, self.user_id, @@ -321,15 +319,15 @@ impl Token { /// - admin_phone pub(crate) async fn get_welcome_message_context( &self, - transaction: &mut PgConnection, + conn: &mut PgConnection, ) -> Result { debug!( "Preparing welcome message context for enrollment token {}", self.id ); - let user = self.fetch_user(&mut *transaction).await?; - let admin = self.fetch_admin(&mut *transaction).await?; + let user = self.fetch_user(&mut *conn).await?; + let admin = self.fetch_admin(&mut *conn).await?; let url = Settings::url()?; let mut context = Context::new(); context.insert("first_name", &user.first_name); @@ -352,107 +350,40 @@ impl Token { // to be displayed on final enrollment page pub async fn get_welcome_page_content( &self, - transaction: &mut PgConnection, + conn: &mut PgConnection, ) -> Result { let settings = Settings::get_current_settings(); // load configured content as template - let mut tera = safe_tera(); + let mut tera = templates::safe_tera(); tera.add_raw_template("welcome_page", &enrollment_welcome_message(&settings)?)?; - let context = self.get_welcome_message_context(&mut *transaction).await?; + let context = self.get_welcome_message_context(&mut *conn).await?; Ok(tera.render("welcome_page", &context)?) } - // Render welcome email content - pub(crate) async fn get_welcome_email_content( + /// Send configured welcome email to a user after finishing enrollment. + pub async fn send_welcome_email( &self, - transaction: &mut PgConnection, + conn: &mut PgConnection, + user: &User, ip_address: &str, device_info: Option<&str>, - ) -> Result { + ) -> Result<(), TokenError> { + debug!("Sending welcome mail to {}", user.username); let settings = Settings::get_current_settings(); // load configured content as template - let mut tera = safe_tera(); + let mut tera = templates::safe_tera(); tera.add_raw_template("welcome_email", &enrollment_welcome_email(&settings)?)?; - let context = self.get_welcome_message_context(&mut *transaction).await?; + let context = self.get_welcome_message_context(conn).await?; let content = tera.render("welcome_email", &context)?; - Ok(templates::enrollment_welcome_mail( - &content, - Some(ip_address), - device_info, - )?) - } - - // Send configured welcome email to user after finishing enrollment - pub async fn send_welcome_email( - &self, - transaction: &mut Transaction<'_, sqlx::Postgres>, - user: &User, - settings: &Settings, - ip_address: &str, - device_info: Option<&str>, - ) -> Result<(), TokenError> { - debug!("Sending welcome mail to {}", user.username); - let mail = Mail::new( - &user.email, - settings - .enrollment_welcome_email_subject - .as_deref() - .unwrap_or(WELCOME_EMAIL_SUBJECT), - self.get_welcome_email_content(&mut *transaction, ip_address, device_info) - .await?, - ); - match mail.send().await { - Ok(()) => { - info!("Sent enrollment welcome mail to {}", user.username); - Ok(()) - } - Err(err) => { - error!("Error sending welcome mail: {err}"); - Err(TokenError::NotificationError(err.to_string())) - } - } - } + templates::enrollment_welcome_mail(&user.email, &content, Some(ip_address), device_info)?; - // Notify admin that a user has completed enrollment - pub async fn send_admin_notification( - admin: &User, - user: &User, - ip_address: &str, - device_info: Option<&str>, - ) -> Result<(), TokenError> { - debug!( - "Sending enrollment success notification for user {} to {}", - user.username, admin.username - ); - let mail = Mail::new( - &admin.email, - "[defguard] User enrollment completed", - templates::enrollment_admin_notification( - &user.into(), - &admin.into(), - ip_address, - device_info, - )?, - ); - match mail.send().await { - Ok(()) => { - info!( - "Sent enrollment success notification for user {} to {}", - user.username, admin.username - ); - Ok(()) - } - Err(err) => { - error!("Error sending welcome mail: {err}"); - Err(TokenError::NotificationError(err.to_string())) - } - } + Ok(()) } } diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index 1067667439..6d00bfcbae 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -126,10 +126,11 @@ impl From for WebError { match error { DeviceError::PubkeyConflict(..) => Self::PubkeyValidation(error.to_string()), DeviceError::DatabaseError(_) => Self::DbError(error.to_string()), - DeviceError::NetworkIpAssignmentError(_) => Self::ModelError(error.to_string()), DeviceError::Unexpected(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR), DeviceError::NetworkFull(_) => Self::NetworkFull(error.to_string()), - DeviceError::ModelError(_) => Self::ModelError(error.to_string()), + DeviceError::NetworkIpAssignmentError(_) | DeviceError::ModelError(_) => { + Self::ModelError(error.to_string()) + } } } } diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs index 4768a472d2..0e2a774c45 100644 --- a/crates/defguard_core/src/handlers/auth.rs +++ b/crates/defguard_core/src/handlers/auth.rs @@ -20,7 +20,7 @@ use defguard_common::{ }, types::user_info::UserInfo, }; -use defguard_mail::templates::mfa_code_mail; +use defguard_mail::templates::{mfa_activation_mail, mfa_code_mail, mfa_configured_mail}; use sqlx::{PgPool, types::Uuid}; use time::Duration; use uaparser::Parser; @@ -40,11 +40,7 @@ use crate::{ enterprise::ldap::{error::LdapError, utils::login_through_ldap}, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, - handlers::{ - SIGN_IN_COOKIE_NAME, cookie_domain, - mail::{send_email_mfa_activation_email, send_mfa_configured_email}, - user_for_admin_or_self, - }, + handlers::{SIGN_IN_COOKIE_NAME, cookie_domain, user_for_admin_or_self}, headers::{USER_AGENT_PARSER, check_new_device_login, get_user_agent_device}, server_config, }; @@ -489,9 +485,16 @@ pub async fn webauthn_finish( .save(&appstate.pool) .await?; if user.mfa_method == MFAMethod::None { - send_mfa_configured_email(Some(&session.session.into()), &user, &MFAMethod::Webauthn)?; - user.set_mfa_method(&appstate.pool, MFAMethod::Webauthn) - .await?; + let mut conn = appstate.pool.begin().await?; + mfa_configured_mail( + &user.email, + &mut conn, + Some(&session.session.into()), + &MFAMethod::Webauthn, + ) + .await?; + user.set_mfa_method(&mut *conn, MFAMethod::Webauthn).await?; + conn.commit().await?; } info!("Finished Webauthn registration for user {}", user.username); @@ -641,16 +644,20 @@ pub async fn totp_enable( let mut user = session.user; debug!("Enabling TOTP for user {}", user.username); if user.verify_totp_code(&data.code) { - let recovery_codes = RecoveryCodes::new(user.get_recovery_codes(&appstate.pool).await?); - user.enable_totp(&appstate.pool).await?; + let mut conn = appstate.pool.begin().await?; + let recovery_codes = RecoveryCodes::new(user.get_recovery_codes(&mut *conn).await?); + user.enable_totp(&mut *conn).await?; if user.mfa_method == MFAMethod::None { - send_mfa_configured_email( + mfa_configured_mail( + &user.email, + &mut conn, Some(&session.session.into()), - &user, &MFAMethod::OneTimePassword, - )?; - user.set_mfa_method(&appstate.pool, MFAMethod::OneTimePassword) + ) + .await?; + user.set_mfa_method(&mut *conn, MFAMethod::OneTimePassword) .await?; + conn.commit().await?; } info!("Enabled TOTP for user {}", user.username); @@ -790,8 +797,19 @@ pub async fn email_mfa_init(session: SessionInfo, State(appstate): State ApiResponse { - error!("Error sending mail to {to}, subject: {subject}, error: {error}"); - ApiResponse::new( - json!({"error": error.to_string()}), - StatusCode::INTERNAL_SERVER_ERROR, - ) -} - -pub async fn test_mail( +pub(crate) async fn test_mail( _admin: AdminRole, session: SessionInfo, + State(appstate): State, Json(data): Json, ) -> ApiResult { debug!( @@ -70,22 +39,15 @@ pub async fn test_mail( session.user.username, data.to ); - let result = Mail::new( - &data.to, - TEST_MAIL_SUBJECT, - templates::test_mail(Some(&session.session.into()))?, - ) - .send() - .await; + let mut conn = appstate.pool.begin().await?; + templates::test_mail(&data.to, &mut conn, Some(&session.session.into())).await?; - let (to, subject) = (&data.to, TEST_MAIL_SUBJECT); - match result { - Ok(()) => { - info!("User {} sent test mail to {to}", session.user.username); - Ok(ApiResponse::with_status(StatusCode::OK)) - } - Err(err) => Ok(internal_error(to, subject, &err)), - } + info!( + "User {} sent test mail to {}", + session.user.username, data.to + ); + + Ok(ApiResponse::with_status(StatusCode::OK)) } async fn read_logs() -> String { @@ -108,13 +70,11 @@ pub async fn send_support_data( session: SessionInfo, State(appstate): State, ) -> ApiResult { - debug!( - "User {} sending support mail to {SUPPORT_EMAIL_ADDRESS}", - session.user.username - ); + debug!("User {} sending support mail", session.user.username); - let proxies = Proxy::all(&appstate.pool).await?; - let gateways = Gateway::all(&appstate.pool).await?; + let mut conn = appstate.pool.begin().await?; + let proxies = Proxy::all(&mut *conn).await?; + let gateways = Gateway::all(&mut *conn).await?; let components_info = json!({ "proxies": proxies.iter().map(|p| json!({ @@ -135,40 +95,37 @@ pub async fn send_support_data( "connected_at": g.connected_at, })).collect::>(), }); - + let now = Utc::now(); let components_json = serde_json::to_vec(&components_info).unwrap_or(b"JSON formatting error".into()); - - let components = Attachment::new( - format!("defguard-components-{}.json", Utc::now()), - components_json, - ); - - let config = dump_config(&appstate.pool).await; + let components = Attachment::new(format!("defguard-components-{now}.json"), components_json); + let config = dump_config(&mut conn) + .await + .unwrap_or(json!({"err": "Failed to dump configuration"})); let config = serde_json::to_vec_pretty(&config).unwrap_or(b"JSON formatting error".into()); - let config = Attachment::new(format!("defguard-support-data-{}.json", Utc::now()), config); + let config = Attachment::new(format!("defguard-support-data-{now}.json"), config); let logs = read_logs().await; - let logs = Attachment::new(format!("defguard-logs-{}.txt", Utc::now()), logs.into()); - let result = Mail::new( + let logs = Attachment::new(format!("defguard-logs-{now}.txt"), logs.into()); + + let result = templates::support_data_mail( SUPPORT_EMAIL_ADDRESS, - SUPPORT_EMAIL_SUBJECT, - support_data_mail()?, + &mut conn, + vec![components, config, logs], ) - .set_attachments(vec![components, config, logs]) - .send() .await; - - let (to, subject) = (SUPPORT_EMAIL_ADDRESS, SUPPORT_EMAIL_SUBJECT); - match result { + Ok(match result { Ok(()) => { - info!( - "User {} sent support mail to {SUPPORT_EMAIL_ADDRESS}", - session.user.username - ); - Ok(ApiResponse::with_status(StatusCode::OK)) + info!("User {} sent support mail", session.user.username); + ApiResponse::with_status(StatusCode::OK) } - Err(err) => Ok(internal_error(to, subject, &err)), - } + Err(err) => { + error!("Error sending support mail: {err}"); + ApiResponse::new( + json!({"error": err.to_string()}), + StatusCode::INTERNAL_SERVER_ERROR, + ) + } + }) } pub async fn send_gateway_disconnected_email( @@ -177,15 +134,18 @@ pub async fn send_gateway_disconnected_email( gateway_adress: &str, pool: &PgPool, ) -> Result<(), WebError> { - debug!("Sending gateway disconnected mail to all admin users"); - let admin_users = User::find_admins(pool).await?; + debug!("Sending Gateway disconnected mail to all admin users"); + let mut conn = pool.begin().await?; + let admin_users = User::find_admins(&mut *conn).await?; for user in admin_users { - Mail::new( + templates::gateway_disconnected_mail( &user.email, - GATEWAY_DISCONNECTED_SUBJECT, - templates::gateway_disconnected_mail(&gateway_name, gateway_adress, &network_name)?, + &mut conn, + &gateway_name, + gateway_adress, + &network_name, ) - .send_and_forget(); + .await?; } Ok(()) @@ -197,15 +157,18 @@ pub async fn send_gateway_reconnected_email( gateway_adress: &str, pool: &PgPool, ) -> Result<(), WebError> { - debug!("Sending gateway reconnect mail to all admin users"); - let admin_users = User::find_admins(pool).await?; + debug!("Sending Gateway reconnect mail to all admin users"); + let mut conn = pool.begin().await?; + let admin_users = User::find_admins(&mut *conn).await?; for user in admin_users { - Mail::new( + templates::gateway_reconnected_mail( &user.email, - GATEWAY_RECONNECTED_SUBJECT, - templates::gateway_reconnected_mail(&gateway_name, gateway_adress, &network_name)?, + &mut conn, + &gateway_name, + gateway_adress, + &network_name, ) - .send_and_forget(); + .await?; } Ok(()) @@ -235,112 +198,3 @@ pub async fn send_user_import_blocked_email(pool: &PgPool) -> Result<(), WebErro Ok(()) } - -pub fn send_new_device_login_email( - user_email: &str, - session: &SessionContext, - created: NaiveDateTime, -) -> Result<(), TemplateError> { - debug!("User {user_email} new device login mail to {SUPPORT_EMAIL_ADDRESS}"); - - Mail::new( - user_email, - NEW_DEVICE_LOGIN_EMAIL_SUBJECT, - templates::new_device_login_mail(session, created)?, - ) - .send_and_forget(); - - Ok(()) -} - -pub fn send_new_device_ocid_login_email( - user_email: &str, - oauth2client_name: &str, - session: &SessionContext, -) -> Result<(), TemplateError> { - debug!("User {user_email} new device OCID login mail to {SUPPORT_EMAIL_ADDRESS}"); - - Mail::new( - user_email, - format!("New login to {oauth2client_name} application with Defguard"), - templates::new_device_ocid_login_mail(session, oauth2client_name)?, - ) - .send_and_forget(); - - Ok(()) -} - -pub fn send_mfa_configured_email( - session: Option<&SessionContext>, - user: &User, - mfa_method: &MFAMethod, -) -> Result<(), TemplateError> { - debug!("Sending MFA configured mail to {}", user.email); - - Mail::new( - &user.email, - format!("MFA method {mfa_method} has been activated on your account"), - templates::mfa_configured_mail(session, mfa_method)?, - ) - .send_and_forget(); - - Ok(()) -} - -pub fn send_email_mfa_activation_email( - user: &User, - session: Option<&SessionContext>, -) -> Result<(), TemplateError> { - debug!("Sending email MFA activation mail to {}", user.email); - - // generate a verification code - let code = user.generate_email_mfa_code().map_err(|err| { - error!("Failed to generate email MFA code: {err}"); - TemplateError::MfaError - })?; - - Mail::new( - &user.email, - EMAIL_MFA_ACTIVATION_EMAIL_SUBJECT, - templates::email_mfa_activation_mail(&user.into(), &code, session)?, - ) - .send_and_forget(); - - Ok(()) -} - -pub fn send_password_reset_email( - user: &User, - service_url: Url, - token: &str, - ip_address: Option<&str>, - device_info: Option<&str>, -) -> Result<(), TokenError> { - debug!("Sending password reset email to {}", user.email); - - Mail::new( - &user.email, - EMAIL_PASSWORD_RESET_START_SUBJECT, - templates::email_password_reset_mail(service_url, token, ip_address, device_info)?, - ) - .send_and_forget(); - - Ok(()) -} - -pub fn send_password_reset_success_email( - user: &User, - ip_address: Option<&str>, - device_info: Option<&str>, -) -> Result<(), TokenError> { - debug!("Sending password reset success email to {}", user.email); - - Mail::new( - &user.email, - EMAIL_PASSWORD_RESET_SUCCESS_SUBJECT, - templates::email_password_reset_success_mail(ip_address, device_info)?, - ) - .send_and_forget(); - - Ok(()) -} diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index 469fc750a0..ea3d23a4bc 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -22,6 +22,7 @@ use defguard_common::db::{ oauth2client::OAuth2Client, }, }; +use defguard_mail::templates::new_device_ocid_login_mail; use openidconnect::{ AccessToken, AdditionalClaims, Audience, AuthUrl, AuthorizationCode, EmptyAdditionalProviderMetadata, EmptyExtraTokenFields, EndUserEmail, EndUserFamilyName, @@ -48,10 +49,7 @@ use crate::{ appstate::AppState, auth::{SessionInfo, UserClaims}, error::WebError, - handlers::{ - SIGN_IN_COOKIE_MAX_AGE, SIGN_IN_COOKIE_NAME, cookie_domain, - mail::send_new_device_ocid_login_email, - }, + handlers::{SIGN_IN_COOKIE_MAX_AGE, SIGN_IN_COOKIE_NAME, cookie_domain}, server_config, }; @@ -581,11 +579,14 @@ pub async fn secure_authorization( let app = OAuth2AuthorizedApp::new(session_info.user.id, oauth2client.id); app.save(&appstate.pool).await?; - send_new_device_ocid_login_email( + let mut conn = appstate.pool.begin().await?; + new_device_ocid_login_mail( &session_info.user.email, + &mut conn, + Some(&session_info.session.into()), &oauth2client.name, - &session_info.session.into(), - )?; + ) + .await?; } info!( "User {} allowed login with client {}", diff --git a/crates/defguard_core/src/handlers/support.rs b/crates/defguard_core/src/handlers/support.rs index 64e370113f..fd906f8254 100644 --- a/crates/defguard_core/src/handlers/support.rs +++ b/crates/defguard_core/src/handlers/support.rs @@ -9,18 +9,30 @@ use crate::{ support::dump_config, }; -pub async fn configuration( +pub(crate) async fn configuration( _admin: AdminRole, State(appstate): State, session: SessionInfo, ) -> ApiResult { debug!("User {} dumping app configuration", session.user.username); - let config = dump_config(&appstate.pool).await; - info!("User {} dumped app configuration", session.user.username); - Ok(ApiResponse::new(config, StatusCode::OK)) + + let mut conn = appstate.pool.begin().await?; + Ok(match dump_config(&mut conn).await { + Ok(config) => { + info!("User {} dumped app configuration", session.user.username); + ApiResponse::new(config, StatusCode::OK) + } + Err(err) => { + warn!("Failed to dump app configuration: {err}"); + ApiResponse::json( + serde_json::json!({"err": err.to_string()}), + StatusCode::BAD_REQUEST, + ) + } + }) } -pub async fn logs(_admin: AdminRole, session: SessionInfo) -> Result { +pub(crate) async fn logs(_admin: AdminRole, session: SessionInfo) -> Result { debug!("User {} dumping app logs", session.user.username); if let Some(ref log_file) = server_config().log_file { match tokio::fs::read_to_string(log_file).await { diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index 850b0db81c..ff4c784aa9 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -14,7 +14,7 @@ use defguard_common::{ }, types::{group_diff::GroupDiff, user_info::UserInfo}, }; -use defguard_mail::{Mail, templates}; +use defguard_mail::templates; use humantime::parse_duration; use serde_json::json; use sqlx::PgPool; @@ -22,8 +22,7 @@ use utoipa::ToSchema; use super::{ AddUserData, ApiResponse, ApiResult, PasswordChange, PasswordChangeSelf, - StartEnrollmentRequest, Username, mail::EMAIL_PASSWORD_RESET_START_SUBJECT, - user_for_admin_or_self, + StartEnrollmentRequest, Username, user_for_admin_or_self, }; use crate::{ appstate::AppState, @@ -1133,34 +1132,15 @@ pub(crate) async fn reset_password( enrollment.save(&mut *transaction).await?; let public_proxy_url = settings.proxy_public_url()?; - let result = Mail::new( - user.email.clone(), - EMAIL_PASSWORD_RESET_START_SUBJECT, - templates::email_password_reset_mail( - public_proxy_url, - enrollment.id.clone().as_str(), - None, - None, - )?, + templates::password_reset_mail( + &user.email, + &mut transaction, + public_proxy_url, + enrollment.id.clone().as_str(), + None, + None, ) - .send() - .await; - - let to = &user.email; - match result { - Ok(()) => { - info!("Password reset email for {username} sent to {to}"); - Ok(()) - } - Err(err) => { - error!( - "Failed to send password reset email for {username} to {to} with error: {err}" - ); - Err(WebError::Serialization(format!( - "Could not send password reset email to user {username}" - ))) - } - }?; + .await?; transaction.commit().await?; diff --git a/crates/defguard_core/src/headers.rs b/crates/defguard_core/src/headers.rs index 778c81031f..a577cd3b2f 100644 --- a/crates/defguard_core/src/headers.rs +++ b/crates/defguard_core/src/headers.rs @@ -5,12 +5,10 @@ use defguard_common::db::{ Id, models::{DeviceLoginEvent, User}, }; -use defguard_mail::templates::{SessionContext, TemplateError}; +use defguard_mail::templates::{SessionContext, TemplateError, new_device_login_mail}; use sqlx::PgPool; use uaparser::{Client, Parser, UserAgentParser}; -use crate::handlers::mail::send_new_device_login_email; - pub(crate) const CONTENT_SECURITY_POLICY_HEADER_NAME: HeaderName = HeaderName::from_static("content-security-policy"); pub(crate) const CONTENT_SECURITY_POLICY_HEADER_VALUE: HeaderValue = @@ -102,7 +100,14 @@ pub(crate) async fn check_new_device_login( .check_if_device_already_logged_in(pool) .await { - send_new_device_login_email(&user.email, session, created_device_login_event.created)?; + let mut conn = pool.begin().await?; + new_device_login_mail( + &user.email, + &mut conn, + Some(session), + created_device_login_event.created, + ) + .await?; } Ok(()) diff --git a/crates/defguard_core/src/support.rs b/crates/defguard_core/src/support.rs index 667dbe1cdc..806ed6e9c1 100644 --- a/crates/defguard_core/src/support.rs +++ b/crates/defguard_core/src/support.rs @@ -12,22 +12,22 @@ use defguard_common::{ }; use serde::Serialize; use serde_json::{Value, json, value::to_value}; -use sqlx::PgPool; +use sqlx::PgConnection; use crate::server_config; /// Unwraps the result returning a JSON representation of value or error -fn unwrap_json(result: Result) -> Value { - match result { - Ok(value) => to_value(value).expect("conversion to JSON failed"), +fn unwrap_json(result: Result) -> Result { + Ok(match result { + Ok(value) => to_value(value)?, Err(err) => json!({"error": err.to_string()}), - } + }) } /// Dumps all data that could be used for debugging. -pub(crate) async fn dump_config(db: &PgPool) -> Value { +pub(crate) async fn dump_config(conn: &mut PgConnection) -> Result { // App settings DB records - let settings = match Settings::get(db).await { + let settings = match Settings::get(&mut *conn).await { Ok(Some(mut settings)) => { settings.smtp_password = None; json!(settings) @@ -36,26 +36,25 @@ pub(crate) async fn dump_config(db: &PgPool) -> Value { Err(err) => json!({"error": err.to_string()}), }; // Networks - let (networks, devices) = match WireguardNetwork::all(db).await { + let (networks, devices) = match WireguardNetwork::all(&mut *conn).await { Ok(networks) => { // Devices for each network let mut devices = HashMap::::new(); for network in &networks { devices.insert( network.id, - unwrap_json(WireguardNetworkDevice::all_for_network(db, network.id).await), + unwrap_json( + WireguardNetworkDevice::all_for_network(&mut *conn, network.id).await, + )?, ); } - ( - to_value(networks).expect("JSON serialization error"), - to_value(devices).expect("JSON serialization error"), - ) + (to_value(networks)?, to_value(devices)?) } Err(err) => (json!({"error": err.to_string()}), Value::Null), }; - let users_diagnostic_data = unwrap_json(User::all_without_sensitive_data(db).await); + let users_diagnostic_data = unwrap_json(User::all_without_sensitive_data(&mut *conn).await)?; - let proxies = match Proxy::all(db).await { + let proxies = match Proxy::all(&mut *conn).await { Ok(proxies) => json!( proxies .iter() @@ -71,7 +70,7 @@ pub(crate) async fn dump_config(db: &PgPool) -> Value { Err(err) => json!({"error": err.to_string()}), }; - let gateways = match Gateway::all(db).await { + let gateways = match Gateway::all(&mut *conn).await { Ok(gateways) => json!( gateways .iter() @@ -89,7 +88,7 @@ pub(crate) async fn dump_config(db: &PgPool) -> Value { Err(err) => json!({"error": err.to_string()}), }; - json!({ + Ok(json!({ "settings": settings, "networks": networks, "version": VERSION, @@ -98,5 +97,5 @@ pub(crate) async fn dump_config(db: &PgPool) -> Value { "config": server_config(), "proxies": proxies, "gateways": gateways, - }) + })) } diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 8be72a7020..ccbbfe8500 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -267,16 +267,12 @@ impl GatewayHandler { return; }; - // FIXME: Try to get rid of spawn and use something like block_on - // To return result instead of logging - tokio::spawn(async move { - if let Err(err) = send_gateway_disconnected_email(name, network.name, &url, &pool).await - { - error!("Failed to send Gateway disconnect notification: {err}"); - } else { - info!("Sent email notification about Gateway being disconnected"); - } - }); + // TODO: return result instead of logging. + if let Err(err) = send_gateway_disconnected_email(name, network.name, &url, &pool).await { + error!("Failed to send Gateway disconnect notification: {err}"); + } else { + info!("Sent email notification about Gateway being disconnected"); + } } /// Send Gateway reconnected notification. diff --git a/crates/defguard_mail/src/mail.rs b/crates/defguard_mail/src/mail.rs index 62ae0e8a9b..5f20e62c4d 100644 --- a/crates/defguard_mail/src/mail.rs +++ b/crates/defguard_mail/src/mail.rs @@ -1,6 +1,9 @@ use std::{str::FromStr, time::Duration}; -use defguard_common::db::models::{Settings, settings::SmtpEncryption}; +use defguard_common::db::models::{ + Settings, + settings::{SmtpEncryption, defaults::WELCOME_EMAIL_SUBJECT}, +}; use lettre::{ AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, message::{Body, Mailbox, MultiPart, SinglePart, header::ContentType}, @@ -89,10 +92,9 @@ pub struct Mail { impl Mail { /// Create new [`Mail`]. #[must_use] - pub fn new(to: T, subject: S, content: String) -> Mail + pub fn new(to: T, subject: String, content: String) -> Mail where T: Into, - S: Into, { // Append images used in all templates. let images = vec![ @@ -104,7 +106,7 @@ impl Mail { Self { to: to.into(), - subject: subject.into(), + subject, content, context: Context::new(), attachments: Vec::new(), @@ -246,7 +248,7 @@ impl Mail { tokio::spawn(self.send()); } - /// Builds mailer object with specified configuration + /// Builds mailer object with specified configuration. fn mailer(settings: SmtpSettings) -> Result, MailError> { type Builder = AsyncSmtpTransport; @@ -276,7 +278,7 @@ pub enum MailMessage { Test, Welcome, /// Information for Defguard support. - Support, + SupportData, DesktopStart, /// Information after starting an enrollment. NewAccount, @@ -287,6 +289,7 @@ pub enum MailMessage { GatewayDisconnect, /// Gateway has reconnected. GatewayReconnect, + /// MFA activated. MFAActivation, MFAConfigured, /// MFA code. @@ -294,71 +297,85 @@ pub enum MailMessage { PasswordReset, PasswordResetDone, UserImportBlocked, + /// Enrollment notification for admins. + EnrollmentNotification, } impl MailMessage { /// Email subject. - pub(crate) const fn subject(&self) -> &'static str { + pub(crate) fn subject(&self) -> String { + // Welcome message's subject should be taken from settings. + if let Self::Welcome = self { + let settings = Settings::get_current_settings(); + if let Some(subject) = settings.enrollment_welcome_email_subject { + return subject; + } + } match self { - Self::Test => "Test message", - Self::Welcome => "Welcome message after enrollment", - Self::Support => "Support data", + Self::Test => "Defguard: Test message", + Self::Welcome => WELCOME_EMAIL_SUBJECT, + Self::SupportData => "Defguard: Support data", Self::DesktopStart => "Defguard: Desktop client configuration", Self::NewAccount => "Defguard: User enrollment", Self::NewDevice => "Defguard: new device added to your account", - Self::NewDeviceLogin => "New device logged in to your account", + Self::NewDeviceLogin => "Defguard: New device logged in to your account", Self::NewDeviceOCIDLogin => "New login to OCID application", - Self::GatewayDisconnect => "Gateway disconnected", - Self::GatewayReconnect => "Gateway reconnected", + Self::GatewayDisconnect => "Defguard: Gateway disconnected", + Self::GatewayReconnect => "Defguard: Gateway reconnected", Self::MFAActivation => "Multi-Factor Authentication activation", Self::MFAConfigured => "Multi-Factor Authentication {method} has been activated", Self::MFACode => "Defguard: Multi-Factor Authentication code for login", - Self::PasswordReset => "Password reset", - Self::PasswordResetDone => "Password reset success", + Self::PasswordReset => "Defguard: Password reset", + Self::PasswordResetDone => "Defguard: Password reset success", Self::UserImportBlocked => "User import blocked", + Self::EnrollmentNotification => "Defguard: User enrollment completed", } + .to_string() } pub(crate) const fn template_name(&self) -> &str { match self { Self::Test => "test", Self::Welcome => "welcome", - Self::Support => "support", + Self::SupportData => "support-data", Self::DesktopStart => "desktop-start", Self::NewAccount => "new-account", Self::NewDevice => "new-device", - Self::NewDeviceLogin => "new-device-loin", - Self::NewDeviceOCIDLogin => "new-device-login-ocid", + Self::NewDeviceLogin => "new-device-login", + Self::NewDeviceOCIDLogin => "new-device-ocid-login", Self::GatewayDisconnect => "gateway-disconnect", Self::GatewayReconnect => "gateway-reconnect", Self::MFAActivation => "mfa-activation", - Self::MFAConfigured => "mfa-configure", + Self::MFAConfigured => "mfa-configured", Self::MFACode => "mfa-code", Self::PasswordReset => "password-reset", Self::PasswordResetDone => "password-reset-done", Self::UserImportBlocked => "user-import-blocked", + Self::EnrollmentNotification => "enrollment-admin-notification", } } pub(crate) const fn mjml_template(&self) -> &str { match self { - // Self::Test => "", - // Self::Welcome => "", - // Self::Support => "", + Self::Test => include_str!("../templates/test.mjml"), + Self::Welcome => include_str!("../templates/enrollment-welcome.mjml"), + Self::SupportData => include_str!("../templates/support-data.mjml"), Self::DesktopStart => include_str!("../templates/desktop-start.mjml"), Self::NewAccount => include_str!("../templates/new-account.mjml"), Self::NewDevice => include_str!("../templates/new-device.mjml"), - // Self::NewDeviceLogin => "", - // Self::NewDeviceOCIDLogin => "", - // Self::GatewayDisconnect => "", - // Self::GatewayReconnect => "", - // Self::MFAActivation => "", - // Self::MFAConfigured => "", + Self::NewDeviceLogin => include_str!("../templates/new-device-login.mjml"), + Self::NewDeviceOCIDLogin => include_str!("../templates/new-device-ocid-login.mjml"), + Self::GatewayDisconnect => include_str!("../templates/gateway-disconnected.mjml"), + Self::GatewayReconnect => include_str!("../templates/gateway-reconnected.mjml"), + Self::MFAActivation => include_str!("../templates/mfa-activation.mjml"), + Self::MFAConfigured => include_str!("../templates/mfa-configured.mjml"), Self::MFACode => include_str!("../templates/mfa-code.mjml"), - // Self::PasswordReset => "", - // Self::PasswordResetDone => "", + Self::PasswordReset => include_str!("../templates/password-reset.mjml"), + Self::PasswordResetDone => include_str!("../templates/password-reset-done.mjml"), Self::UserImportBlocked => include_str!("../templates/plain-notification.mjml"), - _ => "", + Self::EnrollmentNotification => { + include_str!("../templates/enrollment-admin-notification.mjml") + } } } @@ -404,7 +421,7 @@ impl MailMessage { } } } - Self::MFACode => { + Self::MFACode | Self::MFAActivation => { mail.add_png_image("date", DATE_ICON); mail.add_png_image("otp", OTP_ICON); } diff --git a/crates/defguard_mail/src/templates.rs b/crates/defguard_mail/src/templates.rs index 34111807d6..6d23a8f44b 100644 --- a/crates/defguard_mail/src/templates.rs +++ b/crates/defguard_mail/src/templates.rs @@ -3,13 +3,7 @@ use std::{collections::HashMap, time::Duration}; use chrono::{Datelike, NaiveDateTime, Utc}; use defguard_common::{ VERSION, - db::{ - Id, - models::{ - Session, Settings, - user::{MFAMethod, User}, - }, - }, + db::models::{Session, Settings, user::MFAMethod}, types::UrlParseError, }; use reqwest::Url; @@ -18,35 +12,16 @@ use serde_json::Value; use sqlx::PgConnection; use tera::{Context, Function, Tera}; use thiserror::Error; -use tracing::debug; +use tracing::{debug, warn}; -use crate::mail::MailMessage; +use crate::{Attachment, mail::MailMessage}; pub(crate) const DEFAULT_LANG: &str = "en_US"; +pub static SUPPORT_EMAIL_ADDRESS: &str = "support@defguard.net"; + static BASE_MJML: &str = include_str!("../templates/base.mjml"); static MACROS_MJML: &str = include_str!("../templates/macros.mjml"); - -static MAIL_BASE: &str = include_str!("../templates/base.tera"); -static MAIL_MACROS: &str = include_str!("../templates/macros.tera"); -static MAIL_TEST: &str = include_str!("../templates/test.mjml"); -static MAIL_ENROLLMENT_WELCOME: &str = include_str!("../templates/mail_enrollment_welcome.tera"); -static MAIL_ENROLLMENT_ADMIN_NOTIFICATION: &str = - include_str!("../templates/mail_enrollment_admin_notification.tera"); -static MAIL_SUPPORT_DATA: &str = include_str!("../templates/mail_support_data.tera"); -static MAIL_GATEWAY_DISCONNECTED: &str = - include_str!("../templates/mail_gateway_disconnected.tera"); -static MAIL_GATEWAY_RECONNECTED: &str = include_str!("../templates/mail_gateway_reconnected.tera"); -static MAIL_MFA_CONFIGURED: &str = include_str!("../templates/mail_mfa_configured.tera"); -static MAIL_NEW_DEVICE_LOGIN: &str = include_str!("../templates/mail_new_device_login.tera"); -static MAIL_NEW_DEVICE_OCID_LOGIN: &str = - include_str!("../templates/mail_new_device_ocid_login.tera"); -static MAIL_EMAIL_MFA_ACTIVATION: &str = - include_str!("../templates/mail_email_mfa_activation.tera"); -static MAIL_PASSWORD_RESET_START: &str = - include_str!("../templates/mail_password_reset_start.tera"); -static MAIL_PASSWORD_RESET_SUCCESS: &str = - include_str!("../templates/mail_password_reset_success.tera"); static MAIL_DATETIME_FORMAT: &str = "%A, %B %d, %Y at %r"; #[derive(Debug, Error)] @@ -98,52 +73,6 @@ impl From for SessionContext { } } -pub struct UserContext { - last_name: String, - first_name: String, -} - -impl From<&User> for UserContext { - fn from(user: &User) -> Self { - Self { - last_name: user.last_name.clone(), - first_name: user.first_name.clone(), - } - } -} - -fn get_base_tera( - mut context: Context, - session: Option<&SessionContext>, - ip_address: Option<&str>, - device_info: Option<&str>, -) -> Result<(Tera, Context), TemplateError> { - let mut tera = safe_tera(); - tera.add_raw_template("base", MAIL_BASE)?; - tera.add_raw_template("macros", MAIL_MACROS)?; - // Supply context for the base template. - context.insert("application_version", &VERSION); - let now = Utc::now(); - context.insert("current_year", &now.year().to_string()); - context.insert("date_now", &now.format(MAIL_DATETIME_FORMAT).to_string()); - - if let Some(current_session) = session { - let device_info = ¤t_session.device_info; - context.insert("device_type", &device_info); - context.insert("ip_address", ¤t_session.ip_address); - } - - if let Some(ip) = ip_address { - context.insert("ip_address", ip); - } - - if let Some(device_info) = device_info { - context.insert("device_type", device_info); - } - - Ok((tera, context)) -} - fn get_base_tera_mjml( mut context: Context, session: Option<&SessionContext>, @@ -176,18 +105,22 @@ fn get_base_tera_mjml( Ok((tera, context)) } -// Sends test message when requested during SMTP configuration process. -pub fn test_mail(session: Option<&SessionContext>) -> Result { - let (mut tera, context) = get_base_tera_mjml(Context::new(), session, None, None)?; - tera.add_raw_template("mail_test", MAIL_TEST)?; - - let processed = tera.render("mail_test", &context)?; +/// Sends test message when requested during SMTP configuration process. +/// Note: this function waits for the result. +pub async fn test_mail( + to: &str, + conn: &mut PgConnection, + session: Option<&SessionContext>, +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), session, None, None)?; - let parsed = mrml::parse(processed)?; - let opts = mrml::prelude::render::RenderOptions::default(); - let html = parsed.element.render(&opts)?; + let message = MailMessage::Test; + message.fill_context(conn, &mut context).await?; + if let Err(err) = message.mail(&mut tera, &context, to)?.send().await { + warn!("Failed to send test email: {err}"); + } - Ok(html) + Ok(()) } pub async fn user_import_blocked_mail( @@ -258,55 +191,70 @@ pub async fn desktop_start_mail( Ok(()) } -// Welcome message sent when activating an account through enrollment -// content is stored in markdown, so it's parsed into HTML. +/// Welcome message sent when activating an account through enrollment. +/// Its content is stored in markdown, so it's parsed into HTML and plain text. pub fn enrollment_welcome_mail( + to: &str, content: &str, ip_address: Option<&str>, device_info: Option<&str>, -) -> Result { - debug!("Render a welcome mail template for user enrollment."); - let (mut tera, mut context) = get_base_tera(Context::new(), None, ip_address, device_info)?; - tera.add_raw_template("mail_enrollment_welcome", MAIL_ENROLLMENT_WELCOME)?; +) -> Result<(), TemplateError> { + let (mut tera, mut context) = + get_base_tera_mjml(Context::new(), None, ip_address, device_info)?; - // convert content to HTML + debug!("Render welcome mail template for user enrollment"); + // Convert content to HTML. let parser = pulldown_cmark::Parser::new(content); let mut html_output = String::new(); pulldown_cmark::html::push_html(&mut html_output, parser); context.insert("welcome_message_content", &html_output); - Ok(tera.render("mail_enrollment_welcome", &context)?) + let message = MailMessage::Welcome; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -// Notification for admin after user completes an enrollment. -pub fn enrollment_admin_notification( - user: &UserContext, - admin: &UserContext, +/// Notification for admin after user completes an enrollment. +pub async fn enrollment_admin_notification( + to: &str, + conn: &mut PgConnection, + user_name: &str, + admin_name: &str, ip_address: &str, device_info: Option<&str>, -) -> Result { +) -> Result<(), TemplateError> { debug!("Render an admin notification mail template."); let (mut tera, mut context) = - get_base_tera(Context::new(), None, Some(ip_address), device_info)?; - - tera.add_raw_template( - "mail_enrollment_admin_notification", - MAIL_ENROLLMENT_ADMIN_NOTIFICATION, - )?; - context.insert("first_name", &user.first_name); - context.insert("last_name", &user.last_name); - context.insert("admin_first_name", &admin.first_name); - context.insert("admin_last_name", &admin.last_name); - - Ok(tera.render("mail_enrollment_admin_notification", &context)?) + get_base_tera_mjml(Context::new(), None, Some(ip_address), device_info)?; + + context.insert("username", admin_name); + context.insert("user_name", user_name); + + let message = MailMessage::EnrollmentNotification; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -// message with support data -pub fn support_data_mail() -> Result { - let (mut tera, context) = get_base_tera(Context::new(), None, None, None)?; - tera.add_raw_template("mail_support_data", MAIL_SUPPORT_DATA)?; - Ok(tera.render("mail_support_data", &context)?) +/// Email with support data +pub async fn support_data_mail( + to: &str, + conn: &mut PgConnection, + attachments: Vec, +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), None, None, None)?; + + let message = MailMessage::SupportData; + message.fill_context(conn, &mut context).await?; + message + .mail(&mut tera, &context, to)? + .set_attachments(attachments) + .send_and_forget(); + + Ok(()) } #[derive(Serialize)] @@ -338,91 +286,128 @@ pub async fn new_device_added_mail( Ok(()) } -pub fn mfa_configured_mail( +pub async fn mfa_configured_mail( + to: &str, + conn: &mut PgConnection, session: Option<&SessionContext>, method: &MFAMethod, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), session, None, None)?; +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), session, None, None)?; + context.insert("mfa_method", &method); - tera.add_raw_template("mail_base", MAIL_BASE)?; - tera.add_raw_template("mail_mfa_configured", MAIL_MFA_CONFIGURED)?; - Ok(tera.render("mail_mfa_configured", &context)?) + let message = MailMessage::MFAConfigured; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -pub fn new_device_login_mail( - session: &SessionContext, +/// New device login. +pub async fn new_device_login_mail( + to: &str, + conn: &mut PgConnection, + session: Option<&SessionContext>, created: NaiveDateTime, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), Some(session), None, None)?; - tera.add_raw_template("mail_base", MAIL_BASE)?; - context.insert( - "date_now", - &created.format(MAIL_DATETIME_FORMAT).to_string(), - ); +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), session, None, None)?; - tera.add_raw_template("mail_new_device_login", MAIL_NEW_DEVICE_LOGIN)?; - Ok(tera.render("mail_new_device_login", &context)?) + context.insert("created", &created.format(MAIL_DATETIME_FORMAT).to_string()); + + let message = MailMessage::NewDeviceLogin; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -pub fn new_device_ocid_login_mail( - session: &SessionContext, +/// New device login from OpenID Connect. +pub async fn new_device_ocid_login_mail( + to: &str, + conn: &mut PgConnection, + session: Option<&SessionContext>, oauth2client_name: &str, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), Some(session), None, None)?; - tera.add_raw_template("mail_base", MAIL_BASE)?; +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), session, None, None)?; let url = format!("{}me", Settings::url()?); - context.insert("oauth2client_name", &oauth2client_name); context.insert("profile_url", &url); - tera.add_raw_template("mail_new_device_oicd_login", MAIL_NEW_DEVICE_OCID_LOGIN)?; - Ok(tera.render("mail_new_device_oicd_login", &context)?) + let message = MailMessage::NewDeviceOCIDLogin; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -pub fn gateway_disconnected_mail( +/// Notification about disconnected Gateway. +pub async fn gateway_disconnected_mail( + to: &str, + conn: &mut PgConnection, gateway_name: &str, - gateway_ip: &str, - network_name: &str, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), None, None, None)?; + gateway_ip_address: &str, + location_name: &str, +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), None, None, None)?; + context.insert("gateway_name", gateway_name); - context.insert("gateway_ip", gateway_ip); - context.insert("network_name", network_name); - tera.add_raw_template("mail_gateway_disconnected", MAIL_GATEWAY_DISCONNECTED)?; - Ok(tera.render("mail_gateway_disconnected", &context)?) + context.insert("ip_address", gateway_ip_address); + context.insert("location_name", location_name); + + let message = MailMessage::GatewayDisconnect; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -pub fn gateway_reconnected_mail( +/// Notification about reconnected Gateway. +pub async fn gateway_reconnected_mail( + to: &str, + conn: &mut PgConnection, gateway_name: &str, - gateway_ip: &str, - network_name: &str, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), None, None, None)?; + gateway_ip_address: &str, + location_name: &str, +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), None, None, None)?; + context.insert("gateway_name", gateway_name); - context.insert("gateway_ip", gateway_ip); - context.insert("network_name", network_name); - tera.add_raw_template("mail_gateway_reconnected", MAIL_GATEWAY_RECONNECTED)?; - Ok(tera.render("mail_gateway_reconnected", &context)?) + context.insert("ip_address", gateway_ip_address); + context.insert("location_name", location_name); + + let message = MailMessage::GatewayReconnect; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } -pub fn email_mfa_activation_mail( - user: &UserContext, +pub async fn mfa_activation_mail( + to: &str, + conn: &mut PgConnection, + first_name: &str, code: &str, session: Option<&SessionContext>, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), session, None, None)?; +) -> Result<(), TemplateError> { + let (mut tera, mut context) = get_base_tera_mjml(Context::new(), session, None, None)?; let settings = Settings::get_current_settings(); let timeout = humantime::format_duration(Duration::from_secs( settings.mfa_code_timeout_seconds as u64, )); context.insert("code", code); context.insert("timeout", &timeout.to_string()); - context.insert("name", &user.first_name); - tera.add_raw_template("mail_email_mfa_activation", MAIL_EMAIL_MFA_ACTIVATION)?; + context.insert("username", first_name); + context.insert( + "datetime", + &Utc::now().format(MAIL_DATETIME_FORMAT).to_string(), + ); - Ok(tera.render("mail_email_mfa_activation", &context)?) + let message = MailMessage::MFAActivation; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); + + Ok(()) } pub async fn mfa_code_mail( @@ -452,13 +437,17 @@ pub async fn mfa_code_mail( Ok(()) } -pub fn email_password_reset_mail( +/// Password reset email. +pub async fn password_reset_mail( + to: &str, + conn: &mut PgConnection, mut service_url: Url, password_reset_token: &str, ip_address: Option<&str>, device_info: Option<&str>, -) -> Result { - let (mut tera, mut context) = get_base_tera(Context::new(), None, ip_address, device_info)?; +) -> Result<(), TemplateError> { + let (mut tera, mut context) = + get_base_tera_mjml(Context::new(), None, ip_address, device_info)?; context.insert("enrollment_url", &service_url); context.insert("defguard_url", &Settings::url()?); @@ -471,161 +460,26 @@ pub fn email_password_reset_mail( context.insert("link_url", &service_url); - tera.add_raw_template("mail_passowrd_reset_start", MAIL_PASSWORD_RESET_START)?; + let message = MailMessage::PasswordReset; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); - Ok(tera.render("mail_passowrd_reset_start", &context)?) + Ok(()) } -pub fn email_password_reset_success_mail( +/// Successful password reset email. +pub async fn password_reset_success_mail( + to: &str, + conn: &mut PgConnection, ip_address: Option<&str>, device_info: Option<&str>, -) -> Result { - let (mut tera, context) = get_base_tera(Context::new(), None, ip_address, device_info)?; - - tera.add_raw_template("mail_passowrd_reset_success", MAIL_PASSWORD_RESET_SUCCESS)?; - - Ok(tera.render("mail_passowrd_reset_success", &context)?) -} - -#[cfg(test)] -mod test { - use claims::assert_ok; - use defguard_common::{ - config::{DefGuardConfig, SERVER_CONFIG}, - db::{models::settings::initialize_current_settings, setup_pool}, - }; - use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - - use super::*; - - // fn get_welcome_context() -> Context { - // let mut context = Context::new(); - // context.insert("first_name", "test_first"); - // context.insert("last_name", "test_last"); - // context.insert("username", "username"); - // context.insert("defguard_url", "test_url"); - // context.insert("defguard_version", &VERSION); - // context.insert("admin_first_name", "test_first_name"); - // context.insert("admin_last_name", "test_last_name"); - // context.insert("admin_email", "test_email"); - // context.insert("admin_phone", "test_phone"); - // context - // } - - async fn init_config(pool: &sqlx::PgPool) { - let mut config = DefGuardConfig::new_test_config(); - initialize_current_settings(pool) - .await - .expect("Could not initialize current settings in the database"); - config.initialize_post_settings(); - let _ = SERVER_CONFIG.set(config.clone()); - } - - #[test] - fn test_mfa_configured_mail() { - let mfa_method = MFAMethod::OneTimePassword; - assert_ok!(mfa_configured_mail(None, &mfa_method)); - } - - #[test] - fn test_base_mail_no_context() { - assert_ok!(get_base_tera(Context::new(), None, None, None)); - } - - #[test] - fn test_test_mail() { - assert_ok!(test_mail(None)); - } - - // #[sqlx::test] - // async fn test_enrollment_start_mail(_: PgPoolOptions, options: PgConnectOptions) { - // let pool = setup_pool(options).await; - // init_config(&pool).await; - // assert_ok!(enrollment_start_mail( - // Context::new(), - // Url::parse("http://localhost:8080").unwrap(), - // "test_token" - // )); - // } - - #[sqlx::test] - async fn test_enrollment_welcome_mail(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - init_config(&pool).await; - assert_ok!(enrollment_welcome_mail( - "Hi there! Welcome to Defguard.", - None, - None - )); - } - - // #[sqlx::test] - // async fn test_desktop_start_mail(_: PgPoolOptions, options: PgConnectOptions) { - // let pool = setup_pool(options).await; - // init_config(&pool).await; - // let context = get_welcome_context(); - // let url = Url::parse("http://127.0.0.1:8080").unwrap(); - // let token = "TestToken"; - // let mut tranaction = pool.begin().await.unwrap(); - // assert_ok!(desktop_start_mail(&mut tranaction, context, &url, token).await); - // } - - // #[sqlx::test] - // async fn test_new_device_added_mail(_: PgPoolOptions, options: PgConnectOptions) { - // let pool = setup_pool(options).await; - // init_config(&pool).await; - // let template_locations: Vec = vec![ - // TemplateLocation { - // name: "Test 01".into(), - // assigned_ips: "10.0.0.10".into(), - // }, - // TemplateLocation { - // name: "Test 02".into(), - // assigned_ips: "10.0.0.10".into(), - // }, - // ]; - // assert_ok!(new_device_added_mail( - // "Test device", - // "TestKey", - // &template_locations, - // Some("1.1.1.1"), - // None, - // )); - // } - - #[sqlx::test] - async fn test_gateway_disconnected(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - init_config(&pool).await; - assert_ok!(gateway_disconnected_mail( - "Gateway A", - "127.0.0.1", - "Location1" - )); - } +) -> Result<(), TemplateError> { + let (mut tera, mut context) = + get_base_tera_mjml(Context::new(), None, ip_address, device_info)?; - #[sqlx::test] - async fn test_enrollment_admin_notification(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - init_config(&pool).await; - let test_user = UserContext { - last_name: "test_last".into(), - first_name: "test_first".into(), - }; - - assert_ok!(enrollment_admin_notification( - &test_user, - &test_user, - "11.11.11.11", - None - )); - } + let message = MailMessage::PasswordResetDone; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); - #[test] - fn dg25_8_server_side_template_injection() { - let mut tera = safe_tera(); - tera.add_raw_template("text", "PATH={{ get_env(name=\"PATH\") }}") - .unwrap(); - assert!(tera.render("text", &Context::new()).is_err()); - } + Ok(()) } diff --git a/crates/defguard_mail/src/tests.rs b/crates/defguard_mail/src/tests.rs index 1d153ca08d..420ca01509 100644 --- a/crates/defguard_mail/src/tests.rs +++ b/crates/defguard_mail/src/tests.rs @@ -1,10 +1,11 @@ use std::{env, str::FromStr, time::Duration}; +use chrono::Utc; use defguard_common::{ config::{DefGuardConfig, SERVER_CONFIG}, db::{ models::{ - Settings, + MFAMethod, Settings, settings::{SmtpEncryption, initialize_current_settings, set_settings}, }, setup_pool, @@ -18,9 +19,15 @@ use sqlx::{ }; use tera::Context; -use super::templates::{ - TemplateLocation, desktop_start_mail, mfa_code_mail, new_account_mail, new_device_added_mail, -}; +use super::{Attachment, templates}; + +#[test] +fn dg25_8_server_side_template_injection() { + let mut tera = templates::safe_tera(); + tera.add_raw_template("text", "PATH={{ get_env(name=\"PATH\") }}") + .unwrap(); + assert!(tera.render("text", &Context::new()).is_err()); +} /// Set SMTP settings from environment variables. async fn set_smtp_settings(pool: &PgPool) { @@ -39,7 +46,7 @@ async fn set_smtp_settings(pool: &PgPool) { set_settings(Some(settings)); } -#[ignore = "Requires SMTP server"] +#[ignore = "requires SMTP server"] #[sqlx::test] fn send_desktop_start(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; @@ -47,9 +54,9 @@ fn send_desktop_start(_: PgPoolOptions, options: PgConnectOptions) { let mut conn = pool.begin().await.unwrap(); let context = Context::new(); - let url = Url::parse("http://localhost:8000").unwrap(); + let url = Url::parse("http://localhost:8001").unwrap(); let token = "zXc6N1ndXpWFeyBuogiFp1bD1UomAbZc"; - desktop_start_mail( + templates::desktop_start_mail( &env::var("SMTP_TO").unwrap(), &mut conn, context, @@ -63,7 +70,7 @@ fn send_desktop_start(_: PgPoolOptions, options: PgConnectOptions) { tokio::time::sleep(Duration::from_secs(2)).await; } -#[ignore = "Requires SMTP server"] +#[ignore = "requires SMTP server"] #[sqlx::test] fn send_new_device_added(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; @@ -73,16 +80,16 @@ fn send_new_device_added(_: PgPoolOptions, options: PgConnectOptions) { let device_name = "My beloved machine"; let public_key = "6N8h7HILMcQ6nqEfQMBAYQH26X+y3t/WdWSOW4bNNxw="; let locations = &[ - TemplateLocation { + templates::TemplateLocation { name: String::from("Location 1"), assigned_ips: String::from("192.168.1.42"), }, - TemplateLocation { + templates::TemplateLocation { name: String::from("Location 2"), assigned_ips: String::from("192.168.2.69"), }, ]; - new_device_added_mail( + templates::new_device_added_mail( &env::var("SMTP_TO").unwrap(), &mut conn, device_name, @@ -98,7 +105,7 @@ fn send_new_device_added(_: PgPoolOptions, options: PgConnectOptions) { tokio::time::sleep(Duration::from_secs(2)).await; } -#[ignore = "Requires SMTP server"] +#[ignore = "requires SMTP server"] #[sqlx::test] fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; @@ -107,7 +114,7 @@ fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { let mut conn = pool.begin().await.unwrap(); let first_name = "Nebuchadnezzar"; let code = "123456"; - mfa_code_mail( + templates::mfa_code_mail( &env::var("SMTP_TO").unwrap(), &mut conn, first_name, @@ -121,7 +128,7 @@ fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { tokio::time::sleep(Duration::from_secs(2)).await; } -#[ignore = "Requires SMTP server"] +#[ignore = "requires SMTP server"] #[sqlx::test] fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; @@ -131,7 +138,7 @@ fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) { let url = Url::parse("http://localhost:8001").unwrap(); let context = Context::new(); let token = "zXc6N1ndXpWFeyBuogiFp1bD1UomAbZc"; - new_account_mail( + templates::new_account_mail( &env::var("SMTP_TO").unwrap(), &mut conn, context, @@ -144,3 +151,243 @@ fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) { // Delay, so send_and_forget() can process the message. tokio::time::sleep(Duration::from_secs(2)).await; } + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_mfa_activation(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let first_name = "Nebuchadnezzar"; + let code = "123456"; + templates::mfa_activation_mail( + &env::var("SMTP_TO").unwrap(), + &mut conn, + first_name, + code, + None, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_enrollment_admin_notification(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let user_name = "Nebuchadnezzar the Great"; + let admin_name = "Nabopolassar the Admin"; + let ip_address = "1.2.3.4"; + templates::enrollment_admin_notification( + &env::var("SMTP_TO").unwrap(), + &mut conn, + user_name, + admin_name, + ip_address, + None, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_gateway_disconnected_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let gateway_name = "Portal"; + let ip_address = "1.2.3.4"; + let location_name = "Somewhere"; + templates::gateway_disconnected_mail( + &env::var("SMTP_TO").unwrap(), + &mut conn, + gateway_name, + ip_address, + location_name, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_gateway_reconnected_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let gateway_name = "Portal"; + let ip_address = "1.2.3.4"; + let location_name = "Somewhere"; + templates::gateway_reconnected_mail( + &env::var("SMTP_TO").unwrap(), + &mut conn, + gateway_name, + ip_address, + location_name, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_mfa_configured_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + templates::mfa_configured_mail( + &env::var("SMTP_TO").unwrap(), + &mut conn, + None, + &MFAMethod::Email, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_new_device_login_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let created = Utc::now().naive_utc(); + templates::new_device_login_mail(&env::var("SMTP_TO").unwrap(), &mut conn, None, created) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_new_device_ocid_login_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let client_name = "RemoteApp"; + templates::new_device_ocid_login_mail( + &env::var("SMTP_TO").unwrap(), + &mut conn, + None, + client_name, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_password_reset_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let proxy_url = Url::parse("http://localhost:8000").unwrap(); + let token = "blablabla"; + templates::password_reset_mail( + &env::var("SMTP_TO").unwrap(), + &mut conn, + proxy_url, + token, + None, + None, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_password_reset_success_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + templates::password_reset_success_mail(&env::var("SMTP_TO").unwrap(), &mut conn, None, None) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_test_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + templates::test_mail(&env::var("SMTP_TO").unwrap(), &mut conn, None) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_support_data_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut conn = pool.begin().await.unwrap(); + let config = Attachment::new( + "defguard-support-data-test.json".into(), + b"{\"key\":\"value\"}".into(), + ); + templates::support_data_mail(&env::var("SMTP_TO").unwrap(), &mut conn, vec![config]) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} + +#[ignore = "requires SMTP server"] +#[sqlx::test] +fn send_enrollment_welcome_mail(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let markdown = "Paragraph **bold** _italic_."; + templates::enrollment_welcome_mail(&env::var("SMTP_TO").unwrap(), markdown, None, None) + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} diff --git a/crates/defguard_mail/templates/base.tera b/crates/defguard_mail/templates/base.tera deleted file mode 100644 index 10b0f07e41..0000000000 --- a/crates/defguard_mail/templates/base.tera +++ /dev/null @@ -1,554 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
-
-
- -
-
- -
- - - - - - -
- -
- -
- - - - - - -
- - - - - - -
- - - - - - -
- Defguard logo -
-
-
-
- -
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
-
-
- -
-
- - {% block mail_content %} - {% endblock %} - - -
- - - - - - -
-
- - - - - - -
- - - - - - -
-
- {% if date_now %} -

- Date: {{ date_now | safe }} -

- {% endif %} - {% if ip_address %} -

- IP Address: {{ ip_address | safe }} -

- {% endif %} - {% if device_type %} -

- Device type: {{ device_type }} -

- {% endif %} -
-
-
-
-
-
- - - -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- - - - - - - -
- - - - - - -
- - Github - -
-
- - - - - - - -
- - - - - - -
- - Matrix - -
-
- - - - - - - -
- - - - - - -
- - Mastodon - -
-
- - - - - - - -
- - - - - - -
- - Twitter - -
-
- -
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
Copyright © {{ current_year }} teonite
-
Sent by Defguard v.{{ application_version }}
-
-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
-
-
- -
-
- -
- - - diff --git a/crates/defguard_mail/templates/desktop-start.mjml b/crates/defguard_mail/templates/desktop-start.mjml index 484e3b5c3b..683afa990f 100644 --- a/crates/defguard_mail/templates/desktop-start.mjml +++ b/crates/defguard_mail/templates/desktop-start.mjml @@ -28,7 +28,7 @@ - + {{ configure }} diff --git a/crates/defguard_mail/templates/desktop-start.text b/crates/defguard_mail/templates/desktop-start.text index 6418a22fda..06915b2bce 100644 --- a/crates/defguard_mail/templates/desktop-start.text +++ b/crates/defguard_mail/templates/desktop-start.text @@ -1,5 +1,7 @@ -{{ header }} +{{ title }} {{ subtitle }} {{ label_url }}: {{ url }} {{ label_token }}: {{ token }} + +{{ configure }}: {{ url }}/open-desktop?token={{ token }} diff --git a/crates/defguard_mail/templates/enrollment-admin-notification.mjml b/crates/defguard_mail/templates/enrollment-admin-notification.mjml new file mode 100644 index 0000000000..b3c3e41551 --- /dev/null +++ b/crates/defguard_mail/templates/enrollment-admin-notification.mjml @@ -0,0 +1,22 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + +

+ {{ user_name }} {{ message }} +

+

+ {{ goodday }} +

+
+
+
+ +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/enrollment-welcome.mjml b/crates/defguard_mail/templates/enrollment-welcome.mjml new file mode 100644 index 0000000000..1a79df75f4 --- /dev/null +++ b/crates/defguard_mail/templates/enrollment-welcome.mjml @@ -0,0 +1,15 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + + + + +

+ {{ welcome_message_content }} +

+
+
+
+ +{% endblock content %} diff --git a/crates/defguard_mail/templates/gateway-disconnected.mjml b/crates/defguard_mail/templates/gateway-disconnected.mjml new file mode 100644 index 0000000000..450595d287 --- /dev/null +++ b/crates/defguard_mail/templates/gateway-disconnected.mjml @@ -0,0 +1,24 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + +

+ {{ gateway_label }} {{ gateway_name }} +

+

+ {{ ip_address_label }} {{ ip_address }} +

+

+ {{ location_label }} {{ location_name }} +

+
+
+
+ +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/gateway-reconnected.mjml b/crates/defguard_mail/templates/gateway-reconnected.mjml new file mode 100644 index 0000000000..e9755328ff --- /dev/null +++ b/crates/defguard_mail/templates/gateway-reconnected.mjml @@ -0,0 +1,25 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + +

+ {{ gateway_label }} {{ gateway_name }} +

+

+ {{ ip_address_label }} {{ ip_address }} +

+

+ {{ location_label }} {{ location_name }} +

+
+
+
+ +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/macros.tera b/crates/defguard_mail/templates/macros.tera deleted file mode 100644 index 1d82a9a0e8..0000000000 --- a/crates/defguard_mail/templates/macros.tera +++ /dev/null @@ -1,272 +0,0 @@ -{% macro text_section(content_array) %} -
- - - - - - -
-
- - - - - - -
- - - - - - -
-
- {% for content in content_array %} - {{ content | safe }} - {% endfor %} -
-
-
-
-
-
-{% endmacro text_section %} - -{% macro inline_image(src, height="100px", width="100px", alt="") %} -
- - - - - - -
-
- - - - - - -
- - - - - - -
-
- {{ alt }} -
-
-
-
-
-
-{% endmacro inline_image %} - -{% macro paragraph(content="", color="#222", font_size="12px", align="left", line_height="120%", font_weight="400") %} -

- {{ content | safe }} -

-{% endmacro paragraph %} - -{% macro paragraph_with_title(title, content="", color="#222", font_size="12px", align="left", line_height="120%", margin="auto") %} -

- {{ title | safe }} {{ content | safe }} -

-{% endmacro paragraph_with_title %} - -{% macro spacer(height="20px") %} -
- - - - - - -
-
- - - - - - -
- - - - - - -
-
-   -
-
-
-
-
-
-{% endmacro spacer %} - -{% macro link(content="", href="", color="#222", decoration="underline", decoration_color="#222", family="Roboto", -size="12px", line_height="120%", weight="400") %} - - {{ content | safe }} - -{% endmacro link %} - -{% macro title(content="", font_size="28px") %} -
- - - - - - -
-
- - - - - - -
- - - - - - -
-
- {{ content | safe }} -
-
-
-
-
-
-{% endmacro title %} - -{% macro button_link(href="", text="") %} -

- {{ text }} - -

-{% endmacro button_link %} diff --git a/crates/defguard_mail/templates/mail_desktop_start.tera b/crates/defguard_mail/templates/mail_desktop_start.tera deleted file mode 100644 index 4abab68963..0000000000 --- a/crates/defguard_mail/templates/mail_desktop_start.tera +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [ -macros::paragraph(content="You're receiving this email to configure a new desktop client."), -macros::paragraph(content="Please paste this URL and token in your desktop client:"), -macros::paragraph(content="URL: " ~ url), -macros::paragraph(content="Token: " ~ token), -macros::spacer(height="20px"), -macros::paragraph(content="Or use link below"), -macros::spacer(height="20px"), -macros::button_link(href="defguard://addinstance?token=" ~ token ~ "&url=" ~ url, text="Configure your desktop client") -] %} -{{ macros::text_section(content_array=section_content)}} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_email_mfa_activation.tera b/crates/defguard_mail/templates/mail_email_mfa_activation.tera deleted file mode 100644 index cc12a8174f..0000000000 --- a/crates/defguard_mail/templates/mail_email_mfa_activation.tera +++ /dev/null @@ -1,21 +0,0 @@ -{# -Requires context: -code -> 6-digit zero-padded verification code -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [ - macros::title(content="Hello, " ~ name), - macros::paragraph(content="You are activating Multi-Factor Authentication using email verification codes.", align="center"), -] %} -{{ macros::text_section(content_array=section_content) }} -{{ macros::spacer(height="40px") }} -{% set section_content = [ - macros::title(content="" ~ code ~ "", font_size="45px"), - macros::spacer(height="40px"), - macros::paragraph(content="The code is valid for " ~ timeout ~ ".", align="center", font_size="15px"), -] %} -{{ macros::text_section(content_array=section_content) }} -{{ macros::spacer(height="10px") }} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_enrollment_admin_notification.tera b/crates/defguard_mail/templates/mail_enrollment_admin_notification.tera deleted file mode 100644 index 3d775ff2d8..0000000000 --- a/crates/defguard_mail/templates/mail_enrollment_admin_notification.tera +++ /dev/null @@ -1,9 +0,0 @@ -{% import "macros" as macros %} -{% extends "base" %} -{% block mail_content %} -{% set section_content = [ -macros::paragraph(content="Dear " ~ admin_first_name ~ " " ~ admin_last_name), -macros::paragraph(content=first_name ~ " " ~ last_name ~ " just completed their enrollment process."), -macros::paragraph(content="Have a good day!")] %} -{{ macros::text_section(content_array=section_content) }} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_enrollment_welcome.tera b/crates/defguard_mail/templates/mail_enrollment_welcome.tera deleted file mode 100644 index 93298d0cef..0000000000 --- a/crates/defguard_mail/templates/mail_enrollment_welcome.tera +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [macros::paragraph(content=welcome_message_content)] %} -{{ macros::text_section(content_array=section_content)}} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_gateway_disconnected.tera b/crates/defguard_mail/templates/mail_gateway_disconnected.tera deleted file mode 100644 index 17b1862380..0000000000 --- a/crates/defguard_mail/templates/mail_gateway_disconnected.tera +++ /dev/null @@ -1,14 +0,0 @@ -{# -Requires context: -gateway_name -> name of gateway -gateway_ip -> gateway adress -network_name -> name of network -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [ -macros::paragraph(content="Your gateway: " ~ gateway_name ~ " (IP: " ~ gateway_ip ~ ") for VPN Location: " ~ network_name ~ " has just disconnected."), -macros::paragraph(content="Please login to your gateway server and see the logs.")] %} -{{ macros::text_section(content_array=section_content) }} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_gateway_reconnected.tera b/crates/defguard_mail/templates/mail_gateway_reconnected.tera deleted file mode 100644 index a6a8282222..0000000000 --- a/crates/defguard_mail/templates/mail_gateway_reconnected.tera +++ /dev/null @@ -1,14 +0,0 @@ -{# -Requires context: -gateway_name -> name of gateway -gateway_ip -> gateway adress -network_name -> name of network -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [ -macros::paragraph(content="Your gateway: " ~ gateway_name ~ " (IP: " ~ gateway_ip ~ ") for VPN Location: " ~ network_name ~ " has just reconnected.") -] %} -{{ macros::text_section(content_array=section_content) }} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_mfa_configured.tera b/crates/defguard_mail/templates/mail_mfa_configured.tera deleted file mode 100644 index f9f8ed34c7..0000000000 --- a/crates/defguard_mail/templates/mail_mfa_configured.tera +++ /dev/null @@ -1,11 +0,0 @@ -{# -Requires context: -mfa_method -> what method was activated -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [macros::paragraph(content="A Multi-Factor Authorization method: " ~ mfa_method ~ " has been -activated in your account.", align="center")] %} -{{ macros::text_section(content_array=section_content) }} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_new_device_login.tera b/crates/defguard_mail/templates/mail_new_device_login.tera deleted file mode 100644 index abb287c5ed..0000000000 --- a/crates/defguard_mail/templates/mail_new_device_login.tera +++ /dev/null @@ -1,19 +0,0 @@ -{# Requires context -device_name -> name of the device added -public_key -> Public key of device added -locations -> { -name -> location name, -assigned_ip -> ip of device in location -}[] -#} -{% extends "base" %} -{% import "macros" as macros %} - -{# mail content #} -{% block mail_content %} -{# title #} -{% set section_content = [macros::paragraph(content="Your account was just logged into from a new device:")] %} -{{ macros::text_section(content_array=section_content) }} -{{ macros::spacer(height="40px")}} -{# render device section #} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_new_device_ocid_login.tera b/crates/defguard_mail/templates/mail_new_device_ocid_login.tera deleted file mode 100644 index 46ed5f0539..0000000000 --- a/crates/defguard_mail/templates/mail_new_device_ocid_login.tera +++ /dev/null @@ -1,22 +0,0 @@ -{# Requires context -device_name -> name of the device added -public_key -> Public key of device added -locations -> { -name -> location name, -assigned_ip -> ip of device in location -}[] -#} -{% extends "base" %} -{% import "macros" as macros %} - -{# mail content #} -{% block mail_content %} -{# title #} -{% set section_content = [ - macros::paragraph(content="Your account was just logged into a system: " ~ oauth2client_name ~ " using OpenID Connect authorization."), - macros::link(content="You can deauthorize all applications that have access to your account from the web vault under (My Profile > Apps).", href=profile_url), -] %} -{{ macros::text_section(content_array=section_content) }} -{{ macros::spacer(height="40px")}} -{# render device section #} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_password_reset_start.tera b/crates/defguard_mail/templates/mail_password_reset_start.tera deleted file mode 100644 index aea11126d5..0000000000 --- a/crates/defguard_mail/templates/mail_password_reset_start.tera +++ /dev/null @@ -1,39 +0,0 @@ -{# Requires context -enrollment_url -> URL of the enrollment service -link_url -> URL of the enrollment service with the token query param included -defguard_url -> URL of defguard core Web UI -token -> enrollment token -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set client_docs_url="https://docs.defguard.net/help/desktop-client" %} -{% set client_docs_link=macros::link(content=client_docs_url, href=client_docs_url) %} -{% set release_url="https://defguard.net/download/" %} -{% set release_link=macros::link(content=release_url, href=release_url) %} -{% set section_content = [ -macros::paragraph(content="Password reset"), -macros::paragraph(content= "If you wish to reset your password, please copy & paste the following URL in your browser: "), -macros::link(content=link_url, href=link_url), -macros::paragraph(content="Or click the button below:"), -] %} -{{ macros::text_section(content_array=section_content)}} -

Reset password

-{% endblock %} diff --git a/crates/defguard_mail/templates/mail_password_reset_success.tera b/crates/defguard_mail/templates/mail_password_reset_success.tera deleted file mode 100644 index 3facf0f85a..0000000000 --- a/crates/defguard_mail/templates/mail_password_reset_success.tera +++ /dev/null @@ -1,15 +0,0 @@ -{# Requires context -enrollment_url -> URL of the enrollment service -link_url -> URL of the enrollment service with the token query param included -defguard_url -> URL of defguard core Web UI -token -> enrollment token -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [ -macros::paragraph(content="Password reset"), -macros::paragraph(content= "Your password has been successfully changed."), -] %} -{{ macros::text_section(content_array=section_content)}} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_support_data.tera b/crates/defguard_mail/templates/mail_support_data.tera deleted file mode 100644 index 726808ac0e..0000000000 --- a/crates/defguard_mail/templates/mail_support_data.tera +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set section_content = [macros::paragraph(content="Support data in attachments.")] %} -{{ macros::text_section(content_array=section_content)}} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_test.tera b/crates/defguard_mail/templates/mail_test.tera deleted file mode 100644 index c90de0c5b1..0000000000 --- a/crates/defguard_mail/templates/mail_test.tera +++ /dev/null @@ -1,8 +0,0 @@ -{% import "macros" as macros %} -{% extends "base" %} -{% block mail_content %} -{% set section_content = [ -macros::paragraph(content="This is test email from Defguard system."), -macros::paragraph(content="If you received it, your SMTP configuration is ok.")] %} -{{ macros::text_section(content_array=section_content)}} -{% endblock %} diff --git a/crates/defguard_mail/templates/mfa-activation.mjml b/crates/defguard_mail/templates/mfa-activation.mjml new file mode 100644 index 0000000000..3d7f8abf49 --- /dev/null +++ b/crates/defguard_mail/templates/mfa-activation.mjml @@ -0,0 +1,45 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + +

+ {{ code }} +

+
+
+
+ + + + + + + + + + {{ code_is_valid }} {{ timeout }} + + + + + + + + + + + + + {{ datetime }} + + + + +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/mfa-code.mjml b/crates/defguard_mail/templates/mfa-code.mjml index c511b9dd52..3d7f8abf49 100644 --- a/crates/defguard_mail/templates/mfa-code.mjml +++ b/crates/defguard_mail/templates/mfa-code.mjml @@ -22,7 +22,7 @@ - {{ code_is_valid }} + {{ code_is_valid }} {{ timeout }}
diff --git a/crates/defguard_mail/templates/mfa-configured.mjml b/crates/defguard_mail/templates/mfa-configured.mjml new file mode 100644 index 0000000000..b1ab3fbff0 --- /dev/null +++ b/crates/defguard_mail/templates/mfa-configured.mjml @@ -0,0 +1,19 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + +

+ {{ mfa_method_label }} {{ mfa_method }} +

+
+
+
+ +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/new-device-login.mjml b/crates/defguard_mail/templates/new-device-login.mjml new file mode 100644 index 0000000000..14c7829703 --- /dev/null +++ b/crates/defguard_mail/templates/new-device-login.mjml @@ -0,0 +1,34 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + + {% if ip_address %} + + + {{ label_device }} + + + {{ ip_address }} + + + {% endif %} + + + {{ label_date }} + + + {{ created }} + + + + + + +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/new-device-ocid-login.mjml b/crates/defguard_mail/templates/new-device-ocid-login.mjml new file mode 100644 index 0000000000..e312f11976 --- /dev/null +++ b/crates/defguard_mail/templates/new-device-ocid-login.mjml @@ -0,0 +1,32 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + + + + {{ label_profile }} + + + {{ profile_url }} + + + + + {{ label_oauth2client }} + + + {{ oauth2client_name }} + + + + + + +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/password-reset-done.mjml b/crates/defguard_mail/templates/password-reset-done.mjml new file mode 100644 index 0000000000..c56e0b4f8c --- /dev/null +++ b/crates/defguard_mail/templates/password-reset-done.mjml @@ -0,0 +1,18 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + +

+

+
+
+
+ +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/password-reset.mjml b/crates/defguard_mail/templates/password-reset.mjml new file mode 100644 index 0000000000..159e3606d6 --- /dev/null +++ b/crates/defguard_mail/templates/password-reset.mjml @@ -0,0 +1,19 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + +

+ {{ link_url }} +

+
+
+
+ +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/support-data.mjml b/crates/defguard_mail/templates/support-data.mjml new file mode 100644 index 0000000000..d42aa1b34f --- /dev/null +++ b/crates/defguard_mail/templates/support-data.mjml @@ -0,0 +1,9 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + +{{ macros::footer_divider() }} + +{% endblock content %} diff --git a/crates/defguard_mail/templates/test.mjml b/crates/defguard_mail/templates/test.mjml index ebb58aae38..d42aa1b34f 100644 --- a/crates/defguard_mail/templates/test.mjml +++ b/crates/defguard_mail/templates/test.mjml @@ -1,40 +1,9 @@ - - +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} - - - - Defguard - - - +{{ macros::email_header() }} - - - - Defguard: subject - +{{ macros::footer_divider() }} - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim - eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras - id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh. - Proin nec commodo purus. Sed eget nulla elit. Nulla aliquet mollis - faucibus. - - - Learn more - - - - +{% endblock content %} diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index fcf44e7979..3725e2d87b 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -25,14 +25,14 @@ use defguard_core::{ client_version::ClientFeature, utils::{build_device_config_response, parse_client_ip_agent}, }, - handlers::{ - mail::{send_email_mfa_activation_email, send_mfa_configured_email}, - user::check_password_strength, - }, + handlers::user::check_password_strength, headers::get_device_info, is_valid_phone_number, }; -use defguard_mail::templates::{TemplateLocation, new_device_added_mail}; +use defguard_mail::templates::{ + TemplateLocation, enrollment_admin_notification, mfa_activation_mail, mfa_configured_mail, + new_device_added_mail, +}; use defguard_proto::proxy::{ ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, DeviceConfigResponse, @@ -417,22 +417,12 @@ impl EnrollmentServer { Status::internal("unexpected error") })?; debug!("Updating user details ended with success."); - let _ = update_counts(&self.pool).await; - - debug!("Retriving settings to send welcome email..."); - let settings = Settings::get_current_settings(); - debug!("Settings successfully retrieved."); + let _ = update_counts(&mut *transaction).await; // send welcome email debug!("Try to send welcome email..."); enrollment - .send_welcome_email( - &mut transaction, - &user, - &settings, - &ip_address, - device_info.as_deref(), - ) + .send_welcome_email(&mut transaction, &user, &ip_address, device_info.as_deref()) .await?; // send success notification to admin @@ -442,9 +432,19 @@ impl EnrollmentServer { let admin = enrollment.fetch_admin(&mut *transaction).await?; if let Some(admin) = admin { - debug!("Send admin notification mail."); - Token::send_admin_notification(&admin, &user, &ip_address, device_info.as_deref()) - .await?; + debug!("Sending admin notification mail"); + if let Err(err) = enrollment_admin_notification( + &admin.email, + &mut transaction, + user.name().as_str(), + admin.name().as_str(), + &ip_address, + device_info.as_deref(), + ) + .await + { + error!("Failed to send admin notification mail: {err}"); + } } // Unset the enrollment-pending flag (https://github.com/DefGuard/client/issues/647). @@ -914,7 +914,7 @@ impl EnrollmentServer { &self, request: CodeMfaSetupStartRequest, ) -> Result { - debug!("Begin enrollment code mfa setup start"); + debug!("Begin enrollment code MFA setup start"); let method = request.method(); if method != MfaMethod::Email && method != MfaMethod::Totp { return Err(Status::invalid_argument("Method not supported".to_string())); @@ -929,7 +929,7 @@ impl EnrollmentServer { MfaMethod::Email => { let settings = Settings::get_current_settings(); if !settings.smtp_configured() { - error!("Unable to start Email mfa setup. SMTP is not configured"); + error!("Unable to start email MFA setup; SMTP is not configured"); return Err(Status::internal("SMTP not configured".to_string())); } if user.email_mfa_enabled { @@ -942,10 +942,20 @@ impl EnrollmentServer { Status::internal("Failed to setup email mfa".to_string()) })?; info!("Created email secret for {}", &user.username); - send_email_mfa_activation_email(&user, None).map_err(|e| { - error!("Failed to send email mfa activation email.\nReason:{e}"); - Status::internal("Failed to send activation email".to_string()) + let mut transaction = self.pool.begin().await.map_err(|err| { + error!("Failed to begin database transaction\nReason:{err}"); + Status::internal("Failed begin database transaction".to_string()) + })?; + let code = user.generate_email_mfa_code().map_err(|err| { + error!("Failed to generate MFA code for {user}\nReason:{err}"); + Status::internal("Failed to generate MFA code".to_string()) })?; + mfa_activation_mail(&user.email, &mut transaction, &user.first_name, &code, None) + .await + .map_err(|err| { + error!("Failed to send MFA activation email\nReason:{err}"); + Status::internal("Failed to send activation email".to_string()) + })?; Ok(CodeMfaSetupStartResponse { totp_secret: None }) } MfaMethod::Totp => { @@ -955,10 +965,10 @@ impl EnrollmentServer { )); } let secret = user.new_totp_secret(&self.pool).await.map_err(|_| { - error!("Failed to make new totp secret"); - Status::internal(String::new()) + error!("Failed to make new TOTP secret"); + Status::internal("Failed to make new TOTP secret".to_string()) })?; - info!("New totp secret created for {}", &user.username); + info!("New TOTP secret created for {}", &user.username); Ok(CodeMfaSetupStartResponse { totp_secret: Some(secret), }) @@ -1022,8 +1032,12 @@ impl EnrollmentServer { .await .map_err(|_| Status::internal("Failed to get recovery codes.".to_string()))? .ok_or_else(|| Status::internal("Recovery codes not found".to_string()))?; - if let Err(e) = send_mfa_configured_email(None, &user, &mfa_method) { - error!("Failed to send mfa configured email\nReason: {e}"); + if let Ok(mut conn) = self.pool.begin().await { + if let Err(err) = mfa_configured_mail(&user.email, &mut conn, None, &mfa_method).await { + error!("Failed to send MFA configured email\nReason: {err}"); + } + } else { + error!("Failed to begin database session"); } info!( "Successfully enabled MFA method {} for user {}", @@ -1093,94 +1107,3 @@ pub async fn new_polling_token(pool: &PgPool, device: &Device) -> Result Apps).'), + ('new-device-ocid-login', 'label_profile', 'en_US', 'Profile URL:'), + ('new-device-ocid-login', 'label_oauth2client', 'en_US', 'System name:'), + ('password-reset', 'title', 'en_US', 'Password reset'), + ('password-reset', 'subtitle', 'en_US', 'If you wish to reset your password, please copy and paste the following URL in your browser:'), + ('password-reset-done', 'title', 'en_US', 'Password reset'), + ('password-reset-done', 'subtitle', 'en_US', 'Your password has been successfully changed.'), + ('test', 'title', 'en_US', 'This is test email from Defguard system.'), + ('test', 'subtitle', 'en_US', 'If you received it, your SMTP configuration is correct.'), + ('support-data', 'title', 'en_US', 'Support data'), + ('support-data', 'subtitle', 'en_US', 'Support data can be found in the attachment.');