Repository: Wulf/create-rust-app (language: rust)

Issue:
[BUG] Hard-coded URL's should be set by environment variable(s)
https://github.com/Wulf/create-rust-app/blob/5008413e29aeeaf3efb7df80f18cce0b5e6e6d55/create-rust-app/src/auth/controller.rs#L556-L564

As said in the subject, the hard-coded base URL ("http://localhost:3000/") should be set by an environment variable (maybe `PUBLIC_BASE_URL` or something like that) because in a production environment the recipient of the email, an end-user, will not be running the web server on their local machine, the server will (probably) be hosted on a cloud service provider like Amazon AWS, Linode, etc. and that the user connects to through a browser.

I haven't checked the whole codebase yet, but almost all references to https://localhost:3000 should instead read from an environment variable.

Also, the second link in the code snippet above would be https://localhost:3000, not https://localhost:300
Mail service not using designated templates
looks like we're not using the templates specified in files under `auth/mail`. 

all the rust files there contain `#[allow(dead_code)]`

How can we specify our own email templates?

Main reason is that I noticed a trailing `)` on the email subject lines and would like to customize the link

Thanks


Discussion / hints:
Thanks for pointing this out @AnthonyMichaelTDM -- I've been considering various solutions to this but haven't landed on one yet. I was hoping to finish the email builder (#37) first in order to have the best DX. Regardless, I guess we'll need to address this sooner rather than later.

There are two issues here that need to be addressed:
* hardcoded URLs
* hardcoded mail templates
I think the most pressing of those issues is the hardcoded urls,

for the hardcoded mail templates we could have a trait, like EmailTemplate, that can be passed as an optional variable to the functions that send the emails, allowing users to define their own templates.

I can work on this in the coming days.
If by functions you mean the auth controller, that’s exactly what I was thinking — it’s the perfect stopgap until we have a complete solution 
what do you think I should name the environment variable?
Actually, now that I think about it, if I do the email template trait thing we don't really need an environment variable for the link urls because the user can just change the url in their template
specifically:

> This is not a huge issue, but worth looking into. It appears like the emails are not using 
/the templates located in backend/mail
> 
> for instance, in auth_recover_existent_account.rs, we have
> ```rust
> pub fn send(mailer: &Mailer, to_email: &str, link: &str) {
>     let subject = "Password Reset Instructions";
>     let text = format!(
>         r#"
> (This is an automated message.)
> 
> Hello,
> 
> Someone requested a password reset for the account associated with this email.
> Please visit this link to reset your password:
> {link}
> (valid for 24 hours)
> "#
>     );
>     let html = format!(
>         r#"
> (This is an automated message.)
> 
> Hello,
> 
> Someone requested a password reset for the account associated with this email.
> Please visit this link to reset your password:
> {link}
> (valid for 24 hours)
> "#
>     );
> 
>     mailer.send(to_email, subject, &text, &html);
> }
> ```
>
> but my email looks like
> Reset Password Instructions)
> 
> `(This is an automated message.) Hello, Someone requested a password reset for the account associated with this email. Please visit this link to reset your password: http://localhost:3000/reset?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODEwNjM5MDYsInN1YiI6MSwidG9rZW5fdHlwZSI6InJlc2V0X3Rva2VuIn0.UGRqHOCrz75hkcqQT02b2NrSeTTb8iNKGOy93Yty2QM (valid for 24 hours)`

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5bb68acd..da73a694 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1390,6 +1390,7 @@ dependencies = [
  "diesel_derives",
  "diesel_migrations",
  "dotenv",
+ "dyn-clone",
  "env_logger",
  "fang",
  "futures",
@@ -1720,6 +1721,12 @@ version = "0.15.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 
+[[package]]
+name = "dyn-clone"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
+
 [[package]]
 name = "either"
 version = "1.8.1"
diff --git a/create-rust-app/Cargo.toml b/create-rust-app/Cargo.toml
index f31a33c6..436867d9 100644
--- a/create-rust-app/Cargo.toml
+++ b/create-rust-app/Cargo.toml
@@ -8,7 +8,13 @@ readme = "../README.md"
 repository = "https://github.com/Wulf/create-rust-app"
 license = "MIT OR Apache-2.0"
 keywords = ["react", "typescript", "generation", "backend", "frontend"]
-categories = ["command-line-utilities", "development-tools", "web-programming", "config", "database"]
+categories = [
+  "command-line-utilities",
+  "development-tools",
+  "web-programming",
+  "config",
+  "database",
+]
 
 [dependencies]
 ##
@@ -17,10 +23,14 @@ categories = ["command-line-utilities", "development-tools", "web-programming",
 dotenv = "0.15.0" # + plugin_dev
 serde_json = "1.0.93"
 lettre = "0.10.2"
-tera = { version="1.17.0" }
-lazy_static = { version="1.4.0" }
+tera = { version = "1.17.0" }
+lazy_static = { version = "1.4.0" }
 serde = { version = "1.0.152", features = ["derive"] }
-diesel = { version="2.0.0-rc.1", default-features = false, features = ["uuid", "r2d2", "chrono"] } # + plugin_dev, plugin_auth
+diesel = { version = "2.0.0-rc.1", default-features = false, features = [
+  "uuid",
+  "r2d2",
+  "chrono",
+] } # + plugin_dev, plugin_auth
 once_cell = "1.17.1"
 
 ##
@@ -35,83 +45,150 @@ libsqlite3-sys = { version = "0.25", optional = true, features = ["bundled"] }
 ##
 
 # plugin_auth
-rust-argon2 = { optional=true, version="1.0" }
-rand = { optional=true, version="0.8.5" }
-jsonwebtoken = { optional=true, version="8.2.0" }
-tsync = { optional=true, version="1.6.0" }
-chrono = { optional=true, version = "0.4.23", default-features = false, features = ["clock", "serde"] }
+rust-argon2 = { optional = true, version = "1.0" }
+rand = { optional = true, version = "0.8.5" }
+jsonwebtoken = { optional = true, version = "8.2.0" }
+tsync = { optional = true, version = "1.6.0" }
+chrono = { optional = true, version = "0.4.23", default-features = false, features = [
+  "clock",
+  "serde",
+] }
+dyn-clone = { optional = true, version = "1.0" } # needed to allow the Mailer struct to be cloned
 
 # plugin_dev
-diesel_migrations = { optional=true, version="2.0.0" }
-cargo_metadata = { optional=true, version="0.15.2" }
-watchexec = { optional=true, version="2.2.0" }
+diesel_migrations = { optional = true, version = "2.0.0" }
+cargo_metadata = { optional = true, version = "0.15.2" }
+watchexec = { optional = true, version = "2.2.0" }
 #### tracing = { optional=true, version="0.1" }
 #### tracing-subscriber = { optional=true, version="0.3.16", features=["env-filter"] }
-reqwest = { optional=true, version="0.11.13" }
-clearscreen = { optional=true, version="2.0.0" }
-open = { optional=true, version="3.2.0" }
-cargo_toml = { optional=true, version = "0.14.0" }
+reqwest = { optional = true, version = "0.11.13" }
+clearscreen = { optional = true, version = "2.0.0" }
+open = { optional = true, version = "3.2.0" }
+cargo_toml = { optional = true, version = "0.14.0" }
 
 # plugin_storage
-aws-config = { optional=true, version="0.14.0" }
-aws-types = { optional=true, version="0.8.0" }
-aws-endpoint = { optional=true, version="0.14.0" }
-aws-sdk-s3 = { optional=true, version="0.8.0" }
-http = { optional=true, version="0.2.6" }
-diesel_derives = { optional=true, version="2.0.1" }
-uuid = { optional=true, version="1.2.2", features=["v4", "serde"] }
-md5 = { optional=true, version="0.7.0" }
-base64 = { optional=true, version="0.21.0" }
+aws-config = { optional = true, version = "0.14.0" }
+aws-types = { optional = true, version = "0.8.0" }
+aws-endpoint = { optional = true, version = "0.14.0" }
+aws-sdk-s3 = { optional = true, version = "0.8.0" }
+http = { optional = true, version = "0.2.6" }
+diesel_derives = { optional = true, version = "2.0.1" }
+uuid = { optional = true, version = "1.2.2", features = ["v4", "serde"] }
+md5 = { optional = true, version = "0.7.0" }
+base64 = { optional = true, version = "0.21.0" }
 
 # plugin_utoipa dependencies
-utoipa = { optional=true, version="3", features=["actix_extras", "chrono", "openapi_extensions"] }
-utoipa-swagger-ui = { optional=true, version="3", features=["actix-web"]}
+utoipa = { optional = true, version = "3", features = [
+  "actix_extras",
+  "chrono",
+  "openapi_extensions",
+] }
+utoipa-swagger-ui = { optional = true, version = "3", features = ["actix-web"] }
 
 # plugin_tasks
-fang = { optional=true, version = "0.10.3" }
+fang = { optional = true, version = "0.10.3" }
 
 ##
 ## BACKENDS
 ##
 
 # poem dependencies
-poem = { optional=true, version="1.3.52", features=["anyhow", "cookie", "static-files"] }
+poem = { optional = true, version = "1.3.52", features = [
+  "anyhow",
+  "cookie",
+  "static-files",
+] }
 
 # actix_web dependencies
-actix-multipart = { optional=true, version="0.4.0" }
-actix-files = { optional=true, version="0.6.2" }
-actix-http = { optional=true, version="3.0.4" }
-actix-web = { optional=true, version="4.2.1" }
-actix-web-httpauth = { optional=true, version="0.8.0" }
-derive_more = { optional=true, version="0.99.17" }
-futures = { optional=true, version="0.3.25" }
-env_logger = { optional=true, version= "0.10.0" }
+actix-multipart = { optional = true, version = "0.4.0" }
+actix-files = { optional = true, version = "0.6.2" }
+actix-http = { optional = true, version = "3.0.4" }
+actix-web = { optional = true, version = "4.2.1" }
+actix-web-httpauth = { optional = true, version = "0.8.0" }
+derive_more = { optional = true, version = "0.99.17" }
+futures = { optional = true, version = "0.3.25" }
+env_logger = { optional = true, version = "0.10.0" }
 
 # axum dependencies (not yet released; only used for plugin_dev)
 
-axum = { optional=true, version="0.6.1" }
+axum = { optional = true, version = "0.6.1" }
 
 ##
 ## MISC - here, we list deps which are required by multiple features but are not required in all configurations
 ##
 
-mime_guess = { optional=true, version="2.0.4" } # backend_poem, plugin_storage
-anyhow = { optional=true, version="1.0.57" } # backend_poem, plugin_auth, plugin_dev
-tokio = { optional=true, version = "1", features = ["full"] } # backend_poem, backend_axum, plugin_storage
+mime_guess = { optional = true, version = "2.0.4" } # backend_poem, plugin_storage
+anyhow = { optional = true, version = "1.0.57" } # backend_poem, plugin_auth, plugin_dev
+tokio = { optional = true, version = "1", features = [
+  "full",
+] } # backend_poem, backend_axum, plugin_storage
 async-priority-channel = "0.1.0"
-futures-util = { optional=true, version = "0.3.25" } # plugin_dev, TODO:plugin_storage?
+futures-util = { optional = true, version = "0.3.25" } # plugin_dev, TODO:plugin_storage?
 
 [features]
-default = ["backend_actix-web", "database_postgres", "plugin_auth", "plugin_container", "plugin_dev", "plugin_graphql", "plugin_storage", "plugin_utoipa"]
-plugin_dev = ["backend_axum", "cargo_toml", "open", "reqwest", "anyhow", "clearscreen", "watchexec", "cargo_metadata", "diesel_migrations", "futures-util"]
+default = [
+  "backend_actix-web",
+  "database_postgres",
+  "plugin_auth",
+  "plugin_container",
+  "plugin_dev",
+  "plugin_graphql",
+  "plugin_storage",
+  "plugin_utoipa",
+]
+plugin_dev = [
+  "backend_axum",
+  "cargo_toml",
+  "open",
+  "reqwest",
+  "anyhow",
+  "clearscreen",
+  "watchexec",
+  "cargo_metadata",
+  "diesel_migrations",
+  "futures-util",
+]
 plugin_container = []
-plugin_auth = ["anyhow", "rust-argon2", "rand", "jsonwebtoken", "chrono", "tsync"]
-plugin_storage = [ "aws-config", "aws-types", "aws-endpoint", "aws-sdk-s3", "tokio", "http", "diesel_derives", "uuid", "md5", "mime_guess", "base64" ] # note: might need to add "futures-util"?
+plugin_auth = [
+  "anyhow",
+  "rust-argon2",
+  "rand",
+  "jsonwebtoken",
+  "chrono",
+  "tsync",
+  "dyn-clone",
+]
+plugin_storage = [
+  "aws-config",
+  "aws-types",
+  "aws-endpoint",
+  "aws-sdk-s3",
+  "tokio",
+  "http",
+  "diesel_derives",
+  "uuid",
+  "md5",
+  "mime_guess",
+  "base64",
+] # note: might need to add "futures-util"?
 plugin_graphql = []
 plugin_utoipa = ["utoipa", "utoipa-swagger-ui", "backend_actix-web"]
 plugin_tasks = ["fang"]
 backend_poem = ["poem", "anyhow", "mime_guess", "tokio"]
-backend_actix-web = ["actix-web", "actix-http", "actix-files", "actix-multipart", "actix-web-httpauth","derive_more", "futures", "env_logger"]
+backend_actix-web = [
+  "actix-web",
+  "actix-http",
+  "actix-files",
+  "actix-multipart",
+  "actix-web-httpauth",
+  "derive_more",
+  "futures",
+  "env_logger",
+]
 backend_axum = ["axum", "axum/ws", "tokio"]
-database_sqlite = ["diesel/sqlite", "diesel/returning_clauses_for_sqlite_3_35", "libsqlite3-sys/bundled"]
+database_sqlite = [
+  "diesel/sqlite",
+  "diesel/returning_clauses_for_sqlite_3_35",
+  "libsqlite3-sys/bundled",
+]
 database_postgres = ["diesel/postgres"]
diff --git a/create-rust-app/src/auth/controller.rs b/create-rust-app/src/auth/controller.rs
index b9132bba..052fbd11 100644
--- a/create-rust-app/src/auth/controller.rs
+++ b/create-rust-app/src/auth/controller.rs
@@ -529,11 +529,9 @@ pub fn register(
     )
     .unwrap();
 
-    mail::auth_register::send(
-        mailer,
-        &user.email,
-        &format!("http://localhost:3000/activate?token={token}"),
-    );
+    mailer
+        .templates
+        .send_register(mailer, &user.email, &format!("activate?token={token}"));
 
     Ok(())
 }
@@ -598,7 +596,7 @@ pub fn activate(
         return Err((500, "Could not activate user."));
     }
 
-    mail::auth_activated::send(mailer, &user.email);
+    mailer.templates.send_activated(mailer, &user.email);
 
     Ok(())
 }
@@ -642,11 +640,15 @@ pub fn forgot_password(
         )
         .unwrap();
 
-        let link = &format!("http://localhost:3000/reset?token={reset_token}");
-        mail::auth_recover_existent_account::send(mailer, &user.email, link);
+        let link = &format!("reset?token={reset_token}");
+        mailer
+            .templates
+            .send_recover_existent_account(mailer, &user.email, link);
     } else {
-        let link = &"http://localhost:300/register".to_string();
-        mail::auth_recover_nonexistent_account::send(mailer, &item.email, link);
+        let link = &"register".to_string();
+        mailer
+            .templates
+            .send_recover_nonexistent_account(mailer, &item.email, link);
     }
 
     Ok(())
@@ -718,7 +720,7 @@ pub fn change_password(
         return Err((500, "Could not update password"));
     }
 
-    mail::auth_password_changed::send(mailer, &user.email);
+    mailer.templates.send_password_changed(mailer, &user.email);
 
     Ok(())
 }
@@ -794,7 +796,7 @@ pub fn reset_password(
         return Err((500, "Could not update password"));
     }
 
-    mail::auth_password_reset::send(mailer, &user.email);
+    mailer.templates.send_password_reset(mailer, &user.email);
 
     Ok(())
 }
diff --git a/create-rust-app/src/auth/mail/auth_activated.rs b/create-rust-app/src/auth/mail/auth_activated.rs
index 2dfaeed0..6e7f4b0b 100644
--- a/create-rust-app/src/auth/mail/auth_activated.rs
+++ b/create-rust-app/src/auth/mail/auth_activated.rs
@@ -2,7 +2,7 @@ use crate::Mailer;
 
 #[allow(dead_code)]
 pub fn send(mailer: &Mailer, to_email: &str) {
-    let subject = "Account activated)";
+    let subject = "Account activated";
     let text = r#"
 (This is an automated message.)
 
@@ -12,11 +12,11 @@ Your account has been activated!
 "#
     .to_string();
     let html = r#"
-(This is an automated message.)
+<p>(This is an automated message.)</p>
 
-Hello,
+<p>Hello,</p>
 
-Your account has been activated!
+<p>Your account has been activated!</p>
 "#
     .to_string();
 
diff --git a/create-rust-app/src/auth/mail/auth_password_changed.rs b/create-rust-app/src/auth/mail/auth_password_changed.rs
index c1c4b2de..84538b20 100644
--- a/create-rust-app/src/auth/mail/auth_password_changed.rs
+++ b/create-rust-app/src/auth/mail/auth_password_changed.rs
@@ -2,7 +2,7 @@ use crate::Mailer;
 
 #[allow(dead_code)]
 pub fn send(mailer: &Mailer, to_email: &str) {
-    let subject = "Your password was changed)";
+    let subject = "Your password was changed";
     let text = r#"
 (This is an automated message.)
 
@@ -12,11 +12,11 @@ Your password was changed successfully!
 "#
     .to_string();
     let html = r#"
-(This is an automated message.)
+<p>(This is an automated message.)</p>
 
-Hello,
+<p>Hello,</p>
 
-Your password was changed successfully!
+<p>Your password was changed successfully!</p>
 "#
     .to_string();
 
diff --git a/create-rust-app/src/auth/mail/auth_password_reset.rs b/create-rust-app/src/auth/mail/auth_password_reset.rs
index eee78367..0299c99e 100644
--- a/create-rust-app/src/auth/mail/auth_password_reset.rs
+++ b/create-rust-app/src/auth/mail/auth_password_reset.rs
@@ -2,7 +2,7 @@ use crate::Mailer;
 
 #[allow(dead_code)]
 pub fn send(mailer: &Mailer, to_email: &str) {
-    let subject = "Your password was reset)";
+    let subject = "Your password was reset";
     let text = r#"
 (This is an automated message.)
 
@@ -13,11 +13,11 @@ Your password was successfully reset!
     .to_string();
 
     let html = r#"
-(This is an automated message.)
+<p>(This is an automated message.)</p>
 
-Hello,
+<p>Hello,</p>
 
-Your password was successfully reset!
+<p>Your password was successfully reset!</p>
 "#
     .to_string();
 
diff --git a/create-rust-app/src/auth/mail/auth_recover_existent_account.rs b/create-rust-app/src/auth/mail/auth_recover_existent_account.rs
index 88f6f60e..62e59cf4 100644
--- a/create-rust-app/src/auth/mail/auth_recover_existent_account.rs
+++ b/create-rust-app/src/auth/mail/auth_recover_existent_account.rs
@@ -2,7 +2,7 @@ use crate::Mailer;
 
 #[allow(dead_code)]
 pub fn send(mailer: &Mailer, to_email: &str, link: &str) {
-    let subject = "Reset Password Instructions)";
+    let subject = "Reset Password Instructions";
     let text = format!(
         r#"
 (This is an automated message.)
@@ -17,14 +17,14 @@ Please visit this link to reset your password:
     );
     let html = format!(
         r#"
-(This is an automated message.)
+<p>(This is an automated message.)</p>
 
-Hello,
+<p>Hello,</p>
 
-Someone requested a password reset for the account associated with this email.
-Please visit this link to reset your password:
-{link}
-(valid for 24 hours)
+<p>Someone requested a password reset for the account associated with this email.
+Please visit this link to reset your password:</p>
+<p><a href="{link}">{link}</a></p>
+<p>(valid for 24 hours)</p>
 "#
     );
 
diff --git a/create-rust-app/src/auth/mail/auth_recover_nonexistent_account.rs b/create-rust-app/src/auth/mail/auth_recover_nonexistent_account.rs
index 919f00fa..0fcaefc7 100644
--- a/create-rust-app/src/auth/mail/auth_recover_nonexistent_account.rs
+++ b/create-rust-app/src/auth/mail/auth_recover_nonexistent_account.rs
@@ -2,7 +2,7 @@ use crate::Mailer;
 
 #[allow(dead_code)]
 pub fn send(mailer: &Mailer, to_email: &str, link: &str) {
-    let subject = "Reset Password Instructions)";
+    let subject = "Reset Password Instructions";
     let text = format!(
         r#"
 (This is an automated message.)
@@ -16,13 +16,13 @@ If this was intentional, you can register for a new account using the link below
     );
     let html = format!(
         r#"
-(This is an automated message.)
+<p>(This is an automated message.)</p>
 
-Hello,
+<p>Hello,<p>
 
-Someone requested a password reset for the account associated with this email, but no account exists!
-If this was intentional, you can register for a new account using the link below:
-{link}
+<p>Someone requested a password reset for the account associated with this email, but no account exists!
+If this was intentional, you can register for a new account using the link below:</p>
+<p><a href="{link}">{link}</a></p>
 "#
     );
 
diff --git a/create-rust-app/src/auth/mail/auth_register.rs b/create-rust-app/src/auth/mail/auth_register.rs
index e2e571f1..dda7e6fb 100644
--- a/create-rust-app/src/auth/mail/auth_register.rs
+++ b/create-rust-app/src/auth/mail/auth_register.rs
@@ -2,7 +2,7 @@ use crate::Mailer;
 
 #[allow(dead_code)]
 pub fn send(mailer: &Mailer, to_email: &str, link: &str) {
-    let subject = "Registration Confirmation)";
+    let subject = "Registration Confirmation";
     let text = format!(
         r#"
 (This is an automated message.)
@@ -15,12 +15,12 @@ Please follow the link below to complete your registration:
     );
     let html = format!(
         r#"
-(This is an automated message.)
+<p>(This is an automated message.)</p>
 
-Hello,
+<p>Hello,</p>
 
-Please follow the link below to complete your registration:
-{link}
+<p>Please follow the link below to complete your registration:</p>
+<p><a href="{link}">{link}</a></p>
 "#
     );
 
diff --git a/create-rust-app/src/auth/mod.rs b/create-rust-app/src/auth/mod.rs
index ec6642b4..e8dcb0b0 100644
--- a/create-rust-app/src/auth/mod.rs
+++ b/create-rust-app/src/auth/mod.rs
@@ -15,7 +15,7 @@ pub mod controller;
 mod endpoints;
 pub use endpoints::*;
 
-mod mail;
+pub(crate) mod mail;
 mod permissions;
 mod schema;
 mod user;
diff --git a/create-rust-app/src/database/mod.rs b/create-rust-app/src/database/mod.rs
index c8b9f9a4..56683467 100644
--- a/create-rust-app/src/database/mod.rs
+++ b/create-rust-app/src/database/mod.rs
@@ -1,5 +1,5 @@
-use diesel::PgConnection;
 use diesel::r2d2::{self, ConnectionManager, PooledConnection};
+use diesel::PgConnection;
 use once_cell::sync::OnceCell;
 
 #[cfg(feature = "database_postgres")]
diff --git a/create-rust-app/src/lib.rs b/create-rust-app/src/lib.rs
index 8cae7cb6..998409fd 100644
--- a/create-rust-app/src/lib.rs
+++ b/create-rust-app/src/lib.rs
@@ -44,6 +44,8 @@ pub use storage::{Attachment, AttachmentBlob, AttachmentData, Storage};
 
 mod mailer;
 pub use mailer::Mailer;
+#[cfg(feature = "plugin_auth")]
+pub use mailer::{DefaultMailTemplates, EmailTemplates};
 
 // #[cfg(debug_assertions)]
 // #[macro_use]
@@ -67,6 +69,17 @@ pub struct AppData {
     pub storage: Storage,
 }
 
+#[cfg(feature = "plugin_auth")]
+impl AppData {
+    pub fn with_custom_email_templates<T: EmailTemplates + 'static>(
+        mut self,
+        templates: T,
+    ) -> Self {
+        self.mailer = Mailer::new(Box::new(templates));
+        self
+    }
+}
+
 #[cfg(debug_assertions)]
 fn load_env_vars() {
     static START: std::sync::Once = std::sync::Once::new();
@@ -103,7 +116,7 @@ pub fn setup() -> AppData {
     }
 
     AppData {
-        mailer: Mailer::new(),
+        mailer: Mailer::default(),
         database: Database::new(),
         #[cfg(feature = "plugin_storage")]
         storage: Storage::new(),
diff --git a/create-rust-app/src/mailer.rs b/create-rust-app/src/mailer.rs
index e2b84d0a..ef217d57 100644
--- a/create-rust-app/src/mailer.rs
+++ b/create-rust-app/src/mailer.rs
@@ -1,8 +1,32 @@
+#[cfg(feature = "plugin_auth")]
+use crate::auth::mail::{
+    auth_activated, auth_password_changed, auth_password_reset, auth_recover_existent_account,
+    auth_recover_nonexistent_account, auth_register,
+};
+#[cfg(feature = "plugin_auth")]
+use dyn_clone::{clone_trait_object, DynClone};
+
 use lettre::message::{Message, MultiPart};
 use lettre::transport::smtp::authentication::Credentials;
 use lettre::transport::stub::StubTransport;
 use lettre::{SmtpTransport, Transport};
 
+// the DyncClone trait bound is for cloning, and the
+// Send trait bound is for thread-safety
+#[cfg(feature = "plugin_auth")]
+/// A trait that defines the behavior of an email template
+pub trait EmailTemplates: DynClone + Send {
+    fn send_activated(&self, mailer: &Mailer, to_email: &str);
+    fn send_password_changed(&self, mailer: &Mailer, to_email: &str);
+    fn send_password_reset(&self, mailer: &Mailer, to_email: &str);
+    fn send_recover_existent_account(&self, mailer: &Mailer, to_email: &str, link: &str);
+    fn send_recover_nonexistent_account(&self, mailer: &Mailer, to_email: &str, link: &str);
+    fn send_register(&self, mailer: &Mailer, to_email: &str, link: &str);
+}
+
+#[cfg(feature = "plugin_auth")]
+clone_trait_object!(EmailTemplates);
+
 #[derive(Clone)]
 /// struct used to handle sending emails
 pub struct Mailer {
@@ -29,9 +53,17 @@ pub struct Mailer {
     ///
     /// set by the `SEND_MAIL` environment variable
     pub actually_send: bool,
+    #[cfg(feature = "plugin_auth")]
+    // Structure containing email templates to be used for various purposes
+    pub templates: Box<dyn EmailTemplates>,
 }
 
 impl Default for Mailer {
+    #[cfg(feature = "plugin_auth")]
+    fn default() -> Self {
+        Self::new(Box::new(DefaultMailTemplates::default()))
+    }
+    #[cfg(not(feature = "plugin_auth"))]
     fn default() -> Self {
         Self::new()
     }
@@ -43,6 +75,7 @@ impl Mailer {
     ///
     /// allows webservers to send emails to users for purposes
     /// like marketing, user authentification, etc.
+    #[cfg(not(feature = "plugin_auth"))]
     pub fn new() -> Self {
         Mailer::check_environment_variables();
 
@@ -56,13 +89,36 @@ impl Mailer {
         let actually_send: bool = std::env::var("SEND_MAIL")
             .unwrap_or_else(|_| "false".to_string())
             .eq_ignore_ascii_case("true");
+        Mailer {
+            from_address,
+            smtp_server,
+            smtp_username,
+            smtp_password,
+            actually_send,
+        }
+    }
+
+    #[cfg(feature = "plugin_auth")]
+    pub fn new(templates: Box<dyn EmailTemplates>) -> Self {
+        Mailer::check_environment_variables();
 
+        let from_address: String = std::env::var("SMTP_FROM_ADDRESS")
+            .unwrap_or_else(|_| "create-rust-app@localhost".to_string());
+        let smtp_server: String = std::env::var("SMTP_SERVER").unwrap_or_else(|_| "".to_string());
+        let smtp_username: String =
+            std::env::var("SMTP_USERNAME").unwrap_or_else(|_| "".to_string());
+        let smtp_password: String =
+            std::env::var("SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
+        let actually_send: bool = std::env::var("SEND_MAIL")
+            .unwrap_or_else(|_| "false".to_string())
+            .eq_ignore_ascii_case("true");
         Mailer {
             from_address,
             smtp_server,
             smtp_username,
             smtp_password,
             actually_send,
+            templates,
         }
     }
 
@@ -158,3 +214,56 @@ message:
         }
     }
 }
+
+#[cfg(feature = "plugin_auth")]
+#[derive(Clone)]
+pub struct DefaultMailTemplates {
+    pub base_url: String,
+}
+#[cfg(feature = "plugin_auth")]
+impl DefaultMailTemplates {
+    pub fn new(base_url: &str) -> Self {
+        Self {
+            base_url: base_url.to_string(),
+        }
+    }
+}
+#[cfg(feature = "plugin_auth")]
+impl Default for DefaultMailTemplates {
+    fn default() -> Self {
+        Self::new("http://localhost:3000/")
+    }
+}
+#[cfg(feature = "plugin_auth")]
+impl EmailTemplates for DefaultMailTemplates {
+    fn send_activated(&self, mailer: &Mailer, to_email: &str) {
+        auth_activated::send(mailer, to_email);
+    }
+    fn send_password_changed(&self, mailer: &Mailer, to_email: &str) {
+        auth_password_changed::send(mailer, to_email);
+    }
+    fn send_password_reset(&self, mailer: &Mailer, to_email: &str) {
+        auth_password_reset::send(mailer, to_email);
+    }
+    fn send_recover_existent_account(&self, mailer: &Mailer, to_email: &str, url_path: &str) {
+        auth_recover_existent_account::send(
+            mailer,
+            to_email,
+            format!("{base_url}{url_path}", base_url = self.base_url).as_str(),
+        );
+    }
+    fn send_recover_nonexistent_account(&self, mailer: &Mailer, to_email: &str, url_path: &str) {
+        auth_recover_nonexistent_account::send(
+            mailer,
+            to_email,
+            format!("{base_url}{url_path}", base_url = self.base_url).as_str(),
+        );
+    }
+    fn send_register(&self, mailer: &Mailer, to_email: &str, url_path: &str) {
+        auth_register::send(
+            mailer,
+            to_email,
+            format!("{base_url}{url_path}", base_url = self.base_url).as_str(),
+        );
+    }
+}
diff --git a/create-rust-app/src/tasks/mod.rs b/create-rust-app/src/tasks/mod.rs
index d2ebc9cc..c9cd5525 100644
--- a/create-rust-app/src/tasks/mod.rs
+++ b/create-rust-app/src/tasks/mod.rs
@@ -1,6 +1,6 @@
+use crate::Database;
 use fang::Queue;
 use once_cell::sync::OnceCell;
-use crate::Database;
 // re-export setup for tasks
 pub use crate::setup;
 
@@ -13,8 +13,6 @@ pub fn queue() -> &'static Queue {
     QUEUE.get_or_init(|| {
         let db = Database::new();
 
-        Queue::builder()
-            .connection_pool(db.pool.clone())
-            .build()
+        Queue::builder().connection_pool(db.pool.clone()).build()
     })
 }
diff --git a/create-rust-app_cli/src/content/project.rs b/create-rust-app_cli/src/content/project.rs
index cc411599..23975d43 100644
--- a/create-rust-app_cli/src/content/project.rs
+++ b/create-rust-app_cli/src/content/project.rs
@@ -44,7 +44,10 @@ fn get_current_cra_lib_version() -> String {
     "9".to_string()
 }
 
-fn add_bins_to_cargo_toml(project_dir: &std::path::PathBuf, creations_options: &CreationOptions) -> Result<(), std::io::Error> {
+fn add_bins_to_cargo_toml(
+    project_dir: &std::path::PathBuf,
+    creations_options: &CreationOptions,
+) -> Result<(), std::io::Error> {
     let mut path = std::path::PathBuf::from(project_dir);
     path.push("Cargo.toml");
 
@@ -77,7 +80,10 @@ fn add_bins_to_cargo_toml(project_dir: &std::path::PathBuf, creations_options: &
 
     let updated_toml = toml::to_string(&parsed_toml).unwrap();
 
-    let queue_bin = if creations_options.cra_enabled_features.contains(&"plugin_tasks".to_string()) {
+    let queue_bin = if creations_options
+        .cra_enabled_features
+        .contains(&"plugin_tasks".to_string())
+    {
         r##"
 [[bin]]
 name = "queue"
@@ -115,7 +121,8 @@ path = "backend/main.rs"
 {queue_bin}
 [profile.dev]
 debug-assertions=true
-"#);
+"#
+    );
 
     let mut final_toml = String::default();
 
diff --git a/create-rust-app_cli/src/plugins/tasks.rs b/create-rust-app_cli/src/plugins/tasks.rs
index f7cc6d3d..262e9880 100644
--- a/create-rust-app_cli/src/plugins/tasks.rs
+++ b/create-rust-app_cli/src/plugins/tasks.rs
@@ -2,12 +2,12 @@ use anyhow::Result;
 use indoc::indoc;
 use rust_embed::RustEmbed;
 
-use crate::{BackendDatabase, BackendFramework};
 use crate::content::cargo_toml::add_dependency;
 use crate::plugins::InstallConfig;
 use crate::plugins::Plugin;
 use crate::utils::fs;
 use crate::utils::logger::add_file_msg;
+use crate::{BackendDatabase, BackendFramework};
 
 pub struct Tasks {}
 
@@ -26,15 +26,11 @@ impl Plugin for Tasks {
         // ===============================
 
         if install_config.backend_database != BackendDatabase::Postgres {
-            return Err(anyhow::anyhow!(
-                "The tasks plugin only supports Postgres"
-            ));
+            return Err(anyhow::anyhow!("The tasks plugin only supports Postgres"));
         }
 
         if install_config.backend_framework != BackendFramework::ActixWeb {
-            return Err(anyhow::anyhow!(
-                "The tasks plugin only supports actix-web"
-            ));
+            return Err(anyhow::anyhow!("The tasks plugin only supports actix-web"));
         }
 
         // ===============================
@@ -106,12 +102,7 @@ impl Plugin for Tasks {
         // Add dependencies
         // ===============================
 
-        add_dependency(
-            &install_config.project_dir,
-            "fang",
-            r#"fang = "0.10.3""#,
-        )?;
-
+        add_dependency(&install_config.project_dir, "fang", r#"fang = "0.10.3""#)?;
 
         Ok(())
     }


Test patch: