| Age | Commit message (Collapse) | Author | Files | Lines |
|
The app opened whichever saved query sorted first alphabetically, which
is not a choice anyone made: [queries] is read through childKeys(), so
savedQueries().first() means "Flagged" before "Inbox" before "Unread"
rather than anything the user expressed.
[general] startup_query names the entry to open and defaults to Unread,
so a fresh install comes up on the unified unread list. Saved-query
button order is untouched and stays alphabetical.
A name matching no saved query falls back to the first one rather than
starting with an empty view. That is reported as a problem only when the
user actually wrote the name; the built-in default naming a query they
never created is not something they got wrong, and warning about it
would fire on every launch of a config that has no Unread entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Zoom was Chromium's, not the application's: the web view handled the
keys natively and never told anyone, so there was no value to save.
qtmaildir now owns it. Zoom in, out and reset are real actions, in the
View menu and rebindable through [keys], and the factor is persisted to
the UI state file. Ctrl+wheel over the body zooms and Ctrl+middle-click
resets, both filtered by ancestry from an application-level filter: the
events are delivered to an internal QQuickWidget the web view creates
lazily, so a filter on the view itself never sees them.
The factor is clamped to 0.5 - 3.0, and NaN, infinity, zero and negative
values fall back to 1.0, since a corrupt state file must not be able to
leave the pane unreadable with no visible way back.
Both risks the plan flagged turned out not to exist, verified by probe
rather than assumed. The application QAction wins over the web view's
native zoom key, so the tracked factor cannot diverge from what is on
screen. And the factor survives setHtml(), so the web view is the single
source of truth and needs no reapply per render.
A third finding is worth recording because it produced a wrong fix
first. A probe using QTest::keyClick() reported Ctrl++ as a dead
binding, and a test was written asserting that. Both were wrong: Ctrl++
is exactly what the '+' key emits on an Italian layout, confirmed
against the real keyboard, and it is the shipped default. Whether a
symbol needs Shift is a property of the layout, not of Qt, and
keyClick() reproduces neither. The test now only checks that every
default parses, and the comment in defaultBindings() says not to
re-derive this from synthetic input.
Ctrl+= is a second binding for reset, skipped when [keys] gives it to
something else.
Also fixes a pre-existing bug the new config key exposed. [general]
entries were read as "general/<key>", which matches nothing: QSettings'
INI backend treats a section literally named [general] as its own
fallback section and strips the prefix. notmuch_config had therefore
never worked. Both keys are now read without it; the file format is
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Resizing the window, the splitter or a thread-list column was undone by
the next launch. State now round-trips through a separate settings file
at ~/.local/state/qtmaildir/uistate.conf, written on close and read at
startup.
The state file is deliberately not the user's config: a base64 geometry
blob does not belong in a hand-edited file, and rewriting that file on
exit would drop its comments and key order, which QSettings does not
preserve.
Two details that are easy to get wrong:
QStandardPaths::StateLocation appends both the organization and the
application name, and both are "qtmaildir" here, so it resolves to
~/.local/state/qtmaildir/qtmaildir. The path is built from
GenericStateLocation instead, matching Config::defaultPath().
Restore runs after buildMenus() rather than at the end of buildUi():
QMainWindow::restoreState() matches toolbars by object name and silently
drops the position of one that does not exist yet.
Every restore is conditional on a non-empty blob, so a missing or
rejected state file leaves the built-in defaults instead of producing a
zero-size window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The icon was committed in a previous session and referenced nowhere: no
qrc, no .desktop entry, no setWindowIcon. It is wired up now, as a window
icon, a desktop entry, and install rules placing both into hicolor and
share/applications.
resources.qrc belongs to the executable rather than to qtmaildir_lib. A
qrc compiled into a static library registers itself from a global
initialiser, and the linker discards that object because nothing
references it: the build succeeded, qInitResources_resources() was
present in the .a, and QFile::exists(":/icons/qtmaildir.svg") still
returned false at runtime. Verified loading at 16, 32 and 64 pixels after
the move.
Toolbar and menu actions take icons from the system theme by their
standard names, so they match the rest of the desktop rather than
shipping bespoke art. A theme lacking one leaves that action as text,
which still works.
|
|
Selecting a thread and hitting Delete changed nothing on screen, so
there was no way to tell the action had stuck.
The tag was always applied: applyTagChange() emitted dataChanged across
the row, and the Tags column did update. But Subject was set to stretch
while Tags came after it, so Subject took all free width and pushed Tags
out of view. The feedback lived in the one column that could not be seen.
Columns are now Tags, Date, From, Subject, with Subject stretching last
so nothing can be pushed off the right edge. A thread tagged deleted or
spam fills its whole row, muted red or orange with white struck-through
text, through the background, foreground and font roles, so no cue
depends on one column remaining visible.
Strike-through accompanies the fill on purpose: it survives a theme that
overrides backgrounds and reads without colour. Bold for unread still
composes with it.
Archive adds no tag, so an archived row is left unstyled for now.
|
|
Actions were a QHash of std::function dispatched by an event filter,
which nothing could put in a menu. They are QActions now, bound from
KeyMap so a [keys] override reaches the menus as well as the keyboard.
Menu bar covers every action; the toolbar carries only Sync, Archive,
Delete and Undo. Help > Keyboard shortcuts is generated from the actions,
so it shows what the keys really do rather than a copy that drifts.
spam and load_remote gained defaults, having been unreachable without a
hand-written binding.
The event filter is gone. Probing showed QAction shortcuts are dispatched
before the focused widget sees the key, so they beat QAbstractItemView's
type-to-search without one, and Qt already suppresses plain-letter
shortcuts while an editable widget has focus. Dropping the filter's
blanket guard also lets Ctrl+Q work while the query bar has focus.
registeredActionNames() is derived from the actions rather than
hand-maintained, so the two drift tests it needed are replaced by checks
that a configured binding reaches its action.
No confirmation dialogs: tag mutations still answer to undo.
|
|
Collects the items found while actually using 0.1.0. Two clusters dominate:
state that does not survive restart (splitter, zoom, account selection) and
actions reachable only by memorized keys.
The plan is open by design rather than a fixed release scope. Numbering is
stable so notes referring to an item keep meaning the same thing.
Decisions recorded while triaging:
- UI state goes to its own file, not qtmaildir.conf. That file is hand-edited
and rewriting it on exit would eat comments QSettings does not preserve.
- Auto-mark-read stays off the undo stack. An explicit toggle_unread action
already exists, so Ctrl+Z need not undo an action the user never took.
- Zoom is Chromium's, not ours. No setZoomFactor call exists in src/, so
persisting it means taking ownership of zoom first.
Icon is a tag rather than an envelope, since tagging is the core interaction
and an envelope would not distinguish it from any other mail client. Verified
legible at 16x16, which is where it is mostly seen.
|
|
The manual verification notes named the real threads used to test against a
live mailbox. Most of it was incidental, but one was not: the automated
sender identified in items 12, 13 and 16-18 is a healthcare provider's
patient-notification service, and naming it disclosed a medical
relationship. Also removed a correspondent's username, a locatable GitHub
thread id, a real maildir name in the spec's example config, and absolute
home paths.
Every finding is restated generically and none lost substance: "a message
with 3 inline cid: parts" carries the same technical weight as naming the
sender did. Counts and timings are kept deliberately, since those are the
evidence behind the claims rather than anything identifying.
Commit history and the v0.1.0 tag were checked and were already clean, so no
history rewrite is needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Adds two checks the earlier tasks turned up but only recorded in passing:
- Item 21: config.cpp validates the sync command by splitting on a space,
while MailSync splits it with QProcess::splitCommand. A quoted program
path containing a space makes the two disagree. The failure is safe
(sync disables itself) but the message misleads, so the check is to
confirm the behaviour and then pick a side.
- Item 22: deleting the sync script while running exercises the launch
failure path, which config's load-time validation cannot cover.
Also corrects two claims the plan had outgrown: task 13 is no longer the
notmuch layer's only coverage now that task 8 tests against a throwaway
database, and cron runs every 10 minutes rather than hourly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Runs the configured sync script through QProcess, merging stdout and
stderr into one log so a failing mbsync run has something to show. The
script is never run through a shell: the command is a config value, and
splitCommand keeps its arguments literal.
Two fixes against the drafted version:
- start() no longer calls waitForStarted(). It blocked the UI thread for
up to five seconds, which contradicts the spec's requirement that the UI
stay usable during sync, and it swallowed launch failures into a bare
false return. A missing script now surfaces asynchronously through
errorOccurred as finished(false, -1) with an explanatory log line, so
the spinner cannot hang with nothing to explain it.
- Removed a double-emit guard I had added on the assumption that QProcess
follows errorOccurred(FailedToStart) with finished(). Verified it does
not: FailedToStart is emitted instead of finished, never before it. The
guard was dead state and the comment justifying it was wrong.
Also corrects the sync interval throughout: the user's cron runs every 10
minutes, not hourly. The shorter interval strengthens the flock rationale
rather than weakening it, since collisions with a manual sync are that
much more likely.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The spec deferred NotmuchWorker to manual verification on the grounds
that testing it needs a real notmuch database. Building a fake one in a
temporary directory answers that objection, so Task 8 now gets real
coverage.
This is the only code in the project that writes to a notmuch index, so
a bug there corrupts real mail state. It was the wrong place to have the
weakest verification.
Task 13's manual checklist stays, as confirmation against real data
rather than as the only coverage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The interceptor previously trusted the whole qtmaildir: scheme, so a
hostile message body referencing qtmaildir://anything would have been
allowed. That made the interceptor's correctness depend on the scheme
handler in a different, later task.
It now trusts only the exact base URL passed to setHtml() and fails
closed when none is set. Records the resulting obligation on MessageView
to call setDocumentUrl() with that same URL.
Also corrects the attachment path guard to a separator-aware comparison
and notes that it is unreachable defence-in-depth: safeFilename() runs
first, so no caller-supplied name can reach it. A test driving saveTo()
expecting refusal cannot pass; test safeFilename() instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Attachment::saveTo()'s escape guard compared paths with a bare
QString::startsWith(), which is not a path-boundary test: "/tmp/safe-evil"
textually starts with "/tmp/safe", so a sibling directory whose name merely
extends the target's name would incorrectly pass as contained within it.
Extract the check into Attachment::isPathInsideDirectory(), comparing
QDir::cleanPath()'d absolute paths and requiring an exact match or a prefix
ending at a '/' boundary. Not exploitable today since safeFilename() always
reduces the name to a bare basename before saveTo() builds the target, so
the guard is unreachable via saveTo()'s public interface; comments on both
now say so plainly instead of implying it is currently load-bearing.
Add pathInsideDirectoryRejectsSiblingPrefix, testing the guard directly
(independent of safeFilename(), which would mask a broken guard by never
producing an escaping path), and safeFilenameStripsPathComponents, testing
the sanitiser that actually stops traversal today.
|
|
Accounts use [account.work] rather than [account/work]: QSettings' INI
backend treats "/" as its own hierarchical group separator, so a literal
slash in a section header parses as a nested group and trips
QSettings::FormatError, silently breaking childGroups() enumeration. A
dot carries no such meaning and keeps the format flat.
Saved-query order is alphabetical (QSettings::childKeys() sorts), not
file order; documented in code and tests rather than left to a false
assumption.
|
|
Both were narrowed in the first draft and are now in scope. Newsletter
threads make whole-thread rendering fundamental, and bulk archive is the
main thing a GUI buys over neomutt.
Two consequences fell out of thread rendering and are recorded in both
documents. The thread renders as one document in one web view rather than
one view per message, since a view each would spawn a Chromium render
process each. Sharing a document makes cid: references collide when two
messages use the same Content-ID, which is common across newsletters from
one sender, so every reference is namespaced to cid:<prefix>!<id>.
Deciding which messages render expanded needs the query, so loadThread now
intersects it with the thread and MessageRef carries a matched flag.
Batch tagging resolves thread ids to message ids on the worker in one
combined query. Undo stores thread ids and re-resolves, staying correct
after the selection moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Fourteen TDD tasks building from pure-logic units outward: keymap, config,
MIME parsing, and the request interceptor are fully tested first, then the
notmuch layer, then the UI that wires them together.
Seven test binaries, up from the three the spec named; config, htmlbuilder,
thread model, and sync each earned one. NotmuchWorker stays untested by
design, with Task 13 as the compensating manual checklist against a real
database.
Two narrowings from the spec are recorded rather than silently dropped:
thread rendering shows the newest message instead of a flat list of all
messages, and tagging applies to the loaded thread rather than every
selected one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|