rules

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package rules holds the rule types, the condition matcher, validation, the evaluation order for one email and YAML import/export. It never calls a model and never touches a mailbox.

Index

Constants

View Source
const (
	OpEq          = "eq"
	OpNe          = "ne"
	OpIn          = "in"
	OpContains    = "contains"
	OpContainsAny = "contains_any"
	OpNotContains = "not_contains"
	OpMatches     = "matches"
	OpGt          = "gt"
	OpLt          = "lt"
	OpExists      = "exists"
)

Condition operators.

View Source
const (
	MatchAddress = "address"
	MatchDomain  = "domain"

	VerdictRoute = "route"
	VerdictKeep  = "keep"
	VerdictBlock = "block"
	VerdictMove  = "move"
)

Sender rule match types and verdicts.

View Source
const (
	ActMove    = "move"
	ActArchive = "archive"
	ActTrash   = "trash"
	ActJunk    = "junk"
	ActFlag    = "flag"
	ActUnflag  = "unflag"
	ActRead    = "read"
	ActUnread  = "unread"
	ActKeep    = "keep"
)

Action types, in the order the plan lists them.

View Source
const AccountByAddress = "" /* 169-byte string literal not displayed */

AccountByAddress is what is wrong with a mailbox number in a rules file's account condition.

View Source
const AllMailboxes = "all"

AllMailboxes is the applies_to value of a rule that is not limited to one mailbox. An export never writes it (no applies_to means the rule applies everywhere); an import accepts it to widen a rule that is limited to one.

View Source
const MaxDraftInstruction = 300

MaxDraftInstruction caps a rule's draft instruction, in characters.

View Source
const MinTrashConfidence = 0.85

MinTrashConfidence is the lowest threshold an intent-based trash rule may have.

Variables

This section is empty.

Functions

func ActionTypes

func ActionTypes() []string

ActionTypes lists the action types a rule may name.

func FolderProblem

func FolderProblem(name string) string

FolderProblem says, in a sentence, what is wrong with a folder name a rule would move mail to, or "" when it is fine. It is the check a move action's folder gets.

func MarshalYAML

func MarshalYAML(f File) ([]byte, error)

MarshalYAML writes a rules file in priority order, using the shorthands. A rule limited to a mailbox (AccountID set) needs its address in f.Mailboxes, and an account condition its mailboxes' addresses in place of their ids, or the file would not say which they are.

Types

type Action

type Action struct {
	Type   string `json:"type"`
	Folder string `json:"folder,omitempty"`
}

Action is one step a rule applies to an email. Folder is set for move only.

func (Action) String

func (a Action) String() string

String is the YAML shorthand: "move:Food", "trash".

type Cond

type Cond struct {
	All   []Cond `json:"all,omitempty"`
	Any   []Cond `json:"any,omitempty"`
	Field string `json:"field,omitempty"`
	Op    string `json:"op,omitempty"`
	Value any    `json:"value,omitempty"`
	// contains filtered or unexported fields
}

Cond is one node of a condition tree: a group ("all" or "any" of its children) or a leaf (field, op, value). The zero value is the empty tree, which always matches.

func (Cond) IsEmpty

func (c Cond) IsEmpty() bool

IsEmpty reports whether the tree has no conditions at all.

func (Cond) MapAccounts added in v0.1.3

func (c Cond) MapAccounts(fn func(any) any) Cond

MapAccounts returns the tree with fn applied to every mailbox an account condition names (each item of an "in" list on its own) and everything else as it was; c is not changed. The app names a mailbox by its id and a rules file by its address: whoever has the accounts turns one into the other with this.

func (Cond) Match

func (c Cond) Match(e *message.Summary, now time.Time) bool

Match reports whether the email satisfies the tree. now is only used by age_days. A leaf that would not pass validation never matches.

func (Cond) Text

func (c Cond) Text() string

Text renders the tree as one line of plain English, for showing a rule's "unless" part to a model: `from_domain is one of a.com, b.com and (subject contains refund or is_bulk is true)`. The empty tree renders as "".

func (*Cond) UnmarshalJSON

func (c *Cond) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes a node strictly (a misspelt key must not turn a leaf into an empty, always-true tree) and compiles its regex once.

