diff --git a/services/reports/Cargo.lock b/services/reports/Cargo.lock --- a/services/reports/Cargo.lock +++ b/services/reports/Cargo.lock @@ -369,6 +369,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -1791,6 +1802,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "postmark" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5dd703e0cb58b305d882618698682719141a09483868401ba3d0e689a96b" +dependencies = [ + "async-trait", + "bytes", + "http", + "reqwest", + "serde", + "serde_json", + "thiserror", + "typed-builder", + "url", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1940,6 +1968,7 @@ "num-derive", "num-traits", "once_cell", + "postmark", "serde", "serde_json", "serde_repr", @@ -2320,6 +2349,26 @@ "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -2569,6 +2618,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typed-builder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cba322cb9b7bc6ca048de49e83918223f35e7a86311267013afff257004870" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "typenum" version = "1.16.0" diff --git a/services/reports/Cargo.toml b/services/reports/Cargo.toml --- a/services/reports/Cargo.toml +++ b/services/reports/Cargo.toml @@ -24,6 +24,7 @@ num-traits = "0.2" num-derive = "0.4" once_cell = "1.17" +postmark = { version = "0.8", features = ["reqwest"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" diff --git a/services/reports/src/email/config.rs b/services/reports/src/email/config.rs --- a/services/reports/src/email/config.rs +++ b/services/reports/src/email/config.rs @@ -41,6 +41,18 @@ pub mailing_groups: HashMap, } +impl EmailConfig { + pub fn recipient_for_report_type( + &self, + report_type: &ReportType, + ) -> Option<&str> { + self + .mailing_groups + .get(&report_type.into()) + .map(String::as_str) + } +} + impl FromStr for EmailConfig { type Err = serde_json::Error; fn from_str(s: &str) -> Result { diff --git a/services/reports/src/email/mod.rs b/services/reports/src/email/mod.rs --- a/services/reports/src/email/mod.rs +++ b/services/reports/src/email/mod.rs @@ -1,2 +1,73 @@ +use postmark::{ + api::email::{self, SendEmailBatchRequest}, + reqwest::{PostmarkClient, PostmarkClientError}, + Query, +}; +use tracing::{debug, trace, warn}; + +use crate::{ + config::CONFIG, + report_types::{ReportID, ReportInput, ReportType}, +}; + pub mod config; mod template; + +pub type EmailError = postmark::QueryError; + +pub struct ReportEmail { + report_type: ReportType, + rendered_message: String, + subject: String, +} + +pub fn prepare_email( + report_input: &ReportInput, + report_id: &ReportID, + user_id: Option<&str>, +) -> ReportEmail { + let message = + template::render_email_for_report(report_input, report_id, user_id); + let subject = template::subject_for_report(report_input, user_id); + ReportEmail { + report_type: report_input.report_type, + rendered_message: message, + subject, + } +} + +pub async fn send_emails( + emails: impl IntoIterator, +) -> Result<(), EmailError> { + let Some(email_config) = CONFIG.email_config() else { + debug!("E-mail config unavailable. Skipping sending e-mails"); + return Ok(()); + }; + + // it's cheap to build this every time + let client = PostmarkClient::builder() + .token(&email_config.postmark_token) + .build(); + + let requests: SendEmailBatchRequest = emails + .into_iter() + .filter_map(|item| { + let Some(recipient) = email_config.recipient_for_report_type(&item.report_type) else { + warn!("Recipient E-mail for {:?} not configured. Skipping", &item.report_type); + return None; + }; + + let email = email::SendEmailRequest::builder() + .from(&email_config.sender_email) + .to(recipient) + .body(email::Body::html(item.rendered_message)) + .subject(item.subject) + .build(); + Some(email) + }) + .collect(); + + let responses = requests.execute(&client).await?; + trace!(?responses, "E-mails sent successfully."); + Ok(()) +}