summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans
AgeCommit message (Collapse)AuthorFilesLines
13 hoursbuild: add a SlackBuild under assets/slackbuildDanilo M.1-1/+1
Packages the application for Slackware, following SBo conventions with two deliberate departures: the tag is _danix rather than _SBo and the package type is txz rather than tgz, since this is not an SBo submission. sbolint reports exactly those two as errors and nothing else; the template comments it warned about are gone. Written against the install layout the build really produces, checked by staging it: one binary, one .desktop entry and one scalable icon, no libraries and no man or info pages. The template's .la removal, man and info compression and perllocal.pod cleanup would all act on nothing here, so they are left out rather than carried along as dead code. doinst.sh keeps only the desktop-database and icon-cache updates. The download URL and checksum are verified rather than assumed: sbodl fetches the tarball and reports "md5sum matches OK". Adds QTMAILDIR_BUILD_TESTS, defaulting to ON so the ordinary build is unchanged. A packaging build has no use for the test binaries, and building them pulls in Qt6::Test to produce nothing that ships. Note the 0.4.0 tarball predates this option, so with that source the flag is accepted but does nothing; the README says so. notmuch is the only dependency outside Slackware. Qt6 including WebEngine, gmime and cmake are all stock, which is what REQUIRES reflects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 hoursfeat: make attachments reachable from the message paneDanilo M.1-1/+47
The attachment bar had been an empty placeholder since it was written: MessageView created it and added it to the layout, and nothing ever put anything in it. MimeParser had been extracting attachments the whole time and Attachment::saveTo() already carried the path-traversal guard, so the backend needed calling rather than writing. The bar holds one "Attachments (N)..." button whatever the count. One button per file was built first and was wrong: a thread with sixteen of them made the bar as wide as the window, pushed the splitter over and left the thread list a few pixels wide. The button opens a dialog listing message number, filename and size with a Save each, and a "Save all..." when there is more than one. Save all writes into a new subdirectory named "<date> <subject>" inside a parent the user picks, rather than dropping sixteen files loose among whatever is already there. Zipping was considered and rejected: Qt ships no zip API, so a real archive meant a new build dependency or shelling out to /usr/bin/zip at runtime, and a subdirectory answers the actual requirement. The picker names the subfolder before the user commits to a location. The subject is attacker-controlled and becomes a directory name, so attachmentFolderName() sits beside the other guards in mimeparser.cpp: it strips separators, control characters and leading dots, caps the length, and falls back to a generated name. Its test asserts that every hostile subject still resolves inside the parent directory. Two defects surfaced while using it, both silent: saveTo() overwrites an existing file, and several messages in one thread commonly attach the same filename. Saving that thread destroyed six of sixteen files while reporting all sixteen as saved. The batch path now uses saveWithoutOverwriting(), which appends " (2)" before the extension and keeps a compound extension whole. Qt::RFC2822Date rejects a Date header that carries a timezone comment, "+0200 (CEST)", which is legal per RFC 5322 and common in real mail. Qt refuses the entire string rather than ignoring the comment, so every such message lost its date prefix. Comments are stripped before parsing. Opening an attachment in its default application is deliberately not included: handing a file from a stranger to xdg-open is a different security decision from writing it where the user asked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 hoursdocs: add backlog items 15 to 17 from usage notesDanilo M.1-0/+103
Attachments turned out to be a real gap rather than a discoverability one. MessageView creates the attachment bar and adds it to the pane, but nothing ever populates it: m_attachmentBar appears nowhere else in the codebase, so it has never displayed anything. MimeParser already extracts attachments and Attachment::saveTo() already carries the path-traversal guard, so the backend needs calling, not writing. Item 16 makes delete a toggle, with the open question of what a mixed selection should do. Item 17 needs a new worker call, since there is no way to list tags today. Also records that this document's numbering and the user's own notes have diverged, so a reference to "item 13" stays resolvable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 hoursfeat: choose the startup query by nameDanilo M.1-0/+15
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>
13 hoursfeat: own the message-pane zoom and persist itDanilo M.1-1/+33
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>
13 hoursfeat: persist window, splitter and column widthsDanilo M.1-1/+19
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>
13 hoursfeat: use the application iconDanilo M.1-1/+39
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.
13 hoursfeat: show that a tag action landedDanilo M.1-0/+38
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.
13 hoursfeat: add menus, a toolbar and a shortcut referenceDanilo M.1-3/+32
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.
13 hoursdocs: add post-0.1.0 usability backlog and app iconDanilo M.1-0/+349
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.
13 hoursdocs: remove personal details from the verification recordDanilo M.1-1/+1
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>
13 hoursdocs: fold deferred findings into the task 13 checklistDanilo M.1-5/+23
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>
13 hoursfeat: add MailSync process wrapperDanilo M.1-4/+4
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>
13 hoursdocs: unit-test NotmuchWorker against a throwaway databaseDanilo M.1-3/+15
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>
13 hoursdocs: scope the document-load exemption to an exact URLDanilo M.1-3/+22
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>
13 hoursfix: make attachment path-containment guard separator-awareDanilo M.1-5/+20
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.
13 hoursfeat: add Config with account, query, and sync parsingDanilo M.1-6/+6
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.
13 hoursdocs: fold full thread rendering and batch tagging into spec and planDanilo M.1-86/+515
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>
13 hoursdocs: add qtmaildir v1 implementation planDanilo M.1-0/+3988
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>