feat: lettre email adapter: load email config and test account_validation_link

This commit is contained in:
Aravinth Manivannan 2024-05-18 20:42:30 +05:30
parent 26ba1f4e1d
commit 1815637a38
Signed by: realaravinth
GPG key ID: F8F50389936984FF
4 changed files with 356 additions and 52 deletions

275
Cargo.lock generated
View file

@ -39,8 +39,8 @@ dependencies = [
"encoding_rs", "encoding_rs",
"flate2", "flate2",
"futures-core", "futures-core",
"h2", "h2 0.3.26",
"http", "http 0.2.12",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -91,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511"
dependencies = [ dependencies = [
"bytestring", "bytestring",
"http", "http 0.2.12",
"regex", "regex",
"serde", "serde",
"tracing", "tracing",
@ -396,6 +396,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.3.0"
@ -1389,7 +1395,26 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "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", "indexmap",
"slab", "slab",
"tokio", "tokio",
@ -1495,6 +1520,40 @@ dependencies = [
"itoa", "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]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@ -1522,6 +1581,62 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 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]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.60" version = "0.1.60"
@ -1612,6 +1727,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "ipnet"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.12" version = "0.4.12"
@ -2500,6 +2621,48 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 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]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"
@ -3233,6 +3396,33 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.10.1" version = "3.10.1"
@ -3476,6 +3666,34 @@ dependencies = [
"winnow", "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]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.40"
@ -3521,6 +3739,12 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
@ -3757,8 +3981,10 @@ dependencies = [
"mockall", "mockall",
"pretty_env_logger", "pretty_env_logger",
"rand", "rand",
"reqwest",
"rust-embed", "rust-embed",
"serde", "serde",
"serde_json",
"sqlx", "sqlx",
"tera", "tera",
"time", "time",
@ -3790,6 +4016,15 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -3827,6 +4062,18 @@ dependencies = [
"wasm-bindgen-shared", "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]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.92" version = "0.2.92"
@ -3856,6 +4103,16 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 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]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.25.4" version = "0.25.4"
@ -4069,6 +4326,16 @@ dependencies = [
"memchr", "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]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"

View file

@ -33,3 +33,7 @@ tracing = { version = "0.1.40", features = ["log"] }
tracing-actix-web = "0.7.10" tracing-actix-web = "0.7.10"
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.5.0", features = ["serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }
[dev-dependencies]
reqwest = { version = "0.12.4", features = ["json"] }
serde_json = "1.0.117"

View file

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use lettre::{message::header::ContentType, AsyncTransport, Message};
use super::*; use super::*;
use crate::identity::application::port::output::mailer::{account_validation_link::*, errors::*}; use crate::identity::application::port::output::mailer::{account_validation_link::*, errors::*};
@ -13,53 +15,83 @@ impl AccountValidationLinkOutMailerPort for LettreMailer {
username: &str, username: &str,
validation_secret: &str, validation_secret: &str,
) -> OutMailerPortResult<()> { ) -> OutMailerPortResult<()> {
let email = Message::builder() let email = Message::builder()
.from(&self.from) .from(self.from.parse().unwrap())
.reply_to(&self.reply_to) .reply_to(self.reply_to.parse().unwrap())
.to(to) .to(format!("{username} <{to}>").parse().unwrap())
.subject("Please validate your account on Vanikam") // TODO: use better title .subject("Please verify your account on Vanikam") // TODO: use better title
.header(ContentType::TEXT_PLAIN) .header(ContentType::TEXT_PLAIN)
.body(format!(r#"Hello {username}, .body(format!(
Please click here to validate your Vanikam account: {validation_secret} r#"Hello {username},
Please click here to verify your Vanikam account: {validation_secret}
Warm regards, Warm regards,
Vanikam Admin Vanikam Admin
"#)) // TODO: change signature "#
)) // TODO: change signature
.unwrap(); .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)] use serde::Deserialize;
//mod tests {
// use super::*; #[derive(Deserialize, Clone)]
// struct MaildevAddress {
// #[actix_rt::test] address: String,
// async fn test_postgres_create_verification_secret() { name: String,
// let settings = crate::settings::tests::get_settings().await; }
// let db = super::DBOutPostgresAdapter::new( #[derive(Deserialize, Clone)]
// sqlx::postgres::PgPool::connect(&settings.database.url) struct MaildevEmail {
// .await id: String,
// .unwrap(), from: Vec<MaildevAddress>,
// ); to: Vec<MaildevAddress>,
// subject: String,
// let msg = CreateSecretMsgBuilder::default() text: String,
// .secret("secret".into()) html: Option<String>,
// .purpose("purpose".into()) }
// .username("username".into())
// .build() #[actix_rt::test]
// .unwrap(); async fn test_mailer_account_validation_link() {
// let username = "batman";
// db.create_verification_secret(msg.clone()).await.unwrap(); let email = "batman@account_validation_link.example.com";
// let validation_secret = "dafsdfasecret";
// // duplicate: secret exists
// assert_eq!( let settings = crate::settings::tests::get_settings().await;
// db.create_verification_secret(msg).await.err(), let m = LettreMailer::new(&settings).await;
// Some(OutDBPortError::VerificationOTPSecretExists)
// ); m.account_validation_link(email, username, validation_secret)
// .await
// settings.drop_db().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<MaildevEmail> =
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();
}
}

View file

@ -2,10 +2,12 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use lettre::{transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor}; use lettre::{AsyncSmtpTransport, Tokio1Executor};
use crate::settings::Settings; use crate::settings::Settings;
pub mod account_validation_link;
#[derive(Clone)] #[derive(Clone)]
pub struct LettreMailer { pub struct LettreMailer {
mailer: AsyncSmtpTransport<Tokio1Executor>, mailer: AsyncSmtpTransport<Tokio1Executor>,
@ -15,18 +17,17 @@ pub struct LettreMailer {
impl LettreMailer { impl LettreMailer {
pub async fn new(s: &Settings) -> Self { pub async fn new(s: &Settings) -> Self {
let creds = Credentials::new(s.email.username.clone(), s.email.password.clone());
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::relay(&s.email.server_hostname) AsyncSmtpTransport::<Tokio1Executor>::from_url(s.email.url.as_str())
.unwrap() .unwrap()
.credentials(creds)
.build(); .build();
assert!(mailer.test_connection().await.unwrap());
Self { Self {
mailer, mailer,
from: String::default(), // TODO: create settings module to read config from: s.email.from.clone(),
reply_to: String::default(), reply_to: s.email.reply_to.clone(),
} }
} }
} }