package webui import ( "net/http" "strconv" "strings" "mailgoserver/internal/db" ) var validConditionFields = map[string]bool{"from": true, "to": true, "subject": true} var validConditionOps = map[string]bool{"contains": true, "equals": true, "starts_with": true} var validActions = map[string]bool{"move_to_folder": true, "delete": true, "mark_read": true, "mark_as_spam": true} // parseRuleConditions reads the rule-builder's parallel condition_field/op/value // arrays (one value per condition row, same index across all three) — shared by the // admin and self-service "add rule" handlers, which both submit the same form shape. // r.ParseForm() must already have been called. func parseRuleConditions(r *http.Request) ([]db.RuleCondition, bool) { fields := r.PostForm["condition_field"] ops := r.PostForm["condition_op"] values := r.PostForm["condition_value"] if len(fields) == 0 || len(fields) != len(ops) || len(fields) != len(values) { return nil, false } conditions := make([]db.RuleCondition, 0, len(fields)) for i, field := range fields { op := ops[i] value := strings.TrimSpace(values[i]) if !validConditionFields[field] || !validConditionOps[op] || value == "" { return nil, false } conditions = append(conditions, db.RuleCondition{Field: field, Op: op, Value: value}) } return conditions, true } // summarizeConditions renders a rule's condition list as a human-readable string for // display, e.g. `to contains "sales" AND subject contains "invoice"`. func summarizeConditions(r db.MailboxFilterRule) string { conditions, matchType := r.Conditions() joiner := " AND " if matchType == "any" { joiner = " OR " } parts := make([]string, len(conditions)) for i, c := range conditions { parts[i] = c.Field + " " + strings.ReplaceAll(c.Op, "_", " ") + ` "` + c.Value + `"` } return strings.Join(parts, joiner) } func (a *App) rulesList(w http.ResponseWriter, r *http.Request) { mailbox, ok := a.mailboxWithAccess(w, r) if !ok { return } rules, err := a.DB.ListRulesForMailbox(mailbox.ID) if err != nil { setFlash(w, "error", "Error loading rules") } a.render(w, r, "mailbox_rules.html", M{"active": "mailboxes", "mailbox": mailbox, "rules": rules}) } // addRule mirrors the compact list-CRUD pattern used elsewhere: no separate edit // page, just add/remove — a rule needing changes is removed and re-added. func (a *App) addRule(w http.ResponseWriter, r *http.Request) { mailbox, ok := a.mailboxWithAccess(w, r) if !ok { return } if err := r.ParseForm(); err != nil { setFlash(w, "error", "Invalid form submission") http.Redirect(w, r, Prefix+"/mailboxes/"+idStr(r)+"/rules", http.StatusFound) return } priority, _ := strconv.Atoi(r.FormValue("priority")) matchType := r.FormValue("match_type") action := r.FormValue("action") actionValue := strings.TrimSpace(r.FormValue("action_value")) conditions, ok := parseRuleConditions(r) if !ok || !validActions[action] { setFlash(w, "error", "Please fill in a valid condition and action") http.Redirect(w, r, Prefix+"/mailboxes/"+idStr(r)+"/rules", http.StatusFound) return } if action == "move_to_folder" && actionValue == "" { setFlash(w, "error", "Please name the folder to move matching mail into") http.Redirect(w, r, Prefix+"/mailboxes/"+idStr(r)+"/rules", http.StatusFound) return } if _, err := a.DB.CreateRuleMulti(mailbox.ID, priority, conditions, matchType, action, actionValue); err != nil { setFlash(w, "error", "Error creating rule") } else { setFlash(w, "success", "Rule added") } http.Redirect(w, r, Prefix+"/mailboxes/"+idStr(r)+"/rules", http.StatusFound) } func (a *App) removeRule(w http.ResponseWriter, r *http.Request) { mailbox, ok := a.mailboxWithAccess(w, r) if !ok { return } ruleID := int64(atoi(r.PathValue("rule_id"))) if err := a.DB.RemoveRule(ruleID, mailbox.ID); err != nil { setFlash(w, "error", "Error removing rule") } else { setFlash(w, "success", "Rule removed") } http.Redirect(w, r, Prefix+"/mailboxes/"+idStr(r)+"/rules", http.StatusFound) }