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#[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 pub fn new() -> Self {
40 Rules::default()
41 }
42
43 pub fn get_rule(&self, id: usize) -> Option<EolRule> {
45 self.rules.get(&id.to_string()).cloned()
46 }
47
48 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 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 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 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 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 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 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 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 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 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 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 pub fn list_rules(&self) -> Result<()> {
208 for rule in self.rules.values() {
209 println!("{rule}");
210 }
211 Ok(())
212 }
213}