aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-14 18:49:48 +0200
committerDanilo M. <danix@danix.xyz>2026-09-14 18:49:48 +0200
commitc2753771096e7c303da4b351986f7f313917dcf8 (patch)
tree2f96b6c85c4bd5bb74382a2cc3492eeba173e039 /docs
parentc1c196ebeef9a17defe8a3070e8f10d548ffdf40 (diff)
downloadqtmaildir-c2753771096e7c303da4b351986f7f313917dcf8.tar.gz
qtmaildir-c2753771096e7c303da4b351986f7f313917dcf8.zip
fix(hooks): spam is not an inbox arrival
notmuch's new.tags applies inbox to every newly indexed file and the Inbox filter is tag:inbox, so spam-folder mail appeared in the Inbox view. Add spam to the post-new hook's NOT_ARRIVALS set so the existing all-files carve-out covers it, and rename the sent_* helpers to not_arrival_* now that the list means more than sent mail. Trash stays out: measured 0 affected files and it is out of scope.
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md39
1 files changed, 39 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
index 9d3979f..3e22dcd 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
@@ -275,6 +275,7 @@ taking that too literally.
| 200 | qtmaildir cannot be launched at a given account, thread or message | workflow | M | open, **specified 2026-09-13** in `specs/2026-09-13-cli-selectors-design.md`; read that rather than this row. The user settled three things: a second launch STEERS the running window over a `QLocalServer` rather than opening a second one, the selectors are `--account`/`--thread`/`--message` (`--query` dropped as the one with no caller), and a selector matching nothing opens the window normally and says so in the status bar. The design shrank on one side and grew on the other: `recoverStaleThread()` already runs `thread:<id>` with a deferred selection and is reused as a third caller, so the selectors are the small half, while the socket (connect-first ordering, stale-socket recovery, a degrade path when no socket is possible) is the real work and adds `Qt6::Network` to the component list. Original entry: open, 2026-09-13, from the notes ("the program should accept cli parameters like `--account` or `--thread`/`--message`, so that another app can launch qtmaildir opening that account's inbox or a certain message/thread"). Verified: `main.cpp:38-66` hand-rolls a `strcmp` loop over `argv` for `--version` and `--help` only, both answering before `QApplication` exists, which is deliberate and documented. Parsing is the small half and `QCommandLineParser` covers it; the item is bigger than it looks for two reasons. There is NO single-instance mechanism (no `QLocalServer` anywhere in `src/`), so a second launch opens a second window against the same notmuch database rather than steering the running one, and notmuch permits only one open handle per process. And the selector has to reach a query the startup path does not currently take, since `--thread` names a row that may not be in the configured startup view at all. Needs a decision from the user first: whether a second launch should focus the running window (which is the useful behaviour for "another app launches qtmaildir" and is the whole cost of the item) or simply start with a different query |
| 197 | No way to say a message is not spam | workflow | S | open, 2026-09-10, split out of the 187 design at the user's decision rather than built into it. Restore already covers what qtmaildir moved: a message it marked carries `moved-from:` and goes back where it came from. The gap is mail the PROVIDER's filter caught, which was never in an inbox and carries no origin tag, so "not spam" has no recorded destination to return it to. Needs two answers before it can be planned: where such a message goes (the account's inbox is the obvious guess and is a guess), and whether anything should tell the provider its filter was wrong, which is network work this application does not do and would belong in a sidecar like item 194's. No seam is needed in the meantime: `sendMove()` already takes any destination and any tags |
| 201 | A message in the Spam view cannot be un-spammed, even one qtmaildir put there | defect | S | open, 2026-09-14, found while testing the `spam-view` branch. Corrects item 197's claim that Restore already covers what qtmaildir moved: the CAPABILITY does (`restoreSelectedFromTrash()` reads the origin from the database), but no SURFACE offers it for spam, so only Ctrl+Z immediately after the move reverses it. The user asked for it to be built on `spam-view` before that branch merges. See the section |
+| 202 | Mail in a spam folder keeps `inbox`, so it appears in the Inbox view | defect | XS | open, 2026-09-14, found while testing the `spam-view` branch. notmuch's `new.tags` is `new;unread;inbox`, the Inbox filter is `tag:inbox`, and the hook's non-arrival carve-out (`NOT_ARRIVALS` in `assets/hooks/qtmaildirconf.py:84`) lists only sent and drafts, so spam-folder mail keeps `inbox` and shows in the Inbox view. Surfaced as "Not spam is offered in the Inbox view": the action's folder-based predicate is correct, the ROWS are the defect. Measured live: 49 files tagged `inbox` in a `[Gmail]/Spam` folder, 0 in trash/drafts/sent. Fix on `spam-view`. See the section |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1832,3 +1833,41 @@ whether to tell the provider its filter was wrong, stay out of scope here.
mark a message spam, then un-spam it through the new path, asserting the file
returns to the folder it came from, `spam` and `moved-from:` are gone, and the
row leaves the Spam view. Because it is a move, assert the undo as well.
+
+## 202. Mail in a spam folder keeps `inbox`, so it appears in the Inbox view
+
+**Observed (user, 2026-09-14, testing `spam-view`).** "Not spam is available in
+the inbox view; it should appear only when viewing the spam view." The action is
+not at fault: its predicate is folder-based (`everySelectedRowIsInAFolder()`,
+the same rule Restore uses), so it correctly appears on mail whose FILE is in a
+spam folder. Those rows should not have been in the Inbox view at all.
+
+**Cause.** Verified, and not in the application. notmuch's `new.tags` is
+`new;unread;inbox`, so every newly indexed FILE gets `inbox` regardless of the
+folder it sits in. The Inbox built-in filter is `tag:inbox`
+(`Config::generatorTag("inbox")`), not path-scoped, so any file carrying `inbox`
+appears there. The `post-new` hook already corrects this for mail that did not
+ARRIVE: `NOT_ARRIVALS` in `assets/hooks/qtmaildirconf.py:84` is
+`("sent", "drafts")`, and `strip_inbox_from_sent()` removes `inbox` from a
+message whose files are ALL inside one of those folders. `spam` is missing
+because the hook predates the per-account `spam` key this branch adds. Measured
+live: 49 files tagged `inbox` sitting in a `[Gmail]/Spam` folder; 0 in trash,
+drafts or sent.
+
+**Approach.** Add `spam` to `NOT_ARRIVALS` so the existing carve-out covers it
+too, correct the function/doc prose (its names say "sent" while the list means
+"not an arrival"), and extend the hook's tests with the spam cases. Then a
+ONE-TIME cleanup of the existing messages, using the same all-files rule.
+
+**Constraints.** The all-files rule is load-bearing (`sent_only()`): notmuch
+deduplicates by Message-ID, so a message with one file in spam and one in an
+inbox DID genuinely arrive and must keep `inbox`. Only `inbox` is removed;
+`unread` is untouched because `maildir.synchronize_flags` is true. The cleanup
+is a write to the live index and is confirmed with the user before running (it
+was, 2026-09-14). Trash is deliberately out of scope: measured 0 such files, and
+qtmaildir's own Delete strips `inbox` (item 168).
+
+**Verification.** `assets/hooks/test_post_new.py` (and the other hook suites)
+green with a spam-folder case and a two-file (spam + inbox) case; after the
+cleanup, `notmuch search --output=files 'tag:inbox' | grep -i '/spam/'` is
+empty.