use crate::{mail_template::message_template, stripe_orders::StripeCheckoutLineItems};
use lettre::{
message::MultiPart,
transport::smtp::{authentication::Credentials, client::TlsParameters},
Message, SendmailTransport, SmtpTransport, Transport,
};
use rocket::{figment::providers::Env, serde::Deserialize, Config};
use std::fmt::{self, Display};
use std::sync::LazyLock;
static SENDMAILER: LazyLock<SendmailTransport> = LazyLock::new(SendmailTransport::new);
static SMTPMAILER: LazyLock<SmtpTransport> = LazyLock::new(|| {
let config = Config::figment()
.select("application")
.merge(Env::prefixed("FORMULATE_"))
.extract::<AppConfig>()
.unwrap();
let host = if let Some(host) = config.mail_relay_host {
host
} else {
String::from("placeholder")
};
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() {
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()
} else {
let creds = Credentials::new(smtp_username, smtp_password);
let mailer_transport = SmtpTransport::starttls_relay(&host)
.unwrap()
.credentials(creds);
mailer_transport.build()
}
});
fn default_smtp_port() -> u16 {
25
}
#[derive(Debug)]
pub enum MailConfigError {
AppConfig(rocket::figment::error::Error),
AddressParse(lettre::address::AddressError),
EmailBuild(lettre::error::Error),
SendmailTransport(lettre::transport::sendmail::Error),
SmtpTransport(lettre::transport::smtp::Error),
TemplateBuild(tera::Error),
}
impl Display for MailConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MailConfigError::AppConfig(err) => write!(f, "Error loading config: {err}."),
MailConfigError::AddressParse(err) => write!(
f,
"A malformed email was detected. Please format your email address properly: {err}."
),
MailConfigError::EmailBuild(err) => write!(f, "Unable to create email message: {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<rocket::figment::error::Error> for MailConfigError {
fn from(error: rocket::figment::error::Error) -> 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<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)
}
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct AppConfig {
#[serde(alias = "SENDING_EMAIL")]
sending_email: String,
#[serde(alias = "DESTINATION_EMAIL")]
destination_email: String,
#[serde(alias = "MAIL_HOST")]
mail_relay_host: Option<String>,
#[serde(alias = "MAIL_PORT")]
#[serde(default = "default_smtp_port")]
mail_relay_port: u16,
#[serde(alias = "SMTP_USERNAME")]
smtp_username: Option<String>,
#[serde(alias = "SMTP_PASSWORD")]
smtp_password: Option<String>,
}
pub fn default_subject_line() -> &'static str {
"New message from"
}
pub struct OptionalFields<'b> {
pub company_name: Option<&'b str>,
pub last_name: Option<&'b str>,
pub phone_number: Option<&'b str>,
pub message_summary: Option<&'b str>,
pub cta_link: Option<&'b str>,
pub logo_img: Option<&'b str>,
pub ordered_items: Option<&'b StripeCheckoutLineItems>,
}
pub fn send_email(
form_email: &str,
form_full_name: &str,
form_subject: &str,
form_message: &str,
form_site: &str,
form_optional: OptionalFields<'_>,
) -> Result<(), MailConfigError> {
let mail_subject = format!("{} {}", default_subject_line(), form_site);
let config = Config::figment()
.select("application")
.merge(Env::prefixed("FORMULATE_"))
.extract::<AppConfig>()?;
let message_sender = format!("{form_full_name} <{form_email}>");
let html_body = message_template(
&message_sender,
&mail_subject,
form_message,
form_subject,
&form_optional,
"welcome.html",
)?;
let txt_body = message_template(
&message_sender,
&mail_subject,
form_message,
form_subject,
&form_optional,
"welcome.txt",
)?;
let reply_to_email = form_email.parse::<lettre::message::Mailbox>()?;
let sending_email = format!("{form_full_name} <{}>", config.sending_email)
.parse::<lettre::message::Mailbox>()?;
let destination_email = config
.destination_email
.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(())
}