package api import ( "encoding/json" "fmt" "net/http" "strconv" "archivmail/internal/audit" "archivmail/internal/storage" ) // PROJ-51: Archiving rules CRUD (minimal PROJ-43). Rules optionally carry a // retention_days value that determines a matching mail's retention period. // Superadmin only — global, cross-tenant configuration. type archivingRuleBody struct { TenantID *int64 `json:"tenant_id"` CondType string `json:"condition_type"` Pattern string `json:"pattern"` Priority int `json:"priority"` RetentionDays *int `json:"retention_days"` } // handleListArchivingRules returns all archiving rules. // GET /api/admin/archiving-rules — superadmin only. func (s *Server) handleListArchivingRules(w http.ResponseWriter, r *http.Request) { rules, err := s.store.ListArchivingRules(r.Context(), nil) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if rules == nil { rules = []storage.ArchivingRule{} } writeJSON(w, http.StatusOK, map[string]interface{}{ "rules": rules, "min_retention_days": s.store.MinRetentionDays(), }) } // handleCreateArchivingRule creates a new archiving rule. // POST /api/admin/archiving-rules — superadmin only. func (s *Server) handleCreateArchivingRule(w http.ResponseWriter, r *http.Request) { var body archivingRuleBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } id, err := s.store.CreateArchivingRule(r.Context(), storage.ArchivingRule{ TenantID: body.TenantID, CondType: body.CondType, Pattern: body.Pattern, Priority: body.Priority, RetentionDays: body.RetentionDays, }) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.auditRule(r, "archiving_rule_created", fmt.Sprintf("id=%d type=%s pattern=%s retention_days=%s", id, body.CondType, body.Pattern, retDaysStr(body.RetentionDays))) writeJSON(w, http.StatusCreated, map[string]interface{}{"id": id}) } // handleUpdateArchivingRule updates an existing archiving rule. // PUT /api/admin/archiving-rules/{id} — superadmin only. func (s *Server) handleUpdateArchivingRule(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid rule id") return } var body archivingRuleBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } if err := s.store.UpdateArchivingRule(r.Context(), storage.ArchivingRule{ ID: id, TenantID: body.TenantID, CondType: body.CondType, Pattern: body.Pattern, Priority: body.Priority, RetentionDays: body.RetentionDays, }); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.auditRule(r, "archiving_rule_updated", fmt.Sprintf("id=%d type=%s pattern=%s retention_days=%s", id, body.CondType, body.Pattern, retDaysStr(body.RetentionDays))) writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true}) } // handleDeleteArchivingRule deletes an archiving rule. Existing retain_until // values on already-archived mails are left unchanged (PROJ-51). // DELETE /api/admin/archiving-rules/{id} — superadmin only. func (s *Server) handleDeleteArchivingRule(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid rule id") return } if err := s.store.DeleteArchivingRule(r.Context(), id); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } s.auditRule(r, "archiving_rule_deleted", fmt.Sprintf("id=%d", id)) writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true}) } func (s *Server) auditRule(r *http.Request, event, detail string) { if s.audlog == nil { return } sess := sessionFromCtx(r.Context()) s.audlog.Log(audit.Entry{ EventType: event, Username: sess.Username, IPAddress: s.remoteIP(r), Success: true, Detail: detail, }) } func retDaysStr(d *int) string { if d == nil { return "none" } return strconv.Itoa(*d) }