actions

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

Documentation

Overview

Package actions is the only code that changes a mailbox: the executor that applies a rule's actions, and undo. This file holds the types its callers share.

Index

Constants

View Source
const MoveChunk = 100

MoveChunk is how many emails go to the server in one MoveMany: a cleanup Sort's chunk and an undo's. One command that fails takes no more than this many with it.

View Source
const TrashFolder = "MailRules Trash"

TrashFolder is where a trash action moves mail while the trash_to_folder setting is on: an ordinary folder, made on first use and given no special-use role, so the provider never empties it the way it empties Trash.

Variables

View Source
var DraftFlags = []string{`\Draft`, `\Seen`}

DraftFlags are the flags a saved draft reply gets: a draft, and read, so it adds nothing to the mailbox's unread count.

View Source
var ErrDraftSettled = errors.New("the draft reply is settled")

ErrDraftSettled means a draft job has nothing left to do: the rule's actions were undone or corrected since, its email's row is gone, or its draft action is recorded already. No row is written for it.

View Source
var ErrGone = errors.New("message was moved or deleted outside MailRules")

ErrGone means an action cannot be undone because its message is no longer where MailRules left it. The HTTP layer answers 409 with this text.

View Source
var ErrLeftReview = fmt.Errorf("taken off Needs review: %w", ErrGone)

ErrLeftReview means an answer to Needs review found the email no longer where MailRules saw it, so it was taken off the queue (state skipped) and nothing else was done. It wraps ErrGone. The HTTP layer answers 409 message_gone with its own text.

View Source
var ErrTooOld = errors.New("the action is too old to undo")

ErrTooOld means an action was made more than store.UndoDays ago and can no longer be undone. The HTTP layer answers 409 too_old.

Functions

func IsMove added in v0.3.0

func IsMove(acts []rules.Action) bool

IsMove reports whether acts is a single move-type action (move, archive, trash or junk), the kind ApplyMany takes.

Types

type Accounts

type Accounts interface {
	// Mailbox returns the account's open connection.
	Mailbox(accountID int64) (mail.Mailbox, error)
	// Lock waits until the account's processor is between messages and holds it there
	// until unlock is called. Undo and corrections take it, so mail they move back into
	// the watched folder is on record before the processor can mistake it for new mail.
	Lock(accountID int64) (unlock func())
}

Accounts is how the executor reaches a mail account (worker.Manager implements it).

type ActionRecord

type ActionRecord = store.Action

ActionRecord is one row of the actions table.

type Correction

type Correction struct {
	By          store.Viewer // who corrects: they must see the message, and the rule is their tenant's
	MessageID   int64
	RightRuleID int64 // 0 = keep in the inbox
	// Always also stores a user sender rule, so future mail goes the same way: for the
	// message's sender (rules.MatchAddress) or its whole domain (rules.MatchDomain). "" = no.
	Always string
	Review bool // an answer given in Needs review, as opposed to a fix made from the feed
}

Correction is someone saying a message belongs to another rule.

type DecisionRecord

type DecisionRecord struct {
	DecisionID int64       // 0 = no decision, e.g. a correction
	MessageID  int64       // messages.id
	Ref        mail.MsgRef // where the message is now
}

DecisionRecord says which message to act on and for which decision.

type DraftWriter added in v0.3.0

type DraftWriter func(ctx context.Context, mb mail.Mailbox, email mail.MsgRef, msg store.Message) (Reply, error)

DraftWriter writes the reply to msg, which is at email in mb now: it reads the email, has the reply written and builds the message. A *SkipDraft is recorded as the draft action's failure; any other error leaves nothing recorded, for the caller to try again later.

type Exec

type Exec struct {
	Store    *store.Store
	Accounts Accounts
	Hub      *events.Hub // nil = publish nothing
	// DryRunDefault is used until the dry_run setting is stored (MAILRULES_DRY_RUN).
	DryRunDefault bool
	Now           func() time.Time // nil = time.Now
	// Notices, when set, says whether a tenant may have rule notifications (its plan includes
	// them): when not, no notice is written for its mail. nil = every tenant may.
	Notices func(ctx context.Context, tenantID int64) bool
	// Drafts, when set, says whether a tenant's rules may draft replies (the build has a
	// module that writes them and the tenant's plan includes them): when it says yes, a live
	// run of a rule with Draft set queues a draft job (store.FollowUps.Draft). nil = no
	// tenant may: the build drafts nothing.
	Drafts func(ctx context.Context, tenantID int64) bool
}

Exec is the executor: the only code that changes a mailbox.

func (*Exec) Apply

func (x *Exec) Apply(ctx context.Context, d DecisionRecord, acts []rules.Action, batchID int64) ([]ActionRecord, error)

Apply runs acts in order on one message. In dry-run (checked here and nowhere else) it only writes a dry_run row per action and never contacts the server. Otherwise it reads the message's flags once, and for each action writes its row with the before snapshot first, then changes the mailbox, then stores the outcome, so there is no change without a row. A failure stops the remaining actions and is returned; whoever asked decides about a retry. With trash_to_folder on, a trash row names TrashFolder as its folder, in dry-run too, so what it says it did (or would do) is where the mail went. The row that ends a live run also queues the notifications of the rule that decided it, when it names destinations, and, when the run finished, its draft reply (store.FinishAction).

func (*Exec) ApplyMany added in v0.3.0

func (x *Exec) ApplyMany(ctx context.Context, ms []Many, a rules.Action, batchID int64) (dry bool, errs []error)

ApplyMany is Apply for one move-type action on several emails of one folder, as a cleanup Sort replays it: the destination is looked up (or made) once and the emails go to the server together (Mailbox.MoveMany), while each still gets its own row, written with its before snapshot ahead of the change, so each is undone on its own as before. An email the batch did not move is moved on its own, which says why when it cannot be. errs lines up with ms, nil for each email whose action is on record as done (or dry-run); dry reports the tenant's dry-run, in which nothing reaches the server.

func (*Exec) Correct

func (x *Exec) Correct(ctx context.Context, c Correction) (batchID int64, err error)

Correct fixes one message: it undoes what was done to it (newest first), applies the right rule's actions, or none for "keep", and records the correction, which deletes the learned sender rule for that sender and, with Always, stores a user sender rule instead. The new actions are one batch, whose id is returned. It also resolves a message waiting in Needs review. When the new actions cannot be applied, what was undone stays undone; the row as it now is goes out as message.processed all the same, after the action.undone events, so a screen that shows the email does not keep the old picture. An email waiting in Needs review that is no longer where MailRules saw it is taken off the queue instead, with nothing done to any mailbox, and the error is ErrLeftReview.

func (*Exec) Draft added in v0.3.0

func (x *Exec) Draft(ctx context.Context, j store.DraftJob, write DraftWriter) (ActionRecord, error)

Draft saves the reply a rule's live run asked for (store.DraftJob) in the account's Drafts folder, the one the server marks \Drafts: MailRules never guesses one or creates one, and never sends what it saves. It is a mailbox change like any other: in the tenant's dry-run it records a dry_run row and writes nothing (the writer is not called, so no model is paid for); otherwise it writes the row first, then appends, then records the outcome, with the draft's place, so the draft can be undone by itself or with its batch. A draft found in the folder already under the reply's Message-ID (saved before a crash) is not saved again. The writer runs before the account's lock is taken, so a slow model holds up nothing; the rule's run is checked again under the lock. ErrDraftSettled when there is nothing left to do. Any other error is the caller's to retry: nothing is recorded.

func (*Exec) EnsureFolders

func (x *Exec) EnsureFolders(ctx context.Context, accountID int64, names []string) error

EnsureFolders creates folders on an account's server, ahead of the rules that will move mail into them. It is a mailbox change like any other: in the account's tenant's dry-run it does nothing, and the first live move creates the folder then.

func (*Exec) GiveUpDraft added in v0.3.0

func (x *Exec) GiveUpDraft(ctx context.Context, j store.DraftJob, reason string) (ActionRecord, error)

GiveUpDraft records the reply of a draft job that is given up before Draft could save it (its mailbox stayed unreachable) as a failed draft action that says why: reason. In dry-run it is recorded as Draft records it. ErrDraftSettled as Draft.

func (*Exec) Undo

func (x *Exec) Undo(ctx context.Context, actionID int64) error

