Files
steve eb63d8cbc1 Rewrite from Python to Go for single-binary cross-platform builds
Replaces imapdown.py with a multi-file Go implementation using
github.com/emersion/go-imap/v2. All features preserved: SSL/STARTTLS,
incremental UID-based downloads, attachment extraction to zip,
modified UTF-7 folder name decoding, and full-mode safety checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:54:41 +00:00

244 lines
6.0 KiB
Go

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
}