summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 08:52:13 +0200
committerDanilo M. <danix@danix.xyz>2026-08-03 08:52:13 +0200
commitee1b05e2c0f3a5bcb86b2531951337f48bc66c23 (patch)
treef74a1d9d312c2398ed9437cabacdeca9977b92f4
parent9d7bd35ba32bba04d5241cddee428dba041a6e13 (diff)
downloadqtmaildir-ee1b05e2c0f3a5bcb86b2531951337f48bc66c23.tar.gz
qtmaildir-ee1b05e2c0f3a5bcb86b2531951337f48bc66c23.zip
docs: add CLAUDE.md with build commands and architecture
Records the non-obvious constraints that require reading several files to reconstruct: the two-thread split with no notmuch pointer crossing it, generation counters instead of cancellation, the close-before-reopen ordering the exclusive write lock forces, and the web view's fail-closed interceptor contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--CLAUDE.md124
1 files changed, 124 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..fd1283e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,124 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build and test
+
+```bash
+cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
+cmake --build build
+ctest --test-dir build --output-on-failure
+```
+
+Run a single test binary directly for a tighter loop: `./build/tests/test_keymap`.
+Or by ctest name (the name is the suffix, not the binary): `ctest --test-dir build -R keymap`.
+
+Adding a test: create `tests/test_<name>.cpp` and add `add_qtmaildir_test(<name>)` to
+`tests/CMakeLists.txt`. That function links `qtmaildir_lib` and `Qt6::Test` and registers
+the test. Fixture-driven tests get `FIXTURE_DIR` via `target_compile_definitions` (see
+`test_mimeparser`).
+
+Adding a source file: add the `.cpp` to the `qtmaildir_lib` list in `src/CMakeLists.txt`.
+All logic lives in that static library; the `qtmaildir` executable is only `main.cpp`, so
+tests can link everything without duplicating source lists.
+
+## Environment constraints (verified 2026-08-02, Slackware)
+
+- **notmuch installs no `notmuch.pc`.** CMake locates it with `find_path`/`find_library`.
+ Never convert it to `pkg_check_modules`. GMime does ship `gmime-3.0.pc` and uses pkg-config.
+- Qt 6.11.1 including WebEngine ships in Slackware's monolithic `qt6` package; there is no
+ separate `qt6-webengine`.
+- CMake 4.3.4 rejects `cmake_minimum_required(VERSION <3.5)`. Keep 3.21.
+- **gmime headers must be included before any Qt header in the same translation unit.**
+ glib declares a struct field named `signals`, which Qt defines as a macro.
+
+## Architecture
+
+One process, two threads. A GUI counterpart to neomutt over a local notmuch-indexed
+Maildir. **No network protocol work at all** — fetching and sending are external scripts.
+
+```
+UI thread Worker thread
+MainWindow NotmuchWorker
+ ├ QueryBar / SavedQueryBar └ owns the only notmuch_database_t*
+ ├ ThreadListView ── ThreadListModel
+ └ MessageView (HeaderWidget, QWebEngineView, AttachmentBar)
+
+Config (INI) KeyMap MailSync (QProcess) MimeParser (GMime)
+```
+
+**No `notmuch_*` pointer ever crosses the thread boundary.** Data crosses as the plain
+value structs in `src/types.h` (`ThreadSummary`, `MessageRef`, `TagChange`), over queued
+signals in both directions. `notmuchworker.cpp` is the only file that includes `notmuch.h`
+outside `src/nmraii.h`; C handles are owned by the `NmQuery`/`NmMessages`/`NmThread`/…
+RAII aliases there so they cannot leak.
+
+**Generation counters, not cancellation.** Each query bumps a `quint64` generation passed
+through to the worker and back on every result signal. The UI discards results whose
+generation is stale. The worker never needs to know a query was superseded. Threads are
+emitted in batches of `kBatchSize` (200) so a 10k-thread query paints immediately.
+
+**Read-only by default, read-write in bursts.** notmuch's write lock is exclusive
+process-wide, so holding it open would block the user's cron `notmuch new`. `applyTags`
+closes the read-only handle, opens read-write, applies, closes. notmuch permits only one
+open handle per process, so that close-first ordering is required, not stylistic.
+
+**No dry-run, no destructive-action confirmation.** Those gates exist in the companion
+project `../mailctl` to restrain an agent; a human at a GUI gets **undo** instead — every
+mutation pushes its inverse (`TagChange::inverted()`) onto a `QUndoStack`. Do not add
+confirmation dialogs for tag mutations. All actions funnel through one `applyTags` path;
+multi-row selections go through `applyTagsToThreads`, which resolves every thread in ONE
+combined `thread:a or thread:b` query rather than one query per thread.
+
+**The Maildir path is deliberately not configurable.** notmuch stores it as
+`database.path` and libnotmuch reads it; duplicating it would create two sources of truth.
+The only escape hatch is `general/notmuch_config`, pointing at an alternate notmuch config.
+Per-account subdirectories *are* configured, since notmuch does not model accounts at all.
+
+**Config format gotcha:** QSettings treats `/` in a section name as a group separator, so
+account sections are `[account.work]`, not `[account/work]`. `childKeys` returns keys
+sorted alphabetically, never in file order. Config lives at
+`~/.config/qtmaildir/qtmaildir.conf`.
+
+## Web view security
+
+The most security-sensitive area: a browser engine pointed at input from strangers. Do not
+loosen any of these without an explicit decision.
+
+- Off-the-record `QWebEngineProfile`, JavaScript disabled, `LocalContentCanAccessRemoteUrls`
+ and `LocalContentCanAccessFileUrls` both false.
+- The interceptor **blocks every request by default** and **fails closed**: with no document
+ URL set, every `qtmaildir:` URL is denied. The document-load exemption matches the
+ **exact** base URL passed to `setHtml()`, never the `qtmaildir:` scheme as a whole — a
+ scheme-wide allow would let a hostile body reference `qtmaildir://anything` and be trusted.
+ Consequence: `MessageView` **must** call `setDocumentUrl()` with the same URL it gives
+ `setHtml()`, or nothing renders.
+- A whole thread is one HTML document in one web view (a `QWebEngineView` per message would
+ spawn a Chromium render process each). That makes `cid:` ids collide across messages, so
+ every reference is rewritten to `cid:<prefix>!<id>`. **A `cidPrefix` must never contain
+ `!`** — it is the namespace separator.
+- Remote content grants are per-render and never sticky.
+- **Attachment filenames are untrusted input.** Reduce to basename, strip separators, resolve
+ against the chosen directory, and refuse anything escaping it. Compare resolved paths as
+ paths, not with `startsWith` — `/tmp/safe-evil` passes a `startsWith("/tmp/safe")` check.
+
+## Working on this repo
+
+Implementation follows `docs/superpowers/plans/2026-08-02-qtmaildir-v1.md` (14 tasks)
+against `docs/superpowers/specs/2026-08-02-qtmaildir-design.md`.
+
+**Treat every code block in the plan document as a draft, not as correct.** Nine defects
+have already been found in code that was written confidently into it, two of them
+security-relevant. Verify Qt API assumptions empirically rather than from memory — several
+of those defects were wrong assumptions (e.g. `QKeySequence::fromString` never returns an
+empty sequence for garbage input).
+
+TDD, per the user's global preference. `NotmuchWorker` is unit-tested against a throwaway
+notmuch database built in a temporary directory (generated Maildir + `notmuch new` +
+`NOTMUCH_CONFIG` scoped to the test process), superseding the spec's original "no unit
+test" position — it is the only code that writes to a notmuch index.
+
+Work goes directly on `master`, no PR flow. Commits must be GPG-signed (`git commit -S`).
+`HANDOFF.md` is local-only and gitignored; never stage or commit it.
+
+v1 is read-and-organize only. Compose and send are v2.