Keep filter rules pointing at the folder they were aimed at #37

Closed
opened 2026-08-25 19:12:50 +00:00 by jcoffey-dev · 1 comment
Owner

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 quietly stops filing. Delete the folder and the rule is aimed at nothing.

Renaming or moving a folder now rewrites the rules that file into it; deleting one takes its rules with it. Both are reported in a toast, since rules live on the server and are invisible from the folder list.

How it hangs together

One hook, both cases. Before a mailbox changes, the folder and everything beneath it are noted with the paths they have at that moment — renaming a parent rewrites every child's path, and rules naming those children go stale just the same. Afterwards, whatever still exists is retargeted and whatever has gone takes its rules with it.

Rules record the destination twice: mailboxId (set by the rule editor and by "filter messages like this") and mailbox, the path. The id is the reliable half and is preferred; the path is the fallback for older rules, or ones written by hand in the Scripts tab — and a rule matched by path has its id filled in on the way past, so the next rename needs no guessing.

Only the script the visual editor manages is touched. A hand-written script is left alone.

The reconciliation is awaited rather than fired and forgotten: a folder operation should not report success while the rules still disagree with it. It never throws — the folder change has already happened, and failing to tidy the rules must not make it look otherwise; that case reports its own error.

One judgement call worth your eye

Deleting a folder removes the whole rule, as asked. Worth knowing what that means for a rule like from: boss → fileinto Work, markread, stop: deleting Work drops the mark-as-read and the stop as well. The alternative — remove just the fileinto and delete the rule only if nothing is left — is a two-line change if you prefer it. I implemented what you asked for; say the word and I will switch it.

Verified

Unit: 10 new cases over the pure logic — rename by id, rename by path only, a child whose parent moved, a no-op move returning the same array, other rules untouched, other actions preserved, delete by id, delete of a subtree, delete matched by path, and no-match leaving the list alone. npm run typecheck, npm test — 211 web + 88 server, all passing.

End to end, driving the real app against the mock and then reading the stored script from the server rather than trusting the DOM:

  • created a rule filing into Newsletters → renamed the folder to Reading → the rule reads move to Reading
  • deleted the folder → the stored ihasmail script contains no rules at all

One thing I could not pin down

In my scripted harness, a full page reload immediately after the folder delete sometimes leaves the app stuck at boot — a spinner, no JS errors, and the server answering /api/session in 7ms. I could not attribute it to this change: awaiting the save rather than firing it made no difference, and a baseline run with the feature disabled hung rather than giving a clean comparison. The rules themselves are correct on the server in every run. Flagging it rather than leaving it unsaid; it may well be my automation navigating faster than a person can.

🤖 Generated with Claude Code

Merged 2026-08-25 as coffey-labs/ihasmail@b7f4b4e496

Rebuilt from: git history, session transcript.

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 quietly stops filing. Delete the folder and the rule is aimed at nothing. Renaming or moving a folder now rewrites the rules that file into it; deleting one takes its rules with it. Both are reported in a toast, since rules live on the server and are invisible from the folder list. ## How it hangs together One hook, both cases. Before a mailbox changes, the folder **and everything beneath it** are noted with the paths they have at that moment — renaming a parent rewrites every child's path, and rules naming those children go stale just the same. Afterwards, whatever still exists is retargeted and whatever has gone takes its rules with it. Rules record the destination twice: `mailboxId` (set by the rule editor and by "filter messages like this") and `mailbox`, the path. The id is the reliable half and is preferred; the path is the fallback for older rules, or ones written by hand in the Scripts tab — and a rule matched by path has its id filled in on the way past, so the next rename needs no guessing. Only the script the visual editor manages is touched. A hand-written script is left alone. The reconciliation is awaited rather than fired and forgotten: a folder operation should not report success while the rules still disagree with it. It never throws — the folder change has already happened, and failing to tidy the rules must not make it look otherwise; that case reports its own error. ## One judgement call worth your eye **Deleting a folder removes the whole rule**, as asked. Worth knowing what that means for a rule like `from: boss → fileinto Work, markread, stop`: deleting *Work* drops the mark-as-read and the stop as well. The alternative — remove just the `fileinto` and delete the rule only if nothing is left — is a two-line change if you prefer it. I implemented what you asked for; say the word and I will switch it. ## Verified Unit: 10 new cases over the pure logic — rename by id, rename by path only, a child whose parent moved, a no-op move returning the same array, other rules untouched, other actions preserved, delete by id, delete of a subtree, delete matched by path, and no-match leaving the list alone. `npm run typecheck`, `npm test` — 211 web + 88 server, all passing. End to end, driving the real app against the mock and then reading the stored script **from the server** rather than trusting the DOM: - created a rule filing into `Newsletters` → renamed the folder to `Reading` → the rule reads `move to Reading` - deleted the folder → the stored `ihasmail` script contains no rules at all ## One thing I could not pin down In my scripted harness, a full page reload **immediately** after the folder delete sometimes leaves the app stuck at boot — a spinner, no JS errors, and the server answering `/api/session` in 7ms. I could not attribute it to this change: awaiting the save rather than firing it made no difference, and a baseline run with the feature disabled hung rather than giving a clean comparison. The rules themselves are correct on the server in every run. Flagging it rather than leaving it unsaid; it may well be my automation navigating faster than a person can. 🤖 Generated with [Claude Code](https://claude.com/claude-code) **Merged** 2026-08-25 as coffey-labs/ihasmail@b7f4b4e49612 <sub>Rebuilt from: git history, session transcript.</sub>
Author
Owner

Switched to dropping only the filing action, per your call — 3292524.

detachFolders() now removes just the fileinto aimed at the deleted folder. A rule that also marked read, flagged or stopped keeps doing those things. A rule left with no actions at all is still removed, since it has nothing to do, and a rule filing into two folders keeps the one that still exists.

Verified in the running app with two rules aimed at the same folder:

rule actions before after deleting the folder
Only files fileinto Newsletters removed
Files and marks read fileinto Newsletters, markread kept, markread only

The script stored on the server agrees — the surviving rule reads "actions":[{"type":"markread"}].

The toast distinguishes the two outcomes: "1 filter rule no longer files there · 1 filter rule removed, having nothing left to do: “Only files”".

Tests updated: 12 over this module now, covering the filing-only rule, the rule with other actions, a subtree delete where one rule survives and one does not, a rule with a second fileinto pointing elsewhere, and path-only matching. 213 web + 88 server passing.

Switched to dropping only the filing action, per your call — 3292524. `detachFolders()` now removes just the `fileinto` aimed at the deleted folder. A rule that also marked read, flagged or stopped keeps doing those things. A rule left with no actions at all is still removed, since it has nothing to do, and a rule filing into two folders keeps the one that still exists. Verified in the running app with two rules aimed at the same folder: | rule | actions before | after deleting the folder | | --- | --- | --- | | Only files | `fileinto Newsletters` | removed | | Files and marks read | `fileinto Newsletters`, `markread` | kept, `markread` only | The script stored on the server agrees — the surviving rule reads `"actions":[{"type":"markread"}]`. The toast distinguishes the two outcomes: *"1 filter rule no longer files there · 1 filter rule removed, having nothing left to do: “Only files”"*. Tests updated: 12 over this module now, covering the filing-only rule, the rule with other actions, a subtree delete where one rule survives and one does not, a rule with a second `fileinto` pointing elsewhere, and path-only matching. 213 web + 88 server passing.
This repo is archived. You cannot comment on issues.