aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md70
-rw-r--r--README.md198
-rw-r--r--docs/RELEASING.md83
3 files changed, 351 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..90853ce
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,70 @@
+# Changelog
+
+All notable changes to this project are documented here.
+
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+While the version is below 1.0.0, the configuration file format and the
+keybinding action names may change in a minor release. 1.0.0 will mark the
+point at which they are stable.
+
+## [Unreleased]
+
+Nothing yet.
+
+## [0.1.0] - 2026-08-03
+
+First release. Reads and organizes a local notmuch-indexed Maildir; it does
+no network protocol work at all, since fetching and sending are left to
+external commands.
+
+### Added
+
+- Permanent notmuch query bar with saved-query buttons and per-account
+ scoping, over a two-pane thread list and message view.
+- Threads streamed from the database in batches of 200, so a query over tens
+ of thousands of threads paints its first rows immediately and fills in
+ behind. Measured at 21 ms to the first batch against a 36,000-thread
+ database.
+- Whole-thread rendering: every message in a thread renders into one
+ document, oldest first, with messages that did not match the current query
+ collapsed to one-line stubs. The last message always renders expanded, so
+ a thread is never nothing but stubs.
+- HTML mail rendered through QtWebEngine, with a locked-down profile:
+ off-the-record, no cookies, no cache, JavaScript disabled, and a
+ deny-by-default request interceptor. Remote content is blocked until the
+ user asks for it, which defeats tracking pixels and read receipts; the
+ grant applies to one render and is never remembered.
+- Inline `cid:` images served from an in-memory map scoped to the displayed
+ thread. References are namespaced per message, so two newsletters sharing
+ a Content-ID do not resolve to each other's images.
+- Attachment handling that treats MIME filenames as untrusted: names are
+ reduced to a basename and refused if the resolved path escapes the chosen
+ directory.
+- Tagging (archive, delete, spam, flag, read/unread, custom) over a
+ multi-thread selection, applied optimistically and reverted if the write
+ fails. Thread ids are resolved to message ids in a single combined query
+ rather than one query per thread.
+- Undo for tag changes, backed by a `QUndoStack`. Entries store thread ids
+ and re-resolve them, so undo stays correct after the selection moves on.
+ There is deliberately no dry-run and no destructive-action confirmation:
+ undo is the better answer for a human at a GUI.
+- Sync by running the user's existing script through `QProcess`, joining the
+ `flock` that already serializes it against cron rather than reimplementing
+ mbsync orchestration.
+- Configurable keybindings via the `[keys]` section, validated against a
+ known action list so a typo warns instead of binding silently.
+- `--version` and `--help`.
+
+### Known limitations
+
+- Compose and send are not implemented; they are planned for v2 and need a
+ companion send script that does not exist yet.
+- A thread renders as a flat chronological list. Reply structure is
+ available from notmuch but is not drawn as an indented tree.
+- MIME parsing happens on the UI thread. Opening a very long thread parses
+ every message in it, which could stutter; deferred until measured.
+- The sync log pane shows what the sync command writes to stdout and stderr.
+ A script that redirects its own output to a file will leave the pane
+ empty.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4acd215
--- /dev/null
+++ b/README.md
@@ -0,0 +1,198 @@
+# qtmaildir
+
+A Qt6 desktop mail client for reading and organizing a local,
+notmuch-indexed Maildir. A GUI counterpart to neomutt for the parts of mail
+handling that are easier with a mouse and a real HTML renderer.
+
+## What it does not do
+
+This is the important half of the description.
+
+qtmaildir does **no network protocol work at all**. There is no POP, no
+IMAP, no SMTP, and no sync implementation. Mail arrives in your Maildir by
+whatever you already use (mbsync, offlineimap, fetchmail) and is indexed by
+`notmuch new`. qtmaildir reads the result.
+
+Fetching is delegated to a command you configure. Running your existing
+script means joining the `flock` that already serializes it against cron; a
+built-in implementation would sit outside that lock and could run two
+`mbsync` processes over one Maildir, which corrupts UID state.
+
+Sending is not implemented. Compose, reply, forward and send are planned for
+v2 and need a companion send script that does not exist yet.
+
+There is also no dry-run and no "are you sure?" on destructive actions. The
+answer for a human at a GUI is undo, which is implemented, and which is
+strictly better than a dialog that gets dismissed reflexively.
+
+## Requirements
+
+Verified against these versions on Slackware -current:
+
+| Component | Version | Notes |
+|---|---|---|
+| Qt6 | 6.11.1 | Widgets and WebEngine. On Slackware both ship in the monolithic `qt6` package; there is no separate `qt6-webengine`. |
+| libnotmuch | 0.39 (libnotmuch 5.6) | Installs no `notmuch.pc`, so CMake locates it with `find_path`/`find_library` rather than pkg-config. |
+| GMime | 3.2.15 | Does ship `gmime-3.0.pc`. |
+| CMake | 4.3.4 | 3.21 or newer. |
+| GCC | 15.3.0 | C++17. |
+
+You also need a notmuch database that is already set up and working from the
+command line. qtmaildir does not create or configure one.
+
+## Building
+
+```bash
+cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
+cmake --build build
+./build/src/qtmaildir
+```
+
+Running the tests:
+
+```bash
+ctest --test-dir build --output-on-failure
+```
+
+A single test binary, for a tighter loop:
+
+```bash
+./build/tests/test_keymap
+```
+
+## Configuration
+
+`~/.config/qtmaildir/qtmaildir.conf`, INI format.
+
+The Maildir path is deliberately **not** configurable. notmuch already
+stores it as `database.path` and libnotmuch reads it; duplicating it here
+would create two sources of truth and let the GUI index a different tree
+than the CLI. The only escape hatch is pointing at an alternate notmuch
+config, so notmuch remains the authority either way.
+
+Per-account subdirectories *are* configured, because notmuch does not model
+accounts at all: it sees one flat tree. An account is a path prefix plus an
+identity.
+
+```ini
+[general]
+; Optional. Omit to let notmuch resolve its own config, which is what keeps
+; the GUI and the CLI pointed at one database.
+; notmuch_config = /home/you/.notmuch-config
+
+[sync]
+; Optional. Omit and the Sync button disables itself with a tooltip.
+command = /home/you/bin/mailsync.sh
+
+; Section names use a dot, not a slash: QSettings treats "/" as its own
+; group separator, so [account/work] would be parsed as a nested group.
+[account.work]
+name = Your Name
+address = you@example.org
+maildir = work-mail ; relative to notmuch's database.path
+drafts = Drafts ; recorded for v2; unused today
+
+[account.personal]
+name = Your Name
+address = you@example.net
+maildir = personal
+drafts = Drafts
+
+[queries]
+Inbox = tag:inbox
+Unread = tag:unread
+Flagged = tag:flagged
+
+[keys]
+j = next_thread
+k = prev_thread
+Return = open_thread
+a = archive
+d = delete
+N = toggle_unread
+F = flag
+/ = focus_query
+h = toggle_html
+u = undo
+G = sync
+Ctrl+Q = quit
+```
+
+Saved-query buttons appear in alphabetical order rather than file order:
+QSettings returns keys sorted, and preserving file order would mean
+hand-rolling an INI parser.
+
+## Keybindings
+
+Defaults, all rebindable through `[keys]`:
+
+| Key | Action | Does |
+|---|---|---|
+| `j` | `next_thread` | Select the next thread |
+| `k` | `prev_thread` | Select the previous thread |
+| `Return` | `open_thread` | Focus the thread list |
+| `a` | `archive` | Remove `inbox` from every selected thread |
+| `d` | `delete` | Add `deleted` |
+| `N` | `toggle_unread` | Toggle `unread` |
+| `F` | `flag` | Add `flagged` |
+| `/` | `focus_query` | Focus and select the query bar |
+| `h` | `toggle_html` | Switch the thread between HTML and plain text |
+| `u` | `undo` | Undo the last tag change |
+| `G` | `sync` | Run the configured sync command |
+| `Ctrl+Q` | `quit` | Quit |
+
+Two further actions exist but have **no default binding**, so they are
+unreachable until you bind them: `spam` (adds `spam`, removes `inbox`) and
+`load_remote` (the keyboard equivalent of the "Load remote content"
+button).
+
+An unknown action name in `[keys]` produces a warning at startup rather than
+binding silently, so a typo is visible.
+
+Tag actions apply to **every selected thread**, not only the focused one.
+
+## Security posture of the message view
+
+A mail client rendering HTML is a browser engine pointed at input from
+strangers, and it is treated that way.
+
+- A dedicated off-the-record `QWebEngineProfile`: no cookies, no cache,
+ nothing persisted.
+- JavaScript disabled. `LocalContentCanAccessRemoteUrls` and
+ `LocalContentCanAccessFileUrls` both false. Plugins and fullscreen off.
+- A request interceptor that **blocks everything by default**. Remote
+ images, stylesheets and fonts are stopped before a connection opens,
+ which defeats tracking pixels and read receipts.
+- When something was blocked the header shows "Remote content blocked" with
+ a **Load remote content** button. The grant applies to that one render and
+ is never remembered. Switching threads discards both the grant and
+ anything already fetched under it.
+- Link clicks are intercepted and handed to the system browser, so a message
+ can never replace the pane with a page of its own choosing.
+- A whole thread renders as one document rather than one web view per
+ message, which would spawn a Chromium render process per message.
+ Sharing one document means `cid:` references could collide between
+ messages, so every reference is namespaced per message; two newsletters
+ using `cid:logo@example.org` resolve to their own images.
+- Attachment filenames are treated as untrusted input. A name from a MIME
+ header is reduced to its basename and the result is resolved against the
+ chosen directory; anything escaping that directory is refused.
+
+## Documentation
+
+- `CHANGELOG.md` - what changed in each release.
+- `docs/RELEASING.md` - versioning policy and the release steps.
+- `docs/manual-verification.md` - results of the manual checklist run
+ against a real mailbox, including the defects it caught.
+
+## License
+
+GPL-2.0-only. See `LICENSE` for the full text.
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
new file mode 100644
index 0000000..ca1c6d2
--- /dev/null
+++ b/docs/RELEASING.md
@@ -0,0 +1,83 @@
+# Releasing
+
+Short enough to follow without thinking, which is the point: releases here
+are infrequent enough to forget the order.
+
+## Versioning
+
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html). The public
+interface of a desktop mail client is not an API; for this project it means:
+
+- the configuration file format (section names, key names, their meaning)
+- the keybinding action names bindable from `[keys]`
+- the command-line interface
+- where configuration is read from
+
+Below 1.0.0 those may change in a minor release. 1.0.0 is the point at which
+they stop changing under users, which is a decision to make deliberately
+rather than a milestone that arrives on its own.
+
+- **MAJOR**: a config file that worked before now does not, or an action
+ name is removed or changes meaning.
+- **MINOR**: a feature, a new action name, a new config key that older
+ configs simply do not set.
+- **PATCH**: a fix that leaves all of the above alone.
+
+## Steps
+
+1. **Confirm the tree is clean and the tests pass.**
+
+ ```bash
+ git status --short
+ cmake --build build && ctest --test-dir build --output-on-failure
+ ```
+
+2. **Bump the version.** It is declared in exactly one place, the
+ `project()` call in the top-level `CMakeLists.txt`; `src/version.h.in`
+ generates `version.h` from it. Do not write the number anywhere else.
+
+ ```cmake
+ project(qtmaildir VERSION 0.2.0 LANGUAGES CXX)
+ ```
+
+3. **Move `Unreleased` in `CHANGELOG.md`** to a new dated section, and open
+ an empty `Unreleased` above it.
+
+ ```markdown
+ ## [Unreleased]
+
+ Nothing yet.
+
+ ## [0.2.0] - 2026-09-01
+ ```
+
+4. **Rebuild and check the version actually changed.** The generated header
+ is a build artifact, so a stale build directory will happily report the
+ old number.
+
+ ```bash
+ cmake -S . -B build && cmake --build build
+ ./build/src/qtmaildir --version
+ ```
+
+5. **Commit and tag.** Tags are annotated and signed, like every commit in
+ this repository.
+
+ ```bash
+ git add CMakeLists.txt CHANGELOG.md
+ git commit -S -m "release: 0.2.0"
+ git tag -s v0.2.0 -m "qtmaildir 0.2.0"
+ ```
+
+6. **Verify the tag is signed**, then push if there is a remote.
+
+ ```bash
+ git tag -v v0.2.0
+ git push && git push --tags
+ ```
+
+## After a release that changes the config format
+
+Note it in the changelog under `Changed` with the old and new spelling side
+by side. A user whose config silently stops working will not go looking for
+a version number to blame.