aboutsummaryrefslogtreecommitdiffstats
path: root/assets/mailsync.sh
AgeCommit message (Collapse)AuthorFilesLines
37 hoursfeat: have the sync script report what it didDanilo M.1-13/+101
Item 174, and half of item 125. The premise was corrected before any code. The note asks for an external `notmuch new` to clear the pending count; it must not. That count means tag mutations not yet known to have reached the MAIL STORE, which is the server: an edit is in notmuch the moment it is made, and what is outstanding is mbsync pushing the renamed Maildir files. `notmuch new` re-indexes local files and pushes nothing, so clearing on it would tell the user their work was safe to quit on while it was still local. The entry's own proposal to watch notmuch_database_get_revision() was rejected for the same reason: a revision moves when mail ARRIVES too, and in neither case does it say anything about the server. What was actually wrong was the reporting channel. The application inferred a finished run from an inode in /proc/locks and from grepping the log for its RUN END banner, which made a human-readable line into wire format and could not say WHICH channels a run carried. The local sync path has always narrowed its clear to the accounts it carried; the external path could not, and cleared everything, so an edit to an account a run never touched was reported as delivered. So the script reports instead of leaving evidence to be inferred. It writes ~/.local/state/qtmaildir/syncstatus.json atomically at the end of every run, including a skip, naming the channels, both exit statuses and a state of ok, failed or skipped. MailSync::readStatus() reads it, MainWindow prefers it over the log banner and narrows the clear through Account::syncChannel(). A skipped run clears nothing, which is item 125's first half: the application can now see that a run happened and carried nothing. The log banner and lastRunOutcome() stay as the fallback for a missing file, which is what a first run after upgrading looks like. This is the user's own framing of the scope: the script was written for another system and adapted, and is now qtmaildir's only consumer, so it serves the application rather than the reverse. Two facts made it safe to act on: their crontab runs mailsync.sh and nothing else touches mail, and ~/bin/mailsync.sh is a symlink into this repo, so an edit is live on the next tick. Two bugs found while wiring it in, both recorded in the closed item. A test read the developer's real sync state, twice: a [sync] section naming only `log` leaves syncStatus() defaulting to the real file, so two tests asserting that a FAILED run leaves the count alone read the last real cron run, found ok, and cleared. Pinning only `status` has the mirror problem. noSyncTestReadsTheRealSyncState() is the guard, modelled on noTestCanSeeTheRealLockTable(). And Qt::ISODate carries no milliseconds. The status file is preferred only when it describes THIS run, compared against when the lock appeared, so a stale success cannot outrank a fresh failure; but the script writes date -Iseconds, and a round trip of "now" comes back 329 ms behind, measured. A fast sync's own file therefore parsed as stale and fell back to the log, with nothing failing to say so. One second of slack matches the precision the format carries. Design: docs/superpowers/specs/2026-08-29-sync-status-file-design.md Suite: 43 of 44, with undoMovesTheMessageBack failing as it does on master (item 136).
2026-08-07feat(sync): sync only the accounts with unsynced editsDanilo M.1-1/+19
A sync ran mbsync -a regardless of what changed, so tagging mail in one account fetched all of them. The account set was not a parameter anywhere on the path: MailSync::start() took no arguments and the script hardcoded -a, so nothing between a tag edit and mbsync carried which account changed. Track which accounts have edits and pass their mbsync channels through to the script, which now takes channel names and falls back to -a when given none. An empty set means all accounts, per the request: a sync with nothing pending is a fetch, and narrowing that to wherever the last edit landed would quietly stop collecting mail everywhere else. The channel is a new optional per-account key rather than the section key. The two names genuinely diverge, because a QSettings section key may carry dots that the channel does not, and mbsync treats an unknown channel as fatal rather than skipping it, so key-as-channel would fail those accounts' syncs outright rather than degrade. It defaults to the key, so accounts whose two names already agree need no config change. The edited-account set is deliberately not netted the way the pending-edit map is: that map tracks the index, where a tag removed and re-added leaves nothing outstanding, while this tracks the mail store, where both writes have already renamed files that mbsync still has to propagate. It is also snapshotted before flushHeldEdits(), which inserts into it synchronously rather than on a queued reply, so a successful sync cannot clear accounts whose edits it never carried. Closes item 49. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07feat(sync): the status bar says which account is syncingDanilo M.1-1/+9
"Syncing..." was set once and never updated, so a run that takes over a minute reported nothing about what it was doing. The original diagnosis in the backlog was half wrong, and two further wrong ones were made and discarded before the real cause: plain "mbsync -a" prints NOTHING until it exits, then a single summary line. Measured on a real run, one line at 11:11:08 then 73 within the second 11:11:33, at the end of a 46-second run. So there was no stream to read for the part of a sync that takes time. It is not buffering, so stdbuf changes nothing, and the account name is not unavailable either, which was the second wrong conclusion. mbsync -V is what changes both: it announces each channel as it reaches it, which is at once the progress and the account name originally asked for. The shipped script now passes it. SyncPhaseTracker derives a short status from the output as it streams: the channel being synced, the summary counts when mbsync ends, then the notmuch reindex. It lives beside MailSync rather than in the window so the matching rules are one testable thing, and it holds no widget. Matching is loose and case-insensitive, since the wording varies by version, and nothing in it decides success or failure: the exit status remains the only authority on that. Lines are reassembled in MainWindow before being fed, because QProcess::readAll() splits wherever it happens to and a half-line would match nothing. Every status is sanitised and truncated: the channel name comes from a config file this app does not own, and a long one must not stretch the status bar. Two defects in existing code, fixed with it. setSyncBusy(true) ran after start(), so a fast run's output arrived before the per-run reset and wiped its own phase. And a first draft deferred phases while a transient message showed, which let a "Background sync completed" message armed before the sync began suppress the whole run: a running sync's state outranks an expiring event message. Verified by replaying real captured mbsync -V output through the tracker, not only against fixtures. The MainWindow test paces its script with sleeps, since a script that prints everything at once arrives in one readyRead and makes every intermediate phase unobservable. Closes item 42. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04feat(sync): dismissable log pane, progress bar, and a skip that is not a failureDanilo M.1-3/+6
Four changes to the sync UI, three of them from using it. The log pane could not be dismissed. It is hidden at construction and shown on failure, and nothing ever hid it again, so a single failed sync left it on screen until the application restarted. It now sits in a container with its own Close button, and is 200px rather than 120, because mbsync's output is wide and repetitive and the shorter pane showed too little of it to read. It still appears only on failure, which the user confirmed is what they want. A sync gives no feedback while it runs. The status bar now carries an indeterminate progress bar for the duration, and the Sync button is disabled rather than left looking live. The bar is indeterminate on purpose: mbsync reports no percentage and the script's output is unstructured, so a bar filling left to right would be inventing a fraction nobody knows. The log is also cleared at the start of each run, since leaving the previous run's lines in place makes a stale failure look like the current one. The lock skip was reported as a failure. mailsync.sh exits when another run holds the lock, and that was exit 1, which qtmaildir reads as "sync failed": it showed the log pane and, on the exit path, told the user their changes were still unsynced. With a cron timer every ten minutes, a click landing inside a run is routine and none of that is true. The script now exits 75 (EX_TEMPFAIL) and the window reports it as its own case, saying a sync is already running. On the exit path it stays open and says plainly that the other run is most likely carrying the changes over but that this window cannot see it finish, rather than guessing either way. That last hedge is what item 27 records: the application cannot see a sync it did not start. The user chose continuous polling of the lock file over the narrower "only while quitting" version, and the entry notes that the lock is already the signal, so no status file is needed, and that a kernel lock cannot go stale where a written file can. Verified against stub binaries: a second run while the lock is held exits 75 and says SKIPPED, while the run holding it completes at 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04fix(sync): stop mailsync.sh rotating a log logrotate already ownsDanilo M.1-7/+10
Two problems, both visible in the first real run of the moved script. Every line was logged twice. The script writes to the log through tee, and the user's crontab also ended in ">> mailsync.log 2>&1", so both put the same output there. Reproduced: without the caller's redirect one run writes a single RUN START, with it, two. The crontab redirect goes away and the script keeps owning the log, which is what makes it behave the same however it is invoked. A comment in the header now says so, since the obvious cron line is the one that breaks it. The internal size-based rotation is removed outright. /etc/logrotate.d/mailsync already owns this file and keeps seven compressed days, and the two were fighting: the script's "mv $LOGFILE $LOGFILE.1" overwrites whatever logrotate had just placed at .1, which is why that file was 357K and uncompressed while .2 through .5 were properly gzipped. logrotate is better at this in every respect, including recreating the file with the right ownership, so the script should not have a second opinion. Verified against stub mbsync and notmuch in a fake HOME: one RUN START per run, stdout and the log agree, no rotation performed by the script, and a failing mbsync or notmuch still produces its own exit code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04feat(sync): bring mailsync.sh here, print to stdout, exit truthfullyDanilo M.1-0/+102
The reference sync script lived in the companion mailctl project, which documents that it never calls it: sync and organization are separate there on purpose. qtmaildir is the only thing that runs it programmatically, and two of its behaviours were live bugs here, so it belongs in this repo where the constraints on it are. It printed nothing a caller could see. Everything was redirected to $LOGFILE, so MailSync captured an empty stream and the sync log pane stayed blank, a known limitation carried since 0.1.0. It now writes to both through tee: cron keeps its log, and the pane has something to show. It also ended in an unconditional exit 0, discarding statuses it had already computed. qtmaildir believes that exit code: a failed mbsync reported success, cleared the unsynced-changes count, and would have allowed a sync-on-exit to quit over a sync that never happened, which is exactly the case the exit prompt exists to prevent. It now exits with mbsync's status, or notmuch's when mbsync succeeded. The statuses move through files rather than shell variables because the run block is piped into tee, which puts it in a subshell where an assignment does not survive. Verified against stub mbsync and notmuch binaries in a fake HOME, so no mail or database was touched: output reaches stdout, the log still gets every line, and a failing mbsync or notmuch produces its own exit code rather than zero. Both properties are now recorded in CLAUDE.md, since they exist for this application's sake and an edit that looks like a cleanup would remove them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>