Files
dealerweb f5fb7f9db9 feat: out of office forwarding and recipients, filter rule periods
Filter rules can be limited to a period: the rule dialog takes a start
and an end on the user's clock, the rule list shows whether a rule is
planned, active or expired, and the script tests the moment of
delivery with RFC 5260 currentdate in UTC. A rule is never written
without its period.

The vacation card becomes an out of office page: the period on top,
then the automatic reply and forwarding, each with its own switch.
Forwarding runs with or without the auto-reply, within the period,
never for spam, and keeps a copy here or not. Against the server's
redirect limit it counts as a rule ahead of all others, one that stops
the message unless it keeps a copy. The auto-reply can go to
internal or external senders only, told apart by the envelope sender
against the account's own domains. Both run from the filters script
that includes Stalwart's vacation script, which is put back in charge
when the server's own script takes over.

A script that uses any of this says metadata version 2, which older
builds leave alone instead of writing it back without these fields.
2026-10-02 11:29:32 +02:00

166 lines
5.6 KiB
TypeScript

export interface SieveScript {
id: string;
name: string;
blobId: string;
isActive: boolean;
}
export interface SieveCapabilities {
implementation: string;
maxSizeScript: number;
sieveExtensions: string[];
notificationMethods: string[];
externalLists: string[];
/** RFC 9661: redirects run per message; extra ones are skipped silently. */
maxNumberRedirects?: number | null;
}
export type FilterConditionField =
| 'from' | 'to' | 'cc' | 'subject' | 'header' | 'size' | 'body'
| 'attachment'
// Every message (Sieve `true`): no value, and always the comparator 'any'.
| 'all';
export type FilterComparator =
| 'contains' | 'not_contains'
| 'is' | 'not_is'
| 'starts_with' | 'ends_with'
| 'matches'
| 'greater_than' | 'less_than'
// For field === 'attachment':
// has_any → message has any attachment (Content-Disposition: attachment)
// has_type → message has an attachment whose Content-Type matches `value`
// (substring match, e.g. "application/pdf" or "image/")
| 'has_any' | 'has_type'
// For field === 'from' | 'to' | 'cc', compared against the parsed address
// with the Sieve `address` test rather than the raw header text:
// address_is → the whole address equals `value` (anna@acme.com, but not
// joanna@acme.com)
// domain_is → the domain part equals `value` (acme.com, but neither
// sub.acme.com nor acme.com.evil)
// Older Bulwark versions read these as `header :contains`, so a script
// they save keeps working, only less strictly.
| 'address_is' | 'domain_is'
// For field === 'all': any message at all.
| 'any';
export type FilterActionType =
| 'move' | 'copy' | 'forward'
| 'mark_read' | 'star' | 'add_label'
| 'discard' | 'reject' | 'keep' | 'stop';
export interface FilterCondition {
field: FilterConditionField;
comparator: FilterComparator;
/**
* Match value. Use a string array for OR-within-condition semantics
* (e.g. `["@domain1.com", "@domain2.com"]` matches mail from either).
* Sieve emits the array as a list literal which the implementation
* treats as "matches any item". Use a plain string for single-value
* conditions; existing single-value rules continue to work unchanged.
*
* Not supported for: size (numeric), has_any (no value).
*/
value: string | string[];
headerName?: string;
}
export interface FilterAction {
type: FilterActionType;
value?: string;
/**
* move/copy: JMAP id of the target folder. `value` keeps the path as the
* fallback; the id keeps the rule working after the folder is renamed.
*/
mailboxId?: string;
/** forward: also keep the message (`redirect :copy`). */
keepCopy?: boolean;
}
export type FilterOrigin = 'bulwark' | 'external' | 'opaque';
export interface FilterRule {
id: string;
name: string;
enabled: boolean;
matchType: 'all' | 'any';
conditions: FilterCondition[];
actions: FilterAction[];
stopProcessing: boolean;
/**
* Also move/copy messages the server marked as spam. Off by default, so a
* folder rule does not pull spam out of Junk.
*/
includeSpam?: boolean;
/**
* The rule only acts on mail that arrives from `activeFrom` until
* `activeUntil`, both inclusive, either one open-ended. Absolute moments
* as ISO 8601 UTC ("2026-10-05T06:00:00.000Z"), so the script does not
* depend on the time zone of whoever saves it next. Compiled to Sieve
* `currentdate` tests (RFC 5260), which need "date" and "relational".
*/
activeFrom?: string;
activeUntil?: string;
origin?: FilterOrigin;
originLabel?: string;
rawBlock?: string;
}
export interface VacationSieveConfig {
isEnabled: boolean;
subject: string;
textBody: string;
}
/**
* Forwarding set up in the vacation card: mail goes on to one address, with
* or without the auto-reply, in the vacation's period. It is not a filter
* rule and the filter list does not show it; it runs ahead of the rules.
*/
export interface VacationForward {
/** The forwarding switch, independent of the auto-reply's. */
enabled: boolean;
/** The one address mail is forwarded to. */
to: string;
/**
* Keep the message in this mailbox too (`redirect :copy`). Otherwise it
* is only forwarded, and no filter rule runs on it.
*/
keepCopy: boolean;
/** The auto-reply's period when it was saved, as for FilterRule.activeFrom. */
activeFrom?: string;
activeUntil?: string;
}
/**
* Who gets the auto-reply when not everyone should: only senders from the
* account's own domains, or only senders from elsewhere. It is decided on
* the From address while the auto-reply runs from the filters script.
*/
export interface VacationAudience {
only: 'internal' | 'external';
/** The account's own domains (from its identities) when it was saved. */
domains: string[];
}
export interface FilterMetadata {
/**
* 2 once a rule has a period or the vacation's forwarding or recipients are
* stored. Older builds only take version 1 as their own and would write
* such a script back without these fields; anything else they leave alone.
*/
version: 1 | 2;
rules: FilterRule[];
vacation?: VacationSieveConfig;
/** See VacationAudience; absent when everyone gets the auto-reply. */
vacationAudience?: VacationAudience;
/** See VacationForward; kept while switched off, so the card remembers it. */
vacationForward?: VacationForward;
/**
* The script runs the server-managed "vacation" script via `include`.
* Servers like Stalwart keep one active script, so a VacationResponse
* that activates its own script would otherwise switch the filters off.
*/
includeVacation?: boolean;
}