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 {
destination_email: std::env::var(DESTINATION_ENV_VAR).unwrap(),
sending_email: std::env::var(SOURCE_ENV_VAR).unwrap(),
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 {
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()
}
});
#[derive(Debug)]
pub enum MailConfigError {
AppConfig(std::env::VarError),
AddressParse(lettre::address::AddressError),
EmailBuild(lettre::error::Error),
ParseIntError(std::num::ParseIntError),
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, "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)
}
}
pub fn default_subject_line() -> String {
String::from("You have received a new message from")
}
fn default_smtp_port() -> u16 {
25
}
#[derive(Deserialize)]
struct AppConfig {
sending_email: String,
destination_email: String,
mail_relay_host: Option<String>,
#[serde(default = "default_smtp_port")]
mail_relay_port: u16,
smtp_username: Option<String>,
smtp_password: Option<String>,
}
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(())
}