diff --git a/Cargo.lock b/Cargo.lock index 298a8d80..12999fc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2993,6 +2993,7 @@ dependencies = [ "async-std", "async-trait", "base64 0.22.1", + "ed25519-dalek", "email-encoding", "email_address", "fastrand", @@ -3005,9 +3006,11 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", + "rsa", "rustls 0.23.38", "rustls-native-certs", "serde", + "sha2 0.10.9", "socket2 0.6.3", "tokio", "tokio-rustls 0.26.4", diff --git a/Cargo.toml b/Cargo.toml index 79eebec0..5f3d6e45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,7 +136,7 @@ webauthn-rs-core = "0.5.4" url = "2.5.8" # Email libraries -lettre = { version = "0.11.21", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.21", features = ["smtp-transport", "sendmail-transport", "dkim", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" diff --git a/src/config.rs b/src/config.rs index 6ff09467..9bb02ab5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,11 +13,9 @@ use reqwest::Url; use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use crate::{ - error::Error, - util::{ - get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags, - FeatureFlagFilter, - }, + error::Error, mail::check_dkim, util::{ + FeatureFlagFilter, get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags + } }; static CONFIG_FILE: LazyLock = LazyLock::new(|| { @@ -888,6 +886,12 @@ make_config! { smtp_username: String, true, option; /// Password smtp_password: Pass, true, option; + /// Dkim signature (type:privatekey). Private must be base64-encoded ed key or PKCS#1 format RSA key. + dkim_signature: String, true, option; + /// Dkim algo (true if RSA else ed25519) + dkim_use_rsa: bool, true, def, false; + /// Dkim infos (selector:domain) + dkim_infos: String, true, option; /// SMTP Auth mechanism |> 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 ','. smtp_auth_mechanism: String, true, option; /// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server @@ -1150,6 +1154,9 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { if cfg._enable_email_2fa && cfg.email_token_size < 6 { err!("`EMAIL_TOKEN_SIZE` has a minimum size of 6") } + if let Err(e) = check_dkim() { + err!(format!("DKIM config fails. {}",e)) + } } if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) { diff --git a/src/mail.rs b/src/mail.rs index cdbd269a..a53e5e0c 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -3,7 +3,10 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use std::{env::consts::EXE_SUFFIX, str::FromStr}; use lettre::{ - message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart}, + message::{ + dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey}, + dkim_sign, Attachment, Body, Mailbox, Message, MultiPart, SinglePart, + }, transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism}, transport::smtp::client::{Tls, TlsParameters}, transport::smtp::extension::ClientId, @@ -703,7 +706,41 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult { } } } - +pub fn check_dkim() -> Result, String> { + match (CONFIG.dkim_signature(), CONFIG.dkim_infos()) { + (Some(sig), Some(infos)) => { + let config = { + let algo = if CONFIG.dkim_use_rsa() {DkimSigningAlgorithm::Rsa } else { DkimSigningAlgorithm::Ed25519 }; + let sig = match std::fs::read_to_string(sig) { + Err(e) => { + return Err(format!("Cannot read DKIM key. Err is {:?}", e)); + } + Ok(key) => match DkimSigningKey::new(&key, algo) { + Ok(d) => d, + Err(e) => { + return Err(format!("DKIM key is invalid. Err is {:?}", e)); + } + }, + }; + match (sig, infos.split(':').collect::>()) { + (sig, split2) if split2.len() == 2 => { + let (selector, domain, sig) = + (String::from(*split2.first().unwrap()), String::from(*split2.last().unwrap()), sig); + (selector, domain, sig) + } + _ => { + return Err("DKIM issue, invalid domain, selector.".to_string()); + } + } + }; + Ok(Some(DkimConfig::default_config(config.0, config.1, config.2))) + } + (None, None) => Ok(None), + _ => { + Err("DKIM setting is badly implemented. One config is missing (DKIM signature or DKIM infos).".to_string()) + } + } +} async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult { let smtp_from = Address::from_str(&CONFIG.smtp_from())?; @@ -726,12 +763,14 @@ async fn send_email(address: &str, subject: &str, body_html: String, body_text: MultiPart::alternative_plain_html(body_text, body_html) }; - let email = Message::builder() + let mut email = Message::builder() .message_id(Some(format!("<{}@{}>", crate::util::get_uuid(), smtp_from.domain()))) .to(Mailbox::new(None, Address::from_str(address)?)) .from(Mailbox::new(Some(CONFIG.smtp_from_name()), smtp_from)) .subject(subject) .multipart(body)?; - + if let Ok(Some(sig)) = check_dkim() { + dkim_sign(&mut email, &sig); + } send_with_selected_transport(email).await }