reformulate 0.4.0

reformulate is a standalone server that listens for web form data submissions.
use lettre::{
    message::MultiPart,
    transport::smtp::{authentication::Credentials, client::TlsParameters},
    Message, SendmailTransport, SmtpTransport, Transport,
};
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::fmt::{self, Display};

use crate::mail_template::message_template;

const DESTINATION_ENV_VAR: &str = "FORMULATE_DESTINATION_EMAIL";
const SOURCE_ENV_VAR: &str = "FORMULATE_SENDING_EMAIL";
const MAIL_HOST_ENV_VAR: &str = "FORMULATE_MAIL_RELAY_HOST";
const MAIL_PORT_ENV_VAR: &str = "FORMULATE_MAIL_RELAY_PORT";
const SMTP_USERNAME_ENV_VAR: &str = "FORMULATE_SMTP_USERNAME";
const SMTP_PASSWORD_ENV_VAR: &str = "FORMULATE_SMTP_PASSWORD";

static SENDMAILER: Lazy<SendmailTransport> = Lazy::new(SendmailTransport::new);

static SMTPMAILER: Lazy<SmtpTransport> = Lazy::new(|| {
    let config = AppConfig {
        // We do want to panic if this is not defined
        destination_email: std::env::var(DESTINATION_ENV_VAR).unwrap(),
        // We do want to panic if this is not defined
        sending_email: std::env::var(SOURCE_ENV_VAR).unwrap(),
        // Shouldn't panic as we have a parseable default, but if the value provided
        // by the user isn't parseable as a u16, do panic
        mail_relay_port: std::env::var(MAIL_PORT_ENV_VAR)
            .unwrap_or(String::from("25"))
            .parse::<u16>()
            .unwrap(),
        mail_relay_host: match std::env::var(MAIL_HOST_ENV_VAR) {
            Ok(host) => Some(host),
            Err(_) => None,
        },
        smtp_username: match std::env::var(SMTP_USERNAME_ENV_VAR) {
            Ok(user) => Some(user),
            Err(_) => None,
        },
        smtp_password: match std::env::var(SMTP_PASSWORD_ENV_VAR) {
            Ok(pass) => Some(pass),
            Err(_) => None,
        },
    };

    let host = if let Some(host) = config.mail_relay_host {
        host
    } else {
        String::from("placeholder")
    };

    let smtp_username = match config.smtp_username {
        None => String::from(""),
        Some(user) => user,
    };
    let smtp_password = match config.smtp_password {
        None => String::from(""),
        Some(pass) => pass,
    };

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

/// Potential errors which can be created when sending an email
#[derive(Debug)]
pub enum MailConfigError {
    /// Wraps a env var errors that occured while getting app configuration.
    AppConfig(std::env::VarError),
    /// Wraps errors that result from lettre's inability to parse to provided email.
    AddressParse(lettre::address::AddressError),
    /// Wraps errors occuring from lettre's inability to create an email message.
    EmailBuild(lettre::error::Error),
    /// Wraps a env var errors that occured while getting app relay port.
    ParseIntError(std::num::ParseIntError),
    /// Wraps errors occuring from lettre's inability to send email via sendmail command.
    SendmailTransport(lettre::transport::sendmail::Error),
    /// Wraps errors occuring from lettre's inability to relay email to a server via smtp protocol.
    SmtpTransport(lettre::transport::smtp::Error),
    /// Wraps error which may occur generating message body from template.
    TemplateBuild(tera::Error),
}

impl Display for MailConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MailConfigError::AppConfig(err) => write!(f, "Unable to find {err} in config."),
            MailConfigError::AddressParse(err) => write!(
                f,
                "A malformed email was detected. Please format your email address properly: {err}."
            ),
            MailConfigError::EmailBuild(_) => write!(f, "Unable to create email message."),
            MailConfigError::ParseIntError(err) => {
                write!(f, "Unable to get application config: {err}")
            }
            MailConfigError::SendmailTransport(err) => {
                write!(f, "Unable to send email because of {err}")
            }
            MailConfigError::SmtpTransport(err) => {
                write!(f, "Unable to send email because of {err}")
            }
            MailConfigError::TemplateBuild(err) => {
                write!(f, "Unable to create mail template because {err}")
            }
        }
    }
}