type Defaults

type Defaults struct {
	DecisionModel string   `yaml:"decision_model,omitempty"`
	FallbackModel string   `yaml:"fallback_model,omitempty"`
	MinConfidence *float64 `yaml:"min_confidence,omitempty"`
}

Defaults is the optional "defaults" block. The models are daemon settings, so the importer checks them against the install's (an import never changes a model); MinConfidence is copied onto every rule that sets none.

type Draft added in v0.3.0

type Draft struct {
	// Instruction is how the reply should sound or what it should say ("polite no"); empty =
	// the model's own judgement.
	Instruction string `json:"instruction" yaml:"instruction,omitempty"`
	// ReadPrevious also hands the model the thread's previous message, when it is found.
	ReadPrevious bool `json:"read_previous" yaml:"read_previous,omitempty"`
}

Draft is how a rule drafts a reply.

type Evaluation

type Evaluation struct {
	Final      *Result
	Candidates []Rule // passing intent rules above the cut-off, in priority order
	CutOff     *Rule  // the default when the decider picks none; may be nil
	// contains filtered or unexported fields
}

Evaluation is what Evaluate found. When Final is set the email is settled without a model. Otherwise the caller asks the decider to pick among Candidates and passes its answer to Resolve.

func Evaluate

func Evaluate(e message.Summary, rs []Rule, senders []SenderRule, opt Options) Evaluation

Evaluate runs the evaluation order for one email: sender rules, then the priority walk down to the cut-off rule. It makes no model call.

func (Evaluation) Resolve

func (ev Evaluation) Resolve(ruleID int64, confidence float64) Result

Resolve turns the decider's answer into the final result (step 5). A pick outside the candidates counts as "none of these", for which the cut-off rule applies; when none does, the result keeps the decider's confidence that no rule applies. A pick below its threshold goes to Needs review with no action at all, even when there is a cut-off rule.

type Field

type Field struct {
	Name string
	Type string   // string | boolean | number | pass, fail or none | account id
	Ops  []string // the operators it accepts
}

Field describes one condition field for whoever writes conditions: the rule composer's prompt is built from this list.

func Fields

func Fields() []Field

Fields lists every condition field with its type and operators, header:<Name> last.

type File

type File struct {
	Defaults Defaults
	// Rules are as the file has them: an account condition names each mailbox by its
	// address (a string), not by its id, for the same reason as Mailboxes.
	Rules []Rule
	// Mailboxes says which mailbox each rule applies to, by the rule's name: the mailbox's
	// address (its login), or AllMailboxes. A rule with no entry says nothing about it. A
	// file cannot hold numeric account ids, which differ from one install to the next, so
	// whoever has the accounts maps the address to Rule.AccountID and back.
	Mailboxes map[string]string
}

File is a rules YAML document in the PRD shape. The database stays the source of truth; this is import/export only.

func ParseYAML

func ParseYAML(data []byte) (File, error)

ParseYAML reads a rules file and validates every rule, with account conditions naming mailboxes by address. Priority follows the order in the file. All problems are returned together (errors.Join), each a *RuleError.

type Options

type Options struct {
	MinConfidence float64   // threshold for rules that set none
	Now           time.Time // for age_days
	// RouteOnly are rules a sender rule may route to although they take no part in the
	// walk: a manual run of only some rules leaves the others out of the walk, yet the
	// user's standing sender answers still apply (MAI-43). nil in live sorting.
	RouteOnly []Rule
}

Options are the per-run settings Evaluate needs.

type Result

type Result struct {
	Stage       Stage
	RuleID      int64 // 0 = no rule; with Review, the rule the decider suggested
	RuleVersion int
	Confidence  float64
	Actions     []Action // the primary rule's actions, then each stacking rule's
	Stacked     []int64  // stacking rules whose actions were appended
	Review      bool     // below threshold: take no action, show in Needs review
	// SenderRuleID is the sender rule that settled the email (Stage is StageSender), and
	// SenderSource who made it (user | learned); 0 and empty otherwise.
	SenderRuleID int64
	SenderSource string
}

Result is the final outcome for one email.

type Rule

