From 23ea5a82b08d90c3a6f804d4f71ea543d1af91bf Mon Sep 17 00:00:00 2001 From: Jeremiah Russell Date: Thu, 30 Oct 2025 06:48:14 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(rule=5Fprocessor):=20implement?= =?UTF-8?q?=20batch=20operations=20for=20message=20deletion=20and=20trashi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce `call_batch_delete` to permanently delete messages in chunks - introduce `call_batch_trash` to move messages to trash in chunks - refactor `batch_delete` and `batch_trash` to use chunking for large lists - add constants for `INBOX_LABEL` for clarity - update documentation to reflect the new chunking approach and api requirements --- src/rule_processor.rs | 125 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 11 deletions(-) diff --git a/src/rule_processor.rs b/src/rule_processor.rs index 62f5d02..13d95cf 100644 --- a/src/rule_processor.rs +++ b/src/rule_processor.rs @@ -58,6 +58,11 @@ use crate::{EolAction, Error, GmailClient, Result, message_list::MessageList, ru /// This constant ensures consistent usage of the TRASH label throughout the module. const TRASH_LABEL: &str = "TRASH"; +/// Gmail label name for the inbox folder. +/// +/// This constant ensures consistent usage of the INBOX label throughout the module. +const INBOX_LABEL: &str = "INBOX"; + /// Gmail API scope for modifying messages (recommended scope for most operations). /// /// This scope allows adding/removing labels, moving messages to trash, and other @@ -68,7 +73,7 @@ const GMAIL_MODIFY_SCOPE: &str = "https://www.googleapis.com/auth/gmail.modify"; /// /// This scope allows all operations and is required to authorise the batch /// delete operation. It is only used for batch delete. For all other -/// operations `GMAIL_MODIFY_SCOPE` is preferred. +/// operations [`GMAIL_MODIFY_SCOPE`](Self::GMAIL_MODIFY_SCOPE) is preferred. const GMAIL_DELETE_SCOPE: &str = "https://mail.google.com/"; /// Internal trait defining the minimal operations needed for rule processing. @@ -299,9 +304,51 @@ pub trait RuleProcessor { /// /// # Gmail API Requirements /// - /// Requires the `https://www.googleapis.com/auth/gmail.modify` scope or broader. + /// Requires the `https://mail.google.com/` scope or broader. fn batch_delete(&mut self) -> impl std::future::Future> + Send; + /// Calls the Gmail API to permanently deletes a slice from the list of messages. + /// + /// # 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://mail.google.com/` scope or broader. + fn call_batch_delete( + &self, + ids: &[String], + ) -> impl std::future::Future> + Send; + + /// Calls the Gmail API to move a slice of the 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(&mut self) -> impl std::future::Future> + Send; + /// Moves all prepared messages to the Gmail trash folder. /// /// Messages moved to trash can be recovered within 30 days through the Gmail @@ -320,7 +367,10 @@ pub trait RuleProcessor { /// # Gmail API Requirements /// /// Requires the `https://www.googleapis.com/auth/gmail.modify` scope. - fn batch_trash(&mut self) -> impl std::future::Future> + Send; + fn call_batch_trash( + &self, + ids: &[String], + ) -> impl std::future::Future> + Send; } impl RuleProcessor for GmailClient { @@ -419,12 +469,34 @@ impl RuleProcessor for GmailClient { return Ok(()); } - let ids = Some(message_ids); - let batch_request = BatchDeleteMessagesRequest { ids }; - self.log_messages("Message with subject `", "` permanently deleted") .await?; + let (chunks, remainder) = message_ids.as_chunks::<1000>(); + log::trace!( + "Message list chopped into {} chunks with {} ids in the remainder", + chunks.len(), + remainder.len() + ); + + if !chunks.is_empty() { + for (i, chunk) in chunks.iter().enumerate() { + log::trace!("Processing chunk {i}"); + self.call_batch_delete(chunk).await?; + } + } + + if !remainder.is_empty() { + log::trace!("Processing remainder."); + self.call_batch_delete(remainder).await?; + } + + Ok(()) + } + + async fn call_batch_delete(&self, ids: &[String]) -> Result<()> { + let ids = Some(Vec::from(ids)); + let batch_request = BatchDeleteMessagesRequest { ids }; log::trace!("{batch_request:#?}"); let res = self @@ -465,9 +537,35 @@ impl RuleProcessor for GmailClient { return Ok(()); } + self.log_messages("Message with subject `", "` moved to trash") + .await?; + + let (chunks, remainder) = message_ids.as_chunks::<1000>(); + log::trace!( + "Message list chopped into {} chunks with {} ids in the remainder", + chunks.len(), + remainder.len() + ); + + if !chunks.is_empty() { + for (i, chunk) in chunks.iter().enumerate() { + log::trace!("Processing chunk {i}"); + self.call_batch_delete(chunk).await?; + } + } + + if !remainder.is_empty() { + log::trace!("Processing remainder."); + self.call_batch_delete(remainder).await?; + } + + Ok(()) + } + + async fn call_batch_trash(&self, ids: &[String]) -> Result<()> { + let ids = Some(Vec::from(ids)); let add_label_ids = Some(vec![TRASH_LABEL.to_string()]); - let ids = Some(message_ids); - let remove_label_ids = Some(MessageList::label_ids(self)); + let remove_label_ids = Some(vec![INBOX_LABEL.to_string()]); let batch_request = BatchModifyMessagesRequest { add_label_ids, @@ -475,9 +573,6 @@ impl RuleProcessor for GmailClient { remove_label_ids, }; - self.log_messages("Message with subject `", "` moved to trash") - .await?; - log::trace!("{batch_request:#?}"); let _res = self @@ -780,9 +875,17 @@ mod tests { Ok(()) } + async fn call_batch_delete(&self, _ids: &[String]) -> Result<()> { + Ok(()) + } + async fn batch_trash(&mut self) -> Result<()> { Ok(()) } + + async fn call_batch_trash(&self, _ids: &[String]) -> Result<()> { + Ok(()) + } } let mut processor = MockProcessor {