torrust_index_backend/
mailer.rs1use std::sync::Arc;
2
3use jsonwebtoken::{encode, EncodingKey, Header};
4use lettre::message::{MessageBuilder, MultiPart, SinglePart};
5use lettre::transport::smtp::authentication::{Credentials, Mechanism};
6use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
7use sailfish::TemplateOnce;
8use serde::{Deserialize, Serialize};
9
10use crate::config::Configuration;
11use crate::errors::ServiceError;
12use crate::utils::clock;
13use crate::web::api::v1::routes::API_VERSION_URL_PREFIX;
14
15pub struct Service {
16 cfg: Arc<Configuration>,
17 mailer: Arc<Mailer>,
18}
19
20#[derive(Debug, Serialize, Deserialize)]
21pub struct VerifyClaims {
22 pub iss: String,
23 pub sub: i64,
24 pub exp: u64,
25}
26
27#[derive(TemplateOnce)]
28#[template(path = "../templates/verify.html")]
29struct VerifyTemplate {
30 username: String,
31 verification_url: String,
32}
33
34impl Service {
35 pub async fn new(cfg: Arc<Configuration>) -> Service {
36 let mailer = Arc::new(Self::get_mailer(&cfg).await);
37
38 Self { cfg, mailer }
39 }
40
41 async fn get_mailer(cfg: &Configuration) -> Mailer {
42 let settings = cfg.settings.read().await;
43
44 if !settings.mail.username.is_empty() && !settings.mail.password.is_empty() {
45 let creds = Credentials::new(settings.mail.username.clone(), settings.mail.password.clone());
47
48 AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&settings.mail.server)
49 .port(settings.mail.port)
50 .credentials(creds)
51 .authentication(vec![Mechanism::Login, Mechanism::Xoauth2, Mechanism::Plain])
52 .build()
53 } else {
54 AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&settings.mail.server)
56 .port(settings.mail.port)
57 .build()
58 }
59 }
60
61 pub async fn send_verification_mail(
71 &self,
72 to: &str,
73 username: &str,
74 user_id: i64,
75 base_url: &str,
76 ) -> Result<(), ServiceError> {
77 let builder = self.get_builder(to).await;
78 let verification_url = self.get_verification_url(user_id, base_url).await;
79
80 let mail_body = format!(
81 r#"
82 Welcome to Torrust, {username}!
83
84 Please click the confirmation link below to verify your account.
85 {verification_url}
86
87 If this account wasn't made by you, you can ignore this email.
88 "#
89 );
90
91 let ctx = VerifyTemplate {
92 username: String::from(username),
93 verification_url,
94 };
95
96 let mail = builder
97 .subject("Torrust - Email verification")
98 .multipart(
99 MultiPart::alternative()
100 .singlepart(
101 SinglePart::builder()
102 .header(lettre::message::header::ContentType::TEXT_PLAIN)
103 .body(mail_body),
104 )
105 .singlepart(
106 SinglePart::builder()
107 .header(lettre::message::header::ContentType::TEXT_HTML)
108 .body(
109 ctx.render_once()
110 .expect("value `ctx` must have some internal error passed into it"),
111 ),
112 ),
113 )
114 .expect("the `multipart` builder had an error");
115
116 match self.mailer.send(mail).await {
117 Ok(_res) => Ok(()),
118 Err(e) => {
119 eprintln!("Failed to send email: {e}");
120 Err(ServiceError::FailedToSendVerificationEmail)
121 }
122 }
123 }
124
125 async fn get_builder(&self, to: &str) -> MessageBuilder {
126 let settings = self.cfg.settings.read().await;
127
128 Message::builder()
129 .from(settings.mail.from.parse().unwrap())
130 .reply_to(settings.mail.reply_to.parse().unwrap())
131 .to(to.parse().unwrap())
132 }
133
134 async fn get_verification_url(&self, user_id: i64, base_url: &str) -> String {
135 let settings = self.cfg.settings.read().await;
136
137 let key = settings.auth.secret_key.as_bytes();
139
140 let claims = VerifyClaims {
142 iss: String::from("email-verification"),
143 sub: user_id,
144 exp: clock::now() + 315_569_260, };
146
147 let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).unwrap();
148
149 let mut base_url = &base_url.to_string();
150 if let Some(cfg_base_url) = &settings.net.base_url {
151 base_url = cfg_base_url;
152 }
153
154 format!("{base_url}/{API_VERSION_URL_PREFIX}/user/email/verify/{token}")
155 }
156}
157
158pub type Mailer = AsyncSmtpTransport<Tokio1Executor>;