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:
parent
84faf501f1
commit
ee236db3c1
3 changed files with 140 additions and 15 deletions
|
|
@ -77,6 +77,15 @@ pub struct MessageFilter {
|
|||
pub recipient_keywords: Vec<String>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Vec<String>> {
|
||||
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<String> = 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<MailboxInfo> {
|
||||
let session = self.session.as_mut()
|
||||
|
|
|
|||
|
|
@ -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,12 +125,19 @@ 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);
|
||||
// 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()
|
||||
};
|
||||
|
||||
if !all_mailboxes.is_empty() {
|
||||
let filter_summary = get_filter_summary(&all_mailboxes, &filtered_mailboxes, &source.folder_filter);
|
||||
info!("{}", filter_summary);
|
||||
|
||||
|
|
@ -139,6 +146,7 @@ impl SyncCoordinator {
|
|||
for warning in warnings {
|
||||
warn!("{}", warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync each filtered mailbox
|
||||
let mut mailbox_results = Vec::new();
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue