Skip to content

Commit

Permalink
feat: Add Sendgrid client to AppContext (#402)
Browse files Browse the repository at this point in the history
  • Loading branch information
spencewenski authored Oct 10, 2024
1 parent 9d5ae89 commit 7df17a6
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/app/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ impl AppContext {
#[cfg(feature = "email-smtp")]
let mailer = lettre::SmtpTransport::try_from(&config.email.smtp.connection)?;

#[cfg(feature = "email-sendgrid")]
let sendgrid = sendgrid::v3::Sender::try_from(&config.email.sendgrid)?;

let inner = AppContextInner {
config,
metadata,
Expand All @@ -92,6 +95,8 @@ impl AppContext {
redis_fetch,
#[cfg(feature = "email-smtp")]
mailer,
#[cfg(feature = "email-sendgrid")]
sendgrid,
};
AppContext {
inner: Arc::new(inner),
Expand Down Expand Up @@ -167,6 +172,11 @@ impl AppContext {
pub fn mailer(&self) -> &lettre::SmtpTransport {
self.inner.mailer()
}

#[cfg(feature = "email-sendgrid")]
pub fn sendgrid(&self) -> &sendgrid::v3::Sender {
self.inner.sendgrid()
}
}

struct AppContextInner {
Expand All @@ -184,6 +194,8 @@ struct AppContextInner {
redis_fetch: Option<sidekiq::RedisPool>,
#[cfg(feature = "email-smtp")]
mailer: lettre::SmtpTransport,
#[cfg(feature = "email-sendgrid")]
sendgrid: sendgrid::v3::Sender,
}

#[cfg_attr(test, mockall::automock)]
Expand Down Expand Up @@ -231,4 +243,9 @@ impl AppContextInner {
fn mailer(&self) -> &lettre::SmtpTransport {
&self.mailer
}

#[cfg(feature = "email-sendgrid")]
fn sendgrid(&self) -> &sendgrid::v3::Sender {
&self.sendgrid
}
}
48 changes: 48 additions & 0 deletions src/config/email/sendgrid/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use crate::config::email::Email;
use crate::config::environment::Environment;
use crate::util::serde::default_true;
use config::{FileFormat, FileSourceString};
use lettre::message::Mailbox;
use reqwest::Client;
use sendgrid::v3::{Message, Sender};
use serde_derive::{Deserialize, Serialize};
use validator::Validate;

Expand All @@ -18,13 +22,57 @@ pub(crate) fn default_config_per_env(
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Sendgrid {
/// Your Sendgrid API key.
pub api_key: String,

/// Whether messages should be sent in [sandbox mode](https://www.twilio.com/docs/sendgrid/for-developers/sending-email/sandbox-mode).
///
/// Note that this is currently not supported by the [sendgrid crate](https://crates.io/crates/sendgrid).
#[serde(default = "default_true")]
pub sandbox: bool,

/// Whether the Sendgrid client should connect only with https.
///
/// If `true`, the Sendgrid client will only be allowed to connect to the Sendgrid API using
/// https. If `false`, the Sendgrid client could in theory connect using http.
///
/// This is automatically applied if creating a [`Sender`] using the provided
/// [`From<&Sendgrid>`] implementation.
#[serde(default = "default_true")]
pub https_only: bool,
}

impl From<&Email> for Message {
fn from(value: &Email) -> Self {
let message = Message::new(mailbox_to_email(&value.from));
let message = if let Some(reply_to) = value.reply_to.as_ref() {
message.set_reply_to(mailbox_to_email(reply_to))
} else {
message
};
message
}
}

fn mailbox_to_email(mailbox: &Mailbox) -> sendgrid::v3::Email {
let email = sendgrid::v3::Email::new(mailbox.email.to_string());
let email = if let Some(name) = mailbox.name.as_ref() {
email.set_name(name)
} else {
email
};
email
}

impl TryFrom<&Sendgrid> for Sender {
type Error = reqwest::Error;

fn try_from(value: &Sendgrid) -> Result<Self, Self::Error> {
let client = Client::builder().https_only(value.https_only).build()?;
Ok(Sender::new(value.api_key.clone(), Some(client)))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
5 changes: 5 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod config;
#[cfg(feature = "email")]
pub mod email;
pub mod other;
pub mod reqwest;
pub mod serde;
#[cfg(feature = "sidekiq")]
pub mod sidekiq;
Expand All @@ -21,6 +22,7 @@ use crate::error::axum::AxumError;
#[cfg(feature = "email")]
use crate::error::email::EmailError;
use crate::error::other::OtherError;
use crate::error::reqwest::ReqwestError;
use crate::error::serde::SerdeError;
#[cfg(feature = "sidekiq")]
use crate::error::sidekiq::SidekiqError;
Expand Down Expand Up @@ -77,6 +79,9 @@ pub enum Error {
#[error(transparent)]
Tracing(#[from] TracingError),

#[error(transparent)]
Reqwest(#[from] ReqwestError),

#[cfg(feature = "http")]
#[error(transparent)]
Axum(#[from] AxumError),
Expand Down
17 changes: 17 additions & 0 deletions src/error/reqwest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::error::Error;

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ReqwestError {
#[error(transparent)]
Error(#[from] reqwest::Error),

#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}

impl From<reqwest::Error> for Error {
fn from(value: reqwest::Error) -> Self {
Self::Reqwest(ReqwestError::from(value))
}
}

0 comments on commit 7df17a6

Please sign in to comment.