diff --git a/rust/src/config.rs b/rust/src/config.rs index 7744849..209a120 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -77,6 +77,15 @@ pub struct MessageFilter { pub recipient_keywords: Vec, } +impl MessageFilter { + /// Check if this filter has any keyword-based filters that can use IMAP SEARCH + pub fn has_keyword_filters(&self) -> bool { + !self.subject_keywords.is_empty() || !self.sender_keywords.is_empty() + // Note: recipient_keywords not included as IMAP SEARCH doesn't have a TO field search + // that works reliably across all IMAP servers + } +} + fn default_mode() -> String { "archive".to_string() } diff --git a/rust/src/imap.rs b/rust/src/imap.rs index 80ebd00..6b3d553 100644 --- a/rust/src/imap.rs +++ b/rust/src/imap.rs @@ -236,6 +236,91 @@ impl ImapClient { Ok(mailbox_names) } + /// List mailboxes using IMAP LIST with server-side pattern filtering + pub async fn list_filtered_mailboxes(&mut self, filter: &crate::config::FolderFilter) -> Result> { + let session = self.session.as_mut() + .ok_or_else(|| anyhow!("Not connected to IMAP server"))?; + + let mut all_mailboxes = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + // If no include patterns, get all mailboxes + if filter.include.is_empty() { + return self.list_mailboxes().await; + } + + // Use IMAP LIST with each include pattern for server-side filtering + for pattern in &filter.include { + log::debug!("Listing mailboxes with pattern: {}", pattern); + + let mut mailboxes = session.list(Some(""), Some(pattern)).await + .map_err(|e| { + log::warn!("Failed to list mailboxes with pattern '{}': {:?}", pattern, e); + ImapError::Operation(format!("Failed to list mailboxes with pattern '{}': {:?}", pattern, e)) + })?; + + while let Some(mailbox_result) = mailboxes.next().await { + match mailbox_result { + Ok(mailbox) => { + let name = mailbox.name().to_string(); + if seen.insert(name.clone()) { + all_mailboxes.push(name); + } + } + Err(e) => { + log::warn!("Error processing mailbox with pattern '{}': {:?}", pattern, e); + continue; + } + } + } + } + + // Apply exclude filters client-side (IMAP LIST doesn't support exclusion) + if filter.exclude.is_empty() { + return Ok(all_mailboxes); + } + + let filtered_mailboxes: Vec = all_mailboxes + .into_iter() + .filter(|mailbox| { + !filter.exclude.iter().any(|exclude_pattern| { + self.matches_imap_pattern(exclude_pattern, mailbox) + }) + }) + .collect(); + + Ok(filtered_mailboxes) + } + + /// Match IMAP-style patterns (simple * wildcard matching for exclude filters) + fn matches_imap_pattern(&self, pattern: &str, name: &str) -> bool { + // Handle exact match + if pattern == name { + return true; + } + + // Handle simple prefix wildcard: "Work*" should match "Work/Projects" + if pattern.ends_with('*') && !pattern[..pattern.len()-1].contains('*') { + let prefix = &pattern[..pattern.len()-1]; + return name.starts_with(prefix); + } + + // Handle simple suffix wildcard: "*Temp" should match "Work/Temp" + if pattern.starts_with('*') && !pattern[1..].contains('*') { + let suffix = &pattern[1..]; + return name.ends_with(suffix); + } + + // Handle contains wildcard: "*Temp*" should match "Work/Temp/Archive" + if pattern.starts_with('*') && pattern.ends_with('*') { + let middle = &pattern[1..pattern.len()-1]; + return name.contains(middle); + } + + // For other patterns, fall back to basic string comparison + false + } + /// Select a mailbox pub async fn select_mailbox(&mut self, mailbox: &str) -> Result { let session = self.session.as_mut() diff --git a/rust/src/sync.rs b/rust/src/sync.rs index 0e89378..8fdec27 100644 --- a/rust/src/sync.rs +++ b/rust/src/sync.rs @@ -5,7 +5,7 @@ use crate::config::{Config, MailSource, CommandLineArgs}; use crate::couch::CouchClient; -use crate::filters::{filter_folders, get_filter_summary, validate_folder_patterns}; +use crate::filters::{get_filter_summary, validate_folder_patterns}; use crate::imap::{ImapClient, should_process_message}; use crate::schemas::{SyncMetadata, generate_database_name}; use anyhow::{anyhow, Result}; @@ -125,19 +125,27 @@ impl SyncCoordinator { // Connect to IMAP server let mut imap_client = ImapClient::connect(source.clone()).await?; - // Get list of available mailboxes - let all_mailboxes = imap_client.list_mailboxes().await?; - info!("Found {} total mailboxes", all_mailboxes.len()); + // Use IMAP LIST with patterns for server-side filtering + let filtered_mailboxes = imap_client.list_filtered_mailboxes(&source.folder_filter).await?; + info!("Found {} matching mailboxes after server-side filtering", filtered_mailboxes.len()); - // Apply folder filtering - let filtered_mailboxes = filter_folders(&all_mailboxes, &source.folder_filter); - let filter_summary = get_filter_summary(&all_mailboxes, &filtered_mailboxes, &source.folder_filter); - info!("{}", filter_summary); + // For validation and summary, we still need the full list + let all_mailboxes = if !source.folder_filter.include.is_empty() || !source.folder_filter.exclude.is_empty() { + // Only fetch all mailboxes if we have filters (for logging/validation) + imap_client.list_mailboxes().await.unwrap_or_else(|_| Vec::new()) + } else { + filtered_mailboxes.clone() + }; - // Validate folder patterns and show warnings - let warnings = validate_folder_patterns(&source.folder_filter, &all_mailboxes); - for warning in warnings { - warn!("{}", warning); + if !all_mailboxes.is_empty() { + let filter_summary = get_filter_summary(&all_mailboxes, &filtered_mailboxes, &source.folder_filter); + info!("{}", filter_summary); + + // Validate folder patterns and show warnings + let warnings = validate_folder_patterns(&source.folder_filter, &all_mailboxes); + for warning in warnings { + warn!("{}", warning); + } } // Sync each filtered mailbox @@ -222,9 +230,32 @@ impl SyncCoordinator { } }; - // Search for messages - let message_uids = imap_client.search_messages(since_date.as_ref()).await?; - info!(" Found {} messages to process", message_uids.len()); + // Search for messages using server-side IMAP SEARCH with keyword filtering when possible + let message_uids = if source.message_filter.has_keyword_filters() { + // Use advanced IMAP SEARCH with keyword filtering + let subject_keywords = if source.message_filter.subject_keywords.is_empty() { + None + } else { + Some(source.message_filter.subject_keywords.as_slice()) + }; + let from_keywords = if source.message_filter.sender_keywords.is_empty() { + None + } else { + Some(source.message_filter.sender_keywords.as_slice()) + }; + + info!(" Using IMAP SEARCH with keyword filters"); + imap_client.search_messages_advanced( + since_date.as_ref(), + None, // before_date + subject_keywords, + from_keywords, + ).await? + } else { + // Use simple date-based search + imap_client.search_messages(since_date.as_ref()).await? + }; + info!(" Found {} messages matching search criteria", message_uids.len()); // Handle sync mode - check for deleted messages let mut messages_deleted = 0;