impl From<std::env::VarError> for MailConfigError {
    fn from(error: std::env::VarError) -> Self {
        MailConfigError::AppConfig(error)
    }
}

impl From<lettre::address::AddressError> for MailConfigError {
    fn from(error: lettre::address::AddressError) -> Self {
        MailConfigError::AddressParse(error)
    }
}

impl From<lettre::error::Error> for MailConfigError {
    fn from(error: lettre::error::Error) -> Self {
        MailConfigError::EmailBuild(error)
    }
}

impl From<std::num::ParseIntError> for MailConfigError {
    fn from(error: std::num::ParseIntError) -> Self {
        MailConfigError::ParseIntError(error)
    }
}

impl From<lettre::transport::sendmail::Error> for MailConfigError {
    fn from(error: lettre::transport::sendmail::Error) -> Self {
        MailConfigError::SendmailTransport(error)
    }
}

impl From<lettre::transport::smtp::Error> for MailConfigError {
    fn from(error: lettre::transport::smtp::Error) -> Self {
        MailConfigError::SmtpTransport(error)
    }
}

impl From<tera::Error> for MailConfigError {
    fn from(error: tera::Error) -> Self {
        MailConfigError::TemplateBuild(error)
    }
}

/// Provides a default subject line for emails sent, if one is not present with form data.
pub fn default_subject_line() -> String {
    String::from("You have received a new message from")
}

/// Gives the default SMTP port if none is provided via config
fn default_smtp_port() -> u16 {
    25
}

#[derive(Deserialize)]
struct AppConfig {
    /// Email address that the message will be sent from.
    sending_email: String,
    /// Email address that the message will be sent to.
    destination_email: String,
    /// SMTP server hostname to use for sending mail
    mail_relay_host: Option<String>,
    /// SMTP server port used to listen for incoming mail
    #[serde(default = "default_smtp_port")]
    mail_relay_port: u16,
    /// User (email address) used to authenticate with SMTP server defined above
    smtp_username: Option<String>,
    /// Password used to authenticate with SMTP server defined above
    smtp_password: Option<String>,
}

/// Uses sendmail application to send email containing form contents.
pub fn send_email(
    form_email: &str,
    form_full_name: &str,
    form_subject: &str,
    form_message: &str,
    form_site: &str,
) -> Result<(), MailConfigError> {
    let mail_subject = format!("{} {form_site}!", default_subject_line());

    let config = AppConfig {
        destination_email: std::env::var(DESTINATION_ENV_VAR)?,
        sending_email: std::env::var(SOURCE_ENV_VAR)?,
        mail_relay_host: match std::env::var(MAIL_HOST_ENV_VAR) {
            Ok(host) => Some(host),
            Err(_) => None,
        },
        mail_relay_port: match std::env::var(MAIL_PORT_ENV_VAR) {
            Ok(port) => port.parse::<u16>()?,
            Err(_) => 25,
        },
        smtp_username: match std::env::var(SMTP_USERNAME_ENV_VAR) {
            Ok(user) => Some(user),
            Err(_) => None,
        },
        smtp_password: match std::env::var(SMTP_PASSWORD_ENV_VAR) {
            Ok(pass) => Some(pass),
            Err(_) => None,
        },
    };

    let message_sender = format!("\"{form_full_name} <{form_email}>\"");

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

    let reply_to_email = form_email.parse::<lettre::message::Mailbox>()?;
    let sending_email = format!("{form_full_name} <{}>", config.sending_email.trim())
        .parse::<lettre::message::Mailbox>()?;
    let destination_email = config
        .destination_email
        .trim()
        .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.mail_relay_host.is_none() || config.mail_relay_host == Some(String::from("")) {
        SENDMAILER.send(&email_msg)?;
    } else {
        SMTPMAILER.send(&email_msg)?;
    }
    Ok(())
}