use anyhow::{Context, Result}; use axum::extract::FromRef; use lettre::{AsyncSmtpTransport, AsyncTransport, Tokio1Executor}; use serde::{Serialize, Serializer}; use crate::app_state::AppState; const POSTMARK_EMAIL_BATCH_URL: &'static str = "https://api.postmarkapp.com/email/batch"; #[derive(Serialize)] pub struct Message { #[serde(rename = "From")] pub from: lettre::message::Mailbox, #[serde(rename = "To")] #[serde(serialize_with = "serialize_mailboxes")] pub to: lettre::message::Mailboxes, #[serde(rename = "Subject")] pub subject: String, #[serde(rename = "TextBody")] pub text_body: String, } pub trait MailSender: Clone + Sync { async fn send_batch(&self, emails: Vec) -> Result<(), anyhow::Error>; } #[derive(Clone, Debug)] pub enum Mailer { Smtp(SmtpSender), Postmark(PostmarkSender), } impl Mailer { pub fn new_smtp(opts: SmtpOptions) -> Result { Ok(Self::Smtp(SmtpSender::new(opts)?)) } pub fn new_postmark(server_token: String) -> Result { Ok(Self::Postmark(PostmarkSender::new(server_token)?)) } pub fn with_reqwest_client(self, client: reqwest::Client) -> Self { match self { Self::Postmark(sender) => Self::Postmark(sender.with_reqwest_client(client)), _ => self, } } } impl MailSender for Mailer { async fn send_batch(&self, emails: Vec) -> Result<(), anyhow::Error> { match self { Mailer::Smtp(sender) => sender.send_batch(emails).await, Mailer::Postmark(sender) => sender.send_batch(emails).await, } } } #[derive(Clone, Debug)] struct SmtpSender { transport: AsyncSmtpTransport, } #[derive(Debug)] pub struct SmtpOptions { pub server: String, pub username: String, pub password: String, } impl SmtpSender { fn new(opts: SmtpOptions) -> Result { let mailer_creds = lettre::transport::smtp::authentication::Credentials::new(opts.username, opts.password); let transport = lettre::AsyncSmtpTransport::::starttls_relay(&opts.server) .context("unable to initialize starttls_relay")? .credentials(mailer_creds) .build(); Ok(Self { transport }) } } impl TryInto for Message { type Error = anyhow::Error; fn try_into(self) -> Result { let mut builder = lettre::Message::builder() .from(self.from.clone()) .subject(self.subject) .header(lettre::message::header::ContentType::TEXT_PLAIN); for to_addr in self.to { builder = builder.to(to_addr); } let message = builder.body(self.text_body)?; Ok(message) } } fn serialize_mailboxes(t: &lettre::message::Mailboxes, s: S) -> Result where S: Serializer, { Ok(s.serialize_str(&t.to_string())?) } impl MailSender for SmtpSender { async fn send_batch(&self, emails: Vec) -> Result<()> { for email in emails { self.transport.send(email.try_into()?).await?; } Ok(()) } } #[derive(Clone, Debug)] struct PostmarkSender { client: reqwest::Client, server_token: String, } impl PostmarkSender { fn new(server_token: String) -> Result { Ok(Self { client: reqwest::ClientBuilder::new().https_only(true).build()?, server_token, }) } pub fn with_reqwest_client(self, client: reqwest::Client) -> Self { Self { client, ..self } } } impl MailSender for PostmarkSender { async fn send_batch(&self, emails: Vec) -> Result<()> { if emails.len() > 500 { return Err(anyhow::anyhow!( "Postmark sends no more than 500 messages per batch" )); } self.client .post(POSTMARK_EMAIL_BATCH_URL) .header("X-Postmark-Server-Token", &self.server_token) .json(&emails) .send() .await?; Ok(()) } } impl FromRef for Mailer { fn from_ref(state: &AppState) -> Mailer { state.mailer.clone() } }