aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md198
1 files changed, 198 insertions, 0 deletions
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.