Mail-Graveyard/backend/11-folders.go
2026-07-05 23:03:52 +02:00

134 lines
4.5 KiB
Go

package backend
import "strings"
// Dieses Modul löst zwei Umzugs-Fallen:
//
// 1. Standardordner NICHT doppeln. Quelle "Gesendete Objekte" darf am Ziel
// nicht neben "Gesendete Elemente" als Zweitordner landen. Wir bilden jeden
// Ordner auf eine kanonische ROLLE ab und schreiben in den vorhandenen
// Rollen-Ordner des Ziels — statt einen neuen anzulegen.
// 2. Hierarchie-Trenner übersetzen. Quelle nutzt evtl. "." (INBOX.Sent.2023),
// Ziel "/" — Pfad muss umgehängt werden. Sonderzeichen (Umlaute) sind auf
// der Leitung modified-UTF-7; go-imap dekodiert/kodiert das — wir arbeiten
// hier IMMER mit dem dekodierten Klartext-Namen, nie doppelt kodieren.
type FolderRole string
const (
RoleNone FolderRole = ""
RoleInbox FolderRole = "inbox"
RoleSent FolderRole = "sent"
RoleDrafts FolderRole = "drafts"
RoleJunk FolderRole = "junk"
RoleTrash FolderRole = "trash"
RoleArchive FolderRole = "archive"
RoleOutbox FolderRole = "outbox"
)
// specialUseAttr = IMAP SPECIAL-USE (RFC 6154) -> Rolle. Erste Wahl, weil
// sprachunabhängig. Outbox/Postausgang hat kein Standard-Flag (Client-lokal).
var specialUseAttr = map[string]FolderRole{
"\\Sent": RoleSent,
"\\Drafts": RoleDrafts,
"\\Junk": RoleJunk,
"\\Trash": RoleTrash,
"\\Archive": RoleArchive,
}
// nameAliases = Fallback, wenn der Alt-Server kein SPECIAL-USE meldet. Vergleich
// case-insensitiv gegen das LETZTE Pfadsegment (ohne "INBOX."-Präfix).
// Mehrsprachig, weil Provider lokalisiert benennen.
var nameAliases = map[FolderRole][]string{
RoleInbox: {"inbox", "posteingang"},
RoleSent: {"sent", "sent items", "sent messages", "gesendet", "gesendete", "gesendete objekte", "gesendete elemente", "gesendete e-mails"},
RoleDrafts: {"drafts", "draft", "entwurf", "entwürfe", "entwuerfe"},
RoleJunk: {"junk", "junk e-mail", "junk-e-mail", "spam", "bulk mail", "werbung"},
RoleTrash: {"trash", "deleted", "deleted items", "deleted messages", "papierkorb", "gelöscht", "gelöschte", "gelöschte objekte", "gelöschte elemente", "müll"},
RoleArchive: {"archive", "archiv", "archived", "archiviert"},
RoleOutbox: {"outbox", "postausgang"},
}
// RoleFromAttrs bestimmt die Rolle aus den SPECIAL-USE-Attributen (bevorzugt).
func RoleFromAttrs(attrs []string) FolderRole {
for _, a := range attrs {
if role, ok := specialUseAttr[a]; ok {
return role
}
}
return RoleNone
}
// RoleFromName ist der Fallback über den (dekodierten) Ordnernamen. delim ist
// der Hierarchie-Trenner des jeweiligen Servers.
func RoleFromName(name, delim string) FolderRole {
leaf := name
if delim != "" {
parts := strings.Split(name, delim)
leaf = parts[len(parts)-1]
}
if strings.EqualFold(strings.TrimPrefix(name, "INBOX"+delim), "") || strings.EqualFold(name, "INBOX") {
return RoleInbox
}
lc := strings.ToLower(strings.TrimSpace(leaf))
for role, aliases := range nameAliases {
for _, alias := range aliases {
if lc == alias {
return role
}
}
}
return RoleNone
}
// Role kombiniert beide: erst SPECIAL-USE, dann Namensliste.
func Role(attrs []string, name, delim string) FolderRole {
if r := RoleFromAttrs(attrs); r != RoleNone {
return r
}
return RoleFromName(name, delim)
}
// TargetFolder beschreibt einen am Ziel existierenden Ordner (für die Rollen-
// Zuordnung beim Umzug).
type TargetFolder struct {
Name string
Attrs []string
}
// MapSourceToTarget entscheidet, in welchen ZIEL-Ordner eine Quell-Mailbox
// wandert:
// - manuelles folder_map hat Vorrang (übergibt override != "");
// - hat die Quelle eine Rolle und existiert am Ziel ein Ordner gleicher Rolle
// -> dessen Name (kein Neuanlegen, keine Dublette);
// - sonst Pfad vom Quell- auf den Ziel-Trenner umhängen und (ggf. neu) anlegen.
//
// TODO Codex: Implementieren; srcDelim/dstDelim aus dem jeweiligen Server (LIST-
// Antwort). override kommt aus folder_map.
func MapSourceToTarget(srcName string, srcAttrs []string, srcDelim, dstDelim string,
targets []TargetFolder, override string) string {
if strings.TrimSpace(override) != "" {
return override
}
srcRole := Role(srcAttrs, srcName, srcDelim)
if srcRole == RoleInbox {
return "INBOX"
}
if srcRole != RoleNone {
for _, target := range targets {
if Role(target.Attrs, target.Name, dstDelim) == srcRole {
return target.Name
}
}
}
if srcDelim == "" {
srcDelim = "/"
}
if dstDelim == "" {
dstDelim = "/"
}
if srcDelim != dstDelim {
return strings.ReplaceAll(srcName, srcDelim, dstDelim)
}
return srcName
}