feat(mail): INT-06 E-Mail-Regel-Engine über API steuerbar

mailrules.Store (IMP-03) bekommt Update (bislang nur Create/List/
Delete) — gleiches Muster wie Create: Musterprüfung vor dem Schreiben,
streng auf tenant_slug+id beschränkt, ErrNotFound bei fremder/nicht
existierender ID.

Neues Paket mail/internal/mailrulesapi: vier Endpunkte (GET/POST
/api/v1/mail/rules, PUT/DELETE /api/v1/mail/rules/{id}), tenant-Query-
Parameter Pflicht, gleiche Konvention wie mailapi (INT-01).
Akzeptanzkriterium 3 ist strukturell garantiert: mailrulesapi ruft
ausschließlich mailrules.Store auf, denselben Store, den IMP-03s
Import-Pfad ohnehin verwendet — kein zweiter, paralleler Schreibpfad.

Alle drei Pflichtprüfungen mit echten Nachweisen: vollständiger
Anlegen/Priorisieren/Einsehen/Löschen-Zyklus über echte HTTP-Requests;
eine über die API angelegte Regel wird über genau den Weg gelesen und
ausgewertet, den IMP-03s Import-Pfad geht (Store.List ->
mailrules.NewEngine -> Evaluate) und liefert das korrekte
Klassifizierungsergebnis; Mandant Bs Update-Versuch mit der echten,
bekannten ID von Mandant As Regel liefert 404, Mandant As Regel bleibt
unverändert.

go build/go vet/golangci-lint clean, gesamtes Mail-Modul
regressionsfrei getestet — bestehende mailrules-Tests (IMP-03/IMP-09)
bleiben nach der Update-Erweiterung unverändert grün.
This commit is contained in:
sysops
2026-09-01 19:55:30 +02:00
parent 8ee0e6c771
commit b3c8d36b58
5 changed files with 637 additions and 0 deletions
+42
View File
@@ -15,6 +15,7 @@ package mailrules
import (
"context"
_ "embed"
"errors"
"fmt"
"regexp"
"sort"
@@ -25,6 +26,10 @@ import (
//go:embed migrations/0001_mail_rules.sql
var schemaMigration string
// ErrNotFound wird geliefert, wenn keine Regel mit den angegebenen
// Bezugsdaten (tenantSlug+id) existiert.
var ErrNotFound = errors.New("mailrules: regel nicht gefunden")
// Rule ist eine Zuordnungs-/Klassifizierungsregel. *Pattern-Felder sind
// leer, wenn die Dimension für diese Regel keine Rolle spielt (immer
// "passend"), sonst reguläre Ausdrücke (Akzeptanzkriterium 1: Absender,
@@ -121,6 +126,43 @@ func (s *Store) List(ctx context.Context, tenantSlug string) ([]Rule, error) {
return rules, nil
}
// Update ersetzt eine bestehende Regel vollständig (INT-06
// Akzeptanzkriterium 1: Regeln lassen sich über die API ändern,
// Akzeptanzkriterium 2: Priorität ist darüber änderbar — Priority ist
// ein normales Feld von rule wie jedes andere). Wie Create werden
// Muster vor dem Schreiben validiert. Streng auf tenantSlug+id
// beschränkt — ErrNotFound, wenn keine Zeile dieses Mandanten diese ID
// hat (auch wenn die ID bei einem ANDEREN Mandanten existiert).
func (s *Store) Update(ctx context.Context, tenantSlug string, id int64, rule Rule) error {
if _, err := regexp.Compile(rule.SenderPattern); rule.SenderPattern != "" && err != nil {
return fmt.Errorf("mailrules: sender_pattern ungültig: %w", err)
}
if _, err := regexp.Compile(rule.SubjectPattern); rule.SubjectPattern != "" && err != nil {
return fmt.Errorf("mailrules: subject_pattern ungültig: %w", err)
}
if _, err := regexp.Compile(rule.MailboxPattern); rule.MailboxPattern != "" && err != nil {
return fmt.Errorf("mailrules: mailbox_pattern ungültig: %w", err)
}
if _, err := regexp.Compile(rule.AttachmentTypePattern); rule.AttachmentTypePattern != "" && err != nil {
return fmt.Errorf("mailrules: attachment_type_pattern ungültig: %w", err)
}
tag, err := s.pool.Exec(ctx, `
UPDATE mail_rules
SET name = $3, sender_pattern = $4, subject_pattern = $5, mailbox_pattern = $6,
attachment_type_pattern = $7, category = $8, tag = $9, priority = $10
WHERE tenant_slug = $1 AND id = $2
`, tenantSlug, id, rule.Name, rule.SenderPattern, rule.SubjectPattern, rule.MailboxPattern,
rule.AttachmentTypePattern, rule.Category, rule.Tag, rule.Priority)
if err != nil {
return fmt.Errorf("mailrules: regel aktualisieren: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
// Delete entfernt eine Regel.
func (s *Store) Delete(ctx context.Context, tenantSlug string, id int64) error {
if _, err := s.pool.Exec(ctx, `DELETE FROM mail_rules WHERE tenant_slug = $1 AND id = $2`, tenantSlug, id); err != nil {