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
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 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    /// Get a map of the rules indexed by labels
141    pub fn get_rules_by_label(&self) -> BTreeMap<String, EolRule> {
142        let mut rbl = BTreeMap::new();
143
144        for rule in self.rules.values() {
145            for label in rule.labels() {
146                rbl.insert(label, rule.clone());
147            }
148        }
149
150        rbl
151    }
152
153    /// Add a label to the rule identified by the id
154    pub fn add_label_to_rule(&mut self, id: usize, label: &str) -> Result<()> {
155        let Some(rule) = self.rules.get_mut(id.to_string().as_str()) else {
156            return Err(Error::RuleNotFound(id));
157        };
158        rule.add_label(label);
159        self.save()?;
160        println!("Label `{label}` added to rule `#{id}`");
161
162        Ok(())
163    }
164
165    /// Remove a label from the rule identified by the id
166    pub fn remove_label_from_rule(&mut self, id: usize, label: &str) -> Result<()> {
167        let Some(rule) = self.rules.get_mut(id.to_string().as_str()) else {
168            return Err(Error::RuleNotFound(id));
169        };
170        rule.remove_label(label);
171        self.save()?;
172        println!("Label `{label}` removed from rule `#{id}`");
173
174        Ok(())
175    }
176
177    /// Set the action on the rule identified by the id
178    pub fn set_action_on_rule(&mut self, id: usize, action: &EolAction) -> Result<()> {
179        let Some(rule) = self.rules.get_mut(id.to_string().as_str()) else {
180            return Err(Error::RuleNotFound(id));
181        };
182        rule.set_action(action);
183        self.save()?;
184        println!("Action set to `{action}` on rule `#{id}`");
185
186        Ok(())
187    }
188
189    /// Save the current configuration to the file
190    pub fn save(&self) -> Result<()> {
191        let home_dir = env::home_dir().unwrap();
192        let path = PathBuf::new()
193            .join(home_dir)
194            .join(".cull-gmail/cull-gmail.toml");
195
196        let res = toml::to_string(self);
197        log::trace!("toml conversion result: {res:#?}");
198
199        if let Ok(output) = res {
200            fs::write(&path, output)?;
201            log::trace!("Config saved to {}", path.display());
202        }
203
204        Ok(())
205    }
206
207    /// Load the current configuration
208    pub fn load() -> Result<Config> {
209        let home_dir = env::home_dir().unwrap();
210        let path = PathBuf::new()
211            .join(home_dir)
212            .join(".cull-gmail/cull-gmail.toml");
213        log::trace!("Loading config from {}", path.display());
214
215        let input = read_to_string(path)?;
216        let config = toml::from_str::<Config>(&input)?;
217        Ok(config)
218    }
219
220    /// Return the credential file name
221    pub fn credential_file(&self) -> &str {
222        if let Some(file) = &self.credentials {
223            file
224        } else {
225            ""
226        }
227    }
228
229    /// List the end of life rules set in the configuration
230    pub fn list_rules(&self) -> Result<()> {
231        for rule in self.rules.values() {
232            println!("{rule}");
233        }
234        Ok(())
235    }
236}