Files
webmail/lib/sieve/vacation-forward.ts
T
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

56 lines
2.2 KiB
TypeScript

import type { VacationForward } from '@/lib/jmap/sieve-types';
import { isValidEmail } from '@/lib/validation';
import { isPeriodBoundary } from '@/lib/sieve/period';
/**
* Forwarding from the vacation card (see VacationForward). The generator
* writes it as its own block after the vacation include, marked by this
* comment; the parser knows the block by it and leaves it to the generator.
*/
export const VACATION_FORWARD_MARKER = '# Vacation forwarding';
/**
* The marker as the parser looks for it in a block: the whole line, so a
* comment of someone's own that only begins like it is not taken for it.
*/
export const VACATION_FORWARD_MARKER_RE = /^[ \t]*# Vacation forwarding[ \t]*$/m;
/** Whether `value` is forwarding Bulwark can write back as it reads. */
export function isValidVacationForward(value: unknown): value is VacationForward {
if (!value || typeof value !== 'object') return false;
const f = value as Record<string, unknown>;
if (typeof f.enabled !== 'boolean' || typeof f.keepCopy !== 'boolean') return false;
if (typeof f.to !== 'string' || !isValidEmail(f.to)) return false;
if (f.activeFrom !== undefined && !isPeriodBoundary(f.activeFrom)) return false;
if (f.activeUntil !== undefined && !isPeriodBoundary(f.activeUntil)) return false;
return true;
}
/**
* The forwarding with the vacation's period, whatever period it had: it runs
* while the vacation does (an empty date leaves that end open).
*/
export function withVacationPeriod(
forward: Pick<VacationForward, 'enabled' | 'to' | 'keepCopy'>,
period: { from: string | null; until: string | null },
): VacationForward {
return {
enabled: forward.enabled,
to: forward.to,
keepCopy: forward.keepCopy,
...(period.from ? { activeFrom: period.from } : {}),
...(period.until ? { activeUntil: period.until } : {}),
};
}
/** The fields Bulwark stores, in a fixed order. */
export function normalizeVacationForward(forward: VacationForward): VacationForward {
return {
enabled: forward.enabled,
to: forward.to,
keepCopy: forward.keepCopy,
...(forward.activeFrom !== undefined ? { activeFrom: forward.activeFrom } : {}),
...(forward.activeUntil !== undefined ? { activeUntil: forward.activeUntil } : {}),
};
}