package webui import ( "encoding/json" "html/template" "net/http" "strconv" "strings" "mailgoserver/internal/db" ) // webmailRulesList is the self-service mirror of rulesList (mailbox_rules.go) — same // underlying CRUD (ListRulesForMailbox/CreateRuleMulti/UpdateRuleMulti/RemoveRule), // just reached from the mailbox owner's own portal instead of an admin managing it on // their behalf. ?edit= loads an existing rule into the builder instead of a blank // one, mirroring webmailSignaturesPage's ?edit= pattern. func (a *App) webmailRulesList(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) rules, err := a.DB.ListRulesForMailbox(mbox.ID) if err != nil { setFlash(w, "error", "Error loading rules") } var editing *db.MailboxFilterRule editingConditions := []db.RuleCondition{} var editingMatchType string if idStr := r.URL.Query().Get("edit"); idStr != "" { editing, _ = a.DB.GetRuleByID(mbox.ID, int64(atoi(idStr))) if editing != nil { editingConditions, editingMatchType = editing.Conditions() } } // json.Marshal HTML-escapes <, >, & by default (Go's documented behavior // specifically for safe embedding in HTML/script contexts), so this is safe to // mark template.JS and emit raw — a condition value containing "" can't // break out of the tag. Plain string would instead get html/template's own // JS-value auto-escaping applied on top, which quotes the whole blob as a single // JS string literal (mangling it — confirmed live, JSON.parse('"null"') is the // string "null", not an array, so seedData.forEach threw). editingConditionsJSON, _ := json.Marshal(editingConditions) a.render(w, r, "webmail_rules.html", M{ "mailbox": mbox, "rules": rules, "flashes": popFlashes(w, r), "active_section": "rules", "editing": editing, "editing_match_type": editingMatchType, "editing_conditions_json": template.JS(editingConditionsJSON), }) } // webmailSaveRule creates a new rule, or updates one when rule_id (a hidden form // field, not a path segment — one form reused for add and edit, mirroring // webmailSignatureSave) is set and non-zero. func (a *App) webmailSaveRule(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) if err := r.ParseForm(); err != nil { setFlash(w, "error", "Invalid form submission") http.Redirect(w, r, MailboxPrefix+"/rules", http.StatusFound) return } ruleID := int64(atoi(r.FormValue("rule_id"))) redirectTarget := MailboxPrefix + "/rules" if ruleID != 0 { redirectTarget += "?edit=" + strconv.FormatInt(ruleID, 10) } name := strings.TrimSpace(r.FormValue("name")) 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, redirectTarget, 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, redirectTarget, http.StatusFound) return } if action == "forward" && !strings.Contains(actionValue, "@") { setFlash(w, "error", "Please enter a valid address to forward to") http.Redirect(w, r, redirectTarget, http.StatusFound) return } actionOptionsJSON := "" if action == "forward" { opts := db.RuleActionOptions{KeepCopy: r.FormValue("keep_copy") != ""} if b, err := json.Marshal(opts); err == nil { actionOptionsJSON = string(b) } } var err error if ruleID != 0 { err = a.DB.UpdateRuleMulti(mbox.ID, ruleID, priority, conditions, matchType, name, action, actionValue, actionOptionsJSON) // A checkbox only ever appears in the submitted form when checked — its // absence means "unchecked", not "field not present", so this can't be // folded into UpdateRuleMulti's own column list the way the others are. if err == nil { err = a.DB.SetRuleActive(ruleID, mbox.ID, r.FormValue("is_active") != "") } } else { _, err = a.DB.CreateRuleMulti(mbox.ID, priority, conditions, matchType, name, action, actionValue, actionOptionsJSON) } if err != nil { setFlash(w, "error", "Error saving rule") http.Redirect(w, r, redirectTarget, http.StatusFound) return } setFlash(w, "success", "Rule saved") http.Redirect(w, r, MailboxPrefix+"/rules", http.StatusFound) } func (a *App) webmailToggleRule(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) ruleID := int64(atoi(r.PathValue("rule_id"))) rule, _ := a.DB.GetRuleByID(mbox.ID, ruleID) if rule == nil { setFlash(w, "error", "Rule not found") http.Redirect(w, r, MailboxPrefix+"/rules", http.StatusFound) return } if err := a.DB.SetRuleActive(ruleID, mbox.ID, !rule.IsActive); err != nil { setFlash(w, "error", "Error updating rule") } else if rule.IsActive { setFlash(w, "success", "Rule disabled") } else { setFlash(w, "success", "Rule enabled") } http.Redirect(w, r, MailboxPrefix+"/rules", http.StatusFound) } func (a *App) webmailRemoveRule(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) ruleID := int64(atoi(r.PathValue("rule_id"))) if err := a.DB.RemoveRule(ruleID, mbox.ID); err != nil { setFlash(w, "error", "Error removing rule") } else { setFlash(w, "success", "Rule removed") } http.Redirect(w, r, MailboxPrefix+"/rules", http.StatusFound) }