Keep filter rules pointing at the folder they were aimed at
A rule files mail into a folder by path, because that is what Sieve needs. Rename the folder and the path becomes a lie: the rule keeps matching and stops filing, and nothing anywhere says so. Delete the folder and the rule is aimed at nothing at all. Renaming or moving a folder now rewrites the rules that file into it, and deleting one takes its rules with it. Both are reported in a toast, because rules live on the server and are otherwise invisible from the folder list. The reconciliation runs off one hook. Before a mailbox is changed the folder and everything beneath it are noted with the paths they have then -- renaming a parent rewrites the path of every child, and rules naming those children are just as stale. Afterwards, whatever still exists is retargeted and whatever has gone takes its rules with it. Rules record the folder twice, as a mailboxId and as the path. The id is the reliable half and is preferred; the path is the fallback for rules written before the id was recorded, or by hand in the Scripts tab, and a rule matched that way has its id filled in on the way past. Only the script the rule editor manages is touched; a hand-written one is left alone. Awaited rather than fired and forgotten, so a folder operation is not reported complete while the rules still disagree with it.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import type { SieveRule } from "./sieve";
|
||||
|
||||
/** A folder as it was before it moved, so rules that name it can be found again. */
|
||||
export interface FolderRef {
|
||||
id: string;
|
||||
/** The path the folder had when the rules were written, e.g. "Work/Invoices". */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a rule files mail into this folder.
|
||||
*
|
||||
* Rules record the folder both ways: `mailboxId` since the rule editor started
|
||||
* setting it, and `mailbox` as the path Sieve actually needs. The id is the
|
||||
* reliable half — it survives a rename — but rules written before it existed,
|
||||
* or by hand in the Scripts tab, only have the path.
|
||||
*/
|
||||
function filesInto(rule: SieveRule, ref: FolderRef): boolean {
|
||||
return rule.actions.some(
|
||||
(a) => a.type === "fileinto" && (a.mailboxId === ref.id || a.mailbox.toLowerCase() === ref.path.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the paths of rules filing into folders that have moved or been
|
||||
* renamed. Returns the rules unchanged, and `changed: 0`, when none match, so
|
||||
* callers can skip saving.
|
||||
*/
|
||||
export function retargetRules(rules: SieveRule[], moves: Array<FolderRef & { newPath: string }>): { rules: SieveRule[]; changed: number } {
|
||||
const wanted = moves.filter((m) => m.newPath !== m.path);
|
||||
if (!wanted.length) return { rules, changed: 0 };
|
||||
let changed = 0;
|
||||
const next = rules.map((rule) => {
|
||||
const move = wanted.find((m) => filesInto(rule, m));
|
||||
if (!move) return rule;
|
||||
changed++;
|
||||
return {
|
||||
...rule,
|
||||
actions: rule.actions.map((a) =>
|
||||
a.type === "fileinto" && (a.mailboxId === move.id || a.mailbox.toLowerCase() === move.path.toLowerCase())
|
||||
? { ...a, mailbox: move.newPath, mailboxId: move.id }
|
||||
: a,
|
||||
),
|
||||
};
|
||||
});
|
||||
return changed ? { rules: next, changed } : { rules, changed: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops rules that file into folders which no longer exist.
|
||||
*
|
||||
* The whole rule goes, not just its fileinto action: a rule whose destination
|
||||
* has been deleted has no destination, and leaving it behind to match mail and
|
||||
* do nothing is worse than removing it. Rules that merely mention the folder in
|
||||
* some other action are left alone.
|
||||
*/
|
||||
export function dropRulesForFolders(rules: SieveRule[], gone: FolderRef[]): { rules: SieveRule[]; removed: SieveRule[] } {
|
||||
if (!gone.length) return { rules, removed: [] };
|
||||
const removed = rules.filter((r) => gone.some((ref) => filesInto(r, ref)));
|
||||
if (!removed.length) return { rules, removed: [] };
|
||||
return { rules: rules.filter((r) => !removed.includes(r)), removed };
|
||||
}
|
||||
Reference in New Issue
Block a user