147 lines
5.4 KiB
Go
147 lines
5.4 KiB
Go
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=<id> 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 "</script>" 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)
|
|
}
|