write code frame

This commit is contained in:
2025-09-08 19:27:23 +07:00
parent 49930fc968
commit f0dc0f32fa
8 changed files with 739 additions and 1 deletions

220
imap.go Normal file
View 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
}