forked from othman.suseno/Mail-Migrator
write code frame
This commit is contained in:
220
imap.go
Normal file
220
imap.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// IMAPConfig holds connection details
|
||||
type IMAPConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
UseTLS bool
|
||||
}
|
||||
|
||||
// parseIMAPURL parses "user:pass@host:port" format
|
||||
func parseIMAPURL(rawURL string) (*IMAPConfig, error) {
|
||||
if !strings.Contains(rawURL, "://") {
|
||||
rawURL = "imap://" + rawURL
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port := 993 // default IMAPS
|
||||
if u.Port() != "" {
|
||||
port, err = strconv.Atoi(u.Port())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
password, _ := u.User.Password()
|
||||
|
||||
return &IMAPConfig{
|
||||
Host: u.Hostname(),
|
||||
Port: port,
|
||||
Username: u.User.Username(),
|
||||
Password: password,
|
||||
UseTLS: port == 993 || u.Scheme == "imaps",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// connectIMAP creates IMAP client connection
|
||||
func connectIMAP(cfg *IMAPConfig, insecure bool) (*client.Client, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
|
||||
var c *client.Client
|
||||
var err error
|
||||
|
||||
if cfg.UseTLS {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: cfg.Host,
|
||||
InsecureSkipVerify: insecure,
|
||||
}
|
||||
c, err = client.DialTLS(addr, tlsConfig)
|
||||
} else {
|
||||
c, err = client.Dial(addr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.Login(cfg.Username, cfg.Password); err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
logrus.Infof("Connected to %s as %s", addr, cfg.Username)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// listMailboxes returns all mailboxes
|
||||
func listMailboxes(c *client.Client) ([]*imap.MailboxInfo, error) {
|
||||
mailboxes := make(chan *imap.MailboxInfo, 10)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
done <- c.List("", "*", mailboxes)
|
||||
}()
|
||||
|
||||
var result []*imap.MailboxInfo
|
||||
for m := range mailboxes {
|
||||
result = append(result, m)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func copyMessages(srcClient, dstClient *client.Client, srcMbox, dstMbox string, stateKey string, db *bolt.DB) error {
|
||||
// Select source mailbox
|
||||
srcStatus, err := srcClient.Select(srcMbox, true) // read-only
|
||||
if err != nil {
|
||||
return fmt.Errorf("select source %s: %w", srcMbox, err)
|
||||
}
|
||||
|
||||
// Select destination mailbox
|
||||
_, err = dstClient.Select(dstMbox, false)
|
||||
if err != nil {
|
||||
// Try to create mailbox if it doesn't exist
|
||||
if err := dstClient.Create(dstMbox); err != nil {
|
||||
return fmt.Errorf("create dest %s: %w", dstMbox, err)
|
||||
}
|
||||
if _, err := dstClient.Select(dstMbox, false); err != nil {
|
||||
return fmt.Errorf("select dest %s: %w", dstMbox, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get last migrated UID for resume
|
||||
var lastUID uint32 = 0
|
||||
if db != nil {
|
||||
lastUID, _ = getLastUID(db, stateKey)
|
||||
}
|
||||
|
||||
logrus.Infof("Migrating %s: %d messages (resume from UID %d)", srcMbox, srcStatus.Messages, lastUID)
|
||||
|
||||
// Search for messages to copy
|
||||
criteria := &imap.SearchCriteria{}
|
||||
if lastUID > 0 {
|
||||
criteria.Uid = &imap.SeqSet{}
|
||||
criteria.Uid.AddRange(lastUID+1, 0) // from lastUID+1 to end
|
||||
} else {
|
||||
criteria.Uid = &imap.SeqSet{}
|
||||
criteria.Uid.AddRange(1, 0) // all messages
|
||||
}
|
||||
|
||||
uids, err := srcClient.UidSearch(criteria)
|
||||
if err != nil {
|
||||
return fmt.Errorf("search failed: %w", err)
|
||||
}
|
||||
|
||||
if len(uids) == 0 {
|
||||
logrus.Infof("No new messages to migrate in %s", srcMbox)
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.Infof("Found %d messages to migrate", len(uids))
|
||||
|
||||
// Copy messages in batches
|
||||
batchSize := 50
|
||||
for i := 0; i < len(uids); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(uids) {
|
||||
end = len(uids)
|
||||
}
|
||||
|
||||
batch := uids[i:end]
|
||||
seqSet := &imap.SeqSet{}
|
||||
for _, uid := range batch {
|
||||
seqSet.AddNum(uid)
|
||||
}
|
||||
|
||||
// Fetch messages
|
||||
messages := make(chan *imap.Message, batchSize)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
done <- srcClient.UidFetch(seqSet, []imap.FetchItem{imap.FetchRFC822}, messages)
|
||||
}()
|
||||
|
||||
// Process each message
|
||||
for msg := range messages {
|
||||
if len(msg.Body) == 0 {
|
||||
logrus.Warnf("Empty message body for UID %d", msg.Uid)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get message content
|
||||
var msgContent []byte
|
||||
for _, body := range msg.Body {
|
||||
buf := make([]byte, 1024*1024) // 1MB buffer
|
||||
n, err := body.Read(buf)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
logrus.Errorf("Read message UID %d: %v", msg.Uid, err)
|
||||
continue
|
||||
}
|
||||
msgContent = append(msgContent, buf[:n]...)
|
||||
}
|
||||
|
||||
// Append to destination
|
||||
if err := dstClient.Append(dstMbox, nil, time.Time{}, strings.NewReader(string(msgContent))); err != nil {
|
||||
logrus.Errorf("Append message UID %d: %v", msg.Uid, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Update state
|
||||
if db != nil {
|
||||
// TODO: implement setLastUID from state.go
|
||||
// setLastUID(db.(*bolt.DB), stateKey, msg.Uid)
|
||||
}
|
||||
|
||||
logrus.Debugf("Copied message UID %d", msg.Uid)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return fmt.Errorf("fetch batch failed: %w", err)
|
||||
}
|
||||
|
||||
logrus.Infof("Migrated batch %d-%d/%d", i+1, end, len(uids))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user