use async_trait::async_trait; use email_address::EmailAddress; use lettre::{ message::{Mailbox, MultiPart}, Transport, }; use log::{error, warn}; use reqwest::StatusCode; use sendgrid::v3::{Content, Email, Personalization}; use std::sync::Arc; use crate::AddressValidationId; pub const DEFAULT_SIGNUP_EMAIL: &str = "This email was recently used to signup. Please use the following code to complete your validation: {{secd::validation_code}}. If you did not request this signup link, you can safely ignore this email."; pub const DEFAULT_SIGNIN_EMAIL: &str = "An account with this email was recently used to signin. Please use the following code to complete your sign in process: {{secd::validation_code}}. If you did not request this signin link, you can safely ingore this email."; #[derive(Debug, thiserror::Error, derive_more::Display)] pub enum EmailMessengerError { FailedToSendEmail, LibLettreError(#[from] lettre::error::Error), SendgridError(#[from] sendgrid::SendgridError), } pub struct EmailValidationMessage { pub from_address: Mailbox, pub replyto_address: Mailbox, pub recipient: EmailAddress, pub subject: String, pub body: String, } #[async_trait] pub(crate) trait EmailMessenger: Send + Sync { fn get_type(&self) -> MessengerType; fn get_api_key(&self) -> String; } pub enum MessengerType { LocalMailer, Sendgrid, } pub(crate) struct LocalMailer {} impl LocalMailer { pub fn new() -> Arc { warn!("You are using the local mailer, which will not work in production!"); Arc::new(LocalMailer {}) } } impl EmailMessenger for LocalMailer { fn get_type(&self) -> MessengerType { MessengerType::LocalMailer } fn get_api_key(&self) -> String { panic!("unreachable since no API key is expected for LocalMailer"); } } pub(crate) struct Sendgrid { pub api_key: String, } impl Sendgrid { pub fn new(api_key: String) -> Arc { Arc::new(Sendgrid { api_key }) } } impl EmailMessenger for Sendgrid { fn get_type(&self) -> MessengerType { MessengerType::Sendgrid } fn get_api_key(&self) -> String { self.api_key.clone() } } #[async_trait] pub(crate) trait Sendable { async fn send(&self, messenge: Arc) -> Result<(), EmailMessengerError>; } #[async_trait] impl Sendable for EmailValidationMessage { async fn send(&self, messenger: Arc) -> Result<(), EmailMessengerError> { match messenger.get_type() { MessengerType::LocalMailer => { let email = lettre::Message::builder() .from(self.from_address.to_string().parse().unwrap()) .reply_to(self.replyto_address.to_string().parse().unwrap()) .to(self.recipient.to_string().parse().unwrap()) .subject(self.subject.clone()) .multipart(MultiPart::alternative_plain_html( "".to_string(), String::from(self.body.clone()), ))?; let mailer = lettre::SmtpTransport::unencrypted_localhost(); mailer.send(&email).map_err(|e| { error!("failed to send email {:?}", e); EmailMessengerError::FailedToSendEmail })?; } MessengerType::Sendgrid => { let msg = sendgrid::v3::Message::new(Email::new(self.from_address.to_string())) .set_subject(&self.subject) .add_content( Content::new() .set_content_type("text/html") .set_value(&self.body), ) .add_personalization(Personalization::new(Email::new( self.recipient.to_string(), ))); let sender = sendgrid::v3::Sender::new(messenger.get_api_key()); let resp = sender.send(&msg).await?; match resp.status() { StatusCode::ACCEPTED => {} _ => { error!( "sendgrid failed to send message with status: {}", resp.status() ) } } } }; Ok(()) } } pub(crate) fn parse_email_template( template: &str, validation_id: AddressValidationId, validation_secret: Option, validation_code: Option, ) -> Result { let mut t = template.clone().to_string(); // We do not allow substutions for a variety of reasons, but mainly security ones. // The only things we want to share are those which secd allows. In this case, that // means we only send an email with static content as provided by the filter, except // for the $$secd:request_id$$ and $$secd:request_code$$, either of which may be // present in the email. t = t.replace("{{secd::validation_id}}", &validation_id.to_string()); validation_secret.map(|secret| t = t.replace("{{secd::validation_secret}}", &secret)); validation_code.map(|code| t = t.replace("{{secd::validation_code}}", &code)); Ok(t) } #[cfg(test)] mod test { use uuid::Uuid; use super::*; #[test] fn test_parse_and_substitue() { let raw = "This is an email validation message. Navigate to https://www.secd.com/auth/{secd::validation_id}?s={secd::validation_secret} or use the code [{secd::validation_code}]"; let parsed = parse_email_template( raw, Uuid::parse_str("90f42ba9-ed4a-4f56-b371-df05634a1626").unwrap(), Some("s3cr3t".into()), Some("102030".into()), ) .unwrap(); assert_eq!(parsed, "This is an email validation message. Navigate to https://www.secd.com/auth/90f42ba9-ed4a-4f56-b371-df05634a1626?s=s3cr3t or use the code [102030]") } }