From 694ec02eb652fcfdbf65c27f68f4607f88615f76 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 11 Aug 2026 11:33:10 +0200 Subject: docs: specify item 63, sent mail, in its own design file The backlog entry could not be planned because it turned on a fact about the user's mail rather than a design choice. Measured against the real database: no `sent` tag exists at all, so the XS branch is dead. Every account keeps sent mail in a folder, and the folders disagree across three shapes, with one account having no sent folder whatsoever. That rules out a `/Sent` convention and forces a per-account key. Decisions recorded: a per-account `sent` key composed at run time rather than a shipped [queries] entry, which cannot narrow to the selected account and goes stale when an account is added; an account without the key omitted silently, as `drafts` already is; all-accounts combines and one account scopes through the existing wrap; and recipients on the card in a Sent view, since the sender is the user on every row. Three constraints came from probing rather than memory. A bracketed provider path contains `[` and `]`, which are Xapian syntax, so quoting is load-bearing and the composed query returns its real count only because each path is quoted. notmuch has get_authors and no recipients call at any level, so the To summary must be folded per message in the worker, under the same thread-ownership rule as walkReplies. And GMime's internet_address_list_parse returns NULL for an empty string, which is a crash if unguarded; it also parses a comma inside a quoted display name correctly, so comma-splitting is out. Size revised M, up from S: 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. The spec lives in its own file rather than inline. The backlog is past four thousand lines and its job is to say what is open and how big, which a hundred lines of design buries. The entry keeps the finding and the size and points at the spec, following item 53. "Adding to this document" now states the split rule so the next specified item does not land inline again. Account keys and folder names in the spec are placeholders. The counts and the folder shapes are the real measurement and are what the design turns on. Backlog item 63. --- .../plans/2026-08-03-post-0.1.0-usability.md | 60 +++++-- .../specs/2026-08-11-sent-mail-design.md | 180 +++++++++++++++++++++ 2 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 docs/superpowers/specs/2026-08-11-sent-mail-design.md (limited to 'docs') 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 `/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 `--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 `/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.]`, a folder path relative to +`maildir`, beside the existing `drafts`. + +`Account::sentQuery()` returns `path:"//**"`, 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" , 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. -- cgit v1.2.3