Phase 6: rules editor + unsubscribe detection (AUD-72)

- FR-11 rules editor: plain-text area at /rules, saved and versioned
  (SaveRules appends a version; restore re-saves an old version), parse
  errors surfaced, one-click re-classify. The reply model now reads the
  rules document too (FR-11).
- FR-7 unsubscribe: parse and store List-Unsubscribe / List-Unsubscribe-Post
  (migration v6), detect email and web links (internal/unsubscribe), a
  domain-level ignore list, and an explicit record of what was marked. No
  code path contacts an unsubscribe link; acting on one needs Sam's approval.
- Migration v6 is additive; unsubscribe gains url/updated_at.
- Tests: detector, parser, store migration/records/candidates, rules editor,
  unsubscribe handlers, message-detail rendering.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-11 22:24:38 +11:00
parent 7c66e4ad31
commit 00bb0e05c4
24 changed files with 2121 additions and 42 deletions

View File

@@ -149,6 +149,18 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("POST /categories/rename", s.requireAuth(s.handleCategoryRename))
mux.HandleFunc("POST /categories/remove", s.requireAuth(s.handleCategoryRemove))
// Phase 6 (FR-4, FR-11): rules editor, saved and versioned.
mux.HandleFunc("GET /rules", s.requireAuth(s.handleRules))
mux.HandleFunc("POST /rules/save", s.requireAuth(s.handleRulesSave))
mux.HandleFunc("POST /rules/restore", s.requireAuth(s.handleRulesRestore))
mux.HandleFunc("POST /rules/reclassify", s.requireAuth(s.handleRulesReclassify))
// Phase 6 (FR-7): unsubscribe detection, ignore list, and records.
mux.HandleFunc("GET /unsubscribe", s.requireAuth(s.handleUnsubscribe))
mux.HandleFunc("POST /unsubscribe/ignore", s.requireAuth(s.handleUnsubscribeIgnore))
mux.HandleFunc("POST /unsubscribe/unignore", s.requireAuth(s.handleUnsubscribeUnignore))
mux.HandleFunc("POST /unsubscribe/mark", s.requireAuth(s.handleUnsubscribeMark))
return s.recoverPanic(s.logRequests(mux))
}