cull_gmail/
rules.rs

1use std::{
2    collections::BTreeMap,
3    env,
4    fs::{self, read_to_string},
5    path::PathBuf,
6};
7
8use serde::{Deserialize, Serialize};
9
10mod eol_rule;
11
12pub use eol_rule::EolRule;
13
14use crate::{EolAction, Error, MessageAge, Result, Retention};
15
16/// Configuration file for the program
17#[derive(Debug, Serialize, Deserialize)]
18pub struct Rules {
19    rules: BTreeMap<String, EolRule>,
20}
21
22impl Default for Rules {
23    fn default() -> Self {
24        let rules = BTreeMap::new();
25
26        let mut cfg = Self { rules };
27
28        cfg.add_rule(Retention::new(MessageAge::Years(1), true), None, false)
29            .add_rule(Retention::new(MessageAge::Weeks(1), true), None, false)
30            .add_rule(Retention::new(MessageAge::Months(1), true), None, false)
31            .add_rule(Retention::new(MessageAge::Years(5), true), None, false);
32
33        cfg
34    }
35}
36
37impl Rules {
38    /// Create a new configuration file
39    pub fn new() -> Self {
40        Rules::default()
41    }
42
43    /// Get the contents of an existing rule
44    pub fn get_rule(&self, id: usize) -> Option<EolRule> {
45        self.rules.get(&id.to_string()).cloned()
46    }
47
48    /// Add a new rule to the rule set by setting the retention age
49    pub fn add_rule(
50        &mut self,
51        retention: Retention,
52        label: Option<&String>,
53        delete: bool,
54    ) -> &mut Self {
55        let mut current_labels = Vec::new();
56        for rule in self.rules.values() {
57            let mut ls = rule.labels().clone();
58            current_labels.append(&mut ls);
59        }
60
61        if label.is_some() && current_labels.contains(label.unwrap()) {
62            log::warn!("a rule already applies to label {}", label.unwrap());
63            return self;
64        }
65
66        let id = if let Some((_, max)) = self.rules.iter().max_by_key(|(_, r)| r.id()) {
67            max.id() + 1
68        } else {
69            1
70        };
71
72        let mut rule = EolRule::new(id);
73        rule.set_retention(retention);
74        if let Some(l) = label {
75            rule.add_label(l);
76        }
77        if delete {
78            rule.set_action(&EolAction::Delete);
79        }
80        log::info!("added rule: {rule}");
81        self.rules.insert(rule.id().to_string(), rule);
82        self
83    }
84
85    /// Get the labels from the rules
86    pub fn labels(&self) -> Vec<String> {
87        let mut labels = Vec::new();
88        for rule in self.rules.values() {
89            labels.append(&mut rule.labels().clone());
90        }
91        labels
92    }
93
94    /// Find the id of the rule that contains a label
95    fn find_label(&self, label: &str) -> usize {
96        let rules_by_label = self.get_rules_by_label();
97        if let Some(rule) = rules_by_label.get(label) {
98            rule.id()
99        } else {
100            0
101        }
102    }
103
104    /// Remove a rule by the ID specified
105    pub fn remove_rule_by_id(&mut self, id: usize) -> crate::Result<()> {
106        self.rules.remove(&id.to_string());
107        println!("Rule `{id}` has been removed.");
108        Ok(())
109    }
110
111    /// Remove a rule by the Label specified
112    pub fn remove_rule_by_label(&mut self, label: &str) -> crate::Result<()> {
113        let labels = self.labels();
114
115        if !labels.contains(&label.to_string()) {
116            return Err(Error::LabelNotFoundInRules(label.to_string()));
117        }
118
119        let rule_id = self.find_label(label);
120        if rule_id == 0 {
121            return Err(Error::NoRuleFoundForLabel(label.to_string()));
122        }
123
124        self.rules.remove(&rule_id.to_string());
125
126        log::info!("Rule containing the label `{label}` has been removed.");
127        Ok(())
128    }
129
130    /// Get a map of the rules indexed by labels
131    pub fn get_rules_by_label(&self) -> BTreeMap<String, EolRule> {
132        let mut rbl = BTreeMap::new();
133
134        for rule in self.rules.values() {
135            for label in rule.labels() {
136                rbl.insert(label, rule.clone());
137            }
138        }
139
140        rbl
141    }
142
143    /// Add a label to the rule identified by the id
144    pub fn add_label_to_rule(&mut self, id: usize, label: &str) -> Result<()> {
145        let Some(rule) = self.rules.get_mut(id.to_string().as_str()) else {
146            return Err(Error::RuleNotFound(id));
147        };
148        rule.add_label(label);
149        self.save()?;
150        println!("Label `{label}` added to rule `#{id}`");
151
152        Ok(())
153    }
154
155    /// Remove a label from the rule identified by the id
156    pub fn remove_label_from_rule(&mut self, id: usize, label: &str) -> Result<()> {
157        let Some(rule) = self.rules.get_mut(id.to_string().as_str()) else {
158            return Err(Error::RuleNotFound(id));
159        };
160        rule.remove_label(label);
161        self.save()?;
162        println!("Label `{label}` removed from rule `#{id}`");
163
164        Ok(())
165    }
166
167    /// Set the action on the rule identified by the id
168    pub fn set_action_on_rule(&mut self, id: usize, action: &EolAction) -> Result<()> {
169        let Some(rule) = self.rules.get_mut(id.to_string().as_str()) else {
170            return Err(Error::RuleNotFound(id));
171        };
172        rule.set_action(action);
173        self.save()?;
174        println!("Action set to `{action}` on rule `#{id}`");
175
176        Ok(())
177    }
178
179    /// Save the current configuration to the file
180    pub fn save(&self) -> Result<()> {
181        let home_dir = env::home_dir().unwrap();
182        let path = PathBuf::new().join(home_dir).join(".cull-gmail/rules.toml");
183
184        let res = toml::to_string(self);
185        log::trace!("toml conversion result: {res:#?}");
186
187        if let Ok(output) = res {
188            fs::write(&path, output)?;
189            log::trace!("Config saved to {}", path.display());
190        }
191
192        Ok(())
193    }
194
195    /// Load the current configuration
196    pub fn load() -> Result<Rules> {
197        let home_dir = env::home_dir().unwrap();
198        let path = PathBuf::new().join(home_dir).join(".cull-gmail/rules.toml");
199        log::trace!("Loading config from {}", path.display());
200
201        let input = read_to_string(path)?;
202        let config = toml::from_str::<Rules>(&input)?;
203        Ok(config)
204    }
205
206    /// List the end of life rules set in the configuration
207    pub fn list_rules(&self) -> Result<()> {
208        for rule in self.rules.values() {
209            println!("{rule}");
210        }
211        Ok(())
212    }
213}