From b7c4316c778568d23cdd7e3922e03cf16a3fec18 Mon Sep 17 00:00:00 2001 From: soruh Date: Sun, 12 Feb 2023 18:53:55 +0100 Subject: [PATCH 1/3] Add support for sendmail as a mail transport --- .env.template | 7 +++- Cargo.toml | 2 +- src/config.rs | 62 ++++++++++++++++++++++++++------- src/mail.rs | 95 ++++++++++++++++++++++++++++++++++----------------- 4 files changed, 119 insertions(+), 47 deletions(-) diff --git a/.env.template b/.env.template index 1b691298..d2eb768e 100644 --- a/.env.template +++ b/.env.template @@ -373,7 +373,7 @@ # ROCKET_WORKERS=10 # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} -## Mail specific settings, set SMTP_HOST and SMTP_FROM to enable the mail service. +## Mail specific settings, set SMTP_FROM and either SMTP_HOST or USE_SENDMAIL to enable the mail service. ## To make sure the email links are pointing to the correct host, set the DOMAIN variable. ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory # SMTP_HOST=smtp.domain.tld @@ -385,6 +385,11 @@ # SMTP_PASSWORD=password # SMTP_TIMEOUT=15 +# Whether to send mail via the `sendmail` command +# USE_SENDMAIL=false +# Which sendmail command to use. The one found in the $PATH is used if not specified. +# SENDMAIL_COMMAND="/path/to/sendmail" + ## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. ## Possible values: ["Plain", "Login", "Xoauth2"]. ## Multiple options need to be separated by a comma ','. diff --git a/Cargo.toml b/Cargo.toml index c83f8fed..19825465 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,7 +114,7 @@ webauthn-rs = "0.3.2" url = "2.3.1" # Email librariese-Base, Update crates and small change. -lettre = { version = "0.10.1", features = ["smtp-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } +lettre = { version = "0.10.1", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } percent-encoding = "2.2.0" # URL encoding library used for URL's in the emails email_address = "0.2.4" diff --git a/src/config.rs b/src/config.rs index 61700c8f..b7eabd36 100644 --- a/src/config.rs +++ b/src/config.rs @@ -614,6 +614,10 @@ make_config! { smtp: _enable_smtp { /// Enabled _enable_smtp: bool, true, def, true; + /// Use Sendmail |> Whether to send mail via the `sendmail` command + use_sendmail: bool, true, def, false; + /// Sendmail Command |> Which sendmail command to use. The one found in the $PATH is used if not specified. + sendmail_command: String, true, option; /// Host smtp_host: String, true, option; /// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY @@ -653,7 +657,7 @@ make_config! { /// Email 2FA Settings email_2fa: _enable_email_2fa { /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured - _enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some(); + _enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail); /// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting. email_token_size: u8, true, def, 6; /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token. @@ -744,27 +748,59 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { ), } - if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() { - err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support") + if cfg.use_sendmail { + if let Some(ref command) = cfg.sendmail_command { + let path = std::path::Path::new(&command); + + if !path.is_absolute() { + err!(format!("path to sendmail command `{path:?}` is not absolute")); + } + + match path.metadata() { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + err!(format!("sendmail command not found at `{path:?}`")) + } + Err(err) => { + err!(format!("failed to access sendmail command at `{path:?}`: {err}")) + } + Ok(metadata) => { + if metadata.is_dir() { + err!(format!("sendmail command at `{path:?}` isn't a directory")); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if !metadata.permissions().mode() & 0o111 != 0 { + err!(format!("sendmail command at `{path:?}` isn't executable")); + } + } + } + } + } + } else { + if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() { + err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`") + } + + if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() { + err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`") + } } - if cfg.smtp_host.is_some() && !cfg.smtp_from.contains('@') { + if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !cfg.smtp_from.contains('@') { err!("SMTP_FROM does not contain a mandatory @ sign") } - if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() { - err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication") - } - - if cfg._enable_email_2fa && (!cfg._enable_smtp || cfg.smtp_host.is_none()) { - err!("To enable email 2FA, SMTP must be configured") - } - if cfg._enable_email_2fa && cfg.email_token_size < 6 { err!("`EMAIL_TOKEN_SIZE` has a minimum size of 6") } } + if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) { + err!("To enable email 2FA, a mail transport must be configured") + } + // Check if the icon blacklist regex is valid if let Some(ref r) = cfg.icon_blacklist_regex { let validate_regex = regex::Regex::new(r); @@ -1045,7 +1081,7 @@ impl Config { } pub fn mail_enabled(&self) -> bool { let inner = &self.inner.read().unwrap().config; - inner._enable_smtp && inner.smtp_host.is_some() + inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail) } pub fn get_duo_akey(&self) -> String { diff --git a/src/mail.rs b/src/mail.rs index cffa65fb..565a1f2c 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -8,7 +8,7 @@ use lettre::{ transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism}, transport::smtp::client::{Tls, TlsParameters}, transport::smtp::extension::ClientId, - Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, + Address, AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, }; use crate::{ @@ -21,7 +21,15 @@ use crate::{ CONFIG, }; -fn mailer() -> AsyncSmtpTransport { +fn sendmail_transport() -> AsyncSendmailTransport { + if let Some(command) = CONFIG.sendmail_command() { + AsyncSendmailTransport::new_with_command(command) + } else { + AsyncSendmailTransport::new() + } +} + +fn smtp_transport() -> AsyncSmtpTransport { use std::time::Duration; let host = CONFIG.smtp_host().unwrap(); @@ -509,6 +517,58 @@ pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: send_email(address, &subject, body_html, body_text).await } +async fn send_with_selected_transport(email: Message) -> EmptyResult { + if CONFIG.use_sendmail() { + match sendmail_transport().send(email).await { + Ok(_) => Ok(()), + // Match some common errors and make them more user friendly + Err(e) => { + if e.is_client() { + debug!("Sendmail client error: {:#?}", e); + err!(format!("Sendmail client error: {e}")); + } else if e.is_response() { + debug!("Sendmail response error: {:#?}", e); + err!(format!("Sendmail response error: {e}")); + } else { + debug!("Sendmail error: {:#?}", e); + err!(format!("Sendmail error: {e}")); + } + } + } + } else { + match smtp_transport().send(email).await { + Ok(_) => Ok(()), + // Match some common errors and make them more user friendly + Err(e) => { + if e.is_client() { + debug!("SMTP client error: {:#?}", e); + err!(format!("SMTP client error: {e}")); + } else if e.is_transient() { + debug!("SMTP 4xx error: {:#?}", e); + err!(format!("SMTP 4xx error: {e}")); + } else if e.is_permanent() { + debug!("SMTP 5xx error: {:#?}", e); + let mut msg = e.to_string(); + // Add a special check for 535 to add a more descriptive message + if msg.contains("(535)") { + msg = format!("{msg} - Authentication credentials invalid"); + } + err!(format!("SMTP 5xx error: {msg}")); + } else if e.is_timeout() { + debug!("SMTP timeout error: {:#?}", e); + err!(format!("SMTP timeout error: {e}")); + } else if e.is_tls() { + debug!("SMTP encryption error: {:#?}", e); + err!(format!("SMTP encryption error: {e}")); + } else { + debug!("SMTP error: {:#?}", e); + err!(format!("SMTP error: {e}")); + } + } + } + } +} + async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult { let smtp_from = &CONFIG.smtp_from(); @@ -538,34 +598,5 @@ async fn send_email(address: &str, subject: &str, body_html: String, body_text: .subject(subject) .multipart(body)?; - match mailer().send(email).await { - Ok(_) => Ok(()), - // Match some common errors and make them more user friendly - Err(e) => { - if e.is_client() { - debug!("SMTP Client error: {:#?}", e); - err!(format!("SMTP Client error: {e}")); - } else if e.is_transient() { - debug!("SMTP 4xx error: {:#?}", e); - err!(format!("SMTP 4xx error: {e}")); - } else if e.is_permanent() { - debug!("SMTP 5xx error: {:#?}", e); - let mut msg = e.to_string(); - // Add a special check for 535 to add a more descriptive message - if msg.contains("(535)") { - msg = format!("{msg} - Authentication credentials invalid"); - } - err!(format!("SMTP 5xx error: {msg}")); - } else if e.is_timeout() { - debug!("SMTP timeout error: {:#?}", e); - err!(format!("SMTP timeout error: {e}")); - } else if e.is_tls() { - debug!("SMTP Encryption error: {:#?}", e); - err!(format!("SMTP Encryption error: {e}")); - } else { - debug!("SMTP {:#?}", e); - err!(format!("SMTP {e}")); - } - } - } + send_with_selected_transport(email).await } From 8cc6dac893e3a7630a6be438ac4331f0f7023dad Mon Sep 17 00:00:00 2001 From: soruh Date: Wed, 25 Jan 2023 22:54:50 +0100 Subject: [PATCH 2/3] check if SENDMAIL_COMMAND is valid using 'which' crate --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 1 + src/config.rs | 43 +++++++++++++++++++++++-------------------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dcff766c..1215fdb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3230,6 +3230,7 @@ dependencies = [ "url", "uuid", "webauthn-rs", + "which", "yubico", ] @@ -3396,6 +3397,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "widestring" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 19825465..88a3b4b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,6 +151,7 @@ semver = "1.0.16" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow mimalloc = { version = "0.1.34", features = ["secure"], default-features = false, optional = true } +which = "4.4.0" # Strip debuginfo from the release builds # Also enable thin LTO for some optimizations diff --git a/src/config.rs b/src/config.rs index b7eabd36..e362464b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -749,31 +749,34 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } if cfg.use_sendmail { - if let Some(ref command) = cfg.sendmail_command { - let path = std::path::Path::new(&command); + let command = cfg.sendmail_command.as_deref().unwrap_or("sendmail"); - if !path.is_absolute() { - err!(format!("path to sendmail command `{path:?}` is not absolute")); + let mut path = std::path::PathBuf::from(command); + + if !path.is_absolute() { + match which::which(command) { + Ok(result) => path = result, + Err(_) => err!(format!("sendmail command {command:?} not found in $PATH")), } + } - match path.metadata() { - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - err!(format!("sendmail command not found at `{path:?}`")) + match path.metadata() { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + err!(format!("sendmail command not found at `{path:?}`")) + } + Err(err) => { + err!(format!("failed to access sendmail command at `{path:?}`: {err}")) + } + Ok(metadata) => { + if metadata.is_dir() { + err!(format!("sendmail command at `{path:?}` isn't a directory")); } - Err(err) => { - err!(format!("failed to access sendmail command at `{path:?}`: {err}")) - } - Ok(metadata) => { - if metadata.is_dir() { - err!(format!("sendmail command at `{path:?}` isn't a directory")); - } - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if !metadata.permissions().mode() & 0o111 != 0 { - err!(format!("sendmail command at `{path:?}` isn't executable")); - } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if !metadata.permissions().mode() & 0o111 != 0 { + err!(format!("sendmail command at `{path:?}` isn't executable")); } } } From 9e3d7ea44c2813ffaddb0accd768407ad1664a55 Mon Sep 17 00:00:00 2001 From: soruh Date: Thu, 2 Feb 2023 22:05:44 +0100 Subject: [PATCH 3/3] add EXE_SUFFIX to sendmail executable when not specified --- src/config.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index e362464b..fa53c55b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use std::env::consts::EXE_SUFFIX; use std::process::exit; use std::sync::RwLock; @@ -749,12 +750,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } if cfg.use_sendmail { - let command = cfg.sendmail_command.as_deref().unwrap_or("sendmail"); + let command = cfg.sendmail_command.clone().unwrap_or_else(|| format!("sendmail{EXE_SUFFIX}")); - let mut path = std::path::PathBuf::from(command); + let mut path = std::path::PathBuf::from(&command); if !path.is_absolute() { - match which::which(command) { + match which::which(&command) { Ok(result) => path = result, Err(_) => err!(format!("sendmail command {command:?} not found in $PATH")), }