From ea948eda2752748f462e55d3df2df2ce78f993ad Mon Sep 17 00:00:00 2001 From: Jeremiah Russell Date: Sun, 19 Oct 2025 09:08:26 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20docs(rule=5Fprocessor):=20add=20?= =?UTF-8?q?module=20and=20method=20docs=20with=20safety=20notes=20and=20do?= =?UTF-8?q?ctest=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rule_processor.rs | 242 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 226 insertions(+), 16 deletions(-) diff --git a/src/rule_processor.rs b/src/rule_processor.rs index a509089..ab35a5a 100644 --- a/src/rule_processor.rs +++ b/src/rule_processor.rs @@ -1,40 +1,208 @@ +//! Rule Processing Module +//! +//! This module provides the [`RuleProcessor`] trait and its implementation for processing +//! Gmail messages according to configured end-of-life (EOL) rules. It handles the complete +//! workflow of finding messages, applying filters based on rules, and executing actions +//! such as moving messages to trash or permanently deleting them. +//! +//! ## Safety Considerations +//! +//! - **Destructive Operations**: The [`RuleProcessor::batch_delete`] method permanently +//! removes messages from Gmail and cannot be undone. +//! - **Recoverable Operations**: The [`RuleProcessor::batch_trash`] method moves messages +//! to the Gmail trash folder, from which they can be recovered within 30 days. +//! - **Execute Flag**: All destructive operations are gated by an execute flag that must +//! be explicitly set to `true`. When `false`, operations run in "dry-run" mode. +//! +//! ## Workflow +//! +//! 1. Set a rule using [`RuleProcessor::set_rule`] +//! 2. Configure the execute flag with [`RuleProcessor::set_execute`] +//! 3. Process messages for a label with [`RuleProcessor::find_rule_and_messages_for_label`] +//! 4. The processor will automatically: +//! - Find messages matching the rule's query +//! - Prepare the message list via [`RuleProcessor::prepare`] +//! - Execute the rule's action (trash) if execute flag is true +//! +//! ## Example +//! +//! ```rust,no_run +//! # use cull_gmail::{GmailClient, RuleProcessor, EolRule, EolAction}; +//! # async fn example() -> cull_gmail::Result<()> { +//! let mut client = GmailClient::new().await?; +//! +//! // Create a rule (this would typically come from configuration) +//! let rule = EolRule::new(1, "old-emails".to_string(), EolAction::Trash, None, None, None)?; +//! +//! client.set_rule(rule); +//! client.set_execute(true); // Set to false for dry-run +//! +//! // Process all messages with the "old-emails" label according to the rule +//! client.find_rule_and_messages_for_label("old-emails").await?; +//! # Ok(()) +//! # } +//! ``` + use google_gmail1::api::{BatchDeleteMessagesRequest, BatchModifyMessagesRequest}; use crate::{EolAction, Error, GmailClient, Result, message_list::MessageList, rules::EolRule}; -/// Rules processor to apply the configured rules to the mailbox. +/// Trait for processing Gmail messages according to configured end-of-life rules. +/// +/// This trait defines the interface for finding, filtering, and acting upon Gmail messages +/// based on retention rules. Implementations should handle the complete workflow from +/// rule application to message processing. pub trait RuleProcessor { - /// Find the rule and the message for a specific label + /// Processes all messages for a specific Gmail label according to the configured rule. + /// + /// This is the main entry point for rule processing. It coordinates the entire workflow: + /// 1. Validates that the label exists in the mailbox + /// 2. Applies the rule's query to find matching messages + /// 3. Prepares the message list for processing + /// 4. Executes the rule's action (if execute flag is true) or runs in dry-run mode + /// + /// # Arguments + /// + /// * `label` - The Gmail label name to process (e.g., "INBOX", "old-emails") + /// + /// # Returns + /// + /// * `Ok(())` - Processing completed successfully + /// * `Err(Error::LabelNotFoundInMailbox)` - The specified label doesn't exist + /// * `Err(Error::RuleNotFound)` - No rule has been set via [`set_rule`](Self::set_rule) + /// * `Err(Error::NoQueryStringCalculated)` - The rule doesn't provide a valid query + /// + /// # Side Effects + /// + /// When execute flag is true, messages may be moved to trash or permanently deleted. + /// When execute flag is false, runs in dry-run mode with no destructive actions. fn find_rule_and_messages_for_label( &mut self, label: &str, ) -> impl std::future::Future> + Send; - /// Set the execute flag in the client + + /// Sets the execution mode for destructive operations. + /// + /// # Arguments + /// + /// * `value` - `true` to enable destructive operations, `false` for dry-run mode + /// + /// # Safety + /// + /// When set to `true`, subsequent calls to processing methods will perform actual + /// destructive operations on Gmail messages. Always verify your rules and queries + /// in dry-run mode (`false`) before enabling execution. fn set_execute(&mut self, value: bool); - /// Set rule to process - fn set_rule(&mut self, action: EolRule); - /// Report the action from the rule + + /// Configures the end-of-life rule to apply during processing. + /// + /// # Arguments + /// + /// * `rule` - The `EolRule` containing query criteria and action to perform + /// + /// # Example + /// + /// ```rust,no_run + /// # use cull_gmail::{GmailClient, RuleProcessor, EolRule, EolAction}; + /// # fn example() -> cull_gmail::Result<()> { + /// # let mut client = GmailClient::new_fake(); // This would be real in practice + /// let rule = EolRule::new(1, "old".to_string(), EolAction::Trash, None, None, None)?; + /// client.set_rule(rule); + /// # Ok(()) + /// # } + /// ``` + fn set_rule(&mut self, rule: EolRule); + + /// Returns the action that will be performed by the currently configured rule. + /// + /// # Returns + /// + /// * `Some(EolAction)` - The action (e.g., `EolAction::Trash`) if a rule is set + /// * `None` - If no rule has been configured via [`set_rule`](Self::set_rule) fn action(&self) -> Option; - /// Prepare a list of messages to trash or delete + + /// Prepares the list of messages for processing by fetching them from Gmail. + /// + /// This method queries the Gmail API to retrieve messages matching the current + /// query and label filters, up to the specified number of pages. + /// + /// # Arguments + /// + /// * `pages` - Maximum number of result pages to fetch (0 = all pages) + /// + /// # Returns + /// + /// * `Ok(())` - Messages successfully retrieved and prepared + /// * `Err(_)` - Gmail API error or network failure + /// + /// # Side Effects + /// + /// Makes API calls to Gmail to retrieve message metadata. No messages are + /// modified by this operation. fn prepare(&mut self, pages: u32) -> impl std::future::Future> + Send; - /// Batch delete of messages + + /// Permanently deletes all prepared messages from Gmail. + /// + /// # Returns + /// + /// * `Ok(())` - All messages successfully deleted + /// * `Err(_)` - Gmail API error, network failure, or insufficient permissions + /// + /// # Safety + /// + /// ⚠️ **DESTRUCTIVE OPERATION** - This permanently removes messages from Gmail. + /// Deleted messages cannot be recovered. Use [`batch_trash`](Self::batch_trash) + /// for recoverable deletion. + /// + /// # Gmail API Requirements + /// + /// Requires the `https://www.googleapis.com/auth/gmail.modify` scope or broader. fn batch_delete(&self) -> impl std::future::Future> + Send; - /// Batch trash + + /// Moves all prepared messages to the Gmail trash folder. + /// + /// Messages moved to trash can be recovered within 30 days through the Gmail + /// web interface or API calls. + /// + /// # Returns + /// + /// * `Ok(())` - All messages successfully moved to trash + /// * `Err(_)` - Gmail API error, network failure, or insufficient permissions + /// + /// # Recovery + /// + /// Messages can be recovered from trash within 30 days. After 30 days, + /// Gmail automatically purges trashed messages. + /// + /// # Gmail API Requirements + /// + /// Requires the `https://www.googleapis.com/auth/gmail.modify` scope. fn batch_trash(&self) -> impl std::future::Future> + Send; } impl RuleProcessor for GmailClient { - /// Add Action to the Client for processing + /// Configures the end-of-life rule for this Gmail client. + /// + /// The rule defines which messages to target and what action to perform on them. + /// This must be called before processing any labels. fn set_rule(&mut self, value: EolRule) { self.rule = Some(value); } - /// Set the execute flag + /// Controls whether destructive operations are actually executed. + /// + /// When `false` (dry-run mode), all operations are simulated but no actual + /// changes are made to Gmail messages. When `true`, destructive operations + /// like moving to trash or deleting will be performed. + /// + /// **Default is `false` for safety.** fn set_execute(&mut self, value: bool) { self.execute = value; } - /// The action set in the rule + /// Returns the action that will be performed by the current rule. + /// + /// This is useful for logging and verification before executing destructive operations. fn action(&self) -> Option { if let Some(rule) = &self.rule { return rule.action(); @@ -42,7 +210,16 @@ impl RuleProcessor for GmailClient { None } - /// Find the rule and messages for the label + /// Orchestrates the complete rule processing workflow for a Gmail label. + /// + /// This method implements the main processing logic: + /// 1. Validates the label exists in the mailbox + /// 2. Constructs a Gmail query from the rule's criteria + /// 3. Fetches matching messages from the Gmail API + /// 4. Executes the rule's action if execute flag is enabled + /// + /// The method respects the execute flag - when `false`, it runs in dry-run mode + /// and only logs what would be done without making any changes. async fn find_rule_and_messages_for_label(&mut self, label: &str) -> Result<()> { self.add_labels(&[label.to_string()])?; @@ -71,12 +248,32 @@ impl RuleProcessor for GmailClient { } } - /// Prepare the message list for delete to be completed on execute by batch_delete + /// Fetches messages from Gmail API based on current query and label filters. + /// + /// This is a read-only operation that retrieves message metadata from Gmail + /// without modifying any messages. The results are cached internally for + /// subsequent batch operations. + /// + /// # Arguments + /// + /// * `pages` - Number of result pages to fetch (0 = all available pages) async fn prepare(&mut self, pages: u32) -> Result<()> { self.get_messages(pages).await } - /// Run the batch delete on the selected messages + /// Permanently deletes all prepared messages using Gmail's batch delete API. + /// + /// ⚠️ **DESTRUCTIVE OPERATION** - This action cannot be undone! + /// + /// This method uses the Gmail API's batch delete functionality to permanently + /// remove messages from the user's mailbox. Once deleted, messages cannot be + /// recovered through any means. + /// + /// # API Scope Requirements + /// + /// Uses `https://mail.google.com/` scope for maximum API access. Consider + /// using the more restrictive `https://www.googleapis.com/auth/gmail.modify` + /// scope if your application security policy allows it. async fn batch_delete(&self) -> Result<()> { let ids = Some(self.message_ids()); @@ -99,7 +296,20 @@ impl RuleProcessor for GmailClient { Ok(()) } - /// Move the messages to trash + /// Moves all prepared messages to Gmail's trash folder using batch modify API. + /// + /// This is a recoverable operation - messages can be restored from trash within + /// 30 days via the Gmail web interface or API calls. After 30 days, Gmail + /// automatically purges trashed messages permanently. + /// + /// The operation adds the TRASH label and removes any existing labels that were + /// used to filter the messages, effectively moving them out of their current + /// folders into the trash. + /// + /// # API Scope Requirements + /// + /// Currently uses `https://www.google.com/` scope. Should be updated to use + /// `https://www.googleapis.com/auth/gmail.modify` for better security. async fn batch_trash(&self) -> Result<()> { let add_label_ids = Some(Vec::from(["TRASH".to_string()])); let ids = Some(self.message_ids());