134 lines
4.5 KiB
Go
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
|
|
}
|