correlate 0.3.0

correlate is a standalone server that listens for Stripe webhook events and sends notification emails about successful orders.
use super::mail_template::mail_template;
use crate::{order_info::retrieve::StripeCheckoutLineItems, AppConfig, MailConfig};
use lettre::{
    message::MultiPart,
    transport::smtp::{authentication::Credentials, client::TlsParameters},
    Message, SendmailTransport, SmtpTransport, Transport,
};
use rocket::{figment::providers::Env, Config};
use std::sync::LazyLock;
use thiserror::Error;

// Define a global, lazily-initialized Sendmail transport
static SENDMAILER: LazyLock<SendmailTransport> = LazyLock::new(SendmailTransport::new);
// Define a global, lazily-initialized SMTP transport
static SMTPMAILER: LazyLock<Result<SmtpTransport, MailConfigError>> = LazyLock::new(|| {
    let config = Config::figment()
        .merge(Env::prefixed("CORRELATE_"))
        .extract::<AppConfig>()?;

    create_smtp_transport_from_figment(config.email)
});

/// Creates an SMTP transport from the provided mail configuration.
fn create_smtp_transport_from_figment(
    config: MailConfig,
) -> Result<SmtpTransport, MailConfigError> {
    let host = if let Some(host) = config.smtp_relay_host {
        if host.is_empty() {
            return Err(MailConfigError::InvalidSmtpHost);
        }
        host
    } else {
        return Err(MailConfigError::InvalidSmtpHost);
    };

    let smtp_username = config.smtp_username.unwrap_or_default();
    let smtp_password = config.smtp_password.unwrap_or_default();

    if smtp_password.is_empty() || smtp_username.is_empty() {
        // Custom, optional TLS configuration
        let params = TlsParameters::builder(host.to_owned()).build()?;
        let mailer_transport = SmtpTransport::builder_dangerous(host)
            .port(config.smtp_relay_port)
            .tls(lettre::transport::smtp::client::Tls::Opportunistic(params));
        Ok(mailer_transport.build())
    } else {
        let creds = Credentials::new(smtp_username, smtp_password);
        let mailer_transport = SmtpTransport::starttls_relay(&host)?.credentials(creds);
        Ok(mailer_transport.build())
    }
}

pub struct AdditionalFields<'b> {
    pub message_summary: &'b str,
    pub cta_link: Option<&'b str>,
    pub logo_img: Option<&'b str>,
    pub ordered_items: Option<&'b StripeCheckoutLineItems>,
}

/// Provides a default subject line for emails sent, if one is not present with form data.
pub fn default_subject_line() -> &'static str {
    "New message from"
}

/// Potential errors which can be created when sending an email
#[derive(Error, Debug)]
pub enum MailConfigError {
    /// Wraps errors that result from lettre's inability to parse to provided email.
    #[error("A malformed email was detected. Please format your email address properly: {0}.")]
    AddressParse(#[from] lettre::address::AddressError),
    /// Wraps errors occuring from lettre's inability to create an email message.
    #[error("Unable to create email message.")]
    EmailBuild(#[from] lettre::error::Error),
    /// Unknown error while creating or using SMTP transport
    #[error("Unable to send email through SMTP")]
    GenericSmtpTransport,
    /// Invalid SMTP host provided
    #[error("Invalid SMTP host: host is empty or not provided")]
    InvalidSmtpHost,
    /// Wraps a Figment error that occured while getting a configuration value.
    #[error("Unable to deserialize email config: {0}.")]
    MailConfig(#[from] rocket::figment::error::Error),
    /// Wraps errors occuring from lettre's inability to send email via sendmail command.
    #[error("Unable to send email because of {0}")]
    SendmailTransport(#[from] lettre::transport::sendmail::Error),
    /// Wraps errors occuring from lettre's inability to relay email to a server via smtp protocol.
    #[error("Unable to send email because of {0}")]
    SmtpTransportBuild(#[from] lettre::transport::smtp::Error),
    /// Wraps errors occuring due to inability to create mail body from tera template.
    #[error("Unable to create mail template because {0}")]
    Tera(#[from] tera::Error),
}

/// Sends email using the provided form data.
///
/// Message is sent, using sendmail, to address defined in mail config's "recipient".
pub fn send_email(
    form_email: &str,
    form_full_name: &str,
    form_subject: &str,
    form_message: &str,
    form_site: &str,
    form_extra: AdditionalFields<'_>,
    config: &MailConfig,
) -> Result<(), MailConfigError> {
    let mail_subject = format!("{} {}", default_subject_line(), form_site);
    let message_sender = format!("{form_full_name} <{form_email}>");

    let html_body = mail_template(
        &message_sender,
        form_subject,
        form_message,
        &mail_subject,
        &form_extra,
        "welcome.html",
    )?;
    let txt_body = mail_template(
        &message_sender,
        form_subject,
        form_message,
        &mail_subject,
        &form_extra,
        "welcome.txt",
    )?;

    let reply_to_email = form_email.parse::<lettre::message::Mailbox>()?;
    let sending_email =
        format!("{form_full_name} <{}>", config.sender).parse::<lettre::message::Mailbox>()?;

    let destination_email = config.recipient.parse::<lettre::message::Mailbox>()?;

    let email_msg = Message::builder()
        .from(sending_email)
        .reply_to(reply_to_email)
        .to(destination_email)
        .subject(mail_subject)
        .multipart(MultiPart::alternative_plain_html(txt_body, html_body))?;

    if config.smtp_relay_host.is_none() || config.smtp_relay_host == Some(String::from("")) {
        SENDMAILER.send(&email_msg)?;
        Ok(())
    } else {
        match &*SMTPMAILER {
            Ok(mailer) => {
                mailer.send(&email_msg)?;
                Ok(())
            }
            Err(err) => match err {
                MailConfigError::MailConfig(err) => Err(MailConfigError::MailConfig(
                    rocket::figment::error::Error::from(err.to_string()),
                )),
                MailConfigError::InvalidSmtpHost => Err(MailConfigError::InvalidSmtpHost),
                _ => Err(MailConfigError::GenericSmtpTransport),
            },
        }
    }
}