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::{MailSettings, SandboxMode};
use sendgrid::v3::{Message, Sender};
use serde_derive::{Deserialize, Serialize};
use validator::Validate;
pub(crate) fn default_config_per_env(
environment: Environment,
) -> Option<config::File<FileSourceString, FileFormat>> {
let config = match environment {
Environment::Production => Some(include_str!("config/production.toml")),
_ => None,
};
config.map(|c| config::File::from_str(c, FileFormat::Toml))
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Validate, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Sendgrid {
pub api_key: String,
#[serde(default = "default_true")]
pub sandbox: bool,
#[serde(default = "default_true")]
pub https_only: bool,
}
impl<'a> From<&'a Email> for Message<'a> {
fn from(value: &'a Email) -> Self {
let message = Message::new(mailbox_to_email(&value.from)).set_mail_settings(
MailSettings::new()
.set_sandbox_mode(SandboxMode::new().set_enable(value.sendgrid.sandbox)),
);
if let Some(reply_to) = value.reply_to.as_ref() {
message.set_reply_to(mailbox_to_email(reply_to))
} else {
message
}
}
}
fn mailbox_to_email(mailbox: &'_ Mailbox) -> sendgrid::v3::Email<'_> {
let email = sendgrid::v3::Email::new(mailbox.email.as_ref());
if let Some(name) = mailbox.name.as_ref() {
email.set_name(name)
} else {
email
}
}
impl<'a> TryFrom<&Sendgrid> for Sender<'a> {
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.as_ref(), Some(client)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::environment::Environment;
use crate::testing::snapshot::TestCase;
use insta::{assert_debug_snapshot, assert_json_snapshot, assert_toml_snapshot};
use rstest::{fixture, rstest};
#[fixture]
#[cfg_attr(coverage_nightly, coverage(off))]
fn case() -> TestCase {
Default::default()
}
#[rstest]
#[case(
r#"
api-key = "api-key"
"#
)]
#[case(
r#"
api-key = "api-key"
http_only = false
"#
)]
#[case(
r#"
api-key = "api-key"
http_only = true
"#
)]
#[case(
r#"
api-key = "api-key"
sandbox = false
"#
)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn serialization(_case: TestCase, #[case] config: &str) {
let sendgrid: Sendgrid = toml::from_str(config).unwrap();
assert_toml_snapshot!(sendgrid);
}
#[rstest]
#[case(
r#"
from = "No Reply <no-reply@example.com>"
[smtp.connection]
uri = "smtps://username:password@smtp.example.com:425"
[sendgrid]
api-key = "api-key"
"#
)]
#[case(
r#"
from = "no-reply@example.com"
reply-to = "No Reply <no-reply@example.com>"
[smtp.connection]
uri = "smtps://username:password@smtp.example.com:425"
[sendgrid]
api-key = "api-key"
sandbox = false
"#
)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn message_from_email(_case: TestCase, #[case] config: &str) {
let email: Email = toml::from_str(config).unwrap();
let message = Message::from(&email);
assert_json_snapshot!(message);
}
#[rstest]
#[case(
r#"
api-key = "api-key"
"#
)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn sender_from_sendgrid_config(_case: TestCase, #[case] config: &str) {
let sendgrid_config: Sendgrid = toml::from_str(config).unwrap();
let _sender = Sender::try_from(&sendgrid_config).unwrap();
}
#[rstest]
#[case(Environment::Development)]
#[case(Environment::Production)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn default_config_per_env(_case: TestCase, #[case] env: Environment) {
let config = super::default_config_per_env(env);
assert_debug_snapshot!(config);
}
}