feat(mail): ARC-04 Aufbewahrungsstruktur (Mandant/Postfach/Jahr)
Neue Datei storage/archivekey.go: ArchiveKey(mailbox, sentAt, messageID, partIndex) liefert den Objektschlüssel nach dem Schema Postfach/Jahr innerhalb des bereits mandantenspezifischen Buckets (ARC-06) — additiv neben dem bestehenden, flachen ObjectKey (ARC-01), das für bestehende Aufrufer (mailapi/INT-01) unverändert bleibt. ArchiveYearPrefix(mailbox, year) ist der eigenständig berechenbare Präfix eines Postfach-Jahrs — Grundlage für spätere Retention-Regeln ohne Migration. Fehlendes Postfach bzw. Null-Sendedatum führen zu dokumentierten Fallback-Segmenten statt einem Ablagefehler; ArchiveKey liefert bewusst keinen error, da es strukturell keinen Fehlerfall gibt. Alle drei Pflichtprüfungen: mehrjähriger Import erzeugt nachweislich getrennte Jahresordner, fehlendes Postfach/Sendedatum nutzt den dokumentierten Fallback (inkl. Test gegen mehrere ungewöhnliche Eingaben), sowie ergänzend ein Nachweis für Akzeptanzkriterium 2 (Retention-Präfix trifft exakt die zuvor abgelegten Schlüssel desselben Postfach-Jahrs). Pflichtprüfung 3 (Stichprobenreview durch zweite Person) bleibt strukturell offen, im Prüfprotokoll dokumentiert (analog zu ING-10/QA-04/QA-02). go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// ARC-04: Aufbewahrungsstruktur nach Mandant/Postfach/Jahr. Mandant ist
|
||||
// bereits durch den physisch getrennten Bucket (ARC-06) abgebildet —
|
||||
// ArchiveKey deckt Postfach und Jahr INNERHALB des Buckets ab, als
|
||||
// eigener, additiver Schlüssel-Konstruktor neben dem bereits
|
||||
// bestehenden, flachen ObjectKey (ARC-01, weiterhin unverändert für
|
||||
// bestehende Aufrufer wie mail/internal/mailapi — kein Umbau
|
||||
// angrenzender Bereiche).
|
||||
package storage
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Fallback-Segmente (Akzeptanzkriterium 3): fehlende/abweichende
|
||||
// Metadaten führen zu einem DOKUMENTIERTEN Fallback-Pfad statt einem
|
||||
// Ablagefehler.
|
||||
const (
|
||||
FallbackMailboxSegment = "postfach-unbekannt"
|
||||
FallbackYearSegment = "jahr-unbekannt"
|
||||
)
|
||||
|
||||
// ArchiveKey liefert den kanonischen Objektschlüssel für einen
|
||||
// archivierten Mail-Anhang/-Teil nach dem Schema Postfach/Jahr
|
||||
// (Akzeptanzkriterium 1) — innerhalb des bereits mandantenspezifischen
|
||||
// Buckets. sentAt darf der Nullwert sein und mailbox leer
|
||||
// (Akzeptanzkriterium 3): beides führt zum jeweiligen Fallback-Segment,
|
||||
// nie zu einem Fehler.
|
||||
func ArchiveKey(mailbox string, sentAt time.Time, messageID string, partIndex int) string {
|
||||
return ArchiveYearPrefix(mailbox, yearOf(sentAt)) + ObjectKey(messageID, partIndex)
|
||||
}
|
||||
|
||||
// ArchiveYearPrefix liefert den Verzeichnispräfix EINES Postfach-Jahrs
|
||||
// (Akzeptanzkriterium 2: Grundlage für spätere Retention-Regeln OHNE
|
||||
// Migration — ein Retention-Job kann alle Objekte eines Postfachs/
|
||||
// Jahrs direkt über diesen Präfix auflisten, ohne die bereits
|
||||
// abgelegten Schlüssel umzubenennen oder neu zu strukturieren).
|
||||
func ArchiveYearPrefix(mailbox string, year int) string {
|
||||
mailboxSegment := sanitizeSegment(mailbox)
|
||||
if mailboxSegment == "" {
|
||||
mailboxSegment = FallbackMailboxSegment
|
||||
}
|
||||
yearSegment := FallbackYearSegment
|
||||
if year > 0 {
|
||||
yearSegment = strconv.Itoa(year)
|
||||
}
|
||||
return "postfach/" + mailboxSegment + "/" + yearSegment + "/"
|
||||
}
|
||||
|
||||
func yearOf(t time.Time) int {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return t.UTC().Year()
|
||||
}
|
||||
|
||||
// sanitizeSegment entfernt Pfadtrenner aus einem Postfachnamen, damit
|
||||
// er nie versehentlich zusätzliche Verzeichnisebenen erzeugt (z. B. ein
|
||||
// Postfachname, der ein "/" enthält).
|
||||
func sanitizeSegment(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
return strings.ReplaceAll(raw, "/", "_")
|
||||
}
|
||||
Reference in New Issue
Block a user