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