Initial Email-Forwarder scaffold

This commit is contained in:
DonVoo 2026-07-05 17:58:25 +02:00
commit 91e52aecd0
22 changed files with 1007 additions and 0 deletions

112
backend/11-folders.go Normal file
View file

@ -0,0 +1,112 @@
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 {
// TODO Codex.
return ""
}