fix: implement server-side folder filtering using IMAP LIST patterns

Replace client-side wildcard filtering with IMAP LIST pattern matching
for improved efficiency and accuracy. This fixes the issue where patterns
like "Work*" were not matching folders like "Work/Projects".

Key improvements:
- Use IMAP LIST with patterns for server-side filtering
- Remove dependency on doublestar library
- Add ListFilteredMailboxes() method with proper IMAP pattern support
- Remove obsolete ShouldProcessMailbox() client-side filtering
- Significantly reduce network traffic by filtering at server

This ensures the Go implementation correctly processes folder patterns
and achieves feature parity with the Rust implementation.

🤖 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:26:03 +02:00
commit 84faf501f1
2 changed files with 83 additions and 52 deletions

View file

@ -6,7 +6,6 @@ import (
"io"
"log"
"mime"
"path/filepath"
"strings"
"time"
@ -83,6 +82,84 @@ func (c *ImapClient) ListMailboxes() ([]string, error) {
return mailboxes, nil
}
// ListFilteredMailboxes lists mailboxes matching the given folder filters using IMAP LIST
func (c *ImapClient) ListFilteredMailboxes(filter *config.FolderFilter) ([]string, error) {
var allMailboxes []string
// If no include patterns, get all mailboxes
if len(filter.Include) == 0 {
return c.ListMailboxes()
}
// Use IMAP LIST with each include pattern to let the server filter
seen := make(map[string]bool)
for _, pattern := range filter.Include {
cmd := c.List("", pattern, nil)
infos, err := cmd.Collect()
if err != nil {
log.Printf("Failed to list mailboxes with pattern '%s': %v", pattern, err)
continue
}
for _, info := range infos {
if !seen[info.Mailbox] {
allMailboxes = append(allMailboxes, info.Mailbox)
seen[info.Mailbox] = true
}
}
}
// Apply exclude filters client-side (IMAP LIST doesn't support exclusion)
if len(filter.Exclude) == 0 {
return allMailboxes, nil
}
var filteredMailboxes []string
for _, mailbox := range allMailboxes {
excluded := false
for _, excludePattern := range filter.Exclude {
if matched := c.matchesImapPattern(excludePattern, mailbox); matched {
excluded = true
break
}
}
if !excluded {
filteredMailboxes = append(filteredMailboxes, mailbox)
}
}
return filteredMailboxes, nil
}
// matchesImapPattern matches IMAP-style patterns (simple * wildcard matching)
func (c *ImapClient) matchesImapPattern(pattern, name string) bool {
// Handle exact match
if pattern == name {
return true
}
// Handle simple prefix wildcard: "Work*" should match "Work/Projects"
if strings.HasSuffix(pattern, "*") && !strings.Contains(pattern[:len(pattern)-1], "*") {
prefix := strings.TrimSuffix(pattern, "*")
return strings.HasPrefix(name, prefix)
}
// Handle simple suffix wildcard: "*Temp" should match "Work/Temp"
if strings.HasPrefix(pattern, "*") && !strings.Contains(pattern[1:], "*") {
suffix := strings.TrimPrefix(pattern, "*")
return strings.HasSuffix(name, suffix)
}
// Handle contains wildcard: "*Temp*" should match "Work/Temp/Archive"
if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") {
middle := strings.Trim(pattern, "*")
return strings.Contains(name, middle)
}
// For other patterns, fall back to basic string comparison
return false
}
// GetMessages retrieves messages from a specific mailbox with filtering support
// Returns messages and a map of all current UIDs in the mailbox
// maxMessages: 0 means no limit, > 0 limits the number of messages to fetch
@ -381,47 +458,6 @@ func (c *ImapClient) parseMessagePart(entity *message.Entity, msg *Message) erro
return nil
}
// ShouldProcessMailbox checks if a mailbox should be processed based on filters with wildcard support
func (c *ImapClient) ShouldProcessMailbox(mailbox string, filter *config.FolderFilter) bool {
// If include list is specified, mailbox must match at least one pattern
if len(filter.Include) > 0 {
found := false
for _, pattern := range filter.Include {
// Handle special case: "*" means include all folders
if pattern == "*" {
found = true
break
}
// Use filepath.Match for wildcard pattern matching
if matched, err := filepath.Match(pattern, mailbox); err == nil && matched {
found = true
break
}
// Also support exact string matching for backwards compatibility
if mailbox == pattern {
found = true
break
}
}
if !found {
return false
}
}
// If exclude list is specified, mailbox must not match any exclude pattern
for _, pattern := range filter.Exclude {
// Use filepath.Match for wildcard pattern matching
if matched, err := filepath.Match(pattern, mailbox); err == nil && matched {
return false
}
// Also support exact string matching for backwards compatibility
if mailbox == pattern {
return false
}
}
return true
}
// ShouldProcessMessage checks if a message should be processed based on keyword filters
func (c *ImapClient) ShouldProcessMessage(msg *Message, filter *config.MessageFilter) bool {