diff --git a/Cargo.lock b/Cargo.lock index 8f2e0653..996d89ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -352,6 +362,28 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "chrono-tz" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "colored" version = "2.0.4" @@ -522,6 +554,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deunicode" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95203a6a50906215a502507c0f879a0ce7ff205a6111e2db2a5ef8e4bb92e43" + [[package]] name = "digest" version = "0.10.7" @@ -649,18 +687,6 @@ dependencies = [ "log", ] -[[package]] -name = "filetime" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys", -] - [[package]] name = "finl_unicode" version = "1.2.0" @@ -862,6 +888,30 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "globset" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.21" @@ -1008,6 +1058,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.27" @@ -1088,6 +1147,23 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1149,12 +1225,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "itoap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8" - [[package]] name = "jobserver" version = "0.1.27" @@ -1596,6 +1666,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1700,6 +1779,44 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.4.2" @@ -2119,41 +2236,12 @@ dependencies = [ ] [[package]] -name = "sailfish" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7861181faa2e413410444757deca246c70959cee725fbfd8f736a94a660eb377" -dependencies = [ - "itoap", - "ryu", - "sailfish-macros", - "version_check", -] - -[[package]] -name = "sailfish-compiler" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c38d77ced03b393e820ac70109857bd857f93e746f5d7d631829c9ee2e4f3fa" -dependencies = [ - "filetime", - "home", - "memchr", - "proc-macro2", - "quote", - "serde", - "syn 2.0.38", - "toml 0.7.8", -] - -[[package]] -name = "sailfish-macros" -version = "0.8.1" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f73db14456f861a5c89166ab6ac76afd94b4d2a9416638ae2952ae051089c5" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "proc-macro2", - "sailfish-compiler", + "winapi-util", ] [[package]] @@ -2385,6 +2473,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + [[package]] name = "smallvec" version = "1.11.1" @@ -2735,6 +2832,28 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "text-colorizer" version = "1.0.0" @@ -2782,6 +2901,16 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.29" @@ -2922,18 +3051,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", -] - [[package]] name = "toml" version = "0.8.2" @@ -2943,7 +3060,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.2", + "toml_edit", ] [[package]] @@ -2955,19 +3072,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.0.2", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "toml_edit" version = "0.20.2" @@ -3000,6 +3104,7 @@ dependencies = [ "hyper", "indexmap 1.9.3", "jsonwebtoken", + "lazy_static", "lettre", "log", "pbkdf2", @@ -3007,7 +3112,6 @@ dependencies = [ "rand_core", "regex", "reqwest", - "sailfish", "serde", "serde_bencode", "serde_bytes", @@ -3016,6 +3120,7 @@ dependencies = [ "sha-1", "sqlx", "tempfile", + "tera", "text-colorizer", "text-to-png", "thiserror", @@ -3133,6 +3238,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.7.0" @@ -3276,6 +3431,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3410,6 +3575,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index afbec95d..dcac130d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,9 @@ rust-version.workspace = true version.workspace = true [workspace.package] -authors = ["Nautilus Cyberneering , Mick van Dijke "] +authors = [ + "Nautilus Cyberneering , Mick van Dijke ", +] categories = ["network-programming", "web-programming"] description = "A BitTorrent Index" documentation = "https://docs.rs/crate/torrust-tracker/" @@ -74,7 +76,7 @@ lettre = { version = "0", features = [ "tokio1-native-tls", "smtp-transport", ] } -sailfish = "0" +tera = { version = "1", default-features = false, features = ["builtins"] } regex = "1" pbkdf2 = { version = "0", features = ["simple"] } text-colorizer = "1" @@ -91,6 +93,7 @@ tower-http = { version = "0", features = ["cors", "compression-full"] } email_address = "0" hex = "0" uuid = { version = "1", features = ["v4"] } +lazy_static = "1.4.0" [dev-dependencies] rand = "0" diff --git a/project-words.txt b/project-words.txt index 5a52dc61..fdd47e8a 100644 --- a/project-words.txt +++ b/project-words.txt @@ -77,6 +77,7 @@ Swatinem taiki tempdir tempfile +tera thiserror torrust Torrust diff --git a/src/mailer.rs b/src/mailer.rs index 3ef83e0d..0c48acd6 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -1,17 +1,53 @@ +use std::collections::HashMap; use std::sync::Arc; use jsonwebtoken::{encode, EncodingKey, Header}; +use lazy_static::lazy_static; use lettre::message::{MessageBuilder, MultiPart, SinglePart}; use lettre::transport::smtp::authentication::{Credentials, Mechanism}; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; -use sailfish::TemplateOnce; use serde::{Deserialize, Serialize}; +use serde_json::value::{to_value, Value}; +use tera::{try_get_value, Context, Tera}; use crate::config::Configuration; use crate::errors::ServiceError; use crate::utils::clock; use crate::web::api::v1::routes::API_VERSION_URL_PREFIX; +lazy_static! { + pub static ref TEMPLATES: Tera = { + let mut tera = Tera::default(); + + match tera.add_template_file("templates/verify.html", Some("html_verify_email")) { + Ok(()) => {} + Err(e) => { + println!("Parsing error(s): {e}"); + ::std::process::exit(1); + } + }; + + tera.autoescape_on(vec![".html", ".sql"]); + tera.register_filter("do_nothing", do_nothing_filter); + tera + }; +} + +/// This function is a dummy filter for tera. +/// +/// # Panics +/// +/// Panics if unable to convert values. +/// +/// # Errors +/// +/// This function will return an error if... +#[allow(clippy::implicit_hasher)] +pub fn do_nothing_filter(value: &Value, _: &HashMap) -> tera::Result { + let s = try_get_value!("do_nothing_filter", "value", String, value); + Ok(to_value(s).unwrap()) +} + pub struct Service { cfg: Arc, mailer: Arc, @@ -24,13 +60,6 @@ pub struct VerifyClaims { pub exp: u64, } -#[derive(TemplateOnce)] -#[template(path = "../templates/verify.html")] -struct VerifyTemplate { - username: String, - verification_url: String, -} - impl Service { pub async fn new(cfg: Arc) -> Service { let mailer = Arc::new(Self::get_mailer(&cfg).await); @@ -77,41 +106,7 @@ impl Service { let builder = self.get_builder(to).await; let verification_url = self.get_verification_url(user_id, base_url).await; - let mail_body = format!( - r#" - Welcome to Torrust, {username}! - - Please click the confirmation link below to verify your account. - {verification_url} - - If this account wasn't made by you, you can ignore this email. - "# - ); - - let ctx = VerifyTemplate { - username: String::from(username), - verification_url, - }; - - let mail = builder - .subject("Torrust - Email verification") - .multipart( - MultiPart::alternative() - .singlepart( - SinglePart::builder() - .header(lettre::message::header::ContentType::TEXT_PLAIN) - .body(mail_body), - ) - .singlepart( - SinglePart::builder() - .header(lettre::message::header::ContentType::TEXT_HTML) - .body( - ctx.render_once() - .expect("value `ctx` must have some internal error passed into it"), - ), - ), - ) - .expect("the `multipart` builder had an error"); + let mail = build_letter(verification_url.as_str(), username, builder)?; match self.mailer.send(mail).await { Ok(_res) => Ok(()), @@ -155,4 +150,70 @@ impl Service { } } +fn build_letter(verification_url: &str, username: &str, builder: MessageBuilder) -> Result { + let (plain_body, html_body) = build_content(verification_url, username).map_err(|e| { + log::error!("{e}"); + ServiceError::InternalServerError + })?; + + Ok(builder + .subject("Torrust - Email verification") + .multipart( + MultiPart::alternative() + .singlepart( + SinglePart::builder() + .header(lettre::message::header::ContentType::TEXT_PLAIN) + .body(plain_body), + ) + .singlepart( + SinglePart::builder() + .header(lettre::message::header::ContentType::TEXT_HTML) + .body(html_body), + ), + ) + .expect("the `multipart` builder had an error")) +} + +fn build_content(verification_url: &str, username: &str) -> Result<(String, String), tera::Error> { + let plain_body = format!( + r#" + Welcome to Torrust, {username}! + + Please click the confirmation link below to verify your account. + {verification_url} + + If this account wasn't made by you, you can ignore this email. + "# + ); + let mut context = Context::new(); + context.insert("verification", &verification_url); + context.insert("username", &username); + let html_body = TEMPLATES.render("html_verify_email", &context)?; + Ok((plain_body, html_body)) +} + pub type Mailer = AsyncSmtpTransport; + +#[cfg(test)] +mod tests { + use lettre::Message; + + use super::{build_content, build_letter}; + + #[test] + fn it_should_build_a_letter() { + let builder = Message::builder() + .from("from@a.b.c".parse().unwrap()) + .reply_to("reply@a.b.c".parse().unwrap()) + .to("to@a.b.c".parse().unwrap()); + + let _letter = build_letter("https://a.b.c/", "user", builder).unwrap(); + } + + #[test] + fn it_should_build_content() { + let (plain_body, html_body) = build_content("https://a.b.c/", "user").unwrap(); + assert_ne!(plain_body, ""); + assert_ne!(html_body, ""); + } +} diff --git a/templates/verify.html b/templates/verify.html index e43ad6f0..c54bbfb4 100644 --- a/templates/verify.html +++ b/templates/verify.html @@ -1,23 +1,176 @@ - - + + + + + + + + + + + + +
Torrust

Welcome to Torrust, <%= username %>.
Please click the confirmation link below to verify your account.
Verify account
Or copy and paste the following link into your browser:
© Copyright Torrust 2023
\ No newline at end of file + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ Torrust
+
+

+

+ +
+
+ Welcome to Torrust, {{ username }}.
+
+
+ Please click the confirmation link below to verify your account.
+
+ + + + +
+ Verify account +
+
+
+ Or copy and paste the following link into your browser:
+
+ +
+
+ +
+
+ +
+ + + \ No newline at end of file