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({