From dcbfc907fae782eb5f1026263bbd0c5b90cd7f24 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:05:57 +0100 Subject: [PATCH] feat: warn about the server's forward limit and an auto-reply too long for it (bulwarkmail/webmail#1155, bulwarkmail/webmail#1208) --- src/api/sieve-rules.ts | 10 ++ src/components/filters/FilterRuleModal.tsx | 43 ++++- src/components/filters/QuickRuleSheet.tsx | 27 ++- src/components/settings/VacationSettings.tsx | 40 ++++- src/lib/__tests__/vacation-limits.test.ts | 28 +++ .../__tests__/forward-limit-editor.test.ts | 62 +++++++ .../__tests__/forward-limit-vacation.test.ts | 59 +++++++ src/lib/sieve/__tests__/forward-limit.test.ts | 162 ++++++++++++++++++ src/lib/sieve/forward-limit.ts | 136 +++++++++++++++ src/lib/vacation-limits.ts | 29 ++++ src/stores/__tests__/filter-store.test.ts | 11 ++ src/stores/__tests__/vacation-store.test.ts | 2 +- src/stores/filter-store.ts | 8 + src/stores/vacation-store.ts | 4 + 14 files changed, 614 insertions(+), 7 deletions(-) create mode 100644 src/lib/__tests__/vacation-limits.test.ts create mode 100644 src/lib/sieve/__tests__/forward-limit-editor.test.ts create mode 100644 src/lib/sieve/__tests__/forward-limit-vacation.test.ts create mode 100644 src/lib/sieve/__tests__/forward-limit.test.ts create mode 100644 src/lib/sieve/forward-limit.ts create mode 100644 src/lib/vacation-limits.ts diff --git a/src/api/sieve-rules.ts b/src/api/sieve-rules.ts index 672faf77..03ad09df 100644 --- a/src/api/sieve-rules.ts +++ b/src/api/sieve-rules.ts @@ -33,6 +33,16 @@ export class FiltersLoginChangedError extends Error { } } +/** The account's filters as the server has them now (its active script, parsed). */ +export async function readAccountFilters(accountId: string = getSieveAccountId()): Promise { + const all = await getSieveScripts(accountId); + const scripts = all.filter((s) => s.name !== VACATION_SCRIPT_NAME); + const script = scripts.find((s) => s.isActive) || scripts[0] || null; + return script + ? parseScript(await getSieveScriptContent(script.blobId, accountId)) + : { rules: [], isOpaque: false, externalRequires: [] }; +} + /** * Read the account's script from the server, let `modify` change its rules, * and write the result back as the active script, keeping the vacation diff --git a/src/components/filters/FilterRuleModal.tsx b/src/components/filters/FilterRuleModal.tsx index 503cf800..7968b597 100644 --- a/src/components/filters/FilterRuleModal.tsx +++ b/src/components/filters/FilterRuleModal.tsx @@ -42,6 +42,8 @@ import type { FilterActionType, } from '../../lib/sieve/types'; import { SafeAreaModal } from '../SafeAreaModal'; +import { useFilterStore } from '../../stores/filter-store'; +import { forwardsOfEditedRule, ruleForwardLimit } from '../../lib/sieve/forward-limit'; import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations'; const ALL_FIELDS: FilterConditionField[] = ['from', 'to', 'cc', 'subject', 'header', 'size', 'body', 'attachment', 'all']; @@ -72,12 +74,19 @@ interface FilterRuleModalProps { mailboxes: Mailbox[]; /** The server can confine a rule to a period (Sieve "date" and "relational"). */ periodsSupported?: boolean; + /** + * The server's cap on forwards per message, and the forwards a message can + * have collected when it reaches this rule / still collect below it (see + * lib/sieve/forward-limit.ts). Left out: read from the filter list the + * Filters settings show. + */ + forwardContext?: { maxRedirects: number | null | undefined; before: number; after: number }; onSave: (rule: FilterRule) => void; onClose: () => void; } export function FilterRuleModal({ - visible, rule: editedRule, initialRule, mailboxes, periodsSupported = false, onSave, onClose, + visible, rule: editedRule, initialRule, mailboxes, periodsSupported = false, forwardContext, onSave, onClose, }: FilterRuleModalProps) { const c = useColors(); const styles = useMemo(() => makeStyles(c), [c]); @@ -120,6 +129,25 @@ export function FilterRuleModal({ const mailboxTargets = useMemo(() => buildMailboxTargets(mailboxes), [mailboxes]); + // A message can collect only so many forwards on the server (#1155, #1208). + const listedRules = useFilterStore((s) => s.rules); + const listedForward = useFilterStore((s) => s.vacationForward); + const listedMaxRedirects = useFilterStore((s) => s.sieveCapabilities?.maxNumberRedirects); + const listedAround = useMemo( + () => forwardsOfEditedRule(listedRules, listedForward, editedRule?.id), + [listedRules, listedForward, editedRule?.id], + ); + const forwardLimit = ruleForwardLimit( + { actions, stopProcessing }, + forwardContext ? forwardContext.maxRedirects : listedMaxRedirects, + forwardContext ?? listedAround, + ); + const ruleForwardCount = actions.filter((a) => a.type === 'forward').length; + const actionOptionsFor = (action: FilterAction) => + action.type === 'forward' || forwardLimit.canAddForward + ? actionTypeOptions + : actionTypeOptions.filter((o) => o.value !== 'forward'); + const fieldOptions = useMemo( () => ALL_FIELDS.map((f) => ({ value: f, label: t(`settings.filters.condition_fields.${f}`, f) })), [t], @@ -407,7 +435,7 @@ export function FilterRuleModal({