use lettre::message::header::ContentType; use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::response::Response; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use mrml::mjml::Mjml; use mrml::prelude::parse::Error as MjmlParserError; use mrml::prelude::render::{Error as MjmlRenderError, Options}; use serde::{Deserialize, Serialize}; use tera::{Context, Tera}; // error #[derive(thiserror::Error)] // the rest #[derive(Debug)] pub enum Error { #[error("Template error: {0}")] Template(#[from] tera::Error), #[error("Mjml parser error: {0}")] MjmlParser(#[from] MjmlParserError), #[error("Mjml render error: {0}")] MjmlRender(#[from] MjmlRenderError), #[error("Invalid email address: {0}")] EmailAddress(#[from] lettre::address::AddressError), #[error("Invalid email message: {0}")] EmailMessage(#[from] lettre::error::Error), #[error("Error when sending email: {0}")] EmailSend(#[from] lettre::transport::smtp::Error), #[error("Invalid {0}")] InvalidConfig(&'static str), } const TEMPLATE_EXT: &str = ".mjml.tera"; pub fn render_template( name: &str, template_dir: &str, content: C, ) -> Result { let templates = format!("{template_dir}/**/*{TEMPLATE_EXT}"); // Initialize tera templates let tera = Tera::new(&templates)?; // Construct context let context = Context::from_serialize(content)?; // Render template let template_name = format!("{name}{TEMPLATE_EXT}"); Ok(tera.render(&template_name, &context)?) } pub fn render_email(mjml: &str) -> Result { // Parse mjml let root = Mjml::parse(mjml)?; // Render to html let opts = Options::default(); Ok(root.render(&opts)?) } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Transport { Unencrypted, Tls, StartTls, } #[derive(Debug, Deserialize)] pub struct Config { pub template_dir: String, pub from: String, pub transport: Transport, pub host: String, pub port: Option, pub username: Option, pub password: Option, } impl Config { pub fn status(&self) -> Result { match self.transport { Transport::Unencrypted => match self.port { Some(port) => Ok(format!("Using \"{}:{port}\" to send emails", self.host)), None => Err("Port not provided."), }, Transport::Tls | Transport::StartTls => { if self.username.is_none() { return Err("Username not provided."); } if self.password.is_none() { return Err("Password not provided."); } Ok(format!("Using \"{}\" to send emails", self.host)) } } } } fn create_credentials(config: &Config) -> Result { let username = config .username .as_deref() .ok_or(Error::InvalidConfig("username"))?; let password = config .password .as_deref() .ok_or(Error::InvalidConfig("password"))?; Ok(Credentials::from((username, password))) } pub async fn send_email( config: &Config, user: &str, email_title: &str, html: String, ) -> Result { let m = Message::builder() .from(config.from.parse()?) .to(user.parse()?) .subject(email_title) .header(ContentType::TEXT_HTML) .body(html)?; type SmtpTransport = AsyncSmtpTransport; let mailer = match config.transport { Transport::Unencrypted => { let port = config.port.ok_or(Error::InvalidConfig("port"))?; SmtpTransport::builder_dangerous(&config.host).port(port) } Transport::Tls => { SmtpTransport::relay(&config.host)?.credentials(create_credentials(config)?) } Transport::StartTls => { SmtpTransport::starttls_relay(&config.host)?.credentials(create_credentials(config)?) } } .build(); Ok(mailer.send(m).await?) }