diff --git a/go/mail/imap.go b/go/mail/imap.go index 6ba4453..712a0eb 100644 --- a/go/mail/imap.go +++ b/go/mail/imap.go @@ -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 { diff --git a/go/main.go b/go/main.go index 155b195..399d67c 100644 --- a/go/main.go +++ b/go/main.go @@ -66,12 +66,13 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN fmt.Println(" IMAP connection successful.") - mailboxes, err := imapClient.ListMailboxes() + // Use IMAP LIST with patterns for server-side filtering + mailboxes, err := imapClient.ListFilteredMailboxes(&source.FolderFilter) if err != nil { - return fmt.Errorf("failed to list mailboxes: %w", err) + return fmt.Errorf("failed to list filtered mailboxes: %w", err) } - fmt.Printf(" Found %d mailboxes.\n", len(mailboxes)) + fmt.Printf(" Found %d matching mailboxes after filtering.\n", len(mailboxes)) // Parse the since date from config if provided (fallback for first sync) var configSinceDate *time.Time @@ -87,14 +88,8 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN totalMessages := 0 totalStored := 0 - // Process each mailbox + // Process each mailbox (already filtered by IMAP LIST) for _, mailbox := range mailboxes { - // Check if this mailbox should be processed based on filters - if !imapClient.ShouldProcessMailbox(mailbox, &source.FolderFilter) { - fmt.Printf(" Skipping mailbox: %s (filtered)\n", mailbox) - continue - } - fmt.Printf(" Processing mailbox: %s (mode: %s)\n", mailbox, source.Mode) // Get sync metadata to determine incremental sync date