package mailstore import ( "strings" "mailgoserver/internal/db" ) // FilterAction is the outcome of evaluating a mailbox's filter rules against one // incoming message. type FilterAction struct { Folder string // non-empty: store here instead of INBOX MarkRead bool Drop bool // don't store at all } // ApplyRules evaluates a mailbox's filter rules in priority order and returns the // first match's action (zero value if none match, meaning "store in INBOX, unread"). // headers should have "from"/"to"/"subject" keys — rules run at delivery time, before // the message is encrypted and stored, so real header values are available, not just // the plaintext cache columns used for fast IMAP listing. func (s *Store) ApplyRules(mailboxID int64, headers map[string]string) (FilterAction, error) { rules, err := s.DB.ListRulesForMailbox(mailboxID) if err != nil { return FilterAction{}, err } for _, r := range rules { if !r.IsActive { continue } if !ruleMatches(r, headers) { continue } switch r.Action { case "move_to_folder": return FilterAction{Folder: r.ActionValue}, nil case "mark_as_spam": // Reuses the same Spam folder score-based quarantine already delivers // into (see smtpserver/session.go) — from the mailbox owner's // perspective it's the same "goes to Spam" outcome either way. return FilterAction{Folder: "Spam"}, nil case "delete": return FilterAction{Drop: true}, nil case "mark_read": return FilterAction{MarkRead: true}, nil } } return FilterAction{}, nil } // ruleMatches combines a rule's conditions per its match type: "all" requires every // condition to match (AND), "any" requires at least one (OR). func ruleMatches(r db.MailboxFilterRule, headers map[string]string) bool { conditions, matchType := r.Conditions() if matchType == "any" { for _, c := range conditions { if matchCondition(c.Op, headers[c.Field], c.Value) { return true } } return false } for _, c := range conditions { if !matchCondition(c.Op, headers[c.Field], c.Value) { return false } } return true } func matchCondition(op, value, target string) bool { value = strings.ToLower(value) target = strings.ToLower(target) switch op { case "contains": return strings.Contains(value, target) case "equals": return value == target case "starts_with": return strings.HasPrefix(value, target) } return false }