Skip to main content

rustack_ses_core/
receipt_rule.rs

1//! Receipt rule set management.
2//!
3//! Receipt rules are accepted and stored but never actually process
4//! incoming email in the local development emulator. This is for
5//! API compatibility only.
6
7use dashmap::{DashMap, mapref::entry::Entry};
8use parking_lot::RwLock;
9use rustack_ses_model::{
10    error::{SesError, SesErrorCode},
11    types::{ReceiptRule, ReceiptRuleSetMetadata},
12};
13
14/// Internal receipt rule set record.
15#[derive(Debug, Clone)]
16pub struct ReceiptRuleSetRecord {
17    /// Rule set name.
18    pub name: String,
19    /// Rules within this rule set.
20    pub rules: Vec<ReceiptRule>,
21    /// Creation timestamp.
22    pub created_timestamp: chrono::DateTime<chrono::Utc>,
23}
24
25/// Store for receipt rule sets and rules.
26///
27/// Receipt rules are accepted and stored but never actually process
28/// incoming email. This is for API compatibility only.
29#[derive(Debug)]
30pub struct ReceiptRuleSetStore {
31    rule_sets: DashMap<String, ReceiptRuleSetRecord>,
32    active_rule_set: RwLock<Option<String>>,
33}
34
35impl Default for ReceiptRuleSetStore {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl ReceiptRuleSetStore {
42    /// Create a new empty receipt rule set store.
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            rule_sets: DashMap::new(),
47            active_rule_set: RwLock::new(None),
48        }
49    }
50
51    /// Create a new receipt rule set.
52    ///
53    /// # Errors
54    ///
55    /// Returns `AlreadyExistsException` if a rule set with the same name exists.
56    pub fn create_rule_set(&self, name: &str) -> Result<(), SesError> {
57        match self.rule_sets.entry(name.to_owned()) {
58            Entry::Occupied(_) => Err(SesError::with_message(
59                SesErrorCode::AlreadyExistsException,
60                format!("Receipt rule set <{name}> already exists."),
61            )),
62            Entry::Vacant(e) => {
63                e.insert(ReceiptRuleSetRecord {
64                    name: name.to_owned(),
65                    rules: Vec::new(),
66                    created_timestamp: chrono::Utc::now(),
67                });
68                Ok(())
69            }
70        }
71    }
72
73    /// Delete a receipt rule set.
74    ///
75    /// # Errors
76    ///
77    /// Returns `RuleSetDoesNotExistException` if not found.
78    pub fn delete_rule_set(&self, name: &str) -> Result<(), SesError> {
79        self.rule_sets.remove(name).ok_or_else(|| {
80            SesError::with_message(
81                SesErrorCode::RuleSetDoesNotExistException,
82                format!("Receipt rule set <{name}> does not exist."),
83            )
84        })?;
85        // If the deleted rule set was active, clear the active rule set
86        let mut active = self.active_rule_set.write();
87        if active.as_deref() == Some(name) {
88            *active = None;
89        }
90        Ok(())
91    }
92
93    /// Describe a receipt rule set.
94    ///
95    /// # Errors
96    ///
97    /// Returns `RuleSetDoesNotExistException` if not found.
98    pub fn describe_rule_set(&self, name: &str) -> Result<ReceiptRuleSetRecord, SesError> {
99        self.rule_sets
100            .get(name)
101            .map(|entry| entry.value().clone())
102            .ok_or_else(|| {
103                SesError::with_message(
104                    SesErrorCode::RuleSetDoesNotExistException,
105                    format!("Receipt rule set <{name}> does not exist."),
106                )
107            })
108    }
109
110    /// Create a receipt rule within a rule set.
111    ///
112    /// # Errors
113    ///
114    /// Returns `RuleSetDoesNotExistException` if the rule set is not found.
115    pub fn create_rule(
116        &self,
117        rule_set_name: &str,
118        rule: ReceiptRule,
119        after: Option<&str>,
120    ) -> Result<(), SesError> {
121        let mut entry = self.rule_sets.get_mut(rule_set_name).ok_or_else(|| {
122            SesError::with_message(
123                SesErrorCode::RuleSetDoesNotExistException,
124                format!("Receipt rule set <{rule_set_name}> does not exist."),
125            )
126        })?;
127
128        if let Some(after_name) = after {
129            let pos = entry
130                .rules
131                .iter()
132                .position(|r| r.name == after_name)
133                .map_or(entry.rules.len(), |p| p + 1);
134            entry.rules.insert(pos, rule);
135        } else {
136            entry.rules.push(rule);
137        }
138        Ok(())
139    }
140
141    /// Delete a receipt rule from a rule set.
142    ///
143    /// # Errors
144    ///
145    /// Returns `RuleSetDoesNotExistException` if the rule set is not found.
146    pub fn delete_rule(&self, rule_set_name: &str, rule_name: &str) -> Result<(), SesError> {
147        let mut entry = self.rule_sets.get_mut(rule_set_name).ok_or_else(|| {
148            SesError::with_message(
149                SesErrorCode::RuleSetDoesNotExistException,
150                format!("Receipt rule set <{rule_set_name}> does not exist."),
151            )
152        })?;
153        entry.rules.retain(|r| r.name != rule_name);
154        Ok(())
155    }
156
157    /// Clone a receipt rule set to a new name.
158    ///
159    /// # Errors
160    ///
161    /// Returns `RuleSetDoesNotExistException` if the source does not exist.
162    /// Returns `AlreadyExistsException` if the destination already exists.
163    pub fn clone_rule_set(&self, source_name: &str, dest_name: &str) -> Result<(), SesError> {
164        let source = self.describe_rule_set(source_name)?;
165        match self.rule_sets.entry(dest_name.to_owned()) {
166            Entry::Occupied(_) => Err(SesError::with_message(
167                SesErrorCode::AlreadyExistsException,
168                format!("Receipt rule set <{dest_name}> already exists."),
169            )),
170            Entry::Vacant(e) => {
171                e.insert(ReceiptRuleSetRecord {
172                    name: dest_name.to_owned(),
173                    rules: source.rules.clone(),
174                    created_timestamp: chrono::Utc::now(),
175                });
176                Ok(())
177            }
178        }
179    }
180
181    /// Get the active receipt rule set name and metadata.
182    #[must_use]
183    pub fn get_active_rule_set(&self) -> Option<(ReceiptRuleSetMetadata, Vec<ReceiptRule>)> {
184        let active = self.active_rule_set.read();
185        let name = active.as_ref()?;
186        let record = self.rule_sets.get(name)?;
187        Some((
188            ReceiptRuleSetMetadata {
189                name: Some(record.name.clone()),
190                created_timestamp: Some(record.created_timestamp),
191            },
192            record.rules.clone(),
193        ))
194    }
195
196    /// Set the active receipt rule set.
197    ///
198    /// # Errors
199    ///
200    /// Returns `RuleSetDoesNotExistException` if the rule set is not found
201    /// (when a name is provided).
202    pub fn set_active_rule_set(&self, name: Option<&str>) -> Result<(), SesError> {
203        if let Some(name) = name {
204            if !self.rule_sets.contains_key(name) {
205                return Err(SesError::with_message(
206                    SesErrorCode::RuleSetDoesNotExistException,
207                    format!("Receipt rule set <{name}> does not exist."),
208                ));
209            }
210            *self.active_rule_set.write() = Some(name.to_owned());
211        } else {
212            *self.active_rule_set.write() = None;
213        }
214        Ok(())
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_should_create_rule_set() {
224        let store = ReceiptRuleSetStore::new();
225        assert!(store.create_rule_set("my-rules").is_ok());
226    }
227
228    #[test]
229    fn test_should_reject_duplicate_rule_set() {
230        let store = ReceiptRuleSetStore::new();
231        store.create_rule_set("dup").unwrap_or_default();
232        assert!(store.create_rule_set("dup").is_err());
233    }
234
235    #[test]
236    fn test_should_delete_rule_set() {
237        let store = ReceiptRuleSetStore::new();
238        store.create_rule_set("del").unwrap_or_default();
239        assert!(store.delete_rule_set("del").is_ok());
240        assert!(store.describe_rule_set("del").is_err());
241    }
242
243    #[test]
244    fn test_should_describe_rule_set() {
245        let store = ReceiptRuleSetStore::new();
246        store.create_rule_set("desc").unwrap_or_default();
247        let record = store.describe_rule_set("desc");
248        assert!(record.is_ok());
249        assert_eq!(
250            record
251                .unwrap_or_else(|_| ReceiptRuleSetRecord {
252                    name: String::new(),
253                    rules: Vec::new(),
254                    created_timestamp: chrono::Utc::now(),
255                })
256                .name,
257            "desc"
258        );
259    }
260
261    #[test]
262    fn test_should_create_rule_in_set() {
263        let store = ReceiptRuleSetStore::new();
264        store.create_rule_set("set1").unwrap_or_default();
265        let rule = ReceiptRule {
266            name: "rule1".to_owned(),
267            enabled: Some(true),
268            ..ReceiptRule::default()
269        };
270        assert!(store.create_rule("set1", rule, None).is_ok());
271        let record = store
272            .describe_rule_set("set1")
273            .unwrap_or_else(|_| ReceiptRuleSetRecord {
274                name: String::new(),
275                rules: Vec::new(),
276                created_timestamp: chrono::Utc::now(),
277            });
278        assert_eq!(record.rules.len(), 1);
279    }
280
281    #[test]
282    fn test_should_create_rule_after_existing() {
283        let store = ReceiptRuleSetStore::new();
284        store.create_rule_set("set2").unwrap_or_default();
285        let rule1 = ReceiptRule {
286            name: "rule1".to_owned(),
287            ..ReceiptRule::default()
288        };
289        let rule2 = ReceiptRule {
290            name: "rule2".to_owned(),
291            ..ReceiptRule::default()
292        };
293        let rule3 = ReceiptRule {
294            name: "rule3".to_owned(),
295            ..ReceiptRule::default()
296        };
297        store.create_rule("set2", rule1, None).unwrap_or_default();
298        store.create_rule("set2", rule2, None).unwrap_or_default();
299        store
300            .create_rule("set2", rule3, Some("rule1"))
301            .unwrap_or_default();
302        let record = store
303            .describe_rule_set("set2")
304            .unwrap_or_else(|_| ReceiptRuleSetRecord {
305                name: String::new(),
306                rules: Vec::new(),
307                created_timestamp: chrono::Utc::now(),
308            });
309        assert_eq!(record.rules[0].name, "rule1");
310        assert_eq!(record.rules[1].name, "rule3");
311        assert_eq!(record.rules[2].name, "rule2");
312    }
313
314    #[test]
315    fn test_should_delete_rule() {
316        let store = ReceiptRuleSetStore::new();
317        store.create_rule_set("set3").unwrap_or_default();
318        let rule = ReceiptRule {
319            name: "rule1".to_owned(),
320            ..ReceiptRule::default()
321        };
322        store.create_rule("set3", rule, None).unwrap_or_default();
323        assert!(store.delete_rule("set3", "rule1").is_ok());
324        let record = store
325            .describe_rule_set("set3")
326            .unwrap_or_else(|_| ReceiptRuleSetRecord {
327                name: String::new(),
328                rules: Vec::new(),
329                created_timestamp: chrono::Utc::now(),
330            });
331        assert!(record.rules.is_empty());
332    }
333
334    #[test]
335    fn test_should_clone_rule_set() {
336        let store = ReceiptRuleSetStore::new();
337        store.create_rule_set("source").unwrap_or_default();
338        let rule = ReceiptRule {
339            name: "rule1".to_owned(),
340            ..ReceiptRule::default()
341        };
342        store.create_rule("source", rule, None).unwrap_or_default();
343        assert!(store.clone_rule_set("source", "dest").is_ok());
344        let record = store
345            .describe_rule_set("dest")
346            .unwrap_or_else(|_| ReceiptRuleSetRecord {
347                name: String::new(),
348                rules: Vec::new(),
349                created_timestamp: chrono::Utc::now(),
350            });
351        assert_eq!(record.rules.len(), 1);
352        assert_eq!(record.rules[0].name, "rule1");
353    }
354
355    #[test]
356    fn test_should_set_and_get_active_rule_set() {
357        let store = ReceiptRuleSetStore::new();
358        store.create_rule_set("active-set").unwrap_or_default();
359        assert!(store.get_active_rule_set().is_none());
360        assert!(store.set_active_rule_set(Some("active-set")).is_ok());
361        let active = store.get_active_rule_set();
362        assert!(active.is_some());
363        let (metadata, _) = active.unwrap_or_default();
364        assert_eq!(metadata.name, Some("active-set".to_owned()));
365    }
366
367    #[test]
368    fn test_should_clear_active_on_delete() {
369        let store = ReceiptRuleSetStore::new();
370        store.create_rule_set("doomed").unwrap_or_default();
371        store
372            .set_active_rule_set(Some("doomed"))
373            .unwrap_or_default();
374        store.delete_rule_set("doomed").unwrap_or_default();
375        assert!(store.get_active_rule_set().is_none());
376    }
377}