aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/client/email
diff options
context:
space:
mode:
Diffstat (limited to 'crates/secd/src/client/email')
-rw-r--r--crates/secd/src/client/email/mod.rs186
1 files changed, 145 insertions, 41 deletions
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<dyn EmailMessenger + Send + Sync + 'static> {
+ 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<dyn EmailMessenger + Send + Sync + 'static> {
+ 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<dyn EmailMessenger>) -> 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 <iam@branchcontrol.com>".parse().unwrap())
- .reply_to("BranchControl <iam@branchcontrol.com>".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<dyn EmailMessenger>) -> 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<String>,
+ validation_code: Option<String>,
+) -> Result<String, EmailMessengerError> {
+ 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]")
+ }
+}