bestool_alertd/
targets.rs

1use std::collections::HashMap;
2
3use miette::Result;
4
5use crate::{
6	EmailConfig,
7	alert::AlertDefinition,
8	templates::{load_templates, render_alert},
9};
10
11mod default;
12mod email;
13
14pub use default::determine_default_target;
15pub use email::TargetEmail;
16
17#[derive(serde::Deserialize, Debug, Clone)]
18#[serde(rename_all = "snake_case")]
19#[serde(untagged)]
20pub enum SendTarget {
21	// New format: just id, subject, template
22	Simple {
23		id: String,
24		subject: Option<String>,
25		template: String,
26	},
27	// Old format: target: external, id, subject, template
28	External {
29		target: String, // Should be "external" but we ignore the value
30		id: String,
31		subject: Option<String>,
32		template: String,
33	},
34}
35
36impl SendTarget {
37	pub fn id(&self) -> &str {
38		match self {
39			Self::Simple { id, .. } => id,
40			Self::External { id, .. } => id,
41		}
42	}
43
44	pub fn subject(&self) -> &Option<String> {
45		match self {
46			Self::Simple { subject, .. } => subject,
47			Self::External { subject, .. } => subject,
48		}
49	}
50
51	pub fn template(&self) -> &str {
52		match self {
53			Self::Simple { template, .. } => template,
54			Self::External { template, .. } => template,
55		}
56	}
57
58	pub fn resolve_external(
59		&self,
60		external_targets: &HashMap<String, Vec<ExternalTarget>>,
61	) -> Vec<ResolvedTarget> {
62		external_targets
63			.get(self.id())
64			.map(|exts| {
65				exts.iter()
66					.map(|ext| ResolvedTarget {
67						subject: self.subject().clone(),
68						template: self.template().to_string(),
69						conn: ext.conn.clone(),
70					})
71					.collect()
72			})
73			.unwrap_or_default()
74	}
75}
76
77#[derive(Debug, Clone)]
78pub struct ResolvedTarget {
79	pub subject: Option<String>,
80	pub template: String,
81	pub conn: TargetEmail,
82}
83
84impl ResolvedTarget {
85	pub async fn send(
86		&self,
87		alert: &AlertDefinition,
88		tera_ctx: &mut tera::Context,
89		email: Option<&EmailConfig>,
90		dry_run: bool,
91	) -> Result<()> {
92		let tera = load_templates(&self.subject, &self.template)?;
93		let (subject, body) = render_alert(&tera, tera_ctx)?;
94
95		self.conn.send(alert, email, &subject, &body, dry_run).await
96	}
97}
98
99#[derive(serde::Deserialize, Debug)]
100pub struct AlertTargets {
101	pub targets: Vec<ExternalTarget>,
102}
103
104#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
105#[serde(rename_all = "snake_case")]
106pub struct ExternalTarget {
107	pub id: String,
108	#[serde(flatten)]
109	pub conn: TargetEmail,
110}
111
112#[cfg(test)]
113mod tests {
114	use super::*;
115
116	#[test]
117	fn test_send_target_simple_format() {
118		let yaml = r#"
119id: test-target
120subject: Test Subject
121template: Test template
122"#;
123		let target: SendTarget = serde_yaml::from_str(yaml).unwrap();
124		assert_eq!(target.id(), "test-target");
125		assert_eq!(target.subject(), &Some("Test Subject".to_string()));
126		assert_eq!(target.template(), "Test template");
127	}
128
129	#[test]
130	fn test_send_target_external_format() {
131		let yaml = r#"
132target: external
133id: test-target
134subject: Test Subject
135template: Test template
136"#;
137		let target: SendTarget = serde_yaml::from_str(yaml).unwrap();
138		assert_eq!(target.id(), "test-target");
139		assert_eq!(target.subject(), &Some("Test Subject".to_string()));
140		assert_eq!(target.template(), "Test template");
141	}
142
143	#[test]
144	fn test_send_target_without_subject() {
145		let yaml = r#"
146id: test-target
147template: Test template
148"#;
149		let target: SendTarget = serde_yaml::from_str(yaml).unwrap();
150		assert_eq!(target.id(), "test-target");
151		assert_eq!(target.subject(), &None);
152		assert_eq!(target.template(), "Test template");
153	}
154}