#
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}