package backend import ( "fmt" "strings" "github.com/emersion/go-imap/v2" ) // TargetMailbox = das Ziel-Postfach (neuer Host). Der Umzug schreibt hier per // IMAP APPEND hinein -- NICHT per SMTP. APPEND erhaelt Ordner, Flags und // Original-Datum; SMTP wuerde alles als "neu/ungelesen" mit falschem Datum // zustellen und die Ordnerstruktur zerstoeren. type TargetMailbox interface { Folders() ([]TargetFolder, error) // vorhandene Ziel-Ordner (fuer Rollen-Zuordnung) Delim() string // Hierarchie-Trenner des Ziels EnsureFolder(name string) error // anlegen + subscriben, falls fehlt Append(folder string, m RawMessage) error // mit m.Flags und m.InternalDate Headers(folder string, limit, offset int) ([]MessageHeader, error) AllHeaders(folder string) ([]MessageHeader, error) FetchOne(folder string, uid uint32) (RawMessage, error) DeleteUIDs(folder string, uids []uint32) error Close() error } func OpenIMAPTarget(a Account) (TargetMailbox, error) { c, err := dialIMAPClient(a.DstHost, a.DstPort, a.DstSecurity, a.DstInsecure) if err != nil { return nil, err } if err := c.Login(a.DstUser, a.DstPass).Wait(); err != nil { _ = c.Close() return nil, err } return &imapTarget{mailbox: &imapClientMailbox{c: c}}, nil } type imapTarget struct { mailbox *imapClientMailbox delim string } func (t *imapTarget) Folders() ([]TargetFolder, error) { boxes, err := t.mailbox.listMailboxes() if err != nil { return nil, err } folders := make([]TargetFolder, 0, len(boxes)) for _, box := range boxes { if t.delim == "" && box.Delim != "" { t.delim = box.Delim } if hasIMAPAttr(box.Attrs, `\Noselect`) { continue } folders = append(folders, TargetFolder{Name: box.Name, Attrs: box.Attrs}) } if len(folders) == 0 { folders = append(folders, TargetFolder{Name: "INBOX"}) } return folders, nil } func (t *imapTarget) Delim() string { if t.delim != "" { return t.delim } return "/" } func (t *imapTarget) EnsureFolder(name string) error { name = strings.TrimSpace(name) if name == "" || strings.EqualFold(name, "INBOX") { return nil } if err := t.ensureParentFolders(name); err != nil { return err } if err := t.createFolder(name); err != nil { return err } _ = t.mailbox.c.Subscribe(name).Wait() return nil } func (t *imapTarget) ensureParentFolders(name string) error { delim := t.Delim() if delim == "" || !strings.Contains(name, delim) { return nil } parts := strings.Split(name, delim) for i := 1; i < len(parts); i++ { parent := strings.Join(parts[:i], delim) if strings.TrimSpace(parent) == "" || strings.EqualFold(parent, "INBOX") { continue } if err := t.createFolder(parent); err != nil { return err } } return nil } func (t *imapTarget) createFolder(name string) error { err := t.mailbox.c.Create(name, nil).Wait() if err == nil || isAlreadyExistsError(err) { return nil } return fmt.Errorf("imap create %q failed: %w", name, err) } func (t *imapTarget) Append(folder string, m RawMessage) error { opts := &imap.AppendOptions{Flags: stringsToFlags(m.Flags), Time: m.InternalDate} cmd := t.mailbox.c.Append(folder, int64(len(m.Body)), opts) if _, err := cmd.Write(m.Body); err != nil { _ = cmd.Close() return err } if err := cmd.Close(); err != nil { return err } _, err := cmd.Wait() return err } func (t *imapTarget) Headers(folder string, limit, offset int) ([]MessageHeader, error) { return t.mailbox.fetchHeaders(folder, limit, offset) } func (t *imapTarget) AllHeaders(folder string) ([]MessageHeader, error) { return t.mailbox.fetchAllHeadersByUID(folder) } func (t *imapTarget) FetchOne(folder string, uid uint32) (RawMessage, error) { return t.mailbox.fetchOne(folder, uid) } func (t *imapTarget) DeleteUIDs(folder string, uids []uint32) error { if len(uids) == 0 { return nil } if _, err := t.mailbox.c.Select(folder, nil).Wait(); err != nil { return err } uidSet := imap.UIDSetNum(uint32sToUIDs(uids)...) flags := &imap.StoreFlags{Op: imap.StoreFlagsAdd, Silent: true, Flags: []imap.Flag{imap.FlagDeleted}} if err := t.mailbox.c.Store(uidSet, flags, nil).Close(); err != nil { return err } _, err := t.mailbox.c.UIDExpunge(uidSet).Collect() return err } func (t *imapTarget) Close() error { return t.mailbox.Close() } func uint32sToUIDs(values []uint32) []imap.UID { out := make([]imap.UID, 0, len(values)) for _, value := range values { if value != 0 { out = append(out, imap.UID(value)) } } return out }