diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 60 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-08-11-sent-mail-design.md | 180 |
2 files changed, 225 insertions, 15 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 844217e..534dc6e 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 @@ -119,7 +119,7 @@ taking that too literally. | 60 | Next thread dead-ends on the last reply of an expanded thread | defect | XS | **done**; already fixed by 5487d58, see below | | 61 | `test_mainwindow` fails intermittently, about 1 run in 20 | testing | S | open; predates the card list, reproduced on f72dba9 | | 62 | No config option for the date format on a card | presentation | XS | **done** 2026-08-11 | -| 63 | No way to see sent mail, and no filter for it | workflow | S | open | +| 63 | No way to see sent mail, and no filter for it | workflow | M | open; specified 2026-08-11 in `specs/2026-08-11-sent-mail-design.md` | | 64 | The Sync button carries a mailbox icon, not a refresh one | presentation | XS | **done** 2026-08-11 | | 65 | No full code review and optimization pass | correctness | ? | open, unspecified | @@ -4141,20 +4141,33 @@ defaults change and it is XS. If it is a path, `Account` needs a `sent` key beside `drafts`, and the query has to be composed per account, which is where the S comes from. -**Approach, pending that decision.** Ask the user first which their setup -already produces. Their mail is filtered outside qtmaildir (`assets/mailsync.sh` -is `mbsync` plus `notmuch new`), so the answer is a property of their existing -filters, not something to design here. - -**Constraints.** - -- **Do not invent a tag qtmaildir applies itself.** v1 is read-and-organize; - nothing here sends mail, so nothing here can know a message was sent except by - where it landed or what tagged it. -- If it becomes a per-account key, it composes with `scopedQuery()` and must not - bypass it, or a Sent view in one account shows another account's mail. - -**Size: S**, and XS if the answer is "it is a tag". +### Answered and specified 2026-08-11 + +**It is a PATH, not a tag**, so the XS branch above is dead. Measured against +the user's own database: no `sent` tag exists at all, every account keeps sent +mail in a folder, and the folders disagree across three shapes, with one +account having no sent folder whatsoever. That is what forces a per-account key +rather than a `<maildir>/Sent` convention. + +The design is at `docs/superpowers/specs/2026-08-11-sent-mail-design.md`; read +that rather than this entry, which records only the finding. It carries the +measured folder table, the user's four decisions, and the constraints, of which +three are worth knowing before opening it: the bracketed provider paths contain `[` and `]` +and are Xapian syntax, so quoting is load-bearing; notmuch has no recipients +call at any level, so the To summary is folded per message in the worker under +the thread-ownership rule; and GMime's address parser returns NULL for an empty +string. + +One thing settled there that reverses nothing: item 2 refused a `To:` line on a +thread header and that ruling stands. It was scoped to a MIXED conversation, +where the union of recipients misdescribes itself as "To:". A Sent view is +one-directional, so the ambiguity it avoided is absent and recipients on the +card are well posed. + +**Size: M**, revised up from S. The query half is the S scoped here; the +recipients half is a new `ThreadSummary` field, a worker-side per-message walk, +the first GMime address parsing in this codebase, and a card that has to know +which view it is in. ## 64. The Sync button carries a mailbox icon, not a refresh one @@ -4234,3 +4247,20 @@ the same shape: **Observed** (what the user saw), **Cause** (the code, with file and line, verified not assumed), **Approach**, **Constraints**, and **Verification** where it is not obvious. Do not renumber. Do not delete: mark `dropped` with a reason. + +**A fully specified item goes in its own file under `docs/superpowers/specs/`, +not inline here.** This document is a backlog: its job is to say what is open, +how big it is, and what decides whether it can be picked up. A design that runs +to a hundred lines buries that under itself, and this file is already past four +thousand. + +The split is by depth, not by size on the day. An entry stays here while it +records an observation, a cause and an approach. It moves out once it carries +decisions the user made, measured evidence, and constraints that have to be read +before writing code. Items 53 and 63 are the pattern: the entry keeps the +finding and the size, and points at the spec with one line saying to read that +instead. Carry the two or three constraints a reader needs in order to decide +whether to open the spec at all, and leave the rest there. + +Name the spec `<date>-<name>-design.md`, and state in its header which backlog +items it resolves, so the numbering stays traceable in both directions. diff --git a/docs/superpowers/specs/2026-08-11-sent-mail-design.md b/docs/superpowers/specs/2026-08-11-sent-mail-design.md new file mode 100644 index 0000000..53fbb6c --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-sent-mail-design.md @@ -0,0 +1,180 @@ +# Sent mail: a per-account folder, a composed button, and recipients on the card + +**Status:** specified 2026-08-11, not implemented. +**Resolves:** backlog item 63. +**Size:** M, revised up from the backlog's S. The query half is the S that was +scoped correctly; the recipients half is its own piece of work. + +## Why + +The user's notes asked for a "Sent mail filter". Nothing in the codebase knows +what sent means: `Account` carries `name`, `address`, `maildir`, `drafts`, +`label`, `channel` and `color` (`src/config.cpp:254-270`) and no `sent` field, +and no query anywhere composes one. + +The original entry could not be planned because it turned on a fact about the +user's mail rather than a design choice: whether "sent" is a notmuch tag their +filters apply, or a maildir path per account. + +## What the database answered, 2026-08-11 + +Measured rather than asked in the abstract. + +**No `sent` tag exists.** `notmuch search --output=tags '*'` matches nothing +case-insensitively, so the tag branch, which would have made this XS, is dead. + +Every account keeps sent mail in a folder, and the folders disagree. Account +keys below are generic; the shapes and the counts are the real measurement. + +| Account | Sent folder | Messages | +|---|---|---| +| `webmail-primary` | `Sent` | 21 | +| `webmail-secondary` | `Sent` | 52 | +| `provider-a` | `[Provider]/Posta inviata` | 190 | +| `provider-b` | `[Provider]/Posta inviata` | 531 | +| `provider-c` | **none; the account has only `Inbox`** | n/a | + +794 combined. Three shapes across five accounts, and one account with no sent +folder at all. That is what rules out a convention such as `<maildir>/Sent` and +forces a per-account key: a convention would silently produce an empty view for +the two bracket-path accounts and a wrong one for the account that has no such +folder. + +Note the bracket form is a real provider's layout, not an invention: the folder +is nested under a bracketed parent and localised, which is why both halves of +this item have to survive a path that is neither ASCII-simple nor flat. + +**The query already works by hand.** The user pasted the composed `path:` OR +into the query bar and confirmed it returns their sent mail. This item is +therefore not "make it possible" but "make it a button that stays correct as +accounts change", which caps what the query half is allowed to cost. + +## Decisions (user, 2026-08-11) + +**A per-account `sent` key, composed at run time.** Not a shipped `[queries]` +entry. A saved query is one fixed string, so it cannot narrow to the selected +account, and it goes stale silently the moment an account is added or a +provider renames a folder. The user asked for the button to sit beside Inbox, +Unread and Important, and it does; only its query is built rather than stored. + +**An account with no `sent` key is omitted silently.** No config problem. The +key is optional exactly as `drafts` is, and `provider-c` is a real account +that legitimately has no sent folder; reporting it would warn on every launch +about nothing. + +**All accounts combines, one account scopes.** With no account selected the +button runs the OR of every configured sent path. With one selected, +`runCurrentQuery()`'s existing wrap (`src/mainwindow.cpp:1478-1480`) narrows it, +and the intersection is that account's sent mail. + +**A Sent view shows RECIPIENTS on the card, not the sender.** Every message in +it was sent by the user, so a sender column repeats their own name down the +whole list and carries no information. + +### On recipients, which contradicts item 2 and does so correctly + +Item 2 refused a `To:` line on the thread header and deferred a participants +list to its own pass. **That ruling stands where it was made and does not govern +here**, because it was scoped to a different problem. + +Item 2's case is a MIXED conversation: message 1 is To the user, message 2 is To +the other party, so the union of recipients is a participants list wearing a +"To:" label that misdescribes it, and the intersection is frequently empty. The +user's call was that this overcomplicates, and it does. + +A Sent view is one-directional. Every message in it was sent BY the user, so +"who did I send this to" is well posed, and it is the question the view exists +to answer. The ambiguity item 2 avoided is absent here; it was not overruled. + +This distinction is recorded rather than the reversal, because a future reader +finding both entries needs to know why both stand. + +## Approach + +Three pieces, in this order. The first two are independently shippable and give +a working Sent view on their own. + +### 1. Config + +An optional `sent` key on `[account.<key>]`, a folder path relative to +`maildir`, beside the existing `drafts`. + +`Account::sentQuery()` returns `path:"<maildir>/<sent>/**"`, or an empty string +when the key is absent. It sits beside `scopedQuery()` (`src/config.cpp:42-45`) +and follows its shape. + +### 2. The button + +Built beside the saved-query buttons, running the OR of every non-empty +`sentQuery()`. Hidden entirely when no account configures one, so a user with no +`sent` key anywhere does not get a button that finds nothing. + +### 3. Recipients on the card + +`ThreadSummary` gains a recipients summary, filled in the worker, shown by +`CardDelegate` in the sender's place when the row belongs to a Sent view. + +## Constraints + +**Do not invent a tag qtmaildir applies itself.** v1 is read-and-organize; +nothing here sends mail, so nothing here can know a message was sent except by +where it landed. Carried from the original entry and now load-bearing, since the +answer came back "path". + +**`sentQuery()` composes with `scopedQuery()` and must not bypass it.** A Sent +view under one account must not show another account's sent mail. The existing +wrap already does this; the constraint is not to add a second path around it. + +**The folder path is untrusted config and lands inside a notmuch query.** +`[Provider]/Posta inviata` contains `[` and `]`, which are Xapian syntax, and +the quoting is what makes the shipped query work at all: the four-way OR returns +794 only because each path is quoted. Quote every composed path and pin a +bracketed path in a test, or the two accounts that need this most break. + +**notmuch has no recipients call.** `notmuch_thread_get_authors` exists +(`notmuch.h:1435`) and there is no `get_recipients` at any level: To is a +per-message header, not a thread property. The summary is folded in the worker +from `notmuch_message_get_header(msg, "To")` over the thread's messages. + +**The recipient walk obeys the thread-ownership rule.** Messages reached through +the thread are owned by it and freed with it, so they are held as raw +`notmuch_message_t*` and the whole walk finishes while the `NmThread` is alive, +exactly as `walkReplies` does. An `NmMessage` wrapper here is a double-free. +See `CLAUDE.md`. + +**Address parsing is required, and GMime already does it correctly.** Splitting +on commas is wrong: `"Rossi, Mario" <mario@example.org>, info@example.net` is +two addresses, not three. Verified empirically against +`internet_address_list_parse`, which returns 2 for that input, keeps the display +name whole, and reports `undisclosed-recipients:;` as a group. + +**It returns NULL for an empty string**, which is a crash if unguarded and is +the first test case to write. + +**Prefer the display name, fall back to the address.** A row reading +`mario@example.org` where the rest of the list reads `Mario Rossi` is the +inconsistency the card's sender column already avoids. + +**Decide what several recipients render as when building, and keep it in +`CardLayout`** so it is testable without a painter. "Name, Name" elided by the +existing sender rect is the cheap answer; a `+N` suffix mirrors the tag strip's +overflow chip. Do not compute it in the delegate's `paint()`: a card layout must +be testable without a painter, per `CLAUDE.md`. + +**gmime headers before any Qt header** in the same translation unit, if the +address parsing lands in a file that has both. glib declares a struct field +named `signals`, which Qt defines as a macro. + +## Verification + +- The composed query returns the same **794** the hand-written one does, with + the bracketed paths included, and an account with no key contributes + nothing. +- Selecting one account narrows Sent to that account and never leaks another's. +- A message with a comma inside a quoted display name renders **one** recipient. +- An empty or absent `To` renders blank rather than crashing. +- A bracketed path containing `[` and `]` survives composition into a query + and returns its real count rather than zero. + +The first and last are the ones that fail loudest if the quoting is wrong, and +they are the reason to write them before the UI work rather than after. |
