feat: implement server-side IMAP LIST and SEARCH filtering in Rust

Add server-side folder filtering using IMAP LIST patterns and enhance
message filtering to use IMAP SEARCH with keyword filters when available.

Key improvements:
- Add list_filtered_mailboxes() method using IMAP LIST with patterns
- Use server-side filtering instead of client-side folder filtering
- Enhance message search to use IMAP SEARCH for subject/sender keywords
- Add has_keyword_filters() method to MessageFilter
- Reduce network traffic by leveraging IMAP server capabilities
- Remove dependency on client-side filter_folders function

This achieves full feature parity with the updated Go implementation
and ensures both versions use IMAP standards optimally.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-03 14:29:49 +02:00
commit ee236db3c1
3 changed files with 140 additions and 15 deletions

View file

@ -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;