diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-18 14:27:03 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-18 14:27:03 +0200 |
| commit | 196bfaeefdbbbd2ff23dc2648e21ae86d41a57b6 (patch) | |
| tree | c7ffc91268d9714123f7de661d20e6fa359899fc | |
| parent | 299eeff6ab6f61f07f1c0ed9e039f00036c1d31a (diff) | |
| download | qtmaildir-196bfaeefdbbbd2ff23dc2648e21ae86d41a57b6.tar.gz qtmaildir-196bfaeefdbbbd2ff23dc2648e21ae86d41a57b6.zip | |
docs: reconcile the backlog with the notes, and split item 72
The 2026-09-18 pass over the user's notes. One new defect, one item split
into five, and one stale blocker cleared.
Item 203 is new: marking a message as spam crashed the application, and the
user can replicate it. The cause is NOT verified and the entry says so. The
path was read without finding a null dereference, and the account the note
names now carries a spam key, so the observation may predate 0.29.0's config
change. It needs a reproduction with the terminal output or a backtrace
before it can be worked.
Item 72 held four different features in one line of the notes, which is why
it sat unplannable for six weeks. The user named what they want, so it splits
into 204 (recipient completion, in the composer and the query bar), 205
(editing contacts), 206 (a calendar window: its own top-level window and
libical, both settled by the user) and 207 (invitations, blocked on 206).
Item 208 came out of the same conversation: an "add to contacts" gesture,
which writes a vcard and so lands on 205's writer rather than 204's reader.
A sender's card in the message pane was offered and declined; the notes'
edge-tts vocal reminders are their own project, since nothing here runs on a
timer. 72 stays as the index entry the notes' single line maps to.
The data those items need is on disk and was verified rather than assumed:
117 vcards and 312 events under the vdirsyncer paths, libical 3.0.20 with
headers and a .pc file. Two traps are recorded because they cost a wrong
answer each: the contacts vdir is not the obvious path, which belongs to
Akonadi, and libical does not parse vCard at all (that is 4.0; libicalvcal
is the old vCalendar converter), so contacts are a hand parse.
Item 123 (send) was still marked "specified" although it shipped, so five
items read as blocked on it. Both corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 289 |
1 files changed, 270 insertions, 19 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 2a73673..5380b32 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 @@ -140,7 +140,12 @@ taking that too literally. | 69 | `passed` and `replied` read as words where every other state is a glyph | presentation | S | **done** 2026-08-11, inside item 70 | | 70 | Pane icons are a private set where the main window uses the system theme | presentation | M | **done** 2026-08-11; six shipped SVGs | | 71 | A toolbar action does not sync, so the edit sits until the next cron run | workflow | S | **done** 2026-08-11; 2s default, `auto_sync_delay_ms` | -| 72 | No khard/khal integration | workflow | ? | open, unspecified; the user places it after send, so v2 at the earliest | +| 72 | No khard/khal integration | workflow | ? | **split 2026-09-18** into 204 (recipient completion), 205 (contact editing), 206 (calendar window) and 207 (invitations), after the user named what they want. Nothing is planned under this number any more; it stays as the index entry the notes' single line maps to. The edge-tts reminder half is out of scope: its own project | +| 204 | No recipient completion, in the composer or the query bar | workflow | S | open, 2026-09-18, from 72's split and the user's own words: "I have to copy-paste the addresses for any new email from somewhere." Read-only over the 117 synced vcards. **Widened the same day at the user's request** to feed `from:`/`to:` in the query bar, which `QueryCompleter` completes with nothing today. Independent of 205, 206 and 207 and blocks none of them; the user starts here. **No brainstorm needed**, the shape is settled: see the entry | +| 205 | Contacts can be read but not edited from the app | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user** while settling 204. Writing a vdir, so it shares the write-during-sync question with 206 and the fold/escape rules with 204's parser. Needs a brainstorm of its own | +| 206 | No calendar: events cannot be viewed, added or edited | workflow | L | open, 2026-09-18, from 72's split, **asked for by the user** in place of khal. Its own TOP-LEVEL WINDOW and **libical**, both settled by the user. Spec-and-branch scale, not a Tuesday pickup. Blocks 207 | +| 207 | An invitation cannot be accepted, refused or sent | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user**: "as it is today I don't have a way of accepting an invitation." `text/calendar` in the pane, an iMIP reply through the existing `send_command`. **Blocked on 206**, which owns the calendar it writes to | +| 208 | No way to add a new correspondent to the contacts | workflow | ? | open, 2026-09-18, **asked for by the user** while settling 204: an "add to contacts" button on mail from someone not in the store. Writes a vcard, so it lands on 205's writer rather than 204's reader. **Needs a brainstorm when picked up** | | 73 | This backlog is past four thousand lines | maintenance | S | **done** 2026-08-13; 5056 lines to 578, closed sections moved to `2026-08-03-post-0.1.0-usability-closed.md` | | 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | **done** 2026-08-15, unreleased. The status-bar half only: the bar now counts threads per batch. The cold-cache delay itself was measured in 2026-08-11 and is not fixable here | | 75 | The tagging rules window forgets its size and its column widths | persistence | S | **done** 2026-08-13, shipped in 0.17.0. The window-kind question is left open, see the closed-items file | @@ -189,7 +194,7 @@ taking that too literally. | 121 | The thread list shows nothing while a query is running | feedback | S | open, 2026-08-20, from the notes. Follows item 74, which fixed the status-bar half and left the list itself blank | | 122 | The README documents a version of the app that no longer exists | documentation | M | **done** 2026-08-23, unreleased, inside item 123 task 13. `trash`, `send_command` and the whole `[compose]` section were undocumented; a Composing section is added and "sending is not implemented" removed. Every default was read from `config.h` rather than from the prose, which caught `send_html` documented as false when it defaults to true | -| 123 | Sending mail is not designed | v2 | L | **specified** 2026-08-20, on branch `compose-and-send`. Design in `docs/superpowers/specs/2026-08-20-compose-and-send-design.md`; read that, not this row. Send is a per-account `send_command` on stdin, so the no-network-protocol rule stands. Composer is a separate window, body is markdown via cmark-gfm, drafts autosave to the account's drafts folder. Tasks 1 to 13 of 13 built 2026-08-20 to 2026-08-23; task 13 closed the documentation out and retired the v1/v2 split, which semver had made meaningless. **Hand tested 2026-08-22 and 2026-08-23** against a fake send command: New, Reply and Forward all send, a forwarded attachment survives intact, and the sent copy is filed. Found two defects, both fixed (the orphaned composer, and sent mail carrying `inbox`). Twenty-two defects were found in the plan's own draft code across tasks 4 to 12, so treat every code block in it as a draft | +| 123 | Sending mail is not designed | v2 | L | **done** 2026-08-23, shipped; specified 2026-08-20, on branch `compose-and-send`. Design in `docs/superpowers/specs/2026-08-20-compose-and-send-design.md`; read that, not this row. Send is a per-account `send_command` on stdin, so the no-network-protocol rule stands. Composer is a separate window, body is markdown via cmark-gfm, drafts autosave to the account's drafts folder. Tasks 1 to 13 of 13 built 2026-08-20 to 2026-08-23; task 13 closed the documentation out and retired the v1/v2 split, which semver had made meaningless. **Hand tested 2026-08-22 and 2026-08-23** against a fake send command: New, Reply and Forward all send, a forwarded attachment survives intact, and the sent copy is filed. Found two defects, both fixed (the orphaned composer, and sent mail carrying `inbox`). Twenty-two defects were found in the plan's own draft code across tasks 4 to 12, so treat every code block in it as a draft | | 124 | The worker reads the index directory as the mail root | defect | S | **done** 2026-08-20, unreleased. `mailRootOf()` over `NOTMUCH_CONFIG_MAIL_ROOT`, correct under both layouts. Verified by migrating the developer's own index to NVMe the same day: cold start 38.6 s to 0.67 s | @@ -197,12 +202,12 @@ taking that too literally. | 126 | A link with `target="_blank"` does nothing when clicked | defect | S | **done** 2026-08-20, unreleased. `createWindow()` returns a relay page that receives the navigation, hands the URL to the browser and refuses. The URL cannot be read in `createWindow()` itself, which is why a relay rather than a lookup | | 127 | A link's context menu offers four browser actions that cannot work | defect | XS | **done** 2026-08-20, unreleased. Three Open-in actions removed, `CopyLinkToClipboard` kept. Item 126 made them more dangerous rather than less: with a real `createWindow()` they would have started working | -| 128 | No outbox: a send with no network fails instead of queueing | v2 | M | open, 2026-08-20, from the item 123 brainstorm. **Blocked on 123.** The seam is designed in (`MessageSender` is the one funnel), so this wraps it rather than reworking it. Needs its own indicator story first: items 18, 19, 28 and 54 are all an indicator lying, and 125 is one still open | -| 129 | No inline images in a composed message | v2 | M | open, 2026-08-20, from the item 123 brainstorm. **Blocked on 123.** Wanted by the user. `cid:` from the HTML part with `multipart/related` nested inside the alternative, the most nesting-heavy part of MIME assembly, and markdown offers no syntax for it | -| 130 | A message cannot be attached to another message directly | v2 | S | open, 2026-08-20, from the item 123 brainstorm. **Blocked on 123.** A `message/rfc822` part, which GMime builds natively. The manual route exists from 123's first commit: `save_message` writes the `.eml` and it is attached as a file | -| 131 | The markdown dialect and extensions are fixed | v2 | S | open, 2026-08-20, from the item 123 brainstorm. **Blocked on 123.** Configurable in the shape Hugo's config uses. Deliberately fixed initially: CommonMark plus autolink, strikethrough and tasklist | +| 128 | No outbox: a send with no network fails instead of queueing | v2 | M | open, 2026-08-20, from the item 123 brainstorm. Was blocked on 123, which shipped. The seam is designed in (`MessageSender` is the one funnel), so this wraps it rather than reworking it. Needs its own indicator story first: items 18, 19, 28 and 54 are all an indicator lying, and 125 is one still open | +| 129 | No inline images in a composed message | v2 | M | open, 2026-08-20, from the item 123 brainstorm. Was blocked on 123, which shipped. Wanted by the user. `cid:` from the HTML part with `multipart/related` nested inside the alternative, the most nesting-heavy part of MIME assembly, and markdown offers no syntax for it | +| 130 | A message cannot be attached to another message directly | v2 | S | open, 2026-08-20, from the item 123 brainstorm. Was blocked on 123, which shipped. A `message/rfc822` part, which GMime builds natively. The manual route exists from 123's first commit: `save_message` writes the `.eml` and it is attached as a file | +| 131 | The markdown dialect and extensions are fixed | v2 | S | open, 2026-08-20, from the item 123 brainstorm. Was blocked on 123, which shipped. Configurable in the shape Hugo's config uses. Deliberately fixed initially: CommonMark plus autolink, strikethrough and tasklist | | 132 | Every action must have a shortcut, and that no longer serves | policy | S | done, 2026-08-20. `everyActionHasAShortcut` is deleted and nothing replaces it: `everyActionIsReachableFromAMenu()` is the required rule and a shortcut is now a chosen subset. Nothing else needed changing, since `showShortcutReference()` already printed `(unbound)` for an empty sequence. Verified by unbinding `tag_rules` and running the suite green, which would have failed before | -| 133 | The composer shows no markdown syntax highlighting | v2 | S | open, 2026-08-20, from the item 123 brainstorm. **Blocked on 123.** A `QSyntaxHighlighter` over the composer's editor, so `**bold**` reads as bold while the buffer stays plain markdown. Standard Qt, no dependency. Deliberately after 123's formatting toolbar: agreeing with the grammar about nesting and about code spans suppressing what is inside them is the expensive part, and the toolbar is what makes the feature usable . **Subsumed by item 173** 2026-08-27: that is a rich-text editor, this is the cheap answer to the same want. Build one or the other, never both | +| 133 | The composer shows no markdown syntax highlighting | v2 | S | open, 2026-08-20, from the item 123 brainstorm. Was blocked on 123, which shipped. A `QSyntaxHighlighter` over the composer's editor, so `**bold**` reads as bold while the buffer stays plain markdown. Standard Qt, no dependency. Deliberately after 123's formatting toolbar: agreeing with the grammar about nesting and about code spans suppressing what is inside them is the expensive part, and the toolbar is what makes the feature usable . **Subsumed by item 173** 2026-08-27: that is a rich-text editor, this is the cheap answer to the same want. Build one or the other, never both | | 134 | The busy indicator is built inline and is about to be built twice | maintenance | S | done, 2026-08-20, af902e0. `BusyIndicator` (`src/busyindicator.h`) carries both modes: `MainWindow` uses the indeterminate one, and item 123's send popup takes the determinate half for its undo countdown, switching the same widget over when the command starts. Only the BAR was extracted, not the status label this row paired with it. `m_statusLabel` has 34 uses across `MainWindow` for transient messages, selection counts and sync phases, so it belongs to the window rather than to the indicator, and the send popup owns its own phase text | | 135 | The formatting toolbar's buttons stack rather than toggle | v2 | S | open, 2026-08-21, asked for by the user during item 123 task 8 and reverted the same session. **A spec change, not a defect**: it conflicts with spec:236 ("deliberately no live toggle") and spec:187-190. Both sites need amending FIRST, and the amendment must resolve what replaces bold-then-italic, which is the gesture spec:187's preserved selection exists to serve and which a toggle makes unreachable. That question is the work; the state machine is understood and written up in the section | | 136 | `undoMovesTheMessageBack` fails when run ALONE, passes in the full suite | defect | ? | open, 2026-08-21, re-measured 2026-08-24 and it is not what the row said. Filed as an intermittent race (1 in 6); it is in fact **deterministic on the selection**: 6 failures in 6 when named on the command line, and, as of 2026-08-24, it fails in the FULL run too: measured at 58f13ad with the day's work stashed out, 274 passed and this one failed. The "passes in the suite" half of this row is therefore no longer true, and the selection-dependence it was named for may not be either. Re-measure before theorising. All three of its 15s `QTRY` timeouts expire, giving 45s against a 25s whole-suite run, so undo never moves the file rather than losing a race. A test that needs its predecessors is the likely shape (the `init()` lock-table fixture of item 61 is one candidate), which makes it a TEST defect until shown otherwise. Not caused by item 149, and re-confirmed 2026-08-24 as not caused by item 152 either, by running the test at the preceding commit in a throwaway worktree. The assertion that fails names the real question: the restored file is in NEITHER `cur` nor `new` of the account inbox, so establish where it went before theorising about a race | @@ -276,6 +281,7 @@ taking that too literally. | 197 | No way to say a message is not spam | workflow | S | **done 2026-09-14**, released in 0.29.0. A `Not spam` action now exists (item 201): it returns each message to its `moved-from:` origin, strips `spam`, and for provider-caught mail with no origin falls back to the account's inbox, reported in the status bar (the destination question answered as the inbox guess). The provider-notification half stays out of scope as network work. Section in the closed file | | 201 | A message in the Spam view cannot be un-spammed, even one qtmaildir put there | defect | S | **done 2026-09-14**, released in 0.29.0. Built as the distinct `not_spam` action (the second option): shown on a spam-folder selection, hidden on a reply row and in the trash, worker-resolved origin, undoable. Section in the closed file | | 202 | Mail in a spam folder keeps `inbox`, so it appears in the Inbox view | defect | XS | **done 2026-09-14**, released in 0.29.0. Root cause was the `post-new` hook's non-arrival set, not the UI: `spam` added to `NOT_ARRIVALS`, helpers renamed `not_arrival_*`, tests added. Live one-time cleanup stripped `inbox` from the 49 affected messages (`tag:inbox` 5904 -> 5855). Section in the closed file | +| 203 | Marking a message as spam crashed the application | defect | ? | open, 2026-09-18, from the notes. **Cause not verified**: the path was read and no null dereference found, and the account named now carries a `spam` key, so the note may predate 0.29.0's config. Needs a reproduction on the current build with the terminal output or a backtrace before it can be planned | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -432,18 +438,34 @@ S to M; what it finds is the work. ## 72. No khard/khal integration **Observed (user, from the notes):** "investigate khard/khal integration (light -PIM, probably worthy after we add send capabilities)." - -**Cause:** not a defect. v1 is read-and-organize; there is no address book and no -calendar anywhere in the codebase. - -**Why this cannot be planned.** The user's own note places it after send, and -send is v2. What "integration" means is undecided: completing recipients from -khard when composing, showing a sender's card, or acting on an invitation. -Those are three different features. - -**Size: `?`, unspecified**, and out of scope until v2 exists. Ask before designing -anything. +PIM, probably worthy after we add send capabilities)", later "vdirsyncer is the +real piece syncing calendars/vcards to the disk, and it's now installed, so we +can start working on this feature. Calendar and vcards are synced and +kept up-to-date on this system." + +**Split on 2026-09-18 and closed as an item.** One line in the notes held four +different features (a fifth, 208, came out of the same conversation), which is +why this sat unplannable for six weeks. The user named what they want, and it is +now: + +| | | +|---|---| +| 204 | recipient completion, in the composer and the query bar, read-only over the vcards | +| 205 | editing contacts from the app | +| 206 | a calendar window: view, add, remove, edit events | +| 207 | invitations: accept, refuse, send | +| 208 | an "add to contacts" gesture on mail from someone new | + +**Two were removed rather than split out.** A sender's card in the message pane +was offered and declined ("I don't have much use for it"). The notes' edge-tts +vocal reminders are **their own project**, by the user's decision: nothing here +runs on a timer or plays audio, and a reminder daemon over the same vdir is a +sibling to `mail-watcher` (item 184) rather than a feature of a mail client. + +**khard is not part of any of it.** It is not installed on this machine, and the +user prefers the app to work the store directly: "I don't care much for khard." +The dependency was always vdirsyncer, which puts plain files on disk; khard and +khal are just other readers of the same vdirs. ## 99. The unread action is labelled "Toggle unread" whichever way it will go @@ -1558,3 +1580,232 @@ in the notes. quoting like every other query this application builds, rather than being concatenated at the call site. + +## 203. Marking a message as spam crashed the application + +**Observed (user, from the notes):** while browsing unread, marking a message +spam crashed the application, on one of the Gmail accounts. The user was able +to replicate it. + +**Cause. NOT verified, and this entry must not be planned until it is.** The +repository records no crash report, no core file and no stack trace, and the +path was read without finding a null dereference: `spamSelected()` +(`mainwindow.cpp:6588`) resolves per row through `scopeForSelection()`, and both +halves guard an empty list; `messageById()` returns a value struct rather than a +pointer, so a missed lookup yields an empty path rather than a crash; +`spamMessages()` routes an empty-path message into the `unconfigured` branch and +reports it. The account the note names does carry a `spam` key in the live +config, so the unconfigured branch is not what was hit. + +**What the date makes ambiguous.** The note is undated, and every account only +gained a `spam` key while 0.29.0 was being cut (2026-09-14). If the crash predates that config change, the run had no spam +folder for that account at all and the observation may be against code and +config that no longer exist. The first question is therefore whether it still +reproduces on 0.29.0. + +**What is needed before this is an item that can be worked.** A reproduction on +the current build: the view (Unread), whether the row was a conversation, a +thread of one, or a reply, and the terminal output of the run, since the +application prints Qt warnings there and a `qFatal` or an assertion would name +itself. A `coredumpctl` entry or a `gdb` backtrace settles it outright. + +**Constraints.** Item 195's fix moved Mark spam from a tag write to a FILE +MOVE, so a crash on this path is not only a display defect: a process that dies +part-way through `moveMessages()` can leave a message renamed on disk and not +yet reindexed. Treat a reproduction as touching real mail and run it against a +throwaway index if at all possible. + +## 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. + +**No brainstorm is needed for this item.** Four decisions were taken with the +user on 2026-09-18 and are 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. + + +## 205. Contacts can be read but not edited from the app + +**Observed (user, 2026-09-18):** "I don't care much for khard, I'd love to be +able to edit vcs in the app too." + +**Cause:** not a defect. Nothing in `src/` reads or writes a contact store; see +204 for the layout. + +**Why this is a separate item from 204.** 204 is a read over files that already +exist and can ship on its own. This one WRITES a vdir, which brings three +questions 204 never asks: what a card the app did not write must preserve on +save (unknown properties, the base64 `PHOTO`, `CATEGORIES`), how a write avoids +racing a `vdirsyncer` run, and what a new card's `UID` and filename are. The +first is the same fields-we-do-not-understand discipline `TagRule::unknown` +already encodes for the rules file. + +**Shares its write-during-sync question with 206**, and both are the same shape +as the mail side: this application already watches for a sync it did not start +(`SyncMonitor` over `/proc/locks`, item 27), so the answer probably reuses it +rather than inventing a second mechanism. Decide it once, for both items. + +**Unspecified and needs a brainstorm.** Where the editor lives (a dialog from +the composer's completion, a contacts window, or inside 206's window), and +whether it manages collections at all or only cards. + +## 206. No calendar: events cannot be viewed, added or edited + +**Observed (user, 2026-09-18, in place of the notes' khal line):** "I'd prefer +to be able to work on my calendar from inside qtmaildir. I'm talking view, add, +remove, edit events, accept/refuse invitations, send invites to events, etc." + +**Two things are already settled by the user** and are not to be re-opened in +the brainstorm: the calendar is **its own top-level window**, like +`ComposeWindow`, not a third pane in the main one; and the iCalendar work uses +**libical**, not a hand-rolled parse. + +**The dependency is present and easy, unlike notmuch.** libical 3.0.20 with +headers in `/usr/include/libical/` and a `libical.pc`, so this one IS +`pkg_check_modules`. It is the same C-handles-in-C++ shape `nmraii.h` already +solves for notmuch, and the same rule applies: RAII aliases, and no libical +pointer crossing a thread boundary. + +**The data is on disk and verified, 2026-09-18.** 312 `.ics` files across three +collections under `~/.local/share/calendars/`, the `calendars` pair's local +half. A vdir is plain files, so a write is a new file that vdirsyncer pushes on +its next run; no daemon and no protocol work, which keeps the no-network rule +intact exactly as `send_command` does for mail. + +**Why L rather than M.** This is a second application inside this one: a month +or week or agenda VIEW is real UI with no existing counterpart here, recurrence +(`RRULE`, `EXDATE`, and the expansion of an infinite series into a visible +window) is where a naive implementation dies, and timezones are `VTIMEZONE` +rather than the `QDateTime` habits the rest of the tree has. Compose-and-send +(item 123) is the scale precedent: a spec, a branch and a plan of many tasks, +not a backlog item picked up on a Tuesday. + +**Config.** A calendar path key, and which collection is the default to WRITE +to, since there are three and nothing in the files names a preference. + +**Shares the write-during-sync question with 205.** Answer it once. + +## 207. An invitation cannot be accepted, refused or sent + +**Observed (user, 2026-09-18):** "as it is today I don't have a way of accepting +an invitation." + +**Cause (verified in the code).** `MimeParser` walks the MIME tree for bodies +and attachments and has no branch for `text/calendar` at all, so an invitation +reaches the pane as an attachment or as nothing. Nothing anywhere reads or +writes iCalendar. + +**Blocked on 206**, and this is a real dependency rather than a tidy ordering: +accepting an invitation WRITES the event into the local calendar, so it needs +206's store, its libical wiring and its answer to the write-during-sync +question. Only the display half could precede it. + +**Three parts, and the middle one is the mail-shaped half this repository is +actually good at.** Recognise and render the `text/calendar` part in the +message pane (organiser, time, attendees, and the `METHOD`, which distinguishes +`REQUEST` from `CANCEL` and `REPLY`). Write the event into the calendar on +Accept. And send an iMIP reply: a `METHOD:REPLY` iCalendar part with the user's +`PARTSTAT`, built by `MessageBuilder` and sent through the existing per-account +`send_command`, so no new send path. Sending an invite is the same builder with +a `METHOD:REQUEST` and an attendee list. + +**Constraints.** An invitation comes from a stranger, so every displayed value +is plain text, and an Accept must not act on anything the user did not see. A +`CANCEL` for an event that is not in the calendar is normal, not an error. + +## 208. No way to add a new correspondent to the contacts + +**Observed (user, 2026-09-18, while settling 204):** an "add to contacts" +button when a mail arrives from someone not already in the store. + +**Cause:** not a defect. Nothing in `src/` reads or writes a contact store; see +204 for the layout and 205 for the writing half. + +**It belongs to 205, not to 204.** 204 is read-only, and this WRITES a vcard: +the new card needs a `UID`, a filename, a collection to land in, and the same +answer to racing a `vdirsyncer` run that 205 and 206 both need. Filed +separately because the GESTURE is this item's own, and the gesture is where the +undecided parts are. + +**Needs a brainstorm when picked up**, at the user's own request. What is open: +where the button lives (the message pane's bar, the details dialog, or the +sender line), what "a new contact" means when a sender matches an existing card +by name but not by address, whether it opens 205's editor pre-filled or writes +a minimal `FN`+`EMAIL` card silently, and what happens on mail with several +senders or a `Reply-To` that disagrees with `From`. + +**Constraints.** A `From` header comes from a stranger: a display name can hold +anything, and it is being written to a file that syncs to a DAV server and to +every other device. Escaping is the vCard writer's job (205's), but the +DECISION to write unvalidated header text into a synced store is this item's. |
