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<MailingGroup, String>,
 }
 
@@ -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<u16>,
-  state_version: Option<u16>,
+  pub code_version: Option<u16>,
+  pub state_version: Option<u16>,
 }
 
 /// Input report payload - this is the JSON we receive from clients