aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-08-11-sent-mail-design.md
AgeCommit message (Collapse)AuthorFilesLines
2026-08-11feat(sent): add a Sent view, flat and by recipientDanilo M.1-1/+61
Adds a `sent` key to [account.*] naming that account's sent folder, and a Sent button beside the saved queries that composes its query from every account carrying one. An account without the key is omitted silently, as a real account may keep no sent mail locally. With no account selected the button spans all of them; selecting one narrows it through the existing scope wrap rather than a second path. Composed at run time rather than shipped as a [queries] entry. A saved query is one fixed string: it cannot narrow to the selected account, and it goes stale the moment an account is added or a provider renames a folder. The design and the measurements behind it are in docs/superpowers/specs/2026-08-11-sent-mail-design.md. Three things there are worth repeating here. The composed path is QUOTED, and that is load-bearing. A real provider nests its sent folder under a bracketed parent, and "[" and "]" are Xapian syntax: unquoted, the query parses rather than matches and returns nothing while looking entirely plausible. Composition happens in one place so there is one chance to get it right, and a bracketed path is pinned in a test. Recipients are opt-in per query, which is a performance contract rather than a preference. notmuch_message_get_header(m, "To") is not served from the index, it reads the message file: folding every thread of a 4411-thread inbox took 38.2 seconds against 251 ms for the 601-thread sent view. The worker skips the walk entirely unless asked, and the refresh path carries the same flag so a background sync cannot blank the column mid-read. Always folding is mutation-tested: the data would be right and only the cost wrong, which nothing else here would notice. The messages reached through the thread are owned by it and freed with it, so recipientsOf() holds them raw and finishes while the thread is alive, exactly as walkReplies does. An NmMessage wrapper there is a double-free. Sent mail is presented flat, and the pane follows. A message you sent otherwise drags in the replies you received, so a view labelled Sent shows conversations rather than what you sent. ThreadListModel::setFlatMode() makes hasChildren() and ReplyCountRole answer differently and changes nothing else; runQuery() sets it on EVERY run, so any other query restores the tree on its way through and the flag cannot outlive the button that set it. The pane needed its own fix for the same reason: the single-message path depends on a field only filled when a thread is expanded, which never happens in a flat list, so loadThread() gained matchedOnly and drops the messages that did not match instead of rendering them as stubs. Recipients replace the sender through the existing SendersRole rather than a new one, so the delegate needs no branch and cannot disagree with the model about which name a row shows. It falls back to the sender when a To header is absent or unparseable, since a blank where a name belongs reads as a rendering fault. Address parsing uses GMime: a display name may contain a comma, so "Rossi, Mario" <m@example.org>, info@example.net is two addresses and splitting reports three. internet_address_list_parse returns NULL for an empty string, which is a crash if unguarded. Backlog item 63.
2026-08-11docs: specify item 63, sent mail, in its own design fileDanilo M.1-0/+180
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 `<maildir>/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.