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) }