package main import ( "context" "fmt" "log" "time" "mail2couch/config" "mail2couch/couch" "mail2couch/mail" ) func main() { args := config.ParseCommandLine() cfg, err := config.LoadConfigWithDiscovery(args) if err != nil { log.Fatalf("Failed to load configuration: %v", err) } // Initialize CouchDB client couchClient, err := couch.NewClient(&cfg.CouchDb) if err != nil { log.Fatalf("Failed to create CouchDB client: %v", err) } fmt.Printf("Found %d mail source(s) to process.\n", len(cfg.MailSources)) for _, source := range cfg.MailSources { if !source.Enabled { continue } // Generate per-account database name dbName := couch.GenerateAccountDBName(source.Name, source.User) // Ensure the account-specific database exists ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) err = couchClient.EnsureDB(ctx, dbName) cancel() if err != nil { log.Printf("Could not ensure CouchDB database '%s' exists (is it running?): %v", dbName, err) continue } else { fmt.Printf("CouchDB database '%s' is ready for account: %s\n", dbName, source.Name) } fmt.Printf(" - Processing source: %s\n", source.Name) if source.Protocol == "imap" { err := processImapSource(&source, couchClient, dbName, args.MaxMessages) if err != nil { log.Printf(" ERROR: Failed to process IMAP source %s: %v", source.Name, err) } } } } func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int) error { fmt.Printf(" Connecting to IMAP server: %s:%d\n", source.Host, source.Port) imapClient, err := mail.NewImapClient(source) if err != nil { return fmt.Errorf("failed to connect to IMAP server: %w", err) } defer imapClient.Logout() fmt.Println(" IMAP connection successful.") // Use IMAP LIST with patterns for server-side filtering mailboxes, err := imapClient.ListFilteredMailboxes(&source.FolderFilter) if err != nil { return fmt.Errorf("failed to list filtered mailboxes: %w", err) } 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 if source.MessageFilter.Since != "" { parsed, err := time.Parse("2006-01-02", source.MessageFilter.Since) if err != nil { log.Printf(" WARNING: Invalid since date format '%s', ignoring filter", source.MessageFilter.Since) } else { configSinceDate = &parsed } } totalMessages := 0 totalStored := 0 // Process each mailbox (already filtered by IMAP LIST) for _, mailbox := range mailboxes { fmt.Printf(" Processing mailbox: %s (mode: %s)\n", mailbox, source.Mode) // Get sync metadata to determine incremental sync date syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second) syncMetadata, err := couchClient.GetSyncMetadata(syncCtx, dbName, mailbox) syncCancel() if err != nil { log.Printf(" ERROR: Failed to get sync metadata for %s: %v", mailbox, err) continue } // Determine the since date for incremental sync var sinceDate *time.Time if syncMetadata != nil { // Use last sync time for incremental sync sinceDate = &syncMetadata.LastSyncTime fmt.Printf(" Incremental sync since: %s (last synced %d messages)\n", sinceDate.Format("2006-01-02 15:04:05"), syncMetadata.MessageCount) } else { // First sync - use config since date if available sinceDate = configSinceDate if sinceDate != nil { fmt.Printf(" First sync since: %s (from config)\n", sinceDate.Format("2006-01-02")) } else { fmt.Printf(" First full sync (no date filter)\n") } } // Retrieve messages from the mailbox messages, currentUIDs, err := imapClient.GetMessages(mailbox, sinceDate, maxMessages, &source.MessageFilter) if err != nil { log.Printf(" ERROR: Failed to get messages from %s: %v", mailbox, err) continue } // Perform sync/archive logic mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second) err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode()) mailboxSyncCancel() if err != nil { log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err) continue } if len(messages) == 0 { fmt.Printf(" No new messages found in %s\n", mailbox) continue } fmt.Printf(" Found %d messages in %s\n", len(messages), mailbox) totalMessages += len(messages) // Convert messages to CouchDB documents var docs []*couch.MailDocument for _, msg := range messages { doc := couch.ConvertMessage(msg, mailbox) docs = append(docs, doc) } // Store messages in CouchDB with attachments ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) stored := 0 for i, doc := range docs { err := couchClient.StoreMessage(ctx, dbName, doc, messages[i]) if err != nil { log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err) } else { stored++ } } cancel() fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox) totalStored += stored // Update sync metadata after successful processing if len(messages) > 0 { // Find the highest UID processed var maxUID uint32 for _, msg := range messages { if msg.UID > maxUID { maxUID = msg.UID } } // Create/update sync metadata newMetadata := &couch.SyncMetadata{ Mailbox: mailbox, LastSyncTime: time.Now(), LastMessageUID: maxUID, MessageCount: stored, } // Store sync metadata metadataCtx, metadataCancel := context.WithTimeout(context.Background(), 10*time.Second) err = couchClient.StoreSyncMetadata(metadataCtx, dbName, newMetadata) metadataCancel() if err != nil { log.Printf(" WARNING: Failed to store sync metadata for %s: %v", mailbox, err) } else { fmt.Printf(" Updated sync metadata (last UID: %d)\n", maxUID) } } } fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored) return nil }