From fd4fecf945b0140ba77f9636752091bbd4bd222e Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 2 Nov 2024 16:20:16 +0100 Subject: [PATCH] split better oss and ee --- backend/Cargo.toml | 6 +- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/src/lib.rs | 2 + backend/windmill-api/src/settings.rs | 2 +- backend/windmill-api/src/users_ee.rs | 41 +++ backend/windmill-api/src/utils.rs | 19 -- backend/windmill-api/src/workspaces.rs | 304 +--------------------- backend/windmill-api/src/workspaces_ee.rs | 16 ++ backend/windmill-common/src/ee.rs~main | 73 ------ backend/windmill-common/src/ee.rs~main_0 | 76 ------ backend/windmill-common/src/email_ee.rs | 11 + backend/windmill-common/src/lib.rs | 1 + backend/windmill-common/src/utils.rs | 56 +--- 13 files changed, 88 insertions(+), 521 deletions(-) create mode 100644 backend/windmill-api/src/users_ee.rs create mode 100644 backend/windmill-api/src/workspaces_ee.rs delete mode 100644 backend/windmill-common/src/ee.rs~main delete mode 100644 backend/windmill-common/src/ee.rs~main_0 create mode 100644 backend/windmill-common/src/email_ee.rs diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4f14fc5df1..1c0abc299c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -160,7 +160,7 @@ hex = "^0" sql-builder = "^3" argon2 = "^0" quick_cache = "^0" -rand = "0.8.5" +rand = "^0" rand_core = { version = "^0", features = ["std"] } magic-crypt = "^3" git-version = "^0" @@ -172,7 +172,7 @@ urlencoding = "^2" url = "^2" async-oauth2 = "^0" reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] } -time = "0.3.16" +time = "^0" serde_urlencoded = "^0" tokio-tar = "^0" tempfile = "^3" @@ -198,7 +198,7 @@ swc_ecma_visit = "=0.104.8" async-recursion = "^1" -base64 = "0.21.0" +base64 = "^0" base32 = "^0" hmac = "0.12.1" sha2 = "0.10.6" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 78fe26255f..e7dd9365ec 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -150c3176f2c192366f2e8298ef6ece43780b529e \ No newline at end of file +f136a2f499e0fe7c10c54c79488851980d796eb2 \ No newline at end of file diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index dc9dbd2529..3ea78ffd6d 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -84,12 +84,14 @@ mod stripe_ee; mod tracing_init; mod triggers; mod users; +mod users_ee; mod utils; mod variables; mod webhook_util; mod websocket_triggers; mod workers; mod workspaces; +mod workspaces_ee; pub const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version"); diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 3c71f22eb3..0299145a38 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -28,13 +28,13 @@ use serde::Deserialize; #[cfg(feature = "enterprise")] use windmill_common::ee::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel}; use windmill_common::{ + email_ee::send_email, error::{self, JsonResult, Result}, global_settings::{ AUTOMATE_USERNAME_CREATION_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING, }, server::Smtp, - utils::send_email, }; #[cfg(feature = "parquet")] diff --git a/backend/windmill-api/src/users_ee.rs b/backend/windmill-api/src/users_ee.rs new file mode 100644 index 0000000000..e0f15a9317 --- /dev/null +++ b/backend/windmill-api/src/users_ee.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use crate::db::ApiAuthed; + +use crate::users::{EditPassword, NewUser}; +use crate::{db::DB, webhook_util::WebhookShared}; +use argon2::Argon2; + +use http::StatusCode; + +use windmill_common::error::{Error, Result}; + +pub async fn create_user( + _authed: ApiAuthed, + _db: DB, + _webhook: WebhookShared, + _argon2: Arc>, + _rsmq: Option, + mut _nu: NewUser, +) -> Result<(StatusCode, String)> { + Err(Error::InternalErr( + "Not implemented in Windmill's Open Source repository".to_string(), + )) +} + +pub async fn set_password( + _db: DB, + _argon2: Arc>, + _authed: ApiAuthed, + _ep: EditPassword, +) -> Result { + Err(Error::InternalErr( + "Not implemented in Windmill's Open Source repository".to_string(), + )) +} + +pub fn send_email_if_possible(_subject: &str, _content: &str, _to: &str) { + tracing::warn!( + "send_email_if_possible is not implemented in Windmill's Open Source repository" + ); +} diff --git a/backend/windmill-api/src/utils.rs b/backend/windmill-api/src/utils.rs index d8a81ab741..ea4fccc160 100644 --- a/backend/windmill-api/src/utils.rs +++ b/backend/windmill-api/src/utils.rs @@ -155,25 +155,6 @@ pub async fn get_instance_username_or_create_pending<'c>( } } -pub async fn get_and_delete_pending_username_or_generate<'c>( - tx: &mut Transaction<'c, Postgres>, - email: &str, -) -> error::Result { - let username = sqlx::query_scalar!("SELECT username FROM pending_user WHERE email = $1", email) - .fetch_optional(&mut **tx) - .await?; - - if let Some(username) = username { - sqlx::query!("DELETE FROM pending_user WHERE email = $1", email) - .execute(&mut **tx) - .await?; - Ok(username) - } else { - let username = generate_instance_wide_unique_username(&mut *tx, email).await?; - Ok(username) - } -} - pub fn content_plain(body: Body) -> Response { use axum::http::header; Response::builder() diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index cf48781331..018ac8de5f 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -9,14 +9,15 @@ use std::collections::HashMap; use crate::db::ApiAuthed; -use crate::utils::{get_instance_username_or_create_pending, INVALID_USERNAME_CHARS}; +use crate::users_ee::send_email_if_possible; +use crate::utils::get_instance_username_or_create_pending; use crate::BASE_URL; use crate::{ apps::AppWithLastVersion, db::DB, folders::Folder, resources::{Resource, ResourceType}, - users::{send_email_if_possible, WorkspaceInvite, VALID_USERNAME}, + users::{WorkspaceInvite, VALID_USERNAME}, utils::require_super_admin, webhook_util::WebhookShared, }; @@ -34,7 +35,7 @@ use itertools::Itertools; use regex::Regex; use uuid::Uuid; -use windmill_audit::audit_ee::{audit_log, AuditAuthor, AuditAuthorable}; +use windmill_audit::audit_ee::audit_log; use windmill_audit::ActionKind; use windmill_common::db::UserDB; use windmill_common::s3_helpers::LargeFileStorage; @@ -218,11 +219,12 @@ struct EditDeployTo { deploy_to: Option, } +#[allow(dead_code)] #[derive(Deserialize)] -struct EditAutoInvite { - operator: Option, - invite_all: Option, - auto_add: Option, +pub struct EditAutoInvite { + pub operator: Option, + pub invite_all: Option, + pub auto_add: Option, } #[derive(Deserialize)] @@ -573,250 +575,21 @@ async fn edit_deploy_to() -> Result { )); } -const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt"); +pub const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt"); async fn is_allowed_auto_domain(ApiAuthed { email, .. }: ApiAuthed) -> JsonResult { let domain = email.split('@').last().unwrap(); return Ok(Json(!BANNED_DOMAINS.contains(domain))); } -async fn auto_add_user( - email: &str, - w_id: &str, - operator: &bool, - tx: &mut Transaction<'_, Postgres>, - authorable: &impl AuditAuthorable, -) -> Result { - let automate_username_creation = sqlx::query_scalar!( - "SELECT value FROM global_settings WHERE name = $1", - AUTOMATE_USERNAME_CREATION_SETTING, - ) - .fetch_optional(&mut **tx) - .await? - .map(|v| v.as_bool()) - .flatten() - .unwrap_or(false); - - let username = if automate_username_creation { - get_instance_username_or_create_pending(&mut *tx, &email).await? - } else { - let mut username = email - .split('@') - .next() - .unwrap() - .to_string() - .replace(".", ""); - - username = INVALID_USERNAME_CHARS - .replace_all(&mut username, "") - .to_string(); - - if username.is_empty() { - username = "user".to_string() - } - - let base_username = username.clone(); - let mut username_conflict = true; - let mut i = 1; - while username_conflict { - if i > 1000 { - return Err(Error::InternalErr(format!( - "too many username conflicts for {}", - email - ))); - } - if i > 1 { - username = format!("{}{}", base_username, i) - } - username_conflict = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)", - &username, - &w_id - ) - .fetch_one(&mut **tx) - .await? - .unwrap_or(false); - i += 1; - } - username - }; - - sqlx::query!( - "INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING", - &w_id, - &username, - &email, - &operator - ) - .execute(&mut **tx) - .await?; - - sqlx::query_as!( - Group, - "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", - &w_id, - username, - "all", - ) - .execute(&mut **tx) - .await?; - let audit_author = if authorable.username() == authorable.email() && authorable.email() == email - { - // if the user is auto adding themselves (e.g. by joining the instance), we use their newly created workspace username for audit logs - AuditAuthor { - username: username.clone(), - email: email.to_string(), - username_override: None, - } - } else { - AuditAuthor { - username: authorable.username().to_string(), - email: authorable.email().to_string(), - username_override: authorable.username_override().map(|x| x.to_string()), - } - }; - audit_log( - &mut **tx, - &audit_author, - "users.auto_invite_add", - ActionKind::Create, - &w_id, - Some(email), - None, - ) - .await?; - Ok(username) -} - async fn edit_auto_invite( authed: ApiAuthed, Extension(db): Extension, Extension(rsmq): Extension>, Path(w_id): Path, - ApiAuthed { is_admin, email, username, .. }: ApiAuthed, Json(ea): Json, ) -> Result { - require_admin(is_admin, &username)?; - - // #[cfg(not(feature = "enterprise"))] - // { - // return Err(Error::BadRequest( - // "Auto-invite is only available on enterprise".to_string(), - // )); - // } - - let domain = if ea.invite_all.is_some_and(|x| x) { - if *CLOUD_HOSTED { - return Err(Error::BadRequest( - "invite_all is only available locally".to_string(), - )); - } else { - "*" - } - } else { - email.split('@').last().unwrap() - }; - - let mut tx = db.begin().await?; - - let mut users_to_auto_add = Option::None; - - if let (Some(operator), Some(auto_add)) = (ea.operator, ea.auto_add) { - if BANNED_DOMAINS.contains(domain) { - return Err(Error::BadRequest(format!( - "Domain {} is not allowed", - domain - ))); - } - - sqlx::query!( - "UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2, auto_add = $4 WHERE workspace_id = $3", - domain, - operator, - &w_id, - auto_add, - ) - .execute(&mut *tx) - .await?; - - if auto_add { - users_to_auto_add = Some(sqlx::query!( - "SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS ( - SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email - )", - &w_id, - domain - ) - .fetch_all(&mut *tx).await?); - - for user in users_to_auto_add.as_ref().unwrap() { - auto_add_user(&user.email, &w_id, &operator, &mut tx, &authed).await?; - send_email_if_possible( - &format!("Added to Windmill's workspace: {w_id}"), - &format!( - "You have been granted access to Windmill's workspace {w_id} by {email}. - - Access the workspace at {}/?workspace={w_id}", - BASE_URL.read().await.clone() - ), - &user.email, - ); - } - } else { - sqlx::query!( - "INSERT INTO workspace_invite - (workspace_id, email, is_admin, operator) - SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS ( - SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email - ) - ON CONFLICT DO NOTHING", - &w_id, - domain, - operator - ) - .execute(&mut *tx) - .await?; - } - } else { - sqlx::query!( - "UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL, auto_add = NULL WHERE workspace_id = $1", - &w_id, - ) - .execute(&mut *tx) - .await?; - } - audit_log( - &mut *tx, - &authed, - "workspaces.edit_auto_invite_domain", - ActionKind::Update, - &w_id, - Some(&authed.email), - Some([("operator", &format!("{:?}", ea.operator)[..])].into()), - ) - .await?; - tx.commit().await?; - - if let Some(users) = users_to_auto_add { - for user in users { - handle_deployment_metadata( - &email, - &username, - &db, - &w_id, - windmill_git_sync::DeployedObject::User { email: user.email.clone() }, - Some(format!("Auto-added user '{}' to workspace", &user.email)), - rsmq.clone(), - true, - ) - .await?; - } - } - - Ok(format!( - "Edit auto-invite for workspace {} to {}", - &w_id, domain - )) + crate::workspaces_ee::edit_auto_invite(authed, db, rsmq, w_id, ea).await } async fn edit_webhook( @@ -2001,61 +1774,6 @@ async fn delete_workspace( Ok(format!("Deleted workspace {}", &w_id)) } -pub async fn invite_user_to_all_auto_invite_worspaces( - db: &DB, - email: &str, - rsmq: Option, - authorable: &impl AuditAuthorable, -) -> Result<()> { - let mut tx = db.begin().await?; - let domain = email.split('@').last().unwrap(); - let workspaces = sqlx::query!( - "SELECT workspace_id, auto_invite_operator, auto_add FROM workspace_settings ws WHERE (auto_invite_domain = $1 OR auto_invite_domain = '*') AND NOT EXISTS (SELECT 1 FROM usr WHERE workspace_id = ws.workspace_id AND email = $2)", - domain, - email - ) - .fetch_all(&mut *tx) - .await?; - let mut auto_added_workspace_usernames: Vec<(String, String)> = vec![]; - for r in workspaces { - if r.auto_add.is_some() && r.auto_add.unwrap() { - let operator = r.auto_invite_operator.unwrap_or(false); - let username = - auto_add_user(email, &r.workspace_id, &operator, &mut tx, authorable).await?; - auto_added_workspace_usernames.push((r.workspace_id, username)); - } else { - sqlx::query!( - "INSERT INTO workspace_invite - (workspace_id, email, is_admin, operator) - VALUES ($1, $2, false, $3) - ON CONFLICT DO NOTHING", - r.workspace_id, - email, - r.auto_invite_operator - ) - .execute(&mut *tx) - .await?; - } - } - tx.commit().await?; - - for workspace_username_tuple in auto_added_workspace_usernames { - let (w_id, username) = workspace_username_tuple; - handle_deployment_metadata( - &email, - &username, - db, - &w_id, - windmill_git_sync::DeployedObject::User { email: email.to_string() }, - Some(format!("Auto-added user '{}' to workspace", email)), - rsmq.clone(), - true, - ) - .await?; - } - Ok(()) -} - async fn invite_user( ApiAuthed { username, is_admin, .. }: ApiAuthed, Extension(db): Extension, diff --git a/backend/windmill-api/src/workspaces_ee.rs b/backend/windmill-api/src/workspaces_ee.rs new file mode 100644 index 0000000000..565a53b174 --- /dev/null +++ b/backend/windmill-api/src/workspaces_ee.rs @@ -0,0 +1,16 @@ +use crate::{ + db::{ApiAuthed, DB}, + workspaces::EditAutoInvite, +}; + +pub async fn edit_auto_invite( + _authed: ApiAuthed, + _db: DB, + _rsmq: Option, + _w_id: String, + _ea: EditAutoInvite, +) -> windmill_common::error::Result { + Err(windmill_common::error::Error::InternalErr( + "Not implemented on OSS".to_string(), + )) +} diff --git a/backend/windmill-common/src/ee.rs~main b/backend/windmill-common/src/ee.rs~main deleted file mode 100644 index 482b61a0fe..0000000000 --- a/backend/windmill-common/src/ee.rs~main +++ /dev/null @@ -1,73 +0,0 @@ -#[cfg(feature = "enterprise")] -use crate::db::DB; -use crate::ee::LicensePlan::Community; -#[cfg(feature = "enterprise")] -use crate::error; -use serde::Deserialize; -use std::sync::Arc; -use tokio::sync::RwLock; - -lazy_static::lazy_static! { - pub static ref LICENSE_KEY_VALID: Arc> = Arc::new(RwLock::new(true)); - pub static ref LICENSE_KEY_ID: Arc> = Arc::new(RwLock::new("".to_string())); - pub static ref LICENSE_KEY: Arc> = Arc::new(RwLock::new("".to_string())); -} - -pub enum LicensePlan { - Community, - Pro, - Enterprise, -} - -pub async fn get_license_plan() -> LicensePlan { - // Implementation is not open source - return Community; -} - -#[derive(Deserialize)] -#[serde(untagged)] -pub enum CriticalErrorChannel {} - -pub enum CriticalAlertKind { - #[cfg(feature = "enterprise")] - CriticalError, - #[cfg(feature = "enterprise")] - RecoveredCriticalError, -} - -#[cfg(feature = "enterprise")] -pub async fn send_critical_alert( - _error_message: String, - _db: &DB, - _kind: CriticalAlertKind, - _channels: Option>, -) { -} - -#[cfg(feature = "enterprise")] -pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () { - // Implementation is not open source -} - -#[cfg(feature = "enterprise")] -pub async fn renew_license_key( - _http_client: &reqwest::Client, - _db: &crate::db::DB, - _key: Option, - _manual: bool, -) -> String { - // Implementation is not open source - "".to_string() -} - -#[cfg(feature = "enterprise")] -pub async fn create_customer_portal_session( - _http_client: &reqwest::Client, - _key: Option, -) -> error::Result { - // Implementation is not open source - Ok("".to_string()) -} - -#[cfg(feature = "enterprise")] -pub async fn worker_groups_alerts(_db: &DB) {} diff --git a/backend/windmill-common/src/ee.rs~main_0 b/backend/windmill-common/src/ee.rs~main_0 deleted file mode 100644 index 2f32756e45..0000000000 --- a/backend/windmill-common/src/ee.rs~main_0 +++ /dev/null @@ -1,76 +0,0 @@ -#[cfg(feature = "enterprise")] -use crate::db::DB; -use crate::ee::LicensePlan::Community; -#[cfg(feature = "enterprise")] -use crate::error; -use serde::Deserialize; -use std::sync::Arc; -use tokio::sync::RwLock; - -lazy_static::lazy_static! { - pub static ref LICENSE_KEY_VALID: Arc> = Arc::new(RwLock::new(true)); - pub static ref LICENSE_KEY_ID: Arc> = Arc::new(RwLock::new("".to_string())); - pub static ref LICENSE_KEY: Arc> = Arc::new(RwLock::new("".to_string())); -} - -pub enum LicensePlan { - Community, - Pro, - Enterprise, -} - -pub async fn get_license_plan() -> LicensePlan { - // Implementation is not open source - return Community; -} - -#[derive(Deserialize)] -#[serde(untagged)] -pub enum CriticalErrorChannel { - Email { email: String }, - Slack { slack_channel: String }, -} - -pub enum CriticalAlertKind { - #[cfg(feature = "enterprise")] - CriticalError, - #[cfg(feature = "enterprise")] - RecoveredCriticalError, -} - -#[cfg(feature = "enterprise")] -pub async fn send_critical_alert( - _error_message: String, - _db: &DB, - _kind: CriticalAlertKind, - _channels: Option>, -) { -} - -#[cfg(feature = "enterprise")] -pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () { - // Implementation is not open source -} - -#[cfg(feature = "enterprise")] -pub async fn renew_license_key( - _http_client: &reqwest::Client, - _db: &crate::db::DB, - _key: Option, - _manual: bool, -) -> String { - // Implementation is not open source - "".to_string() -} - -#[cfg(feature = "enterprise")] -pub async fn create_customer_portal_session( - _http_client: &reqwest::Client, - _key: Option, -) -> error::Result { - // Implementation is not open source - Ok("".to_string()) -} - -#[cfg(feature = "enterprise")] -pub async fn worker_groups_alerts(_db: &DB) {} diff --git a/backend/windmill-common/src/email_ee.rs b/backend/windmill-common/src/email_ee.rs new file mode 100644 index 0000000000..42aebbeec3 --- /dev/null +++ b/backend/windmill-common/src/email_ee.rs @@ -0,0 +1,11 @@ +use crate::server::Smtp; + +pub async fn send_email( + _subject: &str, + _content: &str, + _to: Vec, + _smtp: Smtp, + _client_timeout: Option, +) -> crate::error::Result<()> { + Ok(()) +} diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index bcac7b85fb..ad96594ac8 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -22,6 +22,7 @@ pub mod auth; pub mod bench; pub mod db; pub mod ee; +pub mod email_ee; pub mod error; pub mod external_ip; pub mod flow_status; diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 3feab853dd..04f95a3406 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -11,13 +11,11 @@ use crate::ee::LICENSE_KEY_ID; use crate::ee::{send_critical_alert, CriticalAlertKind}; use crate::error::{to_anyhow, Error, Result}; use crate::global_settings::UNIQUE_ID_SETTING; -use crate::server::Smtp; use crate::DB; use anyhow::Context; use gethostname::gethostname; use git_version::git_version; -use mail_send::mail_builder::MessageBuilder; -use mail_send::SmtpClientBuilder; + use rand::{distributions::Alphanumeric, thread_rng, Rng}; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -243,58 +241,6 @@ pub fn generate_lock_id(database_name: &str) -> i64 { 0x3d32ad9e * (CRC_IEEE.checksum(database_name.as_bytes()) as i64) } -pub async fn send_email( - subject: &str, - content: &str, - to: Vec, - smtp: Smtp, - client_timeout: Option, -) -> Result<()> { - let mut client = SmtpClientBuilder::new(smtp.host, smtp.port) - .implicit_tls(smtp.tls_implicit.unwrap_or(false)); - if std::env::var("ACCEPT_INVALID_CERTS").is_ok() { - client = client.allow_invalid_certs(); - } - let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) { - if !username.is_empty() { - client.credentials((username, password)) - } else { - client - } - } else { - client - }; - let message = MessageBuilder::new() - .from(("Windmill", smtp.from.as_str())) - .to(to.clone()) - .subject(subject) - .text_body(content); - - match client_timeout { - Some(timeout) => { - tokio::time::timeout(timeout, client.connect()) - .await - .map_err(to_anyhow)? - .map_err(to_anyhow)? - .send(message) - .await - .map_err(to_anyhow)?; - } - None => { - client - .connect() - .await - .map_err(to_anyhow)? - .send(message) - .await - .map_err(to_anyhow)?; - } - } - tracing::info!("Sent email to {:#?}: {subject}", to); - - return Ok(()); -} - pub async fn report_critical_error(error_message: String, _db: DB) -> () { tracing::error!("CRITICAL ERROR: {error_message}");