diff --git a/components/filters/filter-rule-modal.tsx b/components/filters/filter-rule-modal.tsx index 9f6fb2a7c..564f9e8b3 100644 --- a/components/filters/filter-rule-modal.tsx +++ b/components/filters/filter-rule-modal.tsx @@ -23,6 +23,10 @@ import { useKeywordFormat } from "@/hooks/use-keyword-format"; interface FilterRuleModalProps { rule?: FilterRule; mailboxes: Mailbox[]; + /** Server cap on redirects per message (Sieve `maxNumberRedirects`). */ + maxRedirects?: number | null; + /** Forward actions in the other enabled rules, which share that cap. */ + otherForwards?: number; onSave: (rule: FilterRule) => void; onClose: () => void; } @@ -84,6 +88,8 @@ function makeEmptyAction(): FilterAction { export function FilterRuleModal({ rule, mailboxes, + maxRedirects, + otherForwards = 0, onSave, onClose, }: FilterRuleModalProps) { @@ -121,6 +127,20 @@ export function FilterRuleModal({ return { hierarchicalMailboxes: flattenMailboxTree(tree), mailboxPathMap: pathMap }; }, [mailboxes]); + // Rules saved before folder ids were stored only carry the path; resolve it + // so the select shows the folder and the next save adds the id. + const mailboxIdFor = useCallback((action: FilterAction): string => { + if (action.mailboxId) return action.mailboxId; + for (const [id, path] of mailboxPathMap) { + if (path === action.value) return id; + } + return ""; + }, [mailboxPathMap]); + + const forwardCount = actions.filter((a) => a.type === "forward").length; + const forwardLimit = typeof maxRedirects === "number" && maxRedirects > 0 ? maxRedirects : null; + const forwardOverLimit = forwardLimit !== null && forwardCount + otherForwards > forwardLimit; + const handleSave = useCallback(() => { const trimmedName = name.trim(); if (!trimmedName) { @@ -150,9 +170,16 @@ export function FilterRuleModal({ return; } - const validActions = actions.filter( - (a) => !ACTIONS_WITH_VALUE.has(a.type) || a.value?.trim() - ); + const validActions = actions + .map((a) => { + if (!ACTIONS_WITH_MAILBOX.has(a.type)) return a; + // Store the folder id next to the path and refresh the path from it, + // so a renamed folder keeps receiving the rule's mail. + const mailboxId = mailboxIdFor(a); + const path = mailboxId ? mailboxPathMap.get(mailboxId) : undefined; + return mailboxId ? { ...a, mailboxId, value: path ?? a.value } : a; + }) + .filter((a) => !ACTIONS_WITH_VALUE.has(a.type) || a.value?.trim()); if (validActions.length === 0) { toast.error(t("validation_empty_actions")); return; @@ -167,7 +194,7 @@ export function FilterRuleModal({ actions: validActions, stopProcessing, }); - }, [name, matchType, conditions, actions, stopProcessing, rule, onSave, t]); + }, [name, matchType, conditions, actions, stopProcessing, rule, onSave, t, mailboxIdFor, mailboxPathMap]); const updateCondition = (index: number, updates: Partial) => { setConditions((prev) => @@ -213,9 +240,16 @@ export function FilterRuleModal({ if (updates.type && !ACTIONS_WITH_VALUE.has(updates.type)) { delete updated.value; } + if (updates.type && !ACTIONS_WITH_MAILBOX.has(updates.type)) { + delete updated.mailboxId; + } + if (updates.type && updates.type !== "forward") { + delete updated.keepCopy; + } if (updates.type && ACTIONS_WITH_MAILBOX.has(updates.type) && !updated.value) { const firstMb = hierarchicalMailboxes[0]; updated.value = firstMb ? (mailboxPathMap.get(firstMb.id) || firstMb.name) : ""; + if (firstMb) updated.mailboxId = firstMb.id; } return updated; }) @@ -429,36 +463,65 @@ export function FilterRuleModal({ aria-label={t("actions")} > {ALL_ACTION_TYPES.map((a) => ( - ))} - {ACTIONS_WITH_MAILBOX.has(action.type) && ( - - )} + {ACTIONS_WITH_MAILBOX.has(action.type) && (() => { + const selectedId = mailboxIdFor(action); + const missing = !!selectedId && !mailboxPathMap.has(selectedId); + return ( + + ); + })()} {action.type === "forward" && ( - updateAction(index, { value: e.target.value })} - placeholder={t("forward_placeholder")} - type="email" - className="flex-1 min-w-[180px]" - /> + <> + updateAction(index, { value: e.target.value })} + placeholder={t("forward_placeholder")} + type="email" + className="flex-1 min-w-[180px]" + /> + + )} {action.type === "reject" && ( @@ -496,6 +559,11 @@ export function FilterRuleModal({ ))} + {forwardOverLimit && ( +

+ {t("forward_limit", { count: forwardLimit })} +

+ )}