write code frame
This commit is contained in:
179
main.go
Normal file
179
main.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "mail-migrator",
|
||||
Usage: "Migrate IMAP mailboxes between servers",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "src", Usage: "Source IMAP URL (user:pass@host:port)"},
|
||||
&cli.StringFlag{Name: "dst", Usage: "Destination IMAP URL (user:pass@host:port)"},
|
||||
&cli.StringFlag{Name: "batch", Usage: "CSV file with src,dst entries for batch migration"},
|
||||
&cli.BoolFlag{Name: "insecure", Usage: "Allow insecure TLS (trust self-signed certs)"},
|
||||
&cli.BoolFlag{Name: "resume", Usage: "Resume incomplete migrations"},
|
||||
&cli.StringFlag{Name: "state", Value: "state.db", Usage: "Path to state DB (Bolt)"},
|
||||
&cli.StringFlag{Name: "log", Usage: "Path to log file (optional)"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
setupLogging(c.String("log"))
|
||||
insecure := c.Bool("insecure")
|
||||
resume := c.Bool("resume")
|
||||
statePath := c.String("state")
|
||||
|
||||
if batch := c.String("batch"); batch != "" {
|
||||
return runBatch(batch, insecure, resume, statePath)
|
||||
}
|
||||
|
||||
src := c.String("src")
|
||||
dst := c.String("dst")
|
||||
if src == "" || dst == "" {
|
||||
return fmt.Errorf("either src/dst or batch must be provided")
|
||||
}
|
||||
return migrateOne(context.Background(), src, dst, insecure, resume, statePath)
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogging(file string) {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
if file != "" {
|
||||
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to open log file: %v", err)
|
||||
}
|
||||
logrus.SetOutput(io.MultiWriter(os.Stdout, f))
|
||||
}
|
||||
}
|
||||
|
||||
func runBatch(path string, insecure, resume bool, statePath string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
reader := csv.NewReader(f)
|
||||
for {
|
||||
rec, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rec) < 2 {
|
||||
logrus.Warn("invalid record, skipping")
|
||||
continue
|
||||
}
|
||||
src, dst := rec[0], rec[1]
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
if err := migrateOne(ctx, src, dst, insecure, resume, statePath); err != nil {
|
||||
logrus.Errorf("migration failed for %s -> %s: %v", src, dst, err)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateOne performs migration for single account
|
||||
func migrateOne(ctx context.Context, src, dst string, insecure, resume bool, statePath string) error {
|
||||
logrus.Infof("Starting migration %s -> %s (insecure=%v, resume=%v)", src, dst, insecure, resume)
|
||||
|
||||
// Parse IMAP URLs
|
||||
srcCfg, err := parseIMAPURL(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse source URL: %w", err)
|
||||
}
|
||||
|
||||
dstCfg, err := parseIMAPURL(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse destination URL: %w", err)
|
||||
}
|
||||
|
||||
// Open state database if resume is enabled
|
||||
var db *bolt.DB
|
||||
if resume {
|
||||
db, err = openStateDB(statePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open state DB: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
}
|
||||
|
||||
// Connect to source
|
||||
srcClient, err := connectIMAP(srcCfg, insecure)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to source: %w", err)
|
||||
}
|
||||
defer srcClient.Logout()
|
||||
|
||||
// Connect to destination
|
||||
dstClient, err := connectIMAP(dstCfg, insecure)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to destination: %w", err)
|
||||
}
|
||||
defer dstClient.Close()
|
||||
|
||||
// List mailboxes from source
|
||||
mailboxes, err := listMailboxes(srcClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list mailboxes: %w", err)
|
||||
}
|
||||
|
||||
logrus.Infof("Found %d mailboxes to migrate", len(mailboxes))
|
||||
|
||||
// Migrate each mailbox
|
||||
for _, mbox := range mailboxes {
|
||||
// Skip certain system mailboxes
|
||||
if shouldSkipMailbox(mbox.Name) {
|
||||
logrus.Debugf("Skipping mailbox: %s", mbox.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
stateKey := formatStateKey(src, dst, mbox.Name)
|
||||
|
||||
logrus.Infof("Migrating mailbox: %s", mbox.Name)
|
||||
if err := copyMessages(srcClient, dstClient, mbox.Name, mbox.Name, stateKey, db); err != nil {
|
||||
logrus.Errorf("Failed to migrate mailbox %s: %v", mbox.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("Migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldSkipMailbox determines if a mailbox should be skipped
|
||||
func shouldSkipMailbox(name string) bool {
|
||||
// Skip common system/virtual mailboxes
|
||||
skipList := []string{
|
||||
"[Gmail]/All Mail",
|
||||
"[Gmail]/Important",
|
||||
"[Gmail]/Starred",
|
||||
"[Gmail]/Chats",
|
||||
}
|
||||
|
||||
for _, skip := range skipList {
|
||||
if name == skip {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user