type Rule struct {
	ID            int64
	UserID        int64
	AccountID     int64 // 0 = all accounts
	Name          string
	Said          string // the user's original wording; "" when they gave none
	Template      string // the name of the gallery template it was added from; "" = none
	Intent        string // plain-English intent for the decider; "" = condition-only
	Conditions    Cond
	Exceptions    Cond // "unless"
	Actions       []Action
	Priority      int // lower runs first
	Stack         bool
	Model         string   // per-rule decider override
	MinConfidence *float64 // nil = the configured default
	Enabled       bool
	// MailboxRemoved marks a rule whose mailbox was removed (DropAccount): it is off and
	// may not be switched on until it is given a mailbox, or All mailboxes, or new
	// conditions, which clears the mark.
	MailboxRemoved bool
	// Notify are the tenant's notification destinations told each time the rule acts on an
	// email live (store notify_destinations ids); nil = none. A rules file does not carry
	// them: they name this install's destinations.
	Notify []int64
	// Draft, when set, has a reply drafted each time the rule acts on an email live (a
	// module writes it; the daemon saves it in the mailbox's Drafts folder and never sends
	// it); nil = none.
	Draft     *Draft
	Version   int
	CreatedAt int64
	UpdatedAt int64
}

Rule mirrors one row of the rules table.

func (Rule) AccountIDs added in v0.1.3

func (r Rule) AccountIDs() []int64

AccountIDs is every mailbox the rule names, by its mailbox (AccountID) or in an account condition of its conditions or exceptions, without repeats.

func (Rule) DropAccount added in v0.1.3

func (r Rule) DropAccount(id int64) (Rule, bool)

DropAccount is the rule as it is to be once mailbox id is removed, and whether that changes it. No mail comes from that mailbox any more, so:

  • A rule for that mailbox only is kept, switched off and marked (MailboxRemoved), with no mailbox, so the user can give it another one.
  • An account condition naming it is simplified so the rule does on the other mailboxes exactly what it did: "eq" it is false, "ne" it is true, "in" loses it (false when nothing is left), and the constants run up through the groups.
  • When the conditions can then never match, or the exceptions always apply, the rule could never act again: it is switched off and marked instead, with the mailbox taken out of its trees so the user still sees the rest of what they wrote. A condition-only rule left with no condition at all is treated the same way, as a rule without conditions or intent is one MailRules refuses to save.
  • Exceptions that can no longer apply are dropped.

r is not changed.

func (Rule) Validate

func (r Rule) Validate() error

Validate checks a rule before it is saved or imported and returns the first problem as a *ValidationError.

type RuleError

type RuleError struct {
	N   int    // the rule's place in the file, from 1
	ID  string // its id (name) as written
	Err *ValidationError
}

RuleError is a problem with one rule of a rules file: which rule, and what is wrong.

func (*RuleError) Error

func (e *RuleError) Error() string

Error is the line the command line prints: it keeps the path.

func (*RuleError) Sentence

func (e *RuleError) Sentence() string

Sentence is the same for a person: which rule and what is wrong, without the field path.

func (*RuleError) Unwrap

func (e *RuleError) Unwrap() error

type SenderRule

type SenderRule struct {
	ID        int64
	UserID    int64
	MatchType string // address | domain
	Value     string // lower-case
	RuleID    int64  // for route: the rule whose actions apply; 0 otherwise
	Verdict   string // route | keep | block | move
	// Folder is where a move verdict files the sender's mail, on whichever mailbox it
	// arrives in; "" otherwise.
	Folder    string
	Source    string // user | learned
	Hits      int
	CreatedAt int64
}

SenderRule mirrors one row of the sender_rules table.

type Stage

type Stage string

Stage says which step produced a result; it is stored in decisions.stage. The pipeline rewrites StageDecider to "fallback" when the fallback model answered, which only it knows.

const (
	StageSender    Stage = "sender"
	StageCondition Stage = "condition"
	StageDecider   Stage = "decider"
	StageNone      Stage = "none"
)

type ValidationError

type ValidationError struct {
	Path    string
	Message string
}

ValidationError is a rule problem. Path is the offending key as the API reports it, e.g. "conditions.all[0].op". Message is a sentence for a person, complete without the path: capitalised, ending with a full stop, and naming no field path. Error joins the two for a log or the command line.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL