From eb92f823c31a5e702af7005231f0d6915aad3342 Mon Sep 17 00:00:00 2001 From: benj Date: Mon, 24 Apr 2023 13:24:45 -0700 Subject: email templates, sendgrid, creds, and some experimental things Started playing with namespace configs and integrating with zanzibar impls. Still lot's of experimenting and dead code going on. --- crates/secd/src/client/email/mod.rs | 186 ++++++++++++++++++++++++++++-------- 1 file changed, 145 insertions(+), 41 deletions(-) (limited to 'crates/secd/src/client/email') diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs index 915d18c..7c7b233 100644 --- a/crates/secd/src/client/email/mod.rs +++ b/crates/secd/src/client/email/mod.rs @@ -1,68 +1,172 @@ +use async_trait::async_trait; use email_address::EmailAddress; -use lettre::Transport; -use log::error; -use std::collections::HashMap; +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::async_trait] -pub(crate) trait EmailMessenger { - async fn send_email( - &self, - email_address: &EmailAddress, - template: &str, - template_vars: HashMap<&str, &str>, - ) -> Result<(), EmailMessengerError>; +#[async_trait] +pub(crate) trait EmailMessenger: Send + Sync { + fn get_type(&self) -> MessengerType; + fn get_api_key(&self) -> String; } -pub(crate) struct LocalMailer {} +pub enum MessengerType { + LocalMailer, + Sendgrid, +} -#[async_trait::async_trait] +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 { - async fn send_email( - &self, - email_address: &EmailAddress, - template: &str, - template_vars: HashMap<&str, &str>, - ) -> Result<(), EmailMessengerError> { - todo!() + 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::async_trait] +#[async_trait] pub(crate) trait Sendable { - async fn send(&self) -> Result<(), EmailMessengerError>; + async fn send(&self, messenge: Arc) -> Result<(), EmailMessengerError>; } -#[async_trait::async_trait] +#[async_trait] impl Sendable for EmailValidationMessage { - // TODO: We need to break this up as before, especially so we can feature - // gate unwanted things like Lettre... - async fn send(&self) -> Result<(), EmailMessengerError> { - // TODO: Get these things from the template... - let email = lettre::Message::builder() - .from("BranchControl ".parse().unwrap()) - .reply_to("BranchControl ".parse().unwrap()) - .to(self.recipient.to_string().parse().unwrap()) - .subject(self.subject.clone()) - .body(self.body.clone()) - .unwrap(); - - let mailer = lettre::SmtpTransport::unencrypted_localhost(); - - mailer.send(&email).map_err(|e| { - error!("failed to send email {:?}", e); - EmailMessengerError::FailedToSendEmail - })?; + 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]") + } +} -- cgit v1.2.3