2026-08-12 21:14:19 +01:00
|
|
|
package webui
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-08-14 13:04:55 +01:00
|
|
|
|
|
|
|
|
"mailgoserver/internal/db"
|
2026-08-12 21:14:19 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var validConditionFields = map[string]bool{"from": true, "to": true, "subject": true}
|
|
|
|
|
var validConditionOps = map[string]bool{"contains": true, "equals": true, "starts_with": true}
|
2026-08-14 13:04:55 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2026-08-12 21:14:19 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-08-14 13:04:55 +01:00
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
setFlash(w, "error", "Invalid form submission")
|
|
|
|
|
http.Redirect(w, r, Prefix+"/mailboxes/"+idStr(r)+"/rules", http.StatusFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-12 21:14:19 +01:00
|
|
|
priority, _ := strconv.Atoi(r.FormValue("priority"))
|
2026-08-14 13:04:55 +01:00
|
|
|
matchType := r.FormValue("match_type")
|
2026-08-12 21:14:19 +01:00
|
|
|
action := r.FormValue("action")
|
|
|
|
|
actionValue := strings.TrimSpace(r.FormValue("action_value"))
|
|
|
|
|
|
2026-08-14 13:04:55 +01:00
|
|
|
conditions, ok := parseRuleConditions(r)
|
|
|
|
|
if !ok || !validActions[action] {
|
2026-08-12 21:14:19 +01:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-14 13:04:55 +01:00
|
|
|
if _, err := a.DB.CreateRuleMulti(mailbox.ID, priority, conditions, matchType, action, actionValue); err != nil {
|
2026-08-12 21:14:19 +01:00
|
|
|
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)
|
|
|
|
|
}
|