2026-08-12 21:14:19 +01:00
package db
2026-08-14 13:04:55 +01:00
import "encoding/json"
2026-08-15 21:49:25 +01:00
const filterRuleColumns = `id, mailbox_id, name, priority, condition_field, condition_op, condition_value, action, action_value, action_options_json, is_active, conditions_json, match_type, created_at`
func scanFilterRule ( scan func ( dest ... any ) error ) ( MailboxFilterRule , error ) {
var r MailboxFilterRule
var createdAt string
err := scan ( & r . ID , & r . MailboxID , & r . Name , & r . Priority , & r . ConditionField , & r . ConditionOp , & r . ConditionValue , & r . Action , & r . ActionValue , & r . ActionOptionsJSON , & r . IsActive , & r . ConditionsJSON , & r . MatchType , & createdAt )
if err != nil {
return r , err
}
r . CreatedAt , _ = parseTime ( createdAt )
return r , nil
}
2026-08-12 21:14:19 +01:00
func ( d * DB ) ListRulesForMailbox ( mailboxID int64 ) ([] MailboxFilterRule , error ) {
2026-08-15 21:49:25 +01:00
rows , err := d . Query ( `SELECT ` + filterRuleColumns + ` FROM esrv_mailbox_filter_rules WHERE mailbox_id = ? ORDER BY priority ASC, id ASC` , mailboxID )
2026-08-12 21:14:19 +01:00
if err != nil {
return nil , err
}
defer rows . Close ()
var out [] MailboxFilterRule
for rows . Next () {
2026-08-15 21:49:25 +01:00
r , err := scanFilterRule ( rows . Scan )
if err != nil {
2026-08-12 21:14:19 +01:00
return nil , err
}
out = append ( out , r )
}
return out , rows . Err ()
}
2026-08-15 21:49:25 +01:00
// GetRuleByID scopes the lookup to mailboxID, mirroring GetSignatureByID — backs the
// rule builder's edit mode (?edit=<id>).
func ( d * DB ) GetRuleByID ( mailboxID , id int64 ) ( * MailboxFilterRule , error ) {
row := d . QueryRow ( `SELECT ` + filterRuleColumns + ` FROM esrv_mailbox_filter_rules WHERE id = ? AND mailbox_id = ?` , id , mailboxID )
r , err := scanFilterRule ( row . Scan )
if err != nil {
return nil , nil // sql.ErrNoRows and any scan error both just mean "not found/not yours"
}
return & r , nil
}
2026-08-14 13:04:55 +01:00
// CreateRule creates a single-condition rule — a thin wrapper over CreateRuleMulti
// for the common one-condition case (and for existing callers/tests written before
// multi-condition rules existed).
2026-08-12 21:14:19 +01:00
func ( d * DB ) CreateRule ( mailboxID int64 , priority int , field , op , value , action , actionValue string ) ( int64 , error ) {
2026-08-15 21:49:25 +01:00
return d . CreateRuleMulti ( mailboxID , priority , [] RuleCondition {{ Field : field , Op : op , Value : value }}, "all" , "" , action , actionValue , "" )
2026-08-14 13:04:55 +01:00
}
// CreateRuleMulti creates a rule with one or more conditions combined per matchType
// ("all"=AND, "any"=OR, defaulting to "all" for anything else). The first condition
// also mirrors into the legacy condition_field/op/value columns so old code paths
// reading them directly still see something sane.
2026-08-15 21:49:25 +01:00
func ( d * DB ) CreateRuleMulti ( mailboxID int64 , priority int , conditions [] RuleCondition , matchType , name , action , actionValue , actionOptionsJSON string ) ( int64 , error ) {
2026-08-14 13:04:55 +01:00
if matchType != "any" {
matchType = "all"
}
conditionsJSON , err := json . Marshal ( conditions )
if err != nil {
return 0 , err
}
first := conditions [ 0 ]
2026-08-15 21:49:25 +01:00
res , err := d . Exec ( `INSERT INTO esrv_mailbox_filter_rules (mailbox_id, name, priority, condition_field, condition_op, condition_value, action, action_value, action_options_json, conditions_json, match_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` , mailboxID , name , priority , first . Field , first . Op , first . Value , action , actionValue , actionOptionsJSON , string ( conditionsJSON ), matchType )
2026-08-12 21:14:19 +01:00
if err != nil {
return 0 , err
}
return res . LastInsertId ()
}
2026-08-15 21:49:25 +01:00
// UpdateRuleMulti overwrites an existing rule's fields in place — used by the rule
// builder's edit mode, scoped to mailboxID so one mailbox can never modify another's
// rule by guessing an id (mirrors UpdateSignature).
func ( d * DB ) UpdateRuleMulti ( mailboxID , id int64 , priority int , conditions [] RuleCondition , matchType , name , action , actionValue , actionOptionsJSON string ) error {
if matchType != "any" {
matchType = "all"
}
conditionsJSON , err := json . Marshal ( conditions )
if err != nil {
return err
}
first := conditions [ 0 ]
_ , err = d . Exec ( `UPDATE esrv_mailbox_filter_rules SET name = ?, priority = ?, condition_field = ?, condition_op = ?, condition_value = ?, action = ?, action_value = ?, action_options_json = ?, conditions_json = ?, match_type = ?
WHERE id = ? AND mailbox_id = ?` , name , priority , first . Field , first . Op , first . Value , action , actionValue , actionOptionsJSON , string ( conditionsJSON ), matchType , id , mailboxID )
return err
}
// SetRuleActive flips a rule's enabled/disabled state — a quick toggle from the rules
// list, no need to open the full edit builder just to pause a rule.
func ( d * DB ) SetRuleActive ( id , mailboxID int64 , active bool ) error {
_ , err := d . Exec ( `UPDATE esrv_mailbox_filter_rules SET is_active = ? WHERE id = ? AND mailbox_id = ?` , active , id , mailboxID )
return err
}
2026-08-12 21:14:19 +01:00
// RemoveRule deletes a rule, scoped to mailboxID (mirrors RemoveAppPassword/RemoveAlias).
func ( d * DB ) RemoveRule ( id , mailboxID int64 ) error {
_ , err := d . Exec ( `DELETE FROM esrv_mailbox_filter_rules WHERE id = ? AND mailbox_id = ?` , id , mailboxID )
return err
}