settings

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: 18 Imported by: 0

Documentation

Overview

Package settings owns the settings a user changes while the daemon runs: the dry-run switch, the decision models and thresholds, retention, where trashed mail goes, whether the mailbox's own mail is left alone, and the model provider keys. They live in the settings table and lie over the environment (internal/config), which supplies the defaults. Provider keys are stored sealed under the tenant's data key and are never handed back out of this package except inside the config the model adapters are built from.

Index

Constants

View Source
const (
	KeyStored      = "stored"      // saved through the API; it wins over the environment
	KeyEnvironment = "environment" // set in the daemon's environment only
	KeyNone        = "none"
)

Where a provider key in force comes from. Never the key.

View Source
const (
	WorkspaceNoneNeeded = "none_needed" // the key was made for one workspace: nothing to name
	WorkspaceOne        = "one"         // one workspace, which MailRules stored as the setting
	WorkspaceSeveral    = "several"     // several: the user picks one
	WorkspaceFailed     = "failed"      // Anthropic could not say (no permission, network): the user types it
)

What a lookup found out about the Claude key in force.

View Source
const DefaultRetentionDays = 30

DefaultRetentionDays is how long message snippets are kept until the user says otherwise.

Variables

View Source
var Deciders = []string{"jev", "clef", "anthropic", "openai", "ollama"}

Deciders are the values MAILRULES_DECIDER accepts.

View Source
var ErrNoComposer = errors.New("no generative model is configured for the rule composer")

ErrNoComposer means no generative model is configured for the rule composer.

Functions

func CheckModel

func CheckModel(spec string) string

CheckModel says, in a sentence, what is wrong with a rule's model override, or "" when it is a decider this daemon knows: empty (the default), a decider name, or name:model. openai and ollama have no default model.

func KeyNames

func KeyNames() []string

KeyNames lists the provider keys, sorted.

Types

type ComposerMissing

type ComposerMissing struct {
	Model string // composer_model as set, e.g. openai:gpt-4o-mini
	Path  string // the setting to fill in, as the API spells it
	What  string // what to call it, e.g. "an OpenAI API key"
}

ComposerMissing is ErrNoComposer with what the chosen composer model lacks: the one place that decides what the user is told to add.

func (*ComposerMissing) Error

func (e *ComposerMissing) Error() string

func (*ComposerMissing) Is

func (e *ComposerMissing) Is(target error) bool

Is makes errors.Is(err, ErrNoComposer) true.

func (*ComposerMissing) Message

func (e *ComposerMissing) Message() string

Message is the sentence for a person: what the chosen model needs and where to add it.

type ImportProblem added in v0.1.2

type ImportProblem struct{ Path, Message string }

ImportProblem is something in a rules file that this install cannot take as written. Path names where it is (defaults.decision_model, rules[2].applies_to, rules[0].match); Message is a sentence for a person, complete without the path.

func (*ImportProblem) Error added in v0.1.2

func (e *ImportProblem) Error() string

type Invalid

type Invalid struct{ Path, Message string }

Invalid is a settings change the user has to correct. Path names the field; Message is a sentence for a person, complete without the path.

func (*Invalid) Error

func (e *Invalid) Error() string

type Patch

type Patch struct {
	Reset         []string // setting names, as the API spells them
	DryRun        *bool
	Decider       *string
	DeciderModel  *string
	FallbackModel *string
	ComposerModel *string
	EscalateBelow *float64
	MinConfidence *float64
	RetentionDays *int
	TrashToFolder *bool
	LeaveOwnMail  *bool
	OpenAIBaseURL *string // empty = api.openai.com
	OllamaURL     *string // empty = none
	// AnthropicWorkspaceID is empty or a wrkspc_ ID (config.ValidWorkspaceID); empty = none.
	AnthropicWorkspaceID *string
	Keys                 map[string]string
}

Patch is a change: nil fields stay as they are. An empty string is a value like any other, stored as it is: no fallback, the provider's default model, no URL. Where an empty value cannot work (the composer's model, the model of a decider that has no default) it is refused. Reset names the settings to forget, which puts the environment's default back in force. In Keys, an empty value removes the stored key: a key cannot be "set to nothing".

type Settings

type Settings struct {
	Store *store.Store   // with keys (store.Store.WithKeys): it opens the stored provider keys
	Env   *config.Config // the defaults
	Deps  models.Deps    // shared by every router built here
	// Workspaces looks up the workspace a Claude key needs; nil = never looked up, the
	// user types it.
	Workspaces WorkspaceFinder
	// contains filtered or unexported fields
}

Settings reads and changes the stored settings. Every setting belongs to a tenant: each method names the tenant it reads or changes, and a tenant with nothing stored has the environment's defaults.

In MAILRULES_MODE=cloud the provider keys in the environment are the operator's: a tenant's calls use one only while that tenant has no key of its own for the provider, View never reports it (the key reads as not set, with no hint of where one is), and the calls made with it are booked with the operator mark (store.Ledger). In selfhost mode an environment key is the admin's own and is shown as set in the environment.

func (*Settings) Apply

func (s *Settings) Apply(ctx context.Context, tenantID int64, p Patch) error

Apply validates a change against the settings it would produce and stores it in one transaction. A problem the user can fix comes back as *Invalid. Nothing needs a restart: the executor reads dry-run and trash_to_folder before every action, the pipeline reads leave_own_mail for every email, and Live rebuilds the router. A Claude key saved or replaced forgets what was learnt about the old one's workspace, and a workspace MailRules found for it; then, with no workspace set, it is looked up.

func (*Settings) Composer

func (s *Settings) Composer(ctx context.Context, tenantID int64) (models.Generator, error)

Composer returns the generative model the tenant's rule composer writes rules with: composer_model as config.SplitComposerModel reads it, on Anthropic, an OpenAI-compatible endpoint or Ollama. Until that provider has its key or URL it is a *ComposerMissing, which is ErrNoComposer. Its calls are booked by the caller, through Ledger.

func (*Settings) Drafter added in v0.3.0

func (s *Settings) Drafter(ctx context.Context, tenantID int64) (gen models.Generator, ledger store.Ledger, metered bool, err error)

Drafter returns the generative model a tenant's draft replies are written with: its rule composer model (Composer), on the tenant's own key or else the operator's, as sorting is paid. ledger books its calls; metered says they go out on a key the operator holds, so each reply written with it counts against the tenant's monthly allowance of AI-sorted emails.

func (*Settings) Effective

func (s *Settings) Effective(ctx context.Context, tenantID int64) (config.Config, error)

Effective returns the tenant's configuration in force: the environment with the tenant's stored settings laid over it. It holds decrypted provider keys, so it is for building model adapters, never for showing.

func (*Settings) ExportRules added in v0.1.2

func (s *Settings) ExportRules(ctx context.Context, v store.Viewer) (rules.File, error)

ExportRules is the viewer's tenant's rules as a rules file: each rule in priority order, with the address of the mailbox it applies to and of every mailbox its account conditions name (not the account's number, which means nothing on another install), and the models in force as the defaults the rules were written against. It holds no provider key. min_confidence is not a default here: a rule without its own threshold takes the setting at the time, so the file writes none and an import does not pin the current one onto it. A rule whose mailbox was removed has none and is written marked (rules.Rule.MailboxRemoved). Removing a mailbox takes it out of every rule (store.DeleteAccount, ReconcileRemovedMailboxes at start), so a rule naming a mailbox that no longer exists is a broken invariant, and stops the export rather than write a file that names a number. The rules are the tenant's, so a mailbox one of them names is looked up among all of the tenant's mailboxes, a teammate's private one too.

func (*Settings) Ledger added in v0.2.0

func (s *Settings) Ledger(ctx context.Context, tenantID int64) store.Ledger

Ledger books a tenant's model calls made outside a router (the rule composer, a module), marking those made on an operator key as the tenant's settings stand now.

func (*Settings) Live

func (s *Settings) Live(ctx context.Context, tenantID int64) (router *models.Router, minConfidence float64)

Live returns the tenant's decision router and act threshold for its settings as stored right now; pipeline.Pipeline.Live is this method. The router is rebuilt only when one of the tenant's stored settings has changed since the last call, whoever changed it, so a save in the browser reaches the next email without a restart. router is nil while the decider lacks its key.

func (*Settings) LookupWorkspaces

func (s *Settings) LookupWorkspaces(ctx context.Context, tenantID int64) (WorkspaceLookup, error)

LookupWorkspaces says which workspace the tenant's Claude key in force needs, as the Settings screen asks. A known answer (none needed, one, several) is given again without asking Anthropic; otherwise Anthropic is asked, and one workspace found while none is set is stored as the setting. The operator's key (cloud mode) is not the tenant's to ask about: it answers none needed.

func (*Settings) PrepareImport added in v0.1.2

func (s *Settings) PrepareImport(ctx context.Context, v store.Viewer, f rules.File) ([]rules.Rule, error)

PrepareImport checks a parsed rules file against this install and returns the rules to store, each with the mailbox it applies to and the mailbox ids its account conditions name. Nothing is stored here. A problem comes back as one *ImportProblem per fault, joined (errors.Join), and then nothing may be imported:

  • The defaults' models are not applied (they are settings of the daemon, and a shared file must not change which paid model an install uses), so a file written for another model is refused with how to go on; one that names the models in force is fine.
  • applies_to names a mailbox by its address, matched ignoring case. One that is not connected here, or that more than one connected mailbox has, is refused, since importing would make the rule act on every mailbox or none. "all" widens the rule to every mailbox. A rule the file says nothing about keeps the mailbox it has, or has none if it is new.
  • An account condition, in match or unless at any depth, names mailboxes the same way and is refused the same way, as is one that names a mailbox by number.
  • A rule marked mailbox_removed is stored off, marked and with no mailbox, whatever enabled says; one that also has applies_to contradicts itself and is refused. A marked rule here that the file names unmarked, with the same match and unless and no applies_to, keeps its mark: the file chose neither a mailbox nor new conditions.

The rules are compared with the viewer's tenant's, and a mailbox is matched only among the ones the viewer sees.

func (*Settings) RetentionDays

func (s *Settings) RetentionDays(ctx context.Context, tenantID int64) int

RetentionDays returns the tenant's retention_days setting in force: how long message snippets are kept. When the settings cannot be read it is the default.

func (*Settings) RouterFor

func (s *Settings) RouterFor(ctx context.Context, tenantID int64, spec string) *models.Router

RouterFor returns the tenant's router for a rule's model override (a decider spec, see CheckModel), built from the tenant's settings in force and kept until they change. It is nil when that decider cannot be used, for example because its key is not set; the caller then decides with the default router. pipeline.Pipeline.Override is this method.

func (*Settings) View

func (s *Settings) View(ctx context.Context, tenantID int64) (View, error)

View returns the tenant's settings in force.

type View

type View struct {
	DryRun        bool
	Decider       string
	DeciderModel  string
	FallbackModel string // empty = never escalate
	// FallbackActive says the fallback will really be asked: a model is set and the router
	// can build it. FallbackNote is the reason when one is set and it is not, a sentence
	// for a person; empty otherwise.
	FallbackActive bool
	FallbackNote   string
	ComposerModel  string
	EscalateBelow  float64
	MinConfidence  float64
	RetentionDays  int
	TrashToFolder  bool              // trash goes to actions.TrashFolder, not the server's Trash
	LeaveOwnMail   bool              // mail from the mailbox's own address is left alone (pipeline.OwnMail)
	OpenAIBaseURL  string            // an OpenAI-compatible endpoint; empty = api.openai.com
	OllamaURL      string            // a local Ollama server
	Keys           map[string]string // KeyStored | KeyEnvironment | KeyNone
	Warnings       []Warning         // never nil
	// The Claude workspace every Claude request names; empty = none. Its name is known when
	// a lookup listed it; Found says MailRules looked it up and stored it itself.
	AnthropicWorkspaceID    string
	AnthropicWorkspaceName  string
	AnthropicWorkspaceFound bool
}

View is what may be shown: every setting in force, and for each provider key only where it comes from.

type Warning

type Warning struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Path    string `json:"path"`
}

Warning is something about the settings in force that will not work as it stands, which the user may still be on the way to fixing (the first-run wizard saves in steps), so it is reported and not refused. Path names the setting to fill in.

type WorkspaceFinder

type WorkspaceFinder interface {
	Find(ctx context.Context, apiKey string) (models.WorkspaceAnswer, error)
}

WorkspaceFinder asks Anthropic which workspace a Claude key needs. models.AnthropicWorkspaces is the one the daemon uses.

type WorkspaceLookup

type WorkspaceLookup struct {
	Status     string
	Workspaces []models.Workspace
}

WorkspaceLookup is what a lookup answers: its status and, for one or several, the organisation's workspaces (never nil).

Jump to

Keyboard shortcuts

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