diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-24 15:27:12 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-24 15:27:12 +0200 |
| commit | fe9a117bd70fae95196a7cbff1cd88b1ba9478e9 (patch) | |
| tree | 018bf320c52580cf909183d5cfcb13fa87cd4d13 /docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | |
| parent | d3a369d8105a77869900a241b57855e7876ff1ed (diff) | |
| download | qtmaildir-fe9a117bd70fae95196a7cbff1cd88b1ba9478e9.tar.gz qtmaildir-fe9a117bd70fae95196a7cbff1cd88b1ba9478e9.zip | |
docs: close backlog item 204, contact completion shipped
Its section moves to the closed-items file with the execution record,
and the status row points there. The reconcile against the user's notes
found every open note line already covered by an existing item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index 84685c6..951b338 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -10678,3 +10678,102 @@ qtmaildir's own Delete strips `inbox` (item 168). 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. + +## 204. No recipient completion, in the composer or the query bar + +**Observed (user, 2026-09-18, settling item 72):** "as it is today I don't have +a way of accepting an invitation, I have to copy-paste the addresses for any new +email from somewhere." + +**Cause (verified in the code).** The composer's recipient fields are plain +`QLineEdit`s with no completer of any kind, and nothing in `src/` reads a +contact store: `grep` finds no vcard, vCard or CardDAV anywhere. Completion +exists in this application only for tags (`QueryCompleter`, `TagDialog`). + +**The data is on disk and verified, 2026-09-18.** 117 `.vcf` files under +`~/.local/share/vdirsyncer/contacts/`, written by the `contacts` pair in +`~/.config/vdirsyncer/config`. They are vCard **3.0**, one card per file, each +carrying `UID`, `FN`, `N`, `EMAIL` and frequently a base64 `PHOTO`. + +**Two traps in the layout.** The pair writes to +`~/.local/share/vdirsyncer/contacts/`, NOT `~/.local/share/contacts/`, which is +an Akonadi directory holding only a README warning against touching it: the +obvious path reads the wrong store. And the vdirsyncer config is per-machine +and holds a `password.fetch` command, so the path is a key in `qtmaildir.conf` +(as `notmuch_config` is) and nothing about that file is read here. + +**libical does NOT parse these.** libical 3.0.20 is installed with headers and +a `libical.pc`, but its vCard support landed in 4.0; `libicalvcal` is the old +vCalendar-1.0 converter, not a vCard parser. So this is a hand parse, which is +proportionate for four fields, and the reason to keep it in a namespace of free +functions over values: it must be testable against fixture files without a +widget, as `MimeParser` is. + +**What the parse must handle, and it is the whole risk.** vCard 3.0 FOLDS long +lines: a continuation begins with a space or tab and the fold can land anywhere, +including inside a base64 `PHOTO` and inside an address. Unfolding comes before +any field split. `EMAIL` carries parameters before the colon +(`EMAIL;TYPE=INTERNET,PREF:...`), a card may carry SEVERAL, and `N` is +semicolon-structured with backslash escapes. A card with no `EMAIL` is not an +error and simply does not complete. + +**Completion mechanics are already decided by this repository's own scars.** A +recipient field holds MORE THAN ONE value, so `QLineEdit::setCompleter()` is +wrong for exactly the reason `CLAUDE.md` records twice: attach with +`QCompleter::setWidget()`, drive `setCompletionPrefix()` from `textEdited`, and +replace the token under the cursor on `activated`. A test must TYPE the keys; +`setText()` does not drive a completer at all and passes against the bug. + +**Constraints.** Read-only: this item writes no vcard (that is 205). A card is +untrusted input in the ordinary sense, since it arrives from a DAV server, so a +`FN` reaching a label is plain text like every header value +(`MessageDetailsDialog`'s rule). Personal data: fixtures are written by hand +with example.org addresses, never copied from the real store. + +**The query bar gets the same candidates**, widened into this item by the user +on 2026-09-18: "it's nice to be able to search by address from my known +contacts." `QueryCompleter` completes `from:` and `to:` with NOTHING today, and +`querycompleter.cpp:652-656` says why: "addresses need an enumerator libnotmuch +does not expose". The vcards ARE that enumerator, so that comment is half wrong +the moment the store exists and must be corrected rather than left to mislead. + +The seam is already the right shape and needs no new mechanism. `setTags()` is +fed by the worker's `allTagsReady` and replaces the tag candidates; contacts get +the same treatment, and `entriesFor()` grows a `from:`/`to:` branch beside the +`path:` one. Two differences from the composer half, and they are why this is +not literally the same code: the query bar completes ONE token in notmuch's +grammar, so it inserts a bare address rather than `Name <addr>`, and the value +goes through `SearchTerm::quote()` like everything else this application puts in +a query, since a display name can hold a space and a quote. + +**Executed 2026-09-18 to 2026-09-24 via subagent-driven-development**: seven +signed commits, `3303c78..7454096`. `ContactStore` parses the vCard 3.0 +directory, `[general] contacts_dir` locates it (tilde-expanded), the composer's +To/Cc/Bcc and the query bar's `from:`/`to:` complete from one shared +`QCompleter` attached with `setWidget()`, and `MainWindow` loads the store once +and feeds both consumers. Four controller rulings departed from the plan: a +`setContacts()` setter instead of a fifth constructor parameter, the accessor +named `contactsDir()`, flat vCard fixtures, and tilde expansion for +`contacts_dir`. One genuine plan gap: `splitRecipients()` was made quote-aware, +since the plan's quoted-display-name insertion would otherwise be cut in half +by the splitter on the way to `OutgoingMessage`. The final whole-branch review +found one Important issue, untrusted `FN` control characters and RFC 5322 +specials reaching the recipient header, fixed in 7454096 by stripping control +characters and always quoting a non-empty name; re-reviewed clean. Hand-tested +and confirmed working by the user. One residual, not a defect in this work: the +vCard `EMAIL` value reaches the header unsanitised (an embedded `\r`); GMime +fails the send closed on it, so it is a one-line `ContactStore` follow-up if it +is ever hit in practice, not filed as its own item. + +**No brainstorm was needed for this item, and the plan was written**: +`plans/2026-09-18-contact-completion.md`, six tasks. Four decisions were taken +with the user on 2026-09-18 and were not open questions: + +1. The path is a `[general]` key, defaulting to + `~/.local/share/vdirsyncer/contacts/`. Empty means the feature is off, with + no error: a machine with no vdir is the ordinary case for anyone else. +2. The store loads once, whole, when it is first needed. 117 files is nothing, + and a watcher is speculative until a measurement says otherwise. +3. A candidate matches on the name AND the address, and the composer inserts + `Name <addr>`. +4. `FN` and every `EMAIL`; `PHOTO`, `ADR` and `TEL` are ignored entirely. |
