diff --git a/services/reports/Cargo.lock b/services/reports/Cargo.lock --- a/services/reports/Cargo.lock +++ b/services/reports/Cargo.lock @@ -1508,6 +1508,28 @@ "regex-automata 0.1.10", ] +[[package]] +name = "maud" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be95d66c3024ffce639216058e5bae17a83ecaf266ffc6e4d060ad447c9eed2" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "memchr" version = "2.5.0" @@ -1775,6 +1797,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -1890,6 +1936,7 @@ "derive_more", "hex", "http", + "maud", "num-derive", "num-traits", "once_cell", diff --git a/services/reports/Cargo.toml b/services/reports/Cargo.toml --- a/services/reports/Cargo.toml +++ b/services/reports/Cargo.toml @@ -20,6 +20,7 @@ derive_more = "0.99" hex = "0.4" http = "0.2" +maud = "0.25" num-traits = "0.2" num-derive = "0.4" once_cell = "1.17" 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 @@ -9,7 +9,7 @@ #[derive(Clone, Debug, Hash, Eq, PartialEq, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub enum MailingGroup { - ErrorsReports, + ErrorReports, InconsistencyReports, MediaReports, } @@ -18,7 +18,7 @@ fn from(value: &ReportType) -> Self { use ReportType::*; match value { - ErrorReport => Self::ErrorsReports, + ErrorReport => Self::ErrorReports, MediaMission => Self::MediaReports, ThreadInconsistency | EntryInconsistency | UserInconsistency => { Self::InconsistencyReports @@ -37,7 +37,7 @@ pub postmark_token: String, /// E-mail that is used as a sender pub sender_email: String, - /// Receiver e-mails for report types + /// Recipient e-mails for report types pub mailing_groups: HashMap, } @@ -111,6 +111,21 @@ mailing_groups: HashMap::new(), }); + #[test] + fn parse_args_disabled() { + // should return none when config exists but is disabled + let mut args = EmailArgs { + config_content: Some(EXAMPLE_CFG.clone()), + config_file: None, + }; + let cfg = args.parse().expect("failed to parse"); + assert!(cfg.is_some()); + + args.config_content.as_mut().unwrap().disabled = true; + let cfg = args.parse().expect("failed to parse"); + assert!(cfg.is_none()); + } + #[test] fn parse_args_priority() { // direct content should have higher priority than file 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 +1,2 @@ pub mod config; +mod template; diff --git a/services/reports/src/email/template/html_layout.rs b/services/reports/src/email/template/html_layout.rs new file mode 100644 --- /dev/null +++ b/services/reports/src/email/template/html_layout.rs @@ -0,0 +1,89 @@ +use maud::{html, Markup, PreEscaped, DOCTYPE}; + +pub fn render_page(body: Markup) -> String { + let markup = html! { + (DOCTYPE) + html lang="en" { + head { + meta charset="utf-8"; + style { (PreEscaped(CSS)) } + } + body { + (body) + } + } + }; + + markup.into_string() +} + +const CSS: &str = r#" +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + color: #333; + line-height: 1.5; +} + +h2 { + font-size: 24px; + color: #222; + margin: 0 0 10px; + padding: 0; +} + +h3 { + font-size: 18px; + color: #444; + margin: 0 0 8px; + padding: 0; +} + +h4 { + font-size: 16px; + color: #555; + margin: 0; + padding: 0; +} + +p { + margin: 0 0 10px; + padding: 0; +} + +b { + font-weight: bold; +} + +em { + font-style: italic; +} + +code { + background-color: #f0f0f0; + padding: 2px 4px; + border-radius: 2px; + font-family: monospace; +} + +pre { + background-color: #f0f0f0; + padding: 10px; + font-family: monospace; + overflow: auto; +} + +ul { + margin: 0 0 10px 20px; + padding: 0; +} + +a { + color: #007bff; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +"#; diff --git a/services/reports/src/email/template/mod.rs b/services/reports/src/email/template/mod.rs new file mode 100644 --- /dev/null +++ b/services/reports/src/email/template/mod.rs @@ -0,0 +1,58 @@ +use maud::{html, Markup, Render}; + +use crate::report_types::*; + +mod html_layout; + +pub fn render_email_for_report( + report_input: &ReportInput, + report_id: &ReportID, + user_id: Option<&str>, +) -> String { + html_layout::render_page(message_body_for_report( + report_input, + report_id, + user_id, + )) +} + +fn message_body_for_report( + report: &ReportInput, + report_id: &ReportID, + user_id: Option<&str>, +) -> Markup { + let user = user_id.unwrap_or("N/A"); + let platform = &report.platform_details; + let time = report + .time + .unwrap_or_default() + .format("%d/%m/%y %H:%M:%S") + .to_string(); + + html! { + ul { + li { "User ID: " b { (user) } } + li { "Platform: " (platform) } + li { "Time: " b { (time) } } + li { "Report ID: " b { (report_id) } } + } + } +} + +impl Render for PlatformDetails { + fn render(&self) -> Markup { + let code_version = self + .code_version + .map(|it| it.to_string()) + .unwrap_or("N/A".into()); + html! { + b { (self.platform) } ", code version: " b { (code_version) } + } + } +} + +impl Render for ReportID { + fn render(&self) -> Markup { + html! { (self.as_str()) } + } +} diff --git a/services/reports/src/report_types.rs b/services/reports/src/report_types.rs --- a/services/reports/src/report_types.rs +++ b/services/reports/src/report_types.rs @@ -41,6 +41,23 @@ UserInconsistency = 4, } +impl ReportType { + pub fn is_error(&self) -> bool { + matches!(self, Self::ErrorReport) + } + pub fn is_media_mission(&self) -> bool { + matches!(self, Self::MediaMission) + } + pub fn is_inconsistency(&self) -> bool { + matches!( + self, + ReportType::ThreadInconsistency + | ReportType::EntryInconsistency + | ReportType::UserInconsistency + ) + } +} + /// Report platform #[derive(Clone, Debug, Serialize, Deserialize, Display)] #[serde(rename_all = "lowercase")] @@ -56,8 +73,8 @@ #[serde(rename_all = "camelCase")] pub struct PlatformDetails { pub platform: ReportPlatform, - code_version: Option, - state_version: Option, + pub code_version: Option, + pub state_version: Option, } /// Input report payload - this is the JSON we receive from clients