From 1815637a389283fd6a3d889dfb6c9144d97e8f49 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 18 May 2024 20:42:30 +0530 Subject: [PATCH] feat: lettre email adapter: load email config and test account_validation_link --- Cargo.lock | 275 +++++++++++++++++- Cargo.toml | 4 + .../mailer/lettre/account_validation_link.rs | 114 +++++--- .../adapters/output/mailer/lettre/mod.rs | 15 +- 4 files changed, 356 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cf2f62..729c43f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,8 +39,8 @@ dependencies = [ "encoding_rs", "flate2", "futures-core", - "h2", - "http", + "h2 0.3.26", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -91,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" dependencies = [ "bytestring", - "http", + "http 0.2.12", "regex", "serde", "tracing", @@ -396,6 +396,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -1389,7 +1395,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -1495,6 +1520,40 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1522,6 +1581,62 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1612,6 +1727,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.12" @@ -2500,6 +2621,48 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.8" @@ -3233,6 +3396,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -3476,6 +3666,34 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" @@ -3521,6 +3739,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -3757,8 +3981,10 @@ dependencies = [ "mockall", "pretty_env_logger", "rand", + "reqwest", "rust-embed", "serde", + "serde_json", "sqlx", "tera", "time", @@ -3790,6 +4016,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3827,6 +4062,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.92" @@ -3856,6 +4103,16 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -4069,6 +4326,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 13dc5d5..3272e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,7 @@ tracing = { version = "0.1.40", features = ["log"] } tracing-actix-web = "0.7.10" url = { version = "2.5.0", features = ["serde"] } validator = { version = "0.18.1", features = ["derive"] } + +[dev-dependencies] +reqwest = { version = "0.12.4", features = ["json"] } +serde_json = "1.0.117" diff --git a/src/identity/adapters/output/mailer/lettre/account_validation_link.rs b/src/identity/adapters/output/mailer/lettre/account_validation_link.rs index 01d5f45..ecdeeec 100644 --- a/src/identity/adapters/output/mailer/lettre/account_validation_link.rs +++ b/src/identity/adapters/output/mailer/lettre/account_validation_link.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use lettre::{message::header::ContentType, AsyncTransport, Message}; + use super::*; use crate::identity::application::port::output::mailer::{account_validation_link::*, errors::*}; @@ -13,53 +15,83 @@ impl AccountValidationLinkOutMailerPort for LettreMailer { username: &str, validation_secret: &str, ) -> OutMailerPortResult<()> { - let email = Message::builder() - .from(&self.from) - .reply_to(&self.reply_to) - .to(to) - .subject("Please validate your account on Vanikam") // TODO: use better title + .from(self.from.parse().unwrap()) + .reply_to(self.reply_to.parse().unwrap()) + .to(format!("{username} <{to}>").parse().unwrap()) + .subject("Please verify your account on Vanikam") // TODO: use better title .header(ContentType::TEXT_PLAIN) - .body(format!(r#"Hello {username}, - Please click here to validate your Vanikam account: {validation_secret} + .body(format!( + r#"Hello {username}, + Please click here to verify your Vanikam account: {validation_secret} Warm regards, Vanikam Admin - "#)) // TODO: change signature + "# + )) // TODO: change signature .unwrap(); - mailer.send(email).await.unwrap(); + self.mailer.send(email).await.unwrap(); + Ok(()) } } -// TODO: mailer tests +#[cfg(test)] +mod tests { + use super::*; + use reqwest::Client; + use url::Url; -//#[cfg(test)] -//mod tests { -// use super::*; -// -// #[actix_rt::test] -// async fn test_postgres_create_verification_secret() { -// let settings = crate::settings::tests::get_settings().await; -// let db = super::DBOutPostgresAdapter::new( -// sqlx::postgres::PgPool::connect(&settings.database.url) -// .await -// .unwrap(), -// ); -// -// let msg = CreateSecretMsgBuilder::default() -// .secret("secret".into()) -// .purpose("purpose".into()) -// .username("username".into()) -// .build() -// .unwrap(); -// -// db.create_verification_secret(msg.clone()).await.unwrap(); -// -// // duplicate: secret exists -// assert_eq!( -// db.create_verification_secret(msg).await.err(), -// Some(OutDBPortError::VerificationOTPSecretExists) -// ); -// -// settings.drop_db().await; -// } -//} + use serde::Deserialize; + + #[derive(Deserialize, Clone)] + struct MaildevAddress { + address: String, + name: String, + } + #[derive(Deserialize, Clone)] + struct MaildevEmail { + id: String, + from: Vec, + to: Vec, + subject: String, + text: String, + html: Option, + } + + #[actix_rt::test] + async fn test_mailer_account_validation_link() { + let username = "batman"; + let email = "batman@account_validation_link.example.com"; + let validation_secret = "dafsdfasecret"; + + let settings = crate::settings::tests::get_settings().await; + let m = LettreMailer::new(&settings).await; + + m.account_validation_link(email, username, validation_secret) + .await + .unwrap(); + + let c = Client::default(); + + let maildev_url = + std::env::var("MAILDEV_URL").expect("Please set maildev instance URL in MAILDEV_URL"); + + let mut u = Url::parse(&maildev_url).unwrap(); + + u.set_path("/email"); + let maildev_emails: Vec = + c.get(u.clone()).send().await.unwrap().json().await.unwrap(); + let maildev_email = maildev_emails + .iter() + .find(|e| e.to.iter().any(|f| f.address == email)) + .unwrap(); + assert!(maildev_email.text.contains(validation_secret)); + assert!(maildev_email.text.contains(username)); + assert!(maildev_email + .to + .iter() + .any(|t| t.address == email && t.name == username)); + + u.set_path(&format!("/email/{}", maildev_email.id)); + c.delete(u).send().await.unwrap(); + } +} diff --git a/src/identity/adapters/output/mailer/lettre/mod.rs b/src/identity/adapters/output/mailer/lettre/mod.rs index 58abb85..f0e998b 100644 --- a/src/identity/adapters/output/mailer/lettre/mod.rs +++ b/src/identity/adapters/output/mailer/lettre/mod.rs @@ -2,10 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -use lettre::{transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor}; +use lettre::{AsyncSmtpTransport, Tokio1Executor}; use crate::settings::Settings; +pub mod account_validation_link; + #[derive(Clone)] pub struct LettreMailer { mailer: AsyncSmtpTransport, @@ -15,18 +17,17 @@ pub struct LettreMailer { impl LettreMailer { pub async fn new(s: &Settings) -> Self { - let creds = Credentials::new(s.email.username.clone(), s.email.password.clone()); - let mailer: AsyncSmtpTransport = - AsyncSmtpTransport::::relay(&s.email.server_hostname) + AsyncSmtpTransport::::from_url(s.email.url.as_str()) .unwrap() - .credentials(creds) .build(); + assert!(mailer.test_connection().await.unwrap()); + Self { mailer, - from: String::default(), // TODO: create settings module to read config - reply_to: String::default(), + from: s.email.from.clone(), + reply_to: s.email.reply_to.clone(), } } }