package main import ( "flag" "fmt" "os" "path/filepath" ) // Config holds all CLI arguments type Config struct { Server string Email string User string Password string UseSSL bool UseSTARTTLS bool Port int Limit *int Full bool Output string } func main() { config := parseArgs() baseDir := setupBaseDirectory(config) if config.Full { if err := checkFullModeSafety(baseDir); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } // Connect to IMAP server client, err := ConnectIMAP(config) if err != nil { fmt.Fprintf(os.Stderr, "Connection failed: %v\n", err) os.Exit(1) } defer client.Logout() // Login if err := client.Login(config.User, config.Password); err != nil { fmt.Fprintf(os.Stderr, "Authentication failed: %v\n", err) os.Exit(1) } // List folders folders, err := client.ListFolders() if err != nil { fmt.Fprintf(os.Stderr, "Could not list folders: %v\n", err) os.Exit(1) } fmt.Printf("Found %d folders\n", len(folders)) // Load state updateMode := !config.Full state, err := LoadState(baseDir, config.Full) if err != nil { fmt.Fprintf(os.Stderr, "Warning: could not load state: %v\n", err) state = make(State) } if config.Full { fmt.Println("Full download mode: downloading all emails") } else { fmt.Println("Incremental mode: only downloading new emails (use --full to download all)") } // Download folders stats := downloadAllFolders(client, folders, baseDir, config, updateMode, state) // Save state if err := SaveState(baseDir, state); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not save state: %v\n", err) } fmt.Printf("\nDownloaded %d emails to %s\n", stats.TotalDownloaded, baseDir) } // parseArgs parses and validates command line arguments func parseArgs() *Config { config := &Config{} flag.StringVar(&config.Server, "server", "", "IMAP server hostname (required)") flag.StringVar(&config.Email, "email", "", "Email address (required)") flag.StringVar(&config.User, "user", "", "Username for authentication (required)") flag.StringVar(&config.Password, "password", "", "Password for authentication (required)") flag.BoolVar(&config.UseSSL, "ssl", false, "Use implicit SSL/TLS (default port 993)") flag.BoolVar(&config.UseSTARTTLS, "starttls", false, "Use STARTTLS (default port 143)") flag.IntVar(&config.Port, "port", 0, "Custom port (default: 993 for SSL, 143 otherwise)") flag.BoolVar(&config.Full, "full", false, "Download all emails (default: only new emails since last run)") flag.StringVar(&config.Output, "output", "", "Directory to store downloaded emails (default: ./{email})") var limit int flag.IntVar(&limit, "limit", 0, "Limit number of emails to download (for debugging)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Download all emails from an IMAP server to EML files\n\n") fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() } flag.Parse() // Validate required arguments if config.Server == "" { fmt.Fprintf(os.Stderr, "Error: --server is required\n") flag.Usage() os.Exit(1) } if config.Email == "" { fmt.Fprintf(os.Stderr, "Error: --email is required\n") flag.Usage() os.Exit(1) } if config.User == "" { fmt.Fprintf(os.Stderr, "Error: --user is required\n") flag.Usage() os.Exit(1) } if config.Password == "" { fmt.Fprintf(os.Stderr, "Error: --password is required\n") flag.Usage() os.Exit(1) } // Check mutually exclusive flags if config.UseSSL && config.UseSTARTTLS { fmt.Fprintf(os.Stderr, "Error: --ssl and --starttls are mutually exclusive\n") os.Exit(1) } // Set default port if config.Port == 0 { if config.UseSSL { config.Port = 993 } else { config.Port = 143 } } // Set limit pointer if limit > 0 { config.Limit = &limit } return config } // setupBaseDirectory creates and returns the base directory for downloads func setupBaseDirectory(config *Config) string { var baseDir string if config.Output != "" { // Use specified output directory directly baseDir = config.Output } else { // Create email folder in current directory emailFolder := SanitizeFilename(config.Email, 100) cwd, _ := os.Getwd() baseDir = filepath.Join(cwd, emailFolder) } if err := os.MkdirAll(baseDir, 0755); err != nil { fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err) os.Exit(1) } return baseDir } // checkFullModeSafety verifies no existing .eml files in full mode func checkFullModeSafety(baseDir string) error { hasEmails := false err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && filepath.Ext(path) == ".eml" { hasEmails = true return filepath.SkipAll } return nil }) if err != nil { return err } if hasEmails { return fmt.Errorf("--full specified but %s already contains emails.\nDelete the folder first to do a full re-download, or run without --full for incremental update.", baseDir) } return nil } // DownloadStats tracks download statistics type DownloadStats struct { TotalDownloaded int FoldersProcessed int } // downloadAllFolders orchestrates the download of all folders func downloadAllFolders(client *IMAPClient, folders []string, baseDir string, config *Config, updateMode bool, state State) *DownloadStats { stats := &DownloadStats{} for _, folder := range folders { lastUID := state.GetLastUID(folder) downloaded, highestUID, err := client.DownloadFolder( folder, baseDir, config.Limit, stats.TotalDownloaded, updateMode, lastUID, ) if err != nil { fmt.Printf(" Error processing folder %s: %v\n", folder, err) continue } stats.TotalDownloaded += downloaded stats.FoldersProcessed++ if highestUID > 0 { state.UpdateFolder(folder, highestUID) } // Check limit if config.Limit != nil && stats.TotalDownloaded >= *config.Limit { fmt.Printf(" Reached limit of %d emails\n", *config.Limit) break } } return stats }