fields.go: neuer Typ FacetField mit vier geschlossenen Konstanten (FacetFieldSender/Mailbox/AttachmentType/Tag). IsValid() entscheidet über ein erschöpfendes switch/case statt eine []string-Liste zu durchsuchen — genau der aus known-issues-archivmail.md #12 und known-issues-archivdms.md #10 bekannte Fehler (dynamische Tabellen-/ Feldnamen nur durch eine fragile Whitelist-Funktion abgesichert) wird damit strukturell vermieden: ein vergessener Listeneintrag kann nichts mehr durchlassen, weil es keine durchsuchte Liste mehr gibt. ParseFacetField ist die einzige vorgesehene Konstruktionsstelle für FacetField aus einer externen Zeichenkette. facets.go: FacetFilter.Field ist jetzt FacetField statt string, buildFilteredMust prüft f.Field.IsValid() statt Listenmitgliedschaft (isFacetField entfernt, es gibt keine Liste mehr, die die Entscheidung trifft). Alle Pflichtprüfungen mit echten Nachweisen: unbekannte/erfundene Facettenfelder werden abgelehnt, alle vier realen Facettenfelder funktionieren weiterhin, ein FacetField-Wert per direkter Typkonvertierung (nicht über ParseFacetField) wird trotzdem zuverlässig abgelehnt (Akzeptanzkriterium 2: Whitelist ist nicht die einzige Absicherung), automatisiertes Code-Review bestätigt kein fmt.Sprintf in facets.go/fields.go. Entscheidung dokumentiert: Mail-eigene Implementierung, keine geteilte Utility mit dem DMS-Board (Prüfprotokoll). Keine Regression, insbesondere mail/internal/savedsearch (Konsument von FacetFilter) unverändert grün — go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
98 lines
4.2 KiB
Go
98 lines
4.2 KiB
Go
// Package search implementiert SRC-01: den Manticore-RT-Suchindex für
|
|
// Mail-Inhalte. Bekannter Fehler vermeiden (siehe known-issues-archivmail.md
|
|
// #11/#12): archivmail baute WHERE-Klauseln und teils Spalten-/Tabellennamen
|
|
// dynamisch über fmt.Sprintf/strings.Join zusammen. Für dieses Paket gilt
|
|
// verbindlich: Spalten- und Tabellennamen kommen AUSSCHLIESSLICH aus den
|
|
// Konstanten dieser Datei, nirgendwo sonst im Paket wird ein Feld- oder
|
|
// Tabellenname zur Laufzeit zusammengesetzt. Suchanfragen laufen über die
|
|
// strukturierte Manticore-HTTP-JSON-API (Query/Insert-Sub, kein
|
|
// String-Zusammenbau von SQL), nicht über die SQL-Schnittstelle.
|
|
package search
|
|
|
|
import "hash/fnv"
|
|
|
|
// IndexName ist der einzige Ort, an dem der Manticore-Indexname als
|
|
// Literal steht.
|
|
const IndexName = "mail_documents"
|
|
|
|
// Statische Feld-Whitelist des mail_documents-Index (muss deckungsgleich
|
|
// mit migrations/0001_mail_documents.sql bleiben).
|
|
const (
|
|
FieldTenantSlug = "tenant_slug"
|
|
FieldMessageID = "message_id"
|
|
FieldSubject = "subject"
|
|
FieldBody = "body"
|
|
FieldAttachmentText = "attachment_text"
|
|
FieldSentAt = "sent_at"
|
|
// Facettenfelder (SRC-05), nachgezogen über migrations/0002..0005.
|
|
FieldSender = "sender"
|
|
FieldMailbox = "mailbox"
|
|
FieldAttachmentType = "attachment_type"
|
|
FieldTag = "tag"
|
|
// OCR-Sprach-/Qualitätsfelder (SRC-10), nachgezogen über
|
|
// migrations/0006..0007.
|
|
FieldOCRLanguage = "ocr_language"
|
|
FieldOCRConfidence = "ocr_confidence"
|
|
)
|
|
|
|
// FacetField ist ein geschlossener Typ für die vier zulässigen
|
|
// Facetten-/Filterdimensionen (SRC-11, Akzeptanzkriterium 2): die
|
|
// Zulässigkeitsprüfung in facets.go läuft über ein erschöpfendes
|
|
// switch/case auf diesem Typ, NICHT über das Durchsuchen einer Liste —
|
|
// selbst ein vergessener Eintrag in einer Whitelist-Liste könnte dort
|
|
// nichts mehr durchlassen, weil keine solche Liste mehr die Entscheidung
|
|
// trifft. FacetFields (unten) ist nur noch eine abgeleitete
|
|
// Aufzählungshilfe für Iteration, keine Prüfgrundlage.
|
|
type FacetField string
|
|
|
|
const (
|
|
FacetFieldSender FacetField = FacetField(FieldSender)
|
|
FacetFieldMailbox FacetField = FacetField(FieldMailbox)
|
|
FacetFieldAttachmentType FacetField = FacetField(FieldAttachmentType)
|
|
FacetFieldTag FacetField = FacetField(FieldTag)
|
|
)
|
|
|
|
// IsValid entscheidet über Zulässigkeit als Facetten-/Filterfeld über
|
|
// ein geschlossenes switch/case (Akzeptanzkriterium 2) statt eine Liste
|
|
// zu durchsuchen.
|
|
func (f FacetField) IsValid() bool {
|
|
switch f {
|
|
case FacetFieldSender, FacetFieldMailbox, FacetFieldAttachmentType, FacetFieldTag:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ParseFacetField wandelt eine externe Zeichenkette (z. B. aus einem
|
|
// HTTP-Query-Parameter) in ein FacetField um — liefert false bei jedem
|
|
// Wert, der nicht exakt einer der geschlossenen Konstanten entspricht.
|
|
// Einzige vorgesehene Stelle, an der ein Client-Feldname überhaupt zu
|
|
// einem FacetField werden kann.
|
|
func ParseFacetField(raw string) (FacetField, bool) {
|
|
f := FacetField(raw)
|
|
return f, f.IsValid()
|
|
}
|
|
|
|
// FacetFields sind die je Kachel unterstützten Filterdimensionen
|
|
// (Akzeptanzkriterium 1: Absender, Postfach, Anhangstyp, Tag — Zeitraum
|
|
// läuft separat über FieldSentAt als Bereichsfacette, siehe facets.go).
|
|
// Nur zur Iteration gedacht (z. B. "berechne alle Facetten") — die
|
|
// Zulässigkeitsprüfung selbst läuft über FacetField.IsValid(), nicht
|
|
// über Mitgliedschaft in dieser Liste.
|
|
var FacetFields = []FacetField{FacetFieldSender, FacetFieldMailbox, FacetFieldAttachmentType, FacetFieldTag}
|
|
|
|
// DocumentID berechnet deterministisch die Manticore-Dokument-ID aus
|
|
// Mandant und Message-ID (FNV-1a, 64 Bit). Deterministisch statt einer
|
|
// separat vergebenen ID, damit Re-Indexierung (Index) und Löschung
|
|
// (Delete) für dieselbe Mail immer dieselbe Dokument-ID referenzieren,
|
|
// ohne dass der Aufrufer sie zwischenspeichern muss (SRC-02: Löschungen
|
|
// müssen ohne zusätzlichen Zustand nachgezogen werden können).
|
|
func DocumentID(tenantSlug, messageID string) uint64 {
|
|
h := fnv.New64a()
|
|
_, _ = h.Write([]byte(tenantSlug))
|
|
_, _ = h.Write([]byte{0})
|
|
_, _ = h.Write([]byte(messageID))
|
|
return h.Sum64()
|
|
}
|