Undo reverses one action: a move goes back to the folder it came from, a flag change goes back to what the before snapshot says, and a draft reply MailRules saved is deleted: that one message, the only thing MailRules ever deletes for good (undoDraft). Undoing an action that is already undone, or that never changed the mailbox (dry-run, failed), does nothing. An action made more than store.UndoDays ago is refused with ErrTooOld. When the message (for a draft, the reply) is not where the action left it, the error is ErrGone and the action stays as it was. Undo does not look at dry-run: it only restores what MailRules itself changed, and only when asked.

func (*Exec) UndoBatch

func (x *Exec) UndoBatch(ctx context.Context, v store.Viewer, batchID int64) (Undid, error)

UndoBatch undoes the actions of a batch the viewer sees, on the mailboxes they see, newest first, and says what it undid. An action that cannot be undone does not stop the rest: its error is joined into the one returned, and the batch is marked undone only when every action was, a teammate's ones the viewer does not see included. A batch that never changed a mailbox (all its actions were recorded in dry-run, or failed) has nothing to undo: that is a no-op, and the batch stays as it is. A batch the viewer does not see is ErrNotFound.

func (*Exec) UndoMessage

func (x *Exec) UndoMessage(ctx context.Context, v store.Viewer, messageID int64) (batchID int64, u Undid, why, err error)

UndoMessage reverses everything still in effect on one message the viewer sees, newest first, as one batch of kind undo: "undo this email" in one step. An action that cannot be undone does not stop the others; why is the first such failure (ErrGone when the message is gone), nil when there was none. A message the viewer does not see is ErrNotFound.

func (*Exec) UndoSince

func (x *Exec) UndoSince(ctx context.Context, v store.Viewer, ruleID, since int64) (batchID int64, u Undid, err error)

UndoSince reverses every action on the mailboxes the viewer sees made at or after since that is still in effect, newest first: "undo the last hour". With ruleID it only takes the actions that rule led to. The run is recorded as one batch of kind undo in the viewer's tenant, whose id is returned with what was undone and how many actions could not be (their message is gone, or its account is not connected). One that cannot be undone does not stop the rest.

type Executor

type Executor interface {
	// Apply runs acts in order on one message and returns a record per action attempted.
	// A failure stops the remaining actions and is returned with the records so far.
	Apply(ctx context.Context, d DecisionRecord, acts []rules.Action, batchID int64) ([]ActionRecord, error)
	Undo(ctx context.Context, actionID int64) error
	// UndoBatch undoes the actions of a batch the viewer sees on the mailboxes they see.
	UndoBatch(ctx context.Context, v store.Viewer, batchID int64) (Undid, error)
}

Executor applies and undoes actions.

type Many added in v0.3.0

type Many struct {
	DecisionRecord
	Flags []string
}

Many is one email for ApplyMany: its decision, and the flags it had when it was just read, which stand in for the snapshot Apply reads itself.

type NoFolderError

type NoFolderError struct{ Role string } // Archive | Trash | Junk

NoFolderError means an archive, trash or junk action has nowhere to go: the account has no folder the server marks for that use, and MailRules never guesses one or creates one. Its text is what the action's row records ("no Archive folder"). Trash only meets it with trash_to_folder off: otherwise it goes to TrashFolder, which MailRules makes.

func (*NoFolderError) Error

func (e *NoFolderError) Error() string

type Reply added in v0.3.0

type Reply struct {
	Raw       []byte
	MessageID string
}

Reply is a draft reply ready to save: the whole message and its Message-ID, without angle brackets, which must be the same on every attempt for the same job.

type SkipDraft added in v0.3.0

type SkipDraft struct{ Reason string }

SkipDraft is a reply that will not be written, for good: Reason is the sentence the draft action's error records ("This month's AI-sorted emails are used up.").

func (*SkipDraft) Error added in v0.3.0

func (e *SkipDraft) Error() string

type Undid

type Undid struct {
	Actions int // actions undone
	// Emails is how many emails at least one action was undone on: put back as they were.
	// An email a cleanup only recorded in dry-run, passed over, or whose actions all
	// failed was never changed, so it is not counted.
	Emails int
	Failed int // actions that could not be undone and are still in effect
}

Undid counts what one undo run did.

Jump to

Keyboard shortcuts

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