package main import ( "context" "errors" "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 (skip in dry-run mode) var couchClient *couch.Client if !args.DryRun { var err error 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 (skip in dry-run mode) if !args.DryRun { 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) } } else { fmt.Printf("DRY-RUN: Would ensure CouchDB database '%s' exists 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, args.DryRun) 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, dryRun bool) 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 (skip in dry-run mode) var syncMetadata *couch.SyncMetadata if !dryRun { syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second) var err error 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 { if dryRun { fmt.Printf(" DRY-RUN: Would perform first full sync (no date filter)\n") } 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 (skip in dry-run mode) if !dryRun { 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 } } else { fmt.Printf(" DRY-RUN: Would sync mailbox %s with %d current UIDs (mode: %s)\n", mailbox, len(currentUIDs), source.Mode) } 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 (skip in dry-run mode) stored := 0 if !dryRun { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) var skipped int for i, doc := range docs { err := couchClient.StoreMessage(ctx, dbName, doc, messages[i]) if err != nil { var skipErr *couch.DocumentSkippedError if errors.As(err, &skipErr) { skipped++ } else { log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err) } } else { stored++ } } cancel() if skipped > 0 { fmt.Printf(" Stored %d/%d messages from %s (%d skipped as duplicates)\n", stored, len(messages), mailbox, skipped) } else { fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox) } } else { stored = len(messages) // In dry-run, assume all would be stored fmt.Printf(" DRY-RUN: Would store %d messages from %s\n", len(messages), mailbox) // Show sample of what would be stored if len(docs) > 0 { fmt.Printf(" DRY-RUN: Sample message ID: %s (Subject: %s)\n", docs[0].ID, docs[0].Subject) } } totalStored += stored // Update sync metadata after successful processing (skip in dry-run mode) if len(messages) > 0 { // Find the highest UID processed var maxUID uint32 for _, msg := range messages { if msg.UID > maxUID { maxUID = msg.UID } } if !dryRun { // 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) } } else { fmt.Printf(" DRY-RUN: Would update sync metadata (last UID: %d, %d messages)\n", maxUID, stored) } } } if dryRun { fmt.Printf(" DRY-RUN Summary: Found %d messages, would store %d messages\n", totalMessages, totalStored) } else { fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored) } return nil }