1use std::collections::HashMap;
2use std::sync::Arc;
3
4use jsonwebtoken::{encode, EncodingKey, Header};
5use lazy_static::lazy_static;
6use lettre::message::{MessageBuilder, MultiPart, SinglePart};
7use lettre::transport::smtp::authentication::{Credentials, Mechanism};
8use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
9use serde::{Deserialize, Serialize};
10use serde_json::value::{to_value, Value};
11use tera::{try_get_value, Context, Tera};
12
13use crate::config::Configuration;
14use crate::errors::ServiceError;
15use crate::utils::clock;
16use crate::web::api::server::v1::routes::API_VERSION_URL_PREFIX;
17
18lazy_static! {
19 pub static ref TEMPLATES: Tera = {
20 let mut tera = Tera::default();
21
22 match tera.add_template_file("templates/verify.html", Some("html_verify_email")) {
23 Ok(()) => {}
24 Err(e) => {
25 println!("Parsing error(s): {e}");
26 ::std::process::exit(1);
27 }
28 };
29
30 tera.autoescape_on(vec![".html", ".sql"]);
31 tera.register_filter("do_nothing", do_nothing_filter);
32 tera
33 };
34}
35
36#[allow(clippy::implicit_hasher)]
46pub fn do_nothing_filter(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
47 let s = try_get_value!("do_nothing_filter", "value", String, value);
48 Ok(to_value(s).unwrap())
49}
50
51pub struct Service {
52 cfg: Arc<Configuration>,
53 mailer: Arc<Mailer>,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct VerifyClaims {
58 pub iss: String,
59 pub sub: i64,
60 pub exp: u64,
61}
62
63impl Service {
64 pub async fn new(cfg: Arc<Configuration>) -> Service {
65 let mailer = Arc::new(Self::get_mailer(&cfg).await);
66
67 Self { cfg, mailer }
68 }
69
70 async fn get_mailer(cfg: &Configuration) -> Mailer {
71 let settings = cfg.settings.read().await;
72
73 if !settings.mail.smtp.credentials.username.is_empty() && !settings.mail.smtp.credentials.password.is_empty() {
74 let creds = Credentials::new(
76 settings.mail.smtp.credentials.username.clone(),
77 settings.mail.smtp.credentials.password.clone(),
78 );
79
80 AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&settings.mail.smtp.server)
81 .port(settings.mail.smtp.port)
82 .credentials(creds)
83 .authentication(vec![Mechanism::Login, Mechanism::Xoauth2, Mechanism::Plain])
84 .build()
85 } else {
86 AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&settings.mail.smtp.server)
88 .port(settings.mail.smtp.port)
89 .build()
90 }
91 }
92
93 pub async fn send_verification_mail(
103 &self,
104 to: &str,
105 username: &str,
106 user_id: i64,
107 base_url: &str,
108 ) -> Result<(), ServiceError> {
109 let builder = self.get_builder(to).await;
110 let verification_url = self.get_verification_url(user_id, base_url).await;
111
112 let mail = build_letter(verification_url.as_str(), username, builder)?;
113
114 match self.mailer.send(mail).await {
115 Ok(_res) => Ok(()),
116 Err(e) => {
117 eprintln!("Failed to send email: {e}");
118 Err(ServiceError::FailedToSendVerificationEmail)
119 }
120 }
121 }
122
123 async fn get_builder(&self, to: &str) -> MessageBuilder {
124 let settings = self.cfg.settings.read().await;
125
126 Message::builder()
127 .from(settings.mail.from.clone())
128 .reply_to(settings.mail.reply_to.clone())
129 .to(to.parse().unwrap())
130 }
131
132 async fn get_verification_url(&self, user_id: i64, base_url: &str) -> String {
133 let settings = self.cfg.settings.read().await;
134
135 let key = settings.auth.user_claim_token_pepper.as_bytes();
137
138 let claims = VerifyClaims {
140 iss: String::from("email-verification"),
141 sub: user_id,
142 exp: clock::now() + 315_569_260, };
144
145 let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).unwrap();
146
147 let base_url = match &settings.net.base_url {
148 Some(url) => url.to_string(),
149 None => base_url.to_string(),
150 };
151
152 format!("{base_url}/{API_VERSION_URL_PREFIX}/user/email/verify/{token}")
153 }
154}
155
156fn build_letter(verification_url: &str, username: &str, builder: MessageBuilder) -> Result<Message, ServiceError> {
157 let (plain_body, html_body) = build_content(verification_url, username).map_err(|e| {
158 tracing::error!("{e}");
159 ServiceError::InternalServerError
160 })?;
161
162 Ok(builder
163 .subject("Torrust - Email verification")
164 .multipart(
165 MultiPart::alternative()
166 .singlepart(
167 SinglePart::builder()
168 .header(lettre::message::header::ContentType::TEXT_PLAIN)
169 .body(plain_body),
170 )
171 .singlepart(
172 SinglePart::builder()
173 .header(lettre::message::header::ContentType::TEXT_HTML)
174 .body(html_body),
175 ),
176 )
177 .expect("the `multipart` builder had an error"))
178}
179
180fn build_content(verification_url: &str, username: &str) -> Result<(String, String), tera::Error> {
181 let plain_body = format!(
182 r#"
183 Welcome to Torrust, {username}!
184
185 Please click the confirmation link below to verify your account.
186 {verification_url}
187
188 If this account wasn't made by you, you can ignore this email.
189 "#
190 );
191 let mut context = Context::new();
192 context.insert("verification", &verification_url);
193 context.insert("username", &username);
194 let html_body = TEMPLATES.render("html_verify_email", &context)?;
195 Ok((plain_body, html_body))
196}
197
198pub type Mailer = AsyncSmtpTransport<Tokio1Executor>;
199
200#[cfg(test)]
201mod tests {
202 use lettre::Message;
203
204 use super::{build_content, build_letter};
205
206 #[test]
207 fn it_should_build_a_letter() {
208 let builder = Message::builder()
209 .from("from@a.b.c".parse().unwrap())
210 .reply_to("reply@a.b.c".parse().unwrap())
211 .to("to@a.b.c".parse().unwrap());
212
213 let _letter = build_letter("https://a.b.c/", "user", builder).unwrap();
214 }
215
216 #[test]
217 fn it_should_build_content() {
218 let (plain_body, html_body) = build_content("https://a.b.c/", "user").unwrap();
219 assert_ne!(plain_body, "");
220 assert_ne!(html_body, "");
221 }
222}