forgot password: send emails
This commit is contained in:
parent
751a21485f
commit
feb9f16bc9
10 changed files with 676 additions and 18 deletions
269
Cargo.lock
generated
269
Cargo.lock
generated
|
|
@ -2,6 +2,16 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.6"
|
||||
|
|
@ -447,8 +457,18 @@ version = "0.13.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.13.4",
|
||||
"darling_macro 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
||||
dependencies = [
|
||||
"darling_core 0.14.4",
|
||||
"darling_macro 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -465,13 +485,38 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.13.4",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
||||
dependencies = [
|
||||
"darling_core 0.14.4",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
|
@ -608,6 +653,27 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"lettre",
|
||||
"mrml",
|
||||
"serde",
|
||||
"tera",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-encoding"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.4"
|
||||
|
|
@ -668,6 +734,7 @@ dependencies = [
|
|||
"base64 0.21.0",
|
||||
"chrono-tz 0.8.2",
|
||||
"database_pool",
|
||||
"email",
|
||||
"email_address",
|
||||
"erased-serde",
|
||||
"futures",
|
||||
|
|
@ -1056,6 +1123,17 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
|
|
@ -1355,6 +1433,34 @@ dependencies = [
|
|||
"spin 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.21.0",
|
||||
"email-encoding",
|
||||
"email_address",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"httpdate",
|
||||
"idna",
|
||||
"mime",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"quoted_printable",
|
||||
"rustls 0.21.0",
|
||||
"rustls-pemfile",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.0",
|
||||
"webpki-roots 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.140"
|
||||
|
|
@ -1427,6 +1533,12 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1466,6 +1578,88 @@ dependencies = [
|
|||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mrml"
|
||||
version = "2.0.0-rc2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b65039434eb297dfdd343ea1e4f28e95366ce6d957de07a24a940d5e760ede1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"mrml-json-macros",
|
||||
"mrml-macros",
|
||||
"mrml-parse-macros",
|
||||
"mrml-print-macros",
|
||||
"rand",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mrml-common-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "646bbe56cd666665355ddc095162c603c64511e897bd5033376abdcf8b9be623"
|
||||
dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mrml-json-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f08de794f5dae4be523ef4ae7b35c53e70d6b37fe3995db2e2d0d2dec784677"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"darling 0.14.4",
|
||||
"mrml-common-macros",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mrml-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c2c5bffc248ce37d7c1b55389cf4ccae1a7647e4ed295684906c9ab0cd762bd"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"mrml-common-macros",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mrml-parse-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "012179ed2e5f76915544bc78e8aeed10f49fdc06f60f1250c7a993568569c7c7"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"darling 0.14.4",
|
||||
"mrml-common-macros",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mrml-print-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3ed636b13c5df8ad22087f548be69cd1416c2f5929d687406e7c872528bd55"
|
||||
dependencies = [
|
||||
"darling 0.14.4",
|
||||
"mrml-common-macros",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "2.0.4"
|
||||
|
|
@ -2047,6 +2241,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
|
@ -2343,6 +2543,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.36.11"
|
||||
|
|
@ -2369,6 +2575,18 @@ dependencies = [
|
|||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.2"
|
||||
|
|
@ -2378,6 +2596,16 @@ dependencies = [
|
|||
"base64 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.100.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.12"
|
||||
|
|
@ -2526,7 +2754,7 @@ version = "1.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.13.4",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
|
@ -2712,7 +2940,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls 0.20.8",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"sha2",
|
||||
|
|
@ -2723,7 +2951,7 @@ dependencies = [
|
|||
"thiserror",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.22.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2756,7 +2984,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
|
|||
dependencies = [
|
||||
"once_cell",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.23.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2996,11 +3224,21 @@ version = "0.23.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.20.8",
|
||||
"tokio",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
|
||||
dependencies = [
|
||||
"rustls 0.21.0",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.12"
|
||||
|
|
@ -3419,6 +3657,15 @@ dependencies = [
|
|||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125"
|
||||
dependencies = [
|
||||
"rustls-webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
@ -3549,6 +3796,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ FROM alpine:3.17.2
|
|||
COPY --from=builder /ezidam/target/x86_64-unknown-linux-musl/release/ezidam /ezidam/ezidam
|
||||
COPY crates/ezidam/static /ezidam/static
|
||||
COPY crates/ezidam/templates /ezidam/templates
|
||||
COPY crates/email/templates /ezidam/email-templates
|
||||
ENV ROCKET_CLI_COLORS=0
|
||||
ENV ROCKET_ADDRESS=0.0.0.0
|
||||
ENV ROCKET_PORT=8000
|
||||
|
|
|
|||
24
crates/email/Cargo.toml
Normal file
24
crates/email/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "email"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
tera = "1.18.1"
|
||||
mrml = "2.0.0-rc2"
|
||||
serde = { workspace = true }
|
||||
|
||||
[dependencies.lettre]
|
||||
version = "0.10.3"
|
||||
default-features = false
|
||||
features = [
|
||||
# tokio, rustls
|
||||
"tokio1",
|
||||
"tokio1-rustls-tls",
|
||||
# default features
|
||||
"smtp-transport",
|
||||
"pool",
|
||||
"hostname",
|
||||
"builder",
|
||||
]
|
||||
36
crates/email/readme.md
Normal file
36
crates/email/readme.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# email
|
||||
|
||||
create and send emails from templates
|
||||
|
||||
## template
|
||||
|
||||
- `mjml` to create emails
|
||||
- `tera` to render emails with parameters
|
||||
|
||||
## send
|
||||
|
||||
- `lettre` to send the emails with smtp
|
||||
|
||||
## rocket config
|
||||
|
||||
- `from`: Sender of emails, usually name and email address
|
||||
- `transport`: Security method to send the mails. `unencrypted` or `tls` or `starttls`
|
||||
- `host`: Where to send smtp traffic
|
||||
- `port`: *optional*: Used when `transport` is `unencrypted`
|
||||
- `username` and `password`: *optional*: Used when `transport` is `tls` or `starttls`
|
||||
|
||||
### `Rocket.toml`
|
||||
|
||||
```toml
|
||||
[default.email]
|
||||
from = "ezidam <ezidam@mail.local>"
|
||||
transport = "unencrypted"
|
||||
host = "localhost"
|
||||
port = 1025
|
||||
```
|
||||
|
||||
### env
|
||||
|
||||
```dotenv
|
||||
ROCKET_EMAIL='{from="ezidam <ezidam@mail.local>",transport="unencrypted",host="localhost",port=1025}'
|
||||
```
|
||||
151
crates/email/src/lib.rs
Normal file
151
crates/email/src/lib.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::transport::smtp::response::Response;
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use mrml::mjml::Mjml;
|
||||
use mrml::prelude::parse::Error as MjmlParserError;
|
||||
use mrml::prelude::render::{Error as MjmlRenderError, Options};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::{Context, Tera};
|
||||
|
||||
// error
|
||||
#[derive(thiserror::Error)]
|
||||
// the rest
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
#[error("Template error: {0}")]
|
||||
Template(#[from] tera::Error),
|
||||
|
||||
#[error("Mjml parser error: {0}")]
|
||||
MjmlParser(#[from] MjmlParserError),
|
||||
|
||||
#[error("Mjml render error: {0}")]
|
||||
MjmlRender(#[from] MjmlRenderError),
|
||||
|
||||
#[error("Invalid email address: {0}")]
|
||||
EmailAddress(#[from] lettre::address::AddressError),
|
||||
|
||||
#[error("Invalid email message: {0}")]
|
||||
EmailMessage(#[from] lettre::error::Error),
|
||||
|
||||
#[error("Error when sending email: {0}")]
|
||||
EmailSend(#[from] lettre::transport::smtp::Error),
|
||||
|
||||
#[error("Invalid {0}")]
|
||||
InvalidConfig(&'static str),
|
||||
}
|
||||
|
||||
const TEMPLATE_EXT: &str = ".mjml.tera";
|
||||
|
||||
pub fn render_template<C: Serialize>(name: &str, content: C) -> Result<String, Error> {
|
||||
let base_dir = if cfg!(debug_assertions) {
|
||||
format!("{}/templates", env!("CARGO_MANIFEST_DIR"))
|
||||
} else {
|
||||
"./email-templates".into()
|
||||
};
|
||||
let templates = format!("{base_dir}/**/*{TEMPLATE_EXT}",);
|
||||
|
||||
// Initialize tera templates
|
||||
let tera = Tera::new(&templates)?;
|
||||
|
||||
// Construct context
|
||||
let context = Context::from_serialize(content)?;
|
||||
|
||||
// Render template
|
||||
let template_name = format!("{name}{TEMPLATE_EXT}");
|
||||
Ok(tera.render(&template_name, &context)?)
|
||||
}
|
||||
|
||||
pub fn render_email(mjml: &str) -> Result<String, Error> {
|
||||
// Parse mjml
|
||||
let root = Mjml::parse(mjml)?;
|
||||
|
||||
// Render to html
|
||||
let opts = Options::default();
|
||||
Ok(root.render(&opts)?)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Transport {
|
||||
Unencrypted,
|
||||
Tls,
|
||||
StartTls,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub from: String,
|
||||
pub transport: Transport,
|
||||
pub host: String,
|
||||
pub port: Option<u16>,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn status(&self) -> Result<String, &'static str> {
|
||||
match self.transport {
|
||||
Transport::Unencrypted => match self.port {
|
||||
Some(port) => Ok(format!("Using \"{}:{port}\" to send emails", self.host)),
|
||||
None => Err("Port not provided."),
|
||||
},
|
||||
Transport::Tls | Transport::StartTls => {
|
||||
if self.username.is_none() {
|
||||
return Err("Username not provided.");
|
||||
}
|
||||
|
||||
if self.password.is_none() {
|
||||
return Err("Password not provided.");
|
||||
}
|
||||
|
||||
Ok(format!("Using \"{}\" to send emails", self.host))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_credentials(config: &Config) -> Result<Credentials, Error> {
|
||||
let username = config
|
||||
.username
|
||||
.as_deref()
|
||||
.ok_or(Error::InvalidConfig("username"))?;
|
||||
let password = config
|
||||
.password
|
||||
.as_deref()
|
||||
.ok_or(Error::InvalidConfig("password"))?;
|
||||
|
||||
Ok(Credentials::from((username, password)))
|
||||
}
|
||||
|
||||
pub async fn send_email(
|
||||
config: &Config,
|
||||
user: &str,
|
||||
email_title: &str,
|
||||
html: String,
|
||||
) -> Result<Response, Error> {
|
||||
let m = Message::builder()
|
||||
.from(config.from.parse()?)
|
||||
.to(user.parse()?)
|
||||
.subject(email_title)
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html)?;
|
||||
|
||||
type SmtpTransport = AsyncSmtpTransport<Tokio1Executor>;
|
||||
|
||||
let mailer = match config.transport {
|
||||
Transport::Unencrypted => {
|
||||
let port = config.port.ok_or(Error::InvalidConfig("port"))?;
|
||||
SmtpTransport::builder_dangerous(&config.host).port(port)
|
||||
}
|
||||
Transport::Tls => {
|
||||
SmtpTransport::relay(&config.host)?.credentials(create_credentials(config)?)
|
||||
}
|
||||
Transport::StartTls => {
|
||||
SmtpTransport::starttls_relay(&config.host)?.credentials(create_credentials(config)?)
|
||||
}
|
||||
}
|
||||
.build();
|
||||
|
||||
Ok(mailer.send(m).await?)
|
||||
}
|
||||
50
crates/email/templates/password-reset.mjml.tera
Normal file
50
crates/email/templates/password-reset.mjml.tera
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>{{ title }}</mj-title>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Arial"/>
|
||||
<mj-text align="center"/>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body background-color="#eee">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image width="100px" src="{{ logo_url }}" alt="business logo"/>
|
||||
<mj-divider border-color="#3a88fe"/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#fff">
|
||||
<mj-column>
|
||||
<mj-text>
|
||||
<h2>Reset password</h2>
|
||||
<p>You recently requested to reset the password of your ezidam account.</p>
|
||||
<p>Use the button below to reset it. This message will expire in {{ token_duration }} minutes.</p>
|
||||
</mj-text>
|
||||
<mj-button href="{{ reset_password_url }}" background-color="#3a88fe">Reset your password</mj-button>
|
||||
<mj-text>
|
||||
<p>Can't click on the button? Use the link below:</p>
|
||||
<a href="{{ reset_password_url }}">{{ reset_password_url }}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text>
|
||||
<p>{{ user_email }}</p>
|
||||
<p>{{ now(utc=true) | date(format="%F %T %Z", timezone=user_timezone | default(value="UTC")) }}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text color="#696969" font-size="10px">
|
||||
<p>ezidam {{ ezidam_version }}</p>
|
||||
<p>Copyright ©
|
||||
{{ now(utc=true) | date(format="%Y", timezone=user_timezone | default(value="UTC")) }}
|
||||
<a href="https://philippeloctaux.com">philt3r technologies</a>
|
||||
</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
|
|
@ -27,3 +27,4 @@ jwt = { path = "../jwt" }
|
|||
apps = { path = "../apps" }
|
||||
authorization_codes = { path = "../authorization_codes" }
|
||||
refresh_tokens = { path = "../refresh_tokens" }
|
||||
email = { path = "../email" }
|
||||
|
|
@ -1,2 +1,8 @@
|
|||
[default.databases.ezidam]
|
||||
url = "../../database/ezidam.sqlite"
|
||||
|
||||
[default.email]
|
||||
from = "ezidam <ezidam@mail.local>"
|
||||
transport = "unencrypted"
|
||||
host = "localhost"
|
||||
port = 1025
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
use ezidam::rocket_setup;
|
||||
use rocket::{main, Error};
|
||||
use rocket::main;
|
||||
|
||||
// see for using rocket with main function https://github.com/intellij-rust/intellij-rust/issues/5975#issuecomment-920620289
|
||||
#[main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
async fn main() -> Result<(), String> {
|
||||
// Custom config
|
||||
// - rocket defaults
|
||||
// - from `Rocket.toml`
|
||||
|
|
@ -11,17 +11,33 @@ async fn main() -> Result<(), Error> {
|
|||
// - from code below
|
||||
let config = rocket::Config::figment().merge(("ip_header", "x-forwarded-for"));
|
||||
|
||||
// Get email config
|
||||
let email_config = match config.extract_inner::<email::Config>("email") {
|
||||
Ok(email_config) => match email_config.status() {
|
||||
Ok(status) => {
|
||||
println!("{status}");
|
||||
email_config
|
||||
}
|
||||
Err(e) => return Err(format!("Invalid email configuration: {e}")),
|
||||
},
|
||||
Err(e) => return Err(format!("No email configuration was found: {e}")),
|
||||
};
|
||||
println!("Emails will be sent from \"{}\"", email_config.from);
|
||||
|
||||
// Rocket with custom config
|
||||
let rocket_builder = rocket::custom(config);
|
||||
|
||||
// Setup server
|
||||
let rocket_builder = rocket_setup(rocket_builder);
|
||||
|
||||
// Attach email config
|
||||
let rocket_builder = rocket_builder.manage(email_config);
|
||||
|
||||
// Launch server
|
||||
let _ = rocket_builder
|
||||
.launch()
|
||||
.await
|
||||
.expect("Failed to launch server");
|
||||
let _ = rocket_builder.launch().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// After shutdown is done
|
||||
println!("Shutdown is complete. Exiting.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
use crate::routes::prelude::*;
|
||||
use email_address::EmailAddress;
|
||||
use hash::PaperKey;
|
||||
use rocket::serde::Serialize;
|
||||
use rocket::State;
|
||||
use rocket::{get, post};
|
||||
use settings::Settings;
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
use users::{password_reset::PasswordResetToken, User};
|
||||
|
||||
#[get("/forgot-password")]
|
||||
|
|
@ -19,12 +23,26 @@ pub struct ForgotPasswordEmailForm<'r> {
|
|||
pub email: &'r str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
#[derive(Clone)]
|
||||
pub struct ResetPasswordEmail {
|
||||
pub title: String,
|
||||
pub ezidam_version: &'static str,
|
||||
pub logo_url: String,
|
||||
pub token_duration: i64,
|
||||
pub reset_password_url: String,
|
||||
pub user_email: String,
|
||||
pub user_timezone: String,
|
||||
}
|
||||
|
||||
const SUCCESS_MESSAGE: &str = "An email is on the way with instructions to reset your password.";
|
||||
|
||||
#[post("/forgot-password/email", data = "<form>")]
|
||||
pub async fn forgot_password_email_form(
|
||||
mut db: Connection<Database>,
|
||||
form: Form<ForgotPasswordEmailForm<'_>>,
|
||||
email_config: &State<email::Config>,
|
||||
) -> Result<Flash<Redirect>> {
|
||||
if form.email.is_empty() {
|
||||
return Ok(Flash::new(
|
||||
|
|
@ -48,6 +66,24 @@ pub async fn forgot_password_email_form(
|
|||
|
||||
let mut transaction = db.begin().await?;
|
||||
|
||||
// Get settings
|
||||
let settings = Settings::get(&mut transaction).await?;
|
||||
|
||||
// Get server url
|
||||
let url = settings
|
||||
.url()
|
||||
.ok_or_else(|| Error::not_found("Server url"))?;
|
||||
let url = match Url::parse(url) {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
return Ok(Flash::new(
|
||||
Redirect::to(uri!(forgot_password_page)),
|
||||
FlashKind::Danger,
|
||||
"Failed to parse server url",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Get user
|
||||
let user = match User::get_by_email(&mut transaction, &email).await? {
|
||||
Some(user) => user,
|
||||
|
|
@ -61,7 +97,8 @@ pub async fn forgot_password_email_form(
|
|||
};
|
||||
|
||||
// Generate reset token
|
||||
let token = task::spawn_blocking(|| PasswordResetToken::generate(15)).await?;
|
||||
let token_duration = 15;
|
||||
let token = task::spawn_blocking(move || PasswordResetToken::generate(token_duration)).await?;
|
||||
|
||||
// Save in database
|
||||
user.set_password_reset_token(&mut transaction, Some(&token))
|
||||
|
|
@ -69,12 +106,95 @@ pub async fn forgot_password_email_form(
|
|||
|
||||
transaction.commit().await?;
|
||||
|
||||
// TODO: send email here
|
||||
// Construct url to reset password
|
||||
let token = RocketResetPasswordToken(token);
|
||||
let uri = uri!(super::reset_password_page(token)).to_string();
|
||||
let reset_url = match url.join(&uri) {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
return Ok(Flash::new(
|
||||
Redirect::to(uri!(forgot_password_page)),
|
||||
FlashKind::Danger,
|
||||
"Failed to construct url to reset password ",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Url to logo
|
||||
let logo_url = match url.join("/logo") {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
return Ok(Flash::new(
|
||||
Redirect::to(uri!(forgot_password_page)),
|
||||
FlashKind::Danger,
|
||||
"Failed to construct url to business logo",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let email_title = "Reset password - ezidam";
|
||||
|
||||
let content = ResetPasswordEmail {
|
||||
title: email_title.into(),
|
||||
ezidam_version: env!("CARGO_PKG_VERSION"),
|
||||
logo_url: logo_url.to_string(),
|
||||
token_duration,
|
||||
reset_password_url: reset_url.to_string(),
|
||||
user_email: email.to_string(),
|
||||
user_timezone: user.timezone().into(),
|
||||
};
|
||||
|
||||
// Render email template
|
||||
let mjml = match email::render_template("password-reset", &content) {
|
||||
Ok(mjml) => mjml,
|
||||
Err(e) => {
|
||||
return Ok(Flash::new(
|
||||
Redirect::to(uri!(forgot_password_page)),
|
||||
FlashKind::Danger,
|
||||
format!("Failed to create email: {e}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Create html email
|
||||
let html = match email::render_email(&mjml) {
|
||||
Ok(html) => html,
|
||||
Err(e) => {
|
||||
return Ok(Flash::new(
|
||||
Redirect::to(uri!(forgot_password_page)),
|
||||
FlashKind::Danger,
|
||||
format!("Failed to render email: {e}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let user_for_email = match user.name() {
|
||||
Some(name) => {
|
||||
format!("{name} <{email}>")
|
||||
}
|
||||
None => email.to_string(),
|
||||
};
|
||||
|
||||
// Send email
|
||||
let (flash_kind, flash_message) =
|
||||
match email::send_email(email_config, &user_for_email, email_title, html).await {
|
||||
Ok(okay) => {
|
||||
if okay.is_positive() {
|
||||
(FlashKind::Success, SUCCESS_MESSAGE.to_string())
|
||||
} else {
|
||||
(
|
||||
FlashKind::Warning,
|
||||
"Email should be on it's way, but it might not arrive".into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => (FlashKind::Danger, e.to_string()),
|
||||
};
|
||||
|
||||
Ok(Flash::new(
|
||||
Redirect::to(uri!(forgot_password_page)),
|
||||
FlashKind::Success,
|
||||
SUCCESS_MESSAGE,
|
||||
flash_kind,
|
||||
flash_message,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue