refactor: use async email sending

This commit is contained in:
Max Hohlfeld 2025-01-24 15:51:22 +01:00
parent 8b470f2d4f
commit b696aa4fe0
9 changed files with 86 additions and 182 deletions

161
Cargo.lock generated
View File

@ -1038,16 +1038,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@ -1447,21 +1437,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -1554,6 +1529,17 @@ dependencies = [
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "futures-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb"
dependencies = [
"futures-io",
"rustls",
"rustls-pki-types",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@ -1769,17 +1755,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "hostname"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
dependencies = [
"cfg-if",
"libc",
"windows",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -2115,23 +2090,29 @@ version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5" checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5"
dependencies = [ dependencies = [
"async-std",
"async-trait",
"base64 0.22.1", "base64 0.22.1",
"chumsky", "chumsky",
"email-encoding", "email-encoding",
"email_address", "email_address",
"fastrand 2.3.0", "fastrand 2.3.0",
"futures-io",
"futures-rustls",
"futures-util", "futures-util",
"hostname",
"httpdate", "httpdate",
"idna", "idna",
"mime", "mime",
"native-tls",
"nom", "nom",
"percent-encoding", "percent-encoding",
"quoted_printable", "quoted_printable",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"socket2 0.5.8", "socket2 0.5.8",
"tokio", "tokio",
"url", "url",
"webpki-roots",
] ]
[[package]] [[package]]
@ -2306,23 +2287,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -2413,50 +2377,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@ -2955,6 +2875,7 @@ version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [ dependencies = [
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -3001,44 +2922,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.8.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.25" version = "1.0.25"
@ -3939,16 +3828,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"

View File

@ -22,7 +22,7 @@ serde_json = "1.0.114"
pico-args = "0.5.0" pico-args = "0.5.0"
rand = { version = "0.8.5", features = ["getrandom"] } rand = { version = "0.8.5", features = ["getrandom"] }
async-trait = "0.1.79" async-trait = "0.1.79"
lettre = "0.11.11" lettre = { version = "0.11.11", default-features = false, features = ["builder", "smtp-transport", "async-std1-rustls-tls"] }
quick-xml = { version = "0.37", features = ["serde", "serialize"] } quick-xml = { version = "0.37", features = ["serde", "serialize"] }
actix-web-static-files = "4.0" actix-web-static-files = "4.0"
static-files = "0.2.1" static-files = "0.2.1"

View File

@ -49,7 +49,7 @@ pub async fn post_new(
let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?; let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?;
let new_user = User::read_by_id(pool.get_ref(), id).await?.unwrap(); let new_user = User::read_by_id(pool.get_ref(), id).await?.unwrap();
mailer.send_registration_mail(&new_user, &registration.token)?; mailer.send_registration_mail(&new_user, &registration.token).await?;
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.insert_header((LOCATION, "/users")) .insert_header((LOCATION, "/users"))

View File

@ -31,7 +31,7 @@ async fn post(
{ {
if let Ok(user) = User::read_for_login(pool.get_ref(), form.email.as_ref().unwrap()).await { if let Ok(user) = User::read_for_login(pool.get_ref(), form.email.as_ref().unwrap()).await {
let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?; let reset = PasswordReset::insert_new_for_user(pool.get_ref(), user.id).await?;
mailer.send_forgot_password_mail(&user, &reset.token)?; mailer.send_forgot_password_mail(&user, &reset.token).await?;
} }
return Ok(HttpResponse::Ok().body("E-Mail versandt!")); return Ok(HttpResponse::Ok().body("E-Mail versandt!"));

View File

@ -1,6 +1,6 @@
use lettre::{ use lettre::{
message::{Mailbox, MultiPart, SinglePart}, message::{Mailbox, MultiPart, SinglePart},
Message, Transport, AsyncTransport, Message,
}; };
use rinja::Template; use rinja::Template;
@ -9,13 +9,13 @@ use crate::{models::User, utils::ApplicationError};
use super::Mailer; use super::Mailer;
impl Mailer { impl Mailer {
pub fn send_forgot_password_mail( pub async fn send_forgot_password_mail(
&self, &self,
user: &User, user: &User,
token: &str, token: &str,
) -> Result<(), ApplicationError> { ) -> Result<(), ApplicationError> {
let message = build(&self.hostname, &user.name, &user.email, token)?; let message = build(&self.hostname, &user.name, &user.email, token)?;
self.transport.send(&message)?; self.transport.send(message).await?;
Ok(()) Ok(())
} }
@ -74,7 +74,7 @@ fn build(
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::test_helper::assert_mail_snapshot; use crate::utils::test_helper::assert_mail_snapshot;
use lettre::transport::stub::StubTransport; use lettre::{transport::stub::StubTransport, Transport};
#[test] #[test]
fn build_mail_snapshot() { fn build_mail_snapshot() {

View File

@ -1,10 +1,11 @@
use brass_config::{Config, SmtpTlsType}; use brass_config::{Config, SmtpTlsType};
use lettre::{ use lettre::{
address::Envelope,
transport::{ transport::{
smtp::{authentication::Credentials, extension::ClientId}, smtp::{authentication::Credentials, extension::ClientId},
stub::StubTransport, stub::AsyncStubTransport,
}, },
SmtpTransport, Transport, AsyncSmtpTransport, AsyncStd1Executor, AsyncTransport,
}; };
use crate::utils::ApplicationError; use crate::utils::ApplicationError;
@ -21,30 +22,46 @@ pub struct Mailer {
#[derive(Clone)] #[derive(Clone)]
enum Transports { enum Transports {
SmtpTransport(SmtpTransport), SmtpTransport(AsyncSmtpTransport<AsyncStd1Executor>),
#[allow(unused)] #[allow(unused)]
StubTransport(StubTransport), StubTransport(AsyncStubTransport),
} }
impl Transport for Transports { impl AsyncTransport for Transports {
type Ok = (); type Ok = ();
type Error = ApplicationError; type Error = ApplicationError;
fn send_raw( fn send_raw<'life0, 'life1, 'life2, 'async_trait>(
&self, &'life0 self,
envelope: &lettre::address::Envelope, envelope: &'life1 Envelope,
email: &[u8], email: &'life2 [u8],
) -> Result<Self::Ok, Self::Error> { ) -> ::core::pin::Pin<
match self { Box<
Transports::SmtpTransport(smtp_transport) => smtp_transport dyn ::core::future::Future<Output = Result<Self::Ok, Self::Error>>
.send_raw(envelope, email) + ::core::marker::Send
.map(|_| ()) + 'async_trait,
.map_err(|err| ApplicationError::EmailTransport(err)), >,
Transports::StubTransport(stub_transport) => stub_transport >
.send_raw(envelope, email) where
.map(|_| ()) 'life0: 'async_trait,
.map_err(|err| ApplicationError::EmailStubTransport(err)), 'life1: 'async_trait,
} 'life2: 'async_trait,
Self: 'async_trait,
{
return Box::pin(async move {
match self {
Transports::SmtpTransport(smtp_transport) => smtp_transport
.send_raw(envelope, email)
.await
.map(|_| ())
.map_err(|err| ApplicationError::EmailTransport(err)),
Transports::StubTransport(stub_transport) => stub_transport
.send_raw(envelope, email)
.await
.map(|_| ())
.map_err(|err| ApplicationError::EmailStubTransport(err)),
}
});
} }
} }
@ -52,11 +69,16 @@ impl Mailer {
pub fn new(config: &Config) -> anyhow::Result<Self> { pub fn new(config: &Config) -> anyhow::Result<Self> {
let mut builder = match config.smtp_tlstype { let mut builder = match config.smtp_tlstype {
SmtpTlsType::StartTLS => { SmtpTlsType::StartTLS => {
SmtpTransport::starttls_relay(&config.smtp_server)?.port(config.smtp_port) AsyncSmtpTransport::<AsyncStd1Executor>::starttls_relay(&config.smtp_server)?
.port(config.smtp_port)
}
SmtpTlsType::TLS => {
AsyncSmtpTransport::<AsyncStd1Executor>::relay(&config.smtp_server)?
.port(config.smtp_port)
} }
SmtpTlsType::TLS => SmtpTransport::relay(&config.smtp_server)?.port(config.smtp_port),
SmtpTlsType::NoTLS => { SmtpTlsType::NoTLS => {
SmtpTransport::builder_dangerous(&config.smtp_server).port(config.smtp_port) AsyncSmtpTransport::<AsyncStd1Executor>::builder_dangerous(&config.smtp_server)
.port(config.smtp_port)
} }
}; };
@ -80,13 +102,12 @@ impl Mailer {
} }
} }
#[cfg(test)] #[cfg(test)]
impl Mailer { impl Mailer {
pub fn new_stub() -> Self { pub fn new_stub() -> Self {
Mailer { Mailer {
transport: Transports::StubTransport(StubTransport::new_ok()), transport: Transports::StubTransport(AsyncStubTransport::new_ok()),
hostname: String::from("testhostname") hostname: String::from("testhostname"),
} }
} }
} }

View File

@ -1,6 +1,6 @@
use lettre::{ use lettre::{
message::{Mailbox, MultiPart, SinglePart}, message::{Mailbox, MultiPart, SinglePart},
Message, Transport, AsyncTransport, Message,
}; };
use rinja::Template; use rinja::Template;
@ -9,9 +9,13 @@ use crate::{models::User, utils::ApplicationError};
use super::Mailer; use super::Mailer;
impl Mailer { impl Mailer {
pub fn send_registration_mail(&self, user: &User, token: &str) -> Result<(), ApplicationError> { pub async fn send_registration_mail(
&self,
user: &User,
token: &str,
) -> Result<(), ApplicationError> {
let message = build(&self.hostname, &user.name, &user.email, token)?; let message = build(&self.hostname, &user.name, &user.email, token)?;
self.transport.send(&message)?; self.transport.send(message).await?;
Ok(()) Ok(())
} }
@ -73,7 +77,7 @@ fn build(
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::test_helper::assert_mail_snapshot; use crate::utils::test_helper::assert_mail_snapshot;
use lettre::transport::stub::StubTransport; use lettre::{transport::stub::StubTransport, Transport};
#[test] #[test]
fn build_mail_snapshot() { fn build_mail_snapshot() {

View File

@ -1,11 +1,11 @@
use lettre::{message::SinglePart, Message, Transport}; use lettre::{message::SinglePart, AsyncTransport, Message};
use crate::utils::ApplicationError; use crate::utils::ApplicationError;
use super::Mailer; use super::Mailer;
impl Mailer { impl Mailer {
pub fn send_test_mail(&self, to: &str) -> Result<(), ApplicationError> { pub async fn send_test_mail(&self, to: &str) -> Result<(), ApplicationError> {
let message = Message::builder() let message = Message::builder()
.from("noreply <noreply@brasiwa-leipzig.de>".parse()?) .from("noreply <noreply@brasiwa-leipzig.de>".parse()?)
.reply_to("noreply <noreply@brasiwa-leipzig.de>".parse()?) .reply_to("noreply <noreply@brasiwa-leipzig.de>".parse()?)
@ -15,7 +15,7 @@ impl Mailer {
"Testmail von Brass. E-Mail Versand funktioniert!".to_string(), "Testmail von Brass. E-Mail Versand funktioniert!".to_string(),
))?; ))?;
self.transport.send(&message)?; self.transport.send(message).await?;
Ok(()) Ok(())
} }
} }

View File

@ -98,7 +98,7 @@ pub async fn handle_command(
exit(0); exit(0);
} }
Some(Command::TestMail(to)) => { Some(Command::TestMail(to)) => {
match mailer.send_test_mail(&to) { match mailer.send_test_mail(&to).await {
Ok(_) => println!("Successfully sent mail to {to}."), Ok(_) => println!("Successfully sent mail to {to}."),
Err(e) => { Err(e) => {
if let Some(source) = e.source() { if let Some(source) = e.source() {