mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-26 07:50:02 +02:00 
			
		
		
		
	Merge pull request #3147 from soruh/main
add support for system mta though sendmail
This commit is contained in:
		| @@ -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 ','. | ||||
|   | ||||
							
								
								
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| use std::env::consts::EXE_SUFFIX; | ||||
| use std::process::exit; | ||||
| use std::sync::RwLock; | ||||
|  | ||||
| @@ -614,6 +615,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 +658,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 +749,62 @@ 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 { | ||||
|             let command = cfg.sendmail_command.clone().unwrap_or_else(|| format!("sendmail{EXE_SUFFIX}")); | ||||
|  | ||||
|             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:?}`")) | ||||
|                 } | ||||
|                 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 +1085,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 { | ||||
|   | ||||
							
								
								
									
										95
									
								
								src/mail.rs
									
									
									
									
									
								
							
							
						
						
									
										95
									
								
								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<Tokio1Executor> { | ||||
| fn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> { | ||||
|     if let Some(command) = CONFIG.sendmail_command() { | ||||
|         AsyncSendmailTransport::new_with_command(command) | ||||
|     } else { | ||||
|         AsyncSendmailTransport::new() | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> { | ||||
|     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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user