diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 127 |
1 files changed, 127 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 47c46fb..f48479e 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 @@ -266,6 +266,8 @@ taking that too literally. | 190 | Mark spam is not on the message bar, and its icon was never chosen for one | presentation | XS | open, 2026-09-06, from the notes. The bar's ordinary branch carries Reply, Forward, Star, Archive, Delete after item 189 and `spam` is not among them, though it meets the bar's rule (selection-scoped, undoable). Two halves: put it on the bar, and settle the icon, which the note asks to be "a bug, or a skull, or something that signifies bad/evil" and which is `mail-mark-junk` today, chosen for a menu where the label carries the meaning. **Paired with 187**, which changes what the action DOES (moves the file); ordering is the user's call | | 191 | The Sent view collapses two messages you sent in one conversation into one row | defect | S | **done 2026-09-06**, unreleased, from a hand test. The Sent and Drafts views are flat, but the worker emitted one summary per THREAD and picked a single matched message to stand for it, oldest-first. A conversation replied to twice showed one row, dated by the thread and opening the OLDER message, and the newer one was reachable nowhere. Also a data-safety defect: `firstMessagePath` named the wrong file, so Delete would have moved it. A second half, found by hand once the rows appeared: the sort notmuch applies is a THREAD sort, so both rows took their thread's position and an older reply drew above a newer one. Flat rows are now sorted as one list. Section in the closed file | | 192 | A sent message does not appear in the Sent view until the next sync | defect | XS | **done 2026-09-06**, unreleased. The sent copy was filed correctly and never announced, so the index did not know it and the Sent view, a path query, could not show it. Measured as 65 files against 64 indexed. One signal to the worker, mirroring what drafts have had since item 158. The open question, whether the view should also refresh, was answered yes by the user on 2026-09-07 and built: `indexChanged()` to `refreshCurrentQuery()`. Section in the closed file | +| 193 | The composer has no headings control | v2 | S | open, 2026-09-08, from the notes: "headers dropdown in the editor, H1 to H6 translating to #, ## ... already supported by the html render". The note is right about the renderer: cmark-gfm parses ATX headings in the core grammar, so `## x` already renders. The gap is composer-side. A heading is a LINE PREFIX, not a wrap, so it cannot go through `applyFormat()`/`MarkdownFormat::wrap()`; it is `quote()`'s shape, and unlike quote it must REPLACE an existing prefix rather than stack one, or a second press gives `## ## x`. That makes it the first formatting control that has to read the line's current state, which is item 135's question arriving early on one control | +| 194 | No abuse reporting from a flagged message | workflow | L, split | open, 2026-09-08, from the notes and **confirmed by the user the same day as a feature they want and will build**. Parse a flagged `.eml`, extract IOCs, resolve abuse contacts via RDAP, generate X-ARF (RFC 5965), fan out to AbuseIPDB/URLhaus/VirusTotal and to abuse desks, backed by MISP via PyMISP. **One gesture here, the engine in a sidecar**: the split is architectural (four outbound protocols, which `src/` does not do) and not a judgement on the feature. qtmaildir's half is a message-bar button that marks spam and offers to report, with a confirmation; it is S and buildable before the sidecar exists. The user is a security consultant filling a phishing database, so the sidecar is the point rather than an accessory. Needs a spec for the sidecar; the qtmaildir half needs only 187/190 settled. Two of the user's constraints are safety properties: redact recipient identifiers before submission, and never fetch remote content during parsing | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -1475,3 +1477,128 @@ dialog names the scope ("every account" or the account's display name). Recorded rather than dropped so the notes' question has an answer here, which is where the user will look for it. + +## 193. The composer has no headings control + +**Observed (user, from the notes):** "headers dropdown in the editor. H1 to H6 +translating to #,##... for markdown, already supported by the html render." + +**Cause, verified in the code.** Two halves, and the note is right about both. + +The renderer half is already done. `MarkdownRenderer` runs cmark-gfm with +`CMARK_OPT_DEFAULT | CMARK_OPT_SAFE` and attaches the four GFM extensions +(`src/markdownrenderer.cpp:74`). ATX headings are CommonMark CORE rather than an +extension, so `## Heading` already parses and renders today; nothing about the +render path needs touching. A user who types the hashes by hand gets a heading. + +The composer half does not exist. The formatting row is built inline in +`ComposeWindow` (`composewindow.cpp:632` onwards) and offers exactly Bold, +Italic, Code, Strikethrough, Link and Quote. There is no heading action, and +`KeyMap::knownActions()` carries none. + +**The shape is `quote()`'s, not `wrap()`'s, and that is the whole difficulty.** +`applyFormat()` goes through `MarkdownFormat::wrap()`, which brackets a +selection with a token on each side. A heading is a LINE PREFIX, so it cannot be +expressed that way, exactly as `MarkdownFormat::quote()` records for `> `. + +But a heading is not `quote()` either, and the difference is the work. Quote +deliberately STACKS: a second press gives `> > one`, which is a real nesting a +user might want. Headings do not nest, so a second press must REPLACE: `##` +applied to `# x` has to yield `## x`, never `## # x`, and choosing H2 then H1 +has to end at `# x`. That makes this the first formatting control that must read +the line's existing state before deciding what to write, which is precisely the +question item 135 raises for the wrap buttons. This item arrives at it early, on +one control, where it is much smaller: a line prefix is unambiguous to detect +(`^#{1,6} `), where a wrap token is not. + +**Approach.** A `MarkdownFormat::heading(text, start, end, int level)` beside +`quote()`, pure over values and tested there like the rest of the namespace. +Level 0 means "remove the prefix", which is what makes the control reversible +without a toggle. In the UI the note asks for a dropdown, which is right: six +buttons would swamp a row that has six controls in total. + +**Constraints.** + +- A line prefix must be idempotent per level and must not stack. The test that + matters applies H2 to an existing H1 and asserts one prefix survives. +- Multi-line selections: a heading applies per line, like quote. Whether + heading a paragraph of five lines makes five headings is a decision, and the + cheap honest answer is yes, since that is what the markdown means. +- `KeyMap::knownActions()`, the icon table and a menu all need the new action, + per the five-places rule, whether or not it gets a default shortcut. +- Every label needs `tr()`, and the level names (H1..H6) are wire format in the + markdown but labels in the dropdown. + +**Size: S.** + +## 194. No abuse reporting from a flagged message + +**Observed (user, from the notes):** parse a flagged `.eml`, extract IOCs +(sending IP, envelope and href domains, redirect chains), resolve abuse contacts +per IOC via RDAP, generate X-ARF (RFC 5965) reports for each. Fan out to +AbuseIPDB, URLhaus and VirusTotal where keys allow, and to abuse-desk email +where they do not. Back it with MISP via PyMISP for IOC storage, correlation +across reports and dedup. Key submissions on stable IOCs rather than per +message, since campaigns rotate subdomains while reusing infrastructure. Redact +recipient identifiers before submission, and never fetch remote content during +parsing. + +**The user wants this and intends to build the sidecar themselves**, stated on +2026-09-08: they are a cybersecurity consultant, they have been phished, and +they want to act fast on a campaign that targets them. Filling a database with +phishing attempts is a feature no other client offers and is a reason this one +exists. Nothing below is a reason not to build it; it is only about WHERE each +part lives. + +**One gesture here, the engine in a sidecar.** Recorded in this backlog because +it covers the mail system rather than only this binary, which is why item 166 +could land in `assets/hooks/`. The split falls where it does on two grounds: + +- **It is network protocol work.** RDAP lookups, three vendor APIs and + abuse-desk email are all outbound network, and this application does no + network protocol work at all by design. Fetching and sending are external + scripts; this would be another one. +- **Its ecosystem is Python.** PyMISP is the reference MISP client, and the + hooks in `assets/hooks/` already establish Python as the language for the + parts of the mail system that are not the GUI. + +**What is already here.** `save_message` writes a message out as `.eml` +(`mainwindow.cpp:139`), which is exactly the input such a tool takes, and +`spam` already exists as an action. So qtmaildir's contribution is plausibly +one gesture: "report this", handing a path to a sidecar and reporting what came +back. Item 187 is relevant, since a Spam view is where a user would reach for +this, and 194 should follow it rather than lead. + +**Two of the user's own constraints are safety properties**, stated in the note +and not to be traded away for convenience: + +- **Redact recipient identifiers before submission.** A report goes to third + parties, so the user's own addresses, folder names and message ids must not + ride along. This is the personal-data rule with a network on the end of it. +- **Never fetch remote content during parsing.** Resolving a redirect chain by + FOLLOWING it confirms the address is live to the sender, and fetching a + tracking href is exactly the beacon the message wanted. Chains come from + headers and href text, never from a request. + +**Split into two pieces that ship independently.** + +*The qtmaildir half is S and does not wait for the sidecar.* A message-bar +button that marks spam and offers to report it, with a confirmation dialog for +the report. Confirmation is right here and is not a contradiction of the +no-confirmation rule: that rule gives the user undo INSTEAD of a dialog, and a +submission to a third party has no inverse to push, which is the same reasoning +that makes `empty_trash` ask (item 118). Submitting is irreversible in the way +that matters, since a report cannot be recalled from AbuseIPDB. + +It should be built AFTER 187 and 190, which decide what Mark spam does and put +it on the bar; this adds a second act to that gesture rather than a second +button beside it. + +*The sidecar is L and needs a spec.* The user is writing it. What that spec has +to settle before code: which vendors are wanted and where the keys live; +whether MISP is an instance already running or a new dependency; the wire +contract between qtmaildir and the tool (a path on argv is the obvious answer, +with the exit status and stdout as the report); and what qtmaildir shows when a +report succeeds, partially succeeds, or fails, since a fan-out to four +destinations can do all three at once. + |
