dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
mod code_msg;

use std::{str::FromStr, time::Duration};

pub use code_msg::*;
use lettre::{
    AsyncSmtpTransport, AsyncTransport, SmtpTransport, Tokio1Executor, Transport,
    transport::smtp::{
        PoolConfig, SMTP_PORT, SUBMISSION_PORT,
        authentication::{Credentials, DEFAULT_MECHANISMS, Mechanism},
        client::{Tls, TlsParameters},
        extension::ClientId,
    },
};
pub use lettre::{
    message::*,
    transport::smtp::{Error, response::Response},
};

#[derive(Debug)]
pub struct MailerBuilder {
    username: String,
    password: String,
    tls_option: TlsOption,
    /// Name sent during EHLO
    hello_name: ClientId,
    /// Server we are connecting to
    server: String,
    /// Port to connect to
    port: Option<u16>,
    /// Optional enforced authentication mechanism
    authentication: Vec<Mechanism>,
    /// Define network timeout
    /// It can be changed later for specific needs (like a different timeout for
    /// each SMTP command)
    timeout: Option<Duration>,
    pool_config: PoolConfig,
}

/// How to apply TLS to a client connection
#[derive(Debug, Clone, Copy, Default)]
#[allow(missing_copy_implementations)]
#[allow(clippy::exhaustive_enums)]
pub enum TlsOption {
    /// Insecure connection only (for testing purposes)
    None,
    /// Start with insecure connection and use `STARTTLS` when available
    Opportunistic,
    /// Start with insecure connection and require `STARTTLS`
    Required,
    /// Use TLS wrapped connection
    #[default]
    Wrapper,
}
impl TlsOption {
    pub const fn is_none(self) -> bool {
        matches!(self, Self::None)
    }
}

impl FromStr for TlsOption {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "none" => Ok(Self::None),
            "opportunistic" => Ok(Self::Opportunistic),
            "required" => Ok(Self::Required),
            "wrapper" => Ok(Self::Wrapper),
            _ => Err("TlsOption string must be one of none, opportunistic, required and wrapper."),
        }
    }
}

impl MailerBuilder {
    pub fn new(username: String, password: String, server: String) -> MailerBuilder {
        Self {
            username,
            password,
            server,
            port: None,
            tls_option: TlsOption::default(),
            hello_name: ClientId::default(),
            authentication: DEFAULT_MECHANISMS.into(),
            timeout: Some(Duration::from_secs(60)),
            pool_config: PoolConfig::default(),
        }
    }
    pub const fn with_port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }
    pub const fn with_tls_option(mut self, tls_option: TlsOption) -> Self {
        self.tls_option = tls_option;
        self
    }
    pub fn with_hello_name(mut self, hello_name: ClientId) -> Self {
        self.hello_name = hello_name;
        self
    }
    pub fn with_authentication(mut self, authentication: Vec<Mechanism>) -> Self {
        self.authentication = authentication;
        self
    }
    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }
    pub fn mailer(self) -> Result<Mailer, Error> {
        let creds = Credentials::new(self.username, self.password);
        let port = self.port.unwrap_or_else(|| {
            if self.tls_option.is_none() {
                SMTP_PORT
            } else {
                SUBMISSION_PORT
            }
        });
        let mut builder = SmtpTransport::builder_dangerous(self.server.clone())
            .port(port)
            .credentials(creds)
            .authentication(self.authentication)
            .hello_name(self.hello_name)
            .pool_config(self.pool_config)
            .timeout(self.timeout);

        if !self.tls_option.is_none() {
            let tls_parameters = TlsParameters::new(self.server)?;
            builder = builder.tls(match self.tls_option {
                TlsOption::None => unreachable!(),
                TlsOption::Opportunistic => Tls::Opportunistic(tls_parameters),
                TlsOption::Required => Tls::Required(tls_parameters),
                TlsOption::Wrapper => Tls::Wrapper(tls_parameters),
            })
        }

        Ok(Mailer {
            mailer: builder.build(),
        })
    }
    pub fn async_mailer(self) -> Result<AsnycMailer, Error> {
        let creds = Credentials::new(self.username, self.password);
        let port = self.port.unwrap_or_else(|| {
            if self.tls_option.is_none() {
                SMTP_PORT
            } else {
                SUBMISSION_PORT
            }
        });
        let mut builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(self.server.clone())
            .port(port)
            .credentials(creds)
            .authentication(self.authentication)
            .hello_name(self.hello_name)
            .pool_config(self.pool_config)
            .timeout(self.timeout);

        if !self.tls_option.is_none() {
            let tls_parameters = TlsParameters::new(self.server)?;
            builder = builder.tls(match self.tls_option {
                TlsOption::None => unreachable!(),
                TlsOption::Opportunistic => Tls::Opportunistic(tls_parameters),
                TlsOption::Required => Tls::Required(tls_parameters),
                TlsOption::Wrapper => Tls::Wrapper(tls_parameters),
            })
        }
        Ok(AsnycMailer {
            mailer: builder.build(),
        })
    }
}

#[derive(Clone)]
pub struct Mailer {
    mailer: SmtpTransport,
}

impl Mailer {
    pub fn send(&self, message: &Message) -> Result<Response, Error> {
        Transport::send(&self.mailer, message)
    }
    pub fn test_connection(&self) -> Result<bool, Error> {
        self.mailer.test_connection()
    }
}

#[derive(Clone)]
#[allow(clippy::exhaustive_structs)]
pub struct AsnycMailer {
    mailer: AsyncSmtpTransport<Tokio1Executor>,
}

impl AsnycMailer {
    pub async fn send(&self, message: Message) -> Result<Response, Error> {
        AsyncTransport::send(&self.mailer, message).await
    }
    pub async fn test_connection(&self) -> Result<bool, Error> {
        self.mailer.test_connection().await
    }
}

#[allow(clippy::exhaustive_structs)]
pub struct VerificationCode {}

#[cfg(test)]
mod tests {
    use std::env;

    use dotenv::dotenv;

    use super::*;

    #[test]
    fn main() -> anyhow::Result<()> {
        dotenv().ok();

        let username: String = env::var("SMTP_USERNAME").expect("SMTP_USERNAME must be set");
        let password: String = env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD must be set");
        let server: String = env::var("SMTP_SERVER").expect("SMTP_SERVER must be set");
        let port: u16 = env::var("SMTP_PORT")
            .expect("SMTP_PORT must be set")
            .parse()
            .expect("SMTP_PORT must be a number");
        let tls: TlsOption = env::var("SMTP_TLS")
            .expect("SMTP_TLS must be set")
            .parse()
            .expect("SMTP_TLS must be one of none, opportunistic, required and wrapper.");

        let builder = MailerBuilder::new(username, password, server)
            .with_port(port)
            .with_tls_option(tls);
        println!("{builder:?}");
        let mailer = builder.mailer().unwrap();

        let from_email = env::var("FROM_EMAIL").expect("FROM_EMAIL must be set");
        let to_email = env::var("TO_EMAIL").expect("TO_EMAIL must be set");
        let totp_msg = DefaultTotpMessageCN::new("Dipper".to_owned(), true);
        let email = totp_msg.email_message(
            from_email.parse().unwrap(),
            to_email.parse().unwrap(),
            TOTP::gen_secret_base32(),
            10 * 60,
        )?;
        println!("{:?}", mailer.test_connection());
        match mailer.send(&email) {
            Ok(_) => println!("Email sent successfully!"),
            Err(e) => println!("Could not send email: {:?}", e),
        }
        Ok(())
    }
}