mcaptcha/email/
verification.rs

1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6//! Email operations: verification, notification, etc
7use lettre::{
8    message::{header, MultiPart, SinglePart},
9    AsyncTransport, Message,
10};
11use sailfish::TemplateOnce;
12
13use crate::errors::*;
14use crate::Data;
15
16const PAGE: &str = "Login";
17
18#[derive(Clone, TemplateOnce)]
19#[template(path = "email/verification/index.html")]
20struct IndexPage<'a> {
21    verification_link: &'a str,
22}
23
24impl<'a> IndexPage<'a> {
25    fn new(verification_link: &'a str) -> Self {
26        Self { verification_link }
27    }
28}
29
30async fn verification(
31    data: &Data,
32    to: &str,
33    verification_link: &str,
34) -> ServiceResult<()> {
35    if let Some(smtp) = data.settings.smtp.as_ref() {
36        let from = format!("mCaptcha Admin <{}>", smtp.from);
37        let reply_to = format!("mCaptcha Admin <{}>", smtp.reply);
38        const SUBJECT: &str = "[mCaptcha] Please verify your email";
39
40        let plain_text = format!(
41            "
42Welcome to mCaptcha!
43
44Please verify your email address to continue.
45
46VERIFICATION LINK: {}
47
48Please ignore this email if you weren't expecting it.
49
50With best regards,
51Admin
52instance: {}
53project website: {}",
54            verification_link,
55            &data.settings.server.domain,
56            crate::PKG_HOMEPAGE
57        );
58
59        let html = IndexPage::new(verification_link).render_once().unwrap();
60
61        let email = Message::builder()
62            .from(from.parse().unwrap())
63            .reply_to(reply_to.parse().unwrap())
64            .to(to.parse().unwrap())
65            .subject(SUBJECT)
66            .multipart(
67                MultiPart::alternative() // This is composed of two parts.
68                    .singlepart(
69                        SinglePart::builder()
70                            .header(header::ContentType::TEXT_PLAIN)
71                            .body(plain_text), // Every message should have a plain text fallback.
72                    )
73                    .singlepart(
74                        SinglePart::builder()
75                            .header(header::ContentType::TEXT_HTML)
76                            .body(html),
77                    ),
78            )
79            .unwrap();
80
81        data.mailer.as_ref().unwrap().send(email).await?;
82    }
83    Ok(())
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    use awc::Client;
91
92    #[actix_rt::test]
93    async fn email_verification_works_pg() {
94        let data = crate::tests::pg::get_data().await;
95        email_verification_works(data).await;
96    }
97
98    #[actix_rt::test]
99    async fn email_verification_works_maria() {
100        let data = crate::tests::maria::get_data().await;
101        email_verification_works(data).await;
102    }
103
104    async fn email_verification_works(data: crate::ArcData) {
105        const TO_ADDR: &str = "Hello <realaravinth@localhost>";
106        const VERIFICATION_LINK: &str = "https://localhost";
107        let settings = &data.settings;
108        verification(&data, TO_ADDR, VERIFICATION_LINK)
109            .await
110            .unwrap();
111
112        let client = Client::default();
113        let mut resp = client
114            .get("http://localhost:1080/email")
115            .send()
116            .await
117            .unwrap();
118        let data: serde_json::Value = resp.json().await.unwrap();
119        let data = &data[0];
120        let smtp = settings.smtp.as_ref().unwrap();
121
122        let from_addr = &data["headers"]["from"];
123
124        assert!(from_addr.to_string().contains(&smtp.from));
125
126        let body = &data["html"];
127        assert!(body.to_string().contains(VERIFICATION_LINK));
128    }
129}