Add support for sendmail as a mail transport

This commit is contained in:
soruh 2023-02-12 18:53:55 +01:00
parent 0c295d5e6e
commit b7c4316c77
4 changed files with 119 additions and 47 deletions

View file

@ -373,7 +373,7 @@
# ROCKET_WORKERS=10 # ROCKET_WORKERS=10
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} # 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. ## 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 ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
# SMTP_HOST=smtp.domain.tld # SMTP_HOST=smtp.domain.tld
@ -385,6 +385,11 @@
# SMTP_PASSWORD=password # SMTP_PASSWORD=password
# SMTP_TIMEOUT=15 # 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. ## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections.
## Possible values: ["Plain", "Login", "Xoauth2"]. ## Possible values: ["Plain", "Login", "Xoauth2"].
## Multiple options need to be separated by a comma ','. ## Multiple options need to be separated by a comma ','.

View file

@ -114,7 +114,7 @@ webauthn-rs = "0.3.2"
url = "2.3.1" url = "2.3.1"
# Email librariese-Base, Update crates and small change. # 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 percent-encoding = "2.2.0" # URL encoding library used for URL's in the emails
email_address = "0.2.4" email_address = "0.2.4"

View file

@ -614,6 +614,10 @@ make_config! {
smtp: _enable_smtp { smtp: _enable_smtp {
/// Enabled /// Enabled
_enable_smtp: bool, true, def, true; _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 /// Host
smtp_host: String, true, option; smtp_host: String, true, option;
/// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY /// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY
@ -653,7 +657,7 @@ make_config! {
/// Email 2FA Settings /// Email 2FA Settings
email_2fa: _enable_email_2fa { email_2fa: _enable_email_2fa {
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured /// 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 |> 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; 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. /// 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() { if cfg.use_sendmail {
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support") 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") 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 { if cfg._enable_email_2fa && cfg.email_token_size < 6 {
err!("`EMAIL_TOKEN_SIZE` has a minimum size of 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 // Check if the icon blacklist regex is valid
if let Some(ref r) = cfg.icon_blacklist_regex { if let Some(ref r) = cfg.icon_blacklist_regex {
let validate_regex = regex::Regex::new(r); let validate_regex = regex::Regex::new(r);
@ -1045,7 +1081,7 @@ impl Config {
} }
pub fn mail_enabled(&self) -> bool { pub fn mail_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config; 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 { pub fn get_duo_akey(&self) -> String {

View file

@ -8,7 +8,7 @@ use lettre::{
transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism}, transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism},
transport::smtp::client::{Tls, TlsParameters}, transport::smtp::client::{Tls, TlsParameters},
transport::smtp::extension::ClientId, transport::smtp::extension::ClientId,
Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, Address, AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Tokio1Executor,
}; };
use crate::{ use crate::{
@ -21,7 +21,15 @@ use crate::{
CONFIG, 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; use std::time::Duration;
let host = CONFIG.smtp_host().unwrap(); 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 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 { async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {
let smtp_from = &CONFIG.smtp_from(); 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) .subject(subject)
.multipart(body)?; .multipart(body)?;
match mailer().send(email).await { send_with_selected_transport(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}"));
}
}
}
} }