// 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, "/", "_") }