1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

//! Email operations: verification, notification, etc
use lettre::{
    message::{header, MultiPart, SinglePart},
    AsyncTransport, Message,
};
use sailfish::TemplateOnce;

use crate::errors::*;
use crate::Data;

const PAGE: &str = "Login";

#[derive(Clone, TemplateOnce)]
#[template(path = "email/verification/index.html")]
struct IndexPage<'a> {
    verification_link: &'a str,
}

impl<'a> IndexPage<'a> {
    fn new(verification_link: &'a str) -> Self {
        Self { verification_link }
    }
}

async fn verification(
    data: &Data,
    to: &str,
    verification_link: &str,
) -> ServiceResult<()> {
    if let Some(smtp) = data.settings.smtp.as_ref() {
        let from = format!("mCaptcha Admin <{}>", smtp.from);
        let reply_to = format!("mCaptcha Admin <{}>", smtp.reply);
        const SUBJECT: &str = "[mCaptcha] Please verify your email";

        let plain_text = format!(
            "
Welcome to mCaptcha!

Please verify your email address to continue.

VERIFICATION LINK: {}

Please ignore this email if you weren't expecting it.

With best regards,
Admin
instance: {}
project website: {}",
            verification_link,
            &data.settings.server.domain,
            crate::PKG_HOMEPAGE
        );

        let html = IndexPage::new(verification_link).render_once().unwrap();

        let email = Message::builder()
            .from(from.parse().unwrap())
            .reply_to(reply_to.parse().unwrap())
            .to(to.parse().unwrap())
            .subject(SUBJECT)
            .multipart(
                MultiPart::alternative() // This is composed of two parts.
                    .singlepart(
                        SinglePart::builder()
                            .header(header::ContentType::TEXT_PLAIN)
                            .body(plain_text), // Every message should have a plain text fallback.
                    )
                    .singlepart(
                        SinglePart::builder()
                            .header(header::ContentType::TEXT_HTML)
                            .body(html),
                    ),
            )
            .unwrap();

        data.mailer.as_ref().unwrap().send(email).await?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    use awc::Client;

    #[actix_rt::test]
    async fn email_verification_works_pg() {
        let data = crate::tests::pg::get_data().await;
        email_verification_works(data).await;
    }

    #[actix_rt::test]
    async fn email_verification_works_maria() {
        let data = crate::tests::maria::get_data().await;
        email_verification_works(data).await;
    }

    async fn email_verification_works(data: crate::ArcData) {
        const TO_ADDR: &str = "Hello <realaravinth@localhost>";
        const VERIFICATION_LINK: &str = "https://localhost";
        let settings = &data.settings;
        verification(&data, TO_ADDR, VERIFICATION_LINK)
            .await
            .unwrap();

        let client = Client::default();
        let mut resp = client
            .get("http://localhost:1080/email")
            .send()
            .await
            .unwrap();
        let data: serde_json::Value = resp.json().await.unwrap();
        let data = &data[0];
        let smtp = settings.smtp.as_ref().unwrap();

        let from_addr = &data["headers"]["from"];

        assert!(from_addr.to_string().contains(&smtp.from));

        let body = &data["html"];
        assert!(body.to_string().contains(VERIFICATION_LINK));
    }
}