Regelwerk für automatische Zuordnung, Verschlagwortung und Klassifizierung importierter E-Mails nach Absender, Betreff, Postfach und Anhangstyp. - store.go: Postgres-Store für Regeln (Absender-/Betreff-/Postfach-/ Anhangstyp-Muster als reguläre Ausdrücke, Category einwertig, Tag mehrwertig, Priority — niedrigere Zahl = höhere Priorität). - engine.go: Engine.Evaluate wertet Regeln in Prioritätsreihenfolge aus, "first match wins" für Category, alle zutreffenden Regeln tragen zu Tags bei. Muster werden beim Erzeugen der Engine einmal kompiliert. - Bewusst keine Funktion zum rückwirkenden Neuklassifizieren bestehender Nachrichten — nur explizite RunOnce-artige Neuauswertung wirkt. Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-03-PRUEFPROTOKOLL.md): 1. TestEvaluate_ConflictingRulesRespectDocumentedPriority: höherpriorisierte Regel gewinnt real bei widersprüchlichen Kategorien. 2. TestNewEngine_NewRuleDoesNotAffectAlreadyCapturedResult: bereits erfasstes Ergebnis bleibt real unverändert nach neuer Regel. 3. TestEvaluate_TwentyPlusRulesStayPerformant: 31 Regeln, 2,64µs/Auswertung. Kein Umbau: kein bestehendes Paket angefasst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
123 lines
3.7 KiB
Go
123 lines
3.7 KiB
Go
package mailrules
|
|
|
|
import "regexp"
|
|
|
|
// EmailMetadata sind die für die Regelauswertung relevanten Merkmale
|
|
// einer Nachricht — dieses Paket kennt keine Nachrichteninhalte, nur die
|
|
// vom Aufrufer übergebenen Metadaten.
|
|
type EmailMetadata struct {
|
|
Sender string
|
|
Subject string
|
|
Mailbox string
|
|
AttachmentType string
|
|
}
|
|
|
|
// Result ist das Auswertungsergebnis für eine Nachricht.
|
|
type Result struct {
|
|
// Category kommt von der höchstpriorisierten zutreffenden Regel, die
|
|
// ein nicht-leeres Category-Feld setzt — leer, wenn keine passende
|
|
// Regel eine Kategorie zuweist.
|
|
Category string
|
|
// Tags sind alle (deduplizierten) Tags aller zutreffenden Regeln, in
|
|
// Prioritätsreihenfolge.
|
|
Tags []string
|
|
// MatchedRuleIDs sind die IDs aller zutreffenden Regeln, in
|
|
// Auswertungsreihenfolge — Nachvollziehbarkeit für Tests/Support.
|
|
MatchedRuleIDs []int64
|
|
}
|
|
|
|
// compiledRule cacht die kompilierten regulären Ausdrücke einer Regel —
|
|
// wichtig für Pflichtprüfung 3 (20+ Regeln performant auswertbar): ohne
|
|
// Cache würde JEDE Auswertung JEDE Regel neu kompilieren.
|
|
type compiledRule struct {
|
|
rule Rule
|
|
sender, subject *regexp.Regexp
|
|
mailbox, attachType *regexp.Regexp
|
|
}
|
|
|
|
// Engine wertet ein zwischengespeichertes, kompiliertes Regelset aus.
|
|
// Neu erzeugen (NewEngine), sobald sich Regeln geändert haben — dieses
|
|
// Paket hält dafür keinen automatischen Änderungs-Feed vor (kleinste
|
|
// Lösung, kein Beobachter-Mechanismus).
|
|
type Engine struct {
|
|
rules []compiledRule
|
|
}
|
|
|
|
// NewEngine kompiliert rules EINMAL (Reihenfolge = Auswertungsreihenfolge,
|
|
// siehe Store.List). Ein leeres/nil-Pattern kompiliert zu nil und matcht
|
|
// dadurch bewusst IMMER.
|
|
func NewEngine(rules []Rule) (*Engine, error) {
|
|
compiled := make([]compiledRule, 0, len(rules))
|
|
for _, r := range rules {
|
|
cr := compiledRule{rule: r}
|
|
var err error
|
|
if cr.sender, err = compileOrNil(r.SenderPattern); err != nil {
|
|
return nil, err
|
|
}
|
|
if cr.subject, err = compileOrNil(r.SubjectPattern); err != nil {
|
|
return nil, err
|
|
}
|
|
if cr.mailbox, err = compileOrNil(r.MailboxPattern); err != nil {
|
|
return nil, err
|
|
}
|
|
if cr.attachType, err = compileOrNil(r.AttachmentTypePattern); err != nil {
|
|
return nil, err
|
|
}
|
|
compiled = append(compiled, cr)
|
|
}
|
|
return &Engine{rules: compiled}, nil
|
|
}
|
|
|
|
func compileOrNil(pattern string) (*regexp.Regexp, error) {
|
|
if pattern == "" {
|
|
return nil, nil
|
|
}
|
|
return regexp.Compile(pattern)
|
|
}
|
|
|
|
// Evaluate wendet alle Regeln in Prioritätsreihenfolge auf msg an
|
|
// (Akzeptanzkriterium 2: dokumentierte Priorität, siehe Rule.Priority).
|
|
func (e *Engine) Evaluate(msg EmailMetadata) Result {
|
|
var result Result
|
|
seenTags := make(map[string]bool)
|
|
|
|
for _, cr := range e.rules {
|
|
if !matches(cr.sender, msg.Sender) {
|
|
continue
|
|
}
|
|
if !matches(cr.subject, msg.Subject) {
|
|
continue
|
|
}
|
|
if !matches(cr.mailbox, msg.Mailbox) {
|
|
continue
|
|
}
|
|
if !matches(cr.attachType, msg.AttachmentType) {
|
|
continue
|
|
}
|
|
|
|
result.MatchedRuleIDs = append(result.MatchedRuleIDs, cr.rule.ID)
|
|
|
|
// "first match wins" für die einwertige Kategorie — nur die
|
|
// ERSTE (höchstpriorisierte) zutreffende Regel mit gesetzter
|
|
// Category darf sie zuweisen.
|
|
if result.Category == "" && cr.rule.Category != "" {
|
|
result.Category = cr.rule.Category
|
|
}
|
|
if cr.rule.Tag != "" && !seenTags[cr.rule.Tag] {
|
|
seenTags[cr.rule.Tag] = true
|
|
result.Tags = append(result.Tags, cr.rule.Tag)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// matches liefert true, wenn pattern nil ist (Dimension irrelevant für
|
|
// diese Regel — "immer passend") oder der reguläre Ausdruck value
|
|
// matcht.
|
|
func matches(pattern *regexp.Regexp, value string) bool {
|
|
if pattern == nil {
|
|
return true
|
|
}
|
|
return pattern.MatchString(value)
|
|
}
|