eb63d8cbc1
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>
244 lines
6.0 KiB
Go
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
|
|
}
|