torrust_index_backend/
mailer.rs

1use 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            // SMTP authentication
46            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            // SMTP without authentication
55            AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&settings.mail.server)
56                .port(settings.mail.port)
57                .build()
58        }
59    }
60
61    /// Send Verification Email.
62    ///
63    /// # Errors
64    ///
65    /// This function will return an error if unable to send an email.
66    ///
67    /// # Panics
68    ///
69    /// This function will panic if the multipart builder had an error.
70    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        // create verification JWT
138        let key = settings.auth.secret_key.as_bytes();
139
140        // Create non expiring token that is only valid for email-verification
141        let claims = VerifyClaims {
142            iss: String::from("email-verification"),
143            sub: user_id,
144            exp: clock::now() + 315_569_260, // 10 years from now
145        };
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>;