diff --git a/src/cli/main.rs b/src/cli/main.rs index f227260..f5e4b5f 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -488,7 +488,54 @@ fn get_config() -> Result<(Config, ClientConfig)> { /// The function continues processing even if individual rules fail, logging /// warnings for missing rules, processing errors, or action failures. async fn run_rules(client: &mut GmailClient, rules: Rules, execute: bool) -> Result<()> { - let rules_by_labels = rules.get_rules_by_label(); + run_rules_for_action(client, &rules, execute, EolAction::Trash).await?; + run_rules_for_action(client, &rules, execute, EolAction::Delete).await?; + + Ok(()) +} + +/// Executes automated message retention rules across Gmail labels for an action. +/// +/// This function orchestrates the rule-based message processing workflow by: +/// 1. Organizing rules by their target labels +/// 2. Processing each label according to its configured rule +/// 3. Executing or simulating actions based on execution mode +/// +/// # Arguments +/// +/// * `client` - Mutable Gmail client for API operations +/// * `rules` - Loaded rules configuration containing all retention policies +/// * `execute` - Whether to actually perform actions (true) or dry-run (false) +/// +/// # Returns +/// +/// Returns `Result<()>` indicating success or failure of the rule processing. +/// +/// # Rule Processing Flow +/// +/// For each configured label: +/// 1. **Rule Lookup**: Find the retention rule for the label +/// 2. **Rule Application**: Apply rule criteria to find matching messages +/// 3. **Action Determination**: Determine appropriate action (trash/delete) +/// 4. **Execution**: Execute action or simulate for dry-run +/// +/// # Safety Features +/// +/// - **Dry-run mode**: When `execute` is false, actions are logged but not performed +/// - **Error isolation**: Errors for individual labels don't stop processing of other labels +/// - **Detailed logging**: Comprehensive logging of rule execution and results +/// +/// # Error Handling +/// +/// The function continues processing even if individual rules fail, logging +/// warnings for missing rules, processing errors, or action failures. +async fn run_rules_for_action( + client: &mut GmailClient, + rules: &Rules, + execute: bool, + action: EolAction, +) -> Result<()> { + let rules_by_labels = rules.get_rules_by_label_for_action(action); for label in rules.labels() { let Some(rule) = rules_by_labels.get(&label) else { diff --git a/src/rules.rs b/src/rules.rs index 8a2c3f8..d90e89f 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -248,13 +248,25 @@ impl Rules { } /// Find the id of the rule that contains a label - fn find_label(&self, label: &str) -> usize { - let rules_by_label = self.get_rules_by_label(); - if let Some(rule) = rules_by_label.get(label) { - rule.id() - } else { - 0 + fn find_label(&self, label: &str) -> Vec { + let mut rwl = Vec::new(); + + if let Some(t) = self.find_label_for_action(label, EolAction::Trash) { + rwl.push(t); } + + if let Some(d) = self.find_label_for_action(label, EolAction::Delete) { + rwl.push(d); + } + + rwl + } + + /// Find the id of the rule that contains a label + fn find_label_for_action(&self, label: &str, action: EolAction) -> Option { + let rules_by_label = self.get_rules_by_label_for_action(action); + + rules_by_label.get(label).map(|r| r.id()) } /// Removes a rule from the set by its unique ID. @@ -321,12 +333,14 @@ impl Rules { return Err(Error::LabelNotFoundInRules(label.to_string())); } - let rule_id = self.find_label(label); - if rule_id == 0 { + let rule_ids = self.find_label(label); + if rule_ids.is_empty() { return Err(Error::NoRuleFoundForLabel(label.to_string())); } - self.rules.remove(&rule_id.to_string()); + for id in rule_ids { + self.rules.remove(&id.to_string()); + } log::info!("Rule containing the label `{label}` has been removed."); Ok(()) @@ -347,17 +361,19 @@ impl Rules { /// let retention = Retention::new(MessageAge::Days(30), false); /// rules.add_rule(retention, Some("test"), false); /// - /// let label_map = rules.get_rules_by_label(); + /// let label_map = rules.get_rules_by_label(EolAction::Trash); /// if let Some(rule) = label_map.get("test") { /// println!("Rule for 'test' label: {}", rule.describe()); /// } /// ``` - pub fn get_rules_by_label(&self) -> BTreeMap { + pub fn get_rules_by_label_for_action(&self, action: EolAction) -> BTreeMap { let mut rbl = BTreeMap::new(); for rule in self.rules.values() { - for label in rule.labels() { - rbl.insert(label, rule.clone()); + if rule.action() == Some(action) { + for label in rule.labels() { + rbl.insert(label, rule.clone()); + } } } @@ -733,7 +749,7 @@ mod tests { let retention = Retention::new(MessageAge::Days(7), false); rules.add_rule(retention, Some("delete-test"), true); - let rules_by_label = rules.get_rules_by_label(); + let rules_by_label = rules.get_rules_by_label_for_action(EolAction::Delete); let rule = rules_by_label.get("delete-test").unwrap(); assert_eq!(rule.action(), Some(EolAction::Delete)); } @@ -799,7 +815,7 @@ mod tests { let retention = Retention::new(MessageAge::Days(30), false); rules.add_rule(retention, Some("mapped-label"), false); - let label_map = rules.get_rules_by_label(); + let label_map = rules.get_rules_by_label_for_action(EolAction::Trash); let rule = label_map.get("mapped-label"); assert!(rule.is_some()); assert!(rule.unwrap().labels().contains(&"mapped-label".to_string()));