From 3f40fc49c04fab9f739dc269dfaf8dc0c0fd2720 Mon Sep 17 00:00:00 2001 From: Guilhem Zeitoun Date: Thu, 14 Aug 2025 12:31:11 +0200 Subject: [PATCH] changes to pull requests + rebase --- Cargo.lock | 3 +++ Cargo.toml | 2 +- src/config.rs | 6 ++++++ src/mail.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 5 ++++- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9dbf69d..d3f5a936 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2748,6 +2748,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "chumsky", + "ed25519-dalek", "email-encoding", "email_address", "fastrand", @@ -2760,9 +2761,11 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", + "rsa", "rustls 0.23.31", "rustls-native-certs", "serde", + "sha2", "socket2 0.6.0", "tokio", "tokio-rustls 0.26.2", diff --git a/Cargo.toml b/Cargo.toml index f5df3a49..ada8635e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,7 +135,7 @@ webauthn-rs-core = "0.5.2" url = "2.5.4" # Email libraries -lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "dkim", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } percent-encoding = "2.3.1" # 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 545d7dce..2055cef9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -772,6 +772,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, option; + /// 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 diff --git a/src/mail.rs b/src/mail.rs index ca5b7eb5..70d530f8 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, @@ -689,7 +692,44 @@ 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 = match CONFIG.dkim_use_rsa() { + Some(true) => DkimSigningAlgorithm::Rsa, + _ => DkimSigningAlgorithm::Ed25519, + }; + let sig = match std::fs::read_to_string(sig) { + Err(e) => { + return Err(format!("Cannot read DKIM file. Err is {:?}", e)); + } + Ok(key) => match DkimSigningKey::new(&key, algo) { + Ok(d) => d, + Err(e) => { + return Err(format!("Cannot read DKIM file. 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 = &CONFIG.smtp_from(); @@ -712,12 +752,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.split('@').collect::>()[1]))) .to(Mailbox::new(None, Address::from_str(address)?)) .from(Mailbox::new(Some(CONFIG.smtp_from_name()), Address::from_str(smtp_from)?)) .subject(subject) .multipart(body)?; - + if let Ok(Some(sig)) = check_dkim() { + dkim_sign(&mut email, &sig); + } send_with_selected_transport(email).await } diff --git a/src/main.rs b/src/main.rs index e91dcbc4..73d5732f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,7 +90,10 @@ async fn main() -> Result<(), Error> { schedule_jobs(pool.clone()); db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap(); db::models::TwoFactor::migrate_credential_to_passkey(&mut pool.get().await.unwrap()).await.unwrap(); - + if let Err(e) = mail::check_dkim() { + error!("{}", e); + exit(1); + } let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug); launch_rocket(pool, extra_debug).await // Blocks until program termination. }