# 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] ### Added - **Composing and sending.** Ctrl+N opens a composer; Reply, Reply all, Reply without quoting and Forward start one from the selected message. Each is a window in its own right, so several can be open at once and the main window stays usable behind them. - The body is markdown, sent as plain text exactly as typed. "Also send a formatted copy" renders an HTML part from the same source and sends both in a `multipart/alternative`; `[compose] send_html` sets the default, and a reply follows what the message being answered used. - Drafts autosave to the account's `drafts` folder as ordinary Maildir files, so mbsync carries them to the server and another client can pick one up. - **Drafts can be opened and finished.** Double-click one, or use Edit draft in the Message menu. The composer takes ownership of the file, so saving replaces the draft rather than leaving a second copy, and the Bcc list the draft carries is read back rather than dropped. The action is offered only on a message that really is in a drafts folder. - **A Drafts filter** in the query row, beside Sent and Trash. It matches each account's `drafts` folder, so it finds what the composer actually writes rather than trusting a flag. An account that configures no drafts folder contributes nothing and shows no button. - `Ctrl+W` closes a composer, the way it closes a window elsewhere. The draft is saved or discarded exactly as it is when the window is closed by any other route. - **The composer is laid out by scope.** Send is a large button beside the headers rather than one more entry in a row of formatting buttons; the formatting controls are icons on their own bar directly above the editor, with Attach and Send as HTML at its right end; Remove attachment appears beside the attachment list, and only once something is attached. - Cc and Bcc hide behind a disclosure next to To:, and reveal themselves whenever either already carries a value, so a reply or a draft never hides a recipient. - **A button bar over the message pane.** Reply and Forward sit directly above the message, with Toggle HTML at the right end. Forward had been reachable only from the Message menu. - A reply opens with the cursor on a blank line above the quote, and with the body focused, so typing can start immediately. `[compose] quote_position` defaults to `below` (your reply first, the quote under it); `above` puts the quote first and the cursor after it. Closing a composer with unsaved edits asks first, and so does quitting with one open. - Sending goes to a per-account `send_command` on stdin, so any sendmail compatible program works (msmtp, ssmtp, sendmail) and the credentials stay in that program's own store. The application still speaks no network protocol of its own. An account with no `send_command` is receive-only, and the composer says so rather than failing at the end. - A send counts down before it runs, and Undo during that window stops it and returns you to the composer with everything intact. Nothing reaches the network until the countdown ends. `[compose] send_delay_ms` sets the length; 0 removes it. - A copy of every sent message is filed in the account's `sent` folder. - The notmuch hooks that auto-tag incoming mail now live in this repository, under `assets/hooks/`. They moved from the companion `mailctl` project, which is being retired. ### Changed - "Also send a formatted copy" is now "Send as HTML", which says what it does. It moved from a checkbox above the editor to the right end of the formatting bar. - The main toolbar keeps the actions that need no particular message (Compose, Sync, Archive, Delete, Mark all read, Undo). Reply moves to the new message-pane bar, where Forward joins it. ### Fixed - Sent mail and drafts no longer appear in the inbox. notmuch tags every newly indexed file with `inbox`, including the copy this application files after a send and the drafts it autosaves, so both turned up in the Inbox view and in any `tag:inbox` search. The `post-new` hook now removes it from mail inside a configured `sent` or `drafts` folder, which is mail that never arrived. Only `inbox` is touched, and trash is deliberately left alone so Restore can still find where a message came from. - Quitting with a composer open no longer leaves it behind. A composer is a top-level window with no parent, so closing the main window did not take it down and the process stayed alive for it: the main window vanished, the composer stayed on screen, and closing it then asked about unsaved edits for a session that had already ended. ### Upgrading **To send, an account needs a `send_command`.** Without one it is receive-only: it still reads, tags and syncs exactly as before, and the compose actions are simply disabled for it. Nothing breaks by doing nothing. ```ini [account.work] send_command = /usr/bin/msmtp -a work -t ``` The command receives the finished message on stdin and is run **without a shell**, so pipes and redirections do not work; give an absolute path and plain arguments. Credentials belong to that program, not to this one. An account that sends should also name `drafts` and `sent`, both relative to its `maildir`. Without `drafts` a composer cannot autosave and says so; without `sent` no copy of what you sent is kept locally. **If you run the auto-tagging hook, redeploy it.** It moved here from the `mailctl` project and gained the sent-and-drafts carve-out described above. Copy `assets/hooks/post-new`, `mailrules.py` and `qtmaildirconf.py` into `/.notmuch/hooks/`, all three together: `post-new` imports the other two, and the new one reads your `qtmaildir.conf` to learn which folders are yours rather than arrivals. The rules file itself is unchanged, and `mailctl` can still read it. **Existing sent mail and drafts keep their `inbox` tag**, since the hook only sees newly indexed mail. To clear the backlog in one pass: ```sh notmuch tag -inbox -- 'tag:inbox and (path:"work/Sent/**" or path:"work/Drafts/**")' ``` naming your own folders. This is a tag change only: no file moves, nothing reaches the server, and re-adding `inbox` to the same query undoes it. - Clicking a link in a message opens it in the system browser. Links carrying `target="_blank"`, which is most links in HTML mail, did nothing at all: no error, nothing on screen. Chromium routes those to a new-window request rather than to the navigation handler, and nothing answered it, so the click was discarded. Plain links, as in most text mail, were unaffected and already worked, which is what made this look like "HTML mail is broken". - The context menu on a link no longer offers Open in new tab, Open in new window, Open in this window or Save link. The first three cannot work: the pane has no tabs and must never open a window or navigate away from the message. Save link is removed for a stronger reason: it would fetch a sender-chosen remote URL through the message pane, which never fetches remote content by design. Copy link address is kept, and attachments are still saved from the attachment bar, which reads what the message already carries rather than the network. ## [0.26.1] - 2026-08-19 A bugfix release for one defect in 0.26.0, reported the day it shipped: moving a message between folders kept its filename, so mbsync's per-folder UID travelled with it and a later sync failed with `Maildir error: duplicate UID`. Also moves the copy confirmation added below into the message pane itself. ### Fixed - Moving a message between folders now gives its file a fresh Maildir name. 0.26.0 carried the old name across, including mbsync's `,U=` UID infix, which belongs to the folder the file came from. Moving a message out and back reinserted a UID the server had since reassigned, and mbsync refused the folder with `Maildir error: duplicate UID`. If you saw that error, see Upgrading below. ### Upgrading If a sync reported `Maildir error: duplicate UID in ` after deleting or restoring mail with 0.26.0, that folder holds two files claiming one UID. No mail is lost; mbsync simply refuses to sync the folder until it is resolved. Stop any running sync, then strip the `,U=` infix from the newer of each pair and reindex: notmuch new mbsync re-derives the UID on the next sync. The code no longer creates this state. An earlier version of the same bug could also create a wrongly named FOLDER, which `Create Both` then propagated to the mail server. If you have one, delete it on the SERVER first: `Expunge Both` applies to messages, not to mailboxes, so removing the folder locally and syncing simply pulls it back. Note that a webmail sidebar may not show it while IMAP still lists it, so check with `mbsync -l ` rather than by eye, and confirm the messages inside it also exist in their correct folder before deleting anything. ### Added - The message pane's right-click menu offers **Select all**. Chromium's own menu for this pane has never carried it. - Copying from the message pane now says what was copied. Copy, Copy link address, Copy image and Copy image address each show a brief confirmation in the bottom right of the pane, beside the gesture rather than at the far end of the window. ## [0.26.0] - 2026-08-19 Delete now moves mail into the account's trash folder instead of only tagging it. Every version before this one added a `deleted` tag and left the file exactly where it was, so deleted mail accumulated in the inboxes with only a chip to say otherwise. A Trash filter joins the other four, Restore from trash is its inverse, and a menu entry lists the mail the old behaviour stranded so it can be reviewed rather than migrated behind your back. **Every account needs a new `trash` key.** See Upgrading below. ### Added - Delete now moves mail into the account's trash folder instead of only tagging it. A **Trash** filter sits beside Unread, Inbox, Important and Sent, and composes with the account selector like the others. - **Restore from trash** (`Ctrl+R`), enabled while the trash view is showing. A message this application deleted returns to the folder it came from; one trashed by another client returns to the inbox. - **Find stranded deleted mail** (`Ctrl+Alt+T`), in the Message menu. It lists mail tagged `deleted` that never moved anywhere. Run it whenever you like; it reports and moves nothing on its own. - An optional per-account `inbox` key, naming the inbox folder a restore falls back to when a message carries no record of where it came from. It defaults to `Inbox`, so an account whose inbox is named that needs nothing. - `Del` now deletes, alongside `Ctrl+D`. It still edits text in the query bar and in any other text field, so nothing is lost where the key already had a job. ### Changed - Open thread, Clear message pane and Clear selection appear in the View menu. All three existed and were reachable only by their shortcuts. - Restoring from the trash view refreshes the list, so the restored message leaves it straight away instead of sitting there until the Trash filter is clicked again. Other views are unaffected: a deleted message's card deliberately stays where it is. ### Upgrading **Every account now needs a `trash` key** in `qtmaildir.conf`, naming its trash folder relative to `maildir`: [account.work] maildir = work trash = Trash The folder must be one your `mbsync` configuration actually syncs, or the move will never reach the server. Accounts without the key still load and still read mail, but Delete cannot work on them and a warning says so at startup. **Name the folder exactly as it exists on the server.** A trash or inbox name that does not match creates that folder rather than reporting an error, and under mbsync's `Create Both` the wrongly named folder then propagates to the mail server, where other clients will see it. **Mail deleted by earlier versions is not migrated.** It carries the `deleted` tag and sits wherever it always was. Use **Find stranded deleted mail** to review it, and Delete on what should really go. Note that Delete's reversibility depends on your provider: a trash folder the provider purges on a timer will eventually remove the mail for good. ## [0.25.0] - 2026-08-17 Acting on a row now means the message that row displays, not the whole conversation, which is the change most likely to affect your habits: Ctrl+D on a thread's card deletes one message where it used to delete the thread. The whole-thread actions are still there under a Whole thread submenu. Important becomes a toggle, the tagging rules list gains a Note column, and the message pane stops offering browser actions that could never work. The Italian translation catches up with the whole-thread actions, which shipped as English in 0.24.0. ### Upgrading **Acting on a thread row now acts on the message it displays, not on the whole conversation.** Selecting a thread's card has shown one message since 0.22.0, but Delete, Archive, Important, Mark spam and Toggle unread still acted on every message in the thread. They now act on the message you are looking at. The whole-thread versions are still there, under **Whole thread** in the Message menu and in the thread list's right-click menu, with new bindings a modifier out from their old ones: | Action | Message | Whole thread | |---|---|---| | Archive | `Ctrl+E` | `Ctrl+Alt+E` | | Delete | `Ctrl+D` | `Ctrl+Alt+D` | | Mark spam | `Ctrl+Shift+S` | `Ctrl+Alt+S` | | Toggle unread | `Ctrl+U` | `Ctrl+Alt+U` | | Important | `Ctrl+I` | `Ctrl+Alt+I` | Existing `[keys]` entries keep working and keep their meaning: `delete` is still `delete`, now scoped to one message. The thread actions are separate names (`delete_thread`, `archive_thread`, `spam_thread`, `toggle_unread_thread`, `flag_thread`) and can be rebound like any other. "Mark all read" is unchanged: it never used the selection. **Important is now a toggle.** `Ctrl+I` on something already marked important removes the tag, where before it re-applied it and appeared to do nothing. If you were using it to mark a batch that already contained important messages, note that a selection where *everything* is already important now unmarks all of it, matching how Delete and Toggle unread have always behaved. **The tagging rules window forgets its column widths once.** A Note column was added to the rule list, and a saved layout describing the old set of columns cannot be applied to the new one. The first time you open the dialog after upgrading, the columns are sized to fit; drag them once and the new layout is remembered as before. ### Changed - **Reading a message no longer marks its replies read.** The two-second automatic mark-read applied to the whole thread, so opening a conversation marked messages read that had never been shown. Because `maildir.synchronize_flags` is on, that reached the server and could not be undone from here. It now marks only the message on display, and it applies to a reply as well, which previously was never marked read at all. - An unread reply is bold as well as undimmed, which is the pair of cues an unread thread has carried since 0.11.0. Replies were deliberately plain before, on the grounds that the thread row above already flags the conversation; once a thread is expanded, that row cannot say which of its messages are unread, and dimming alone was too quiet to notice. Replies stay a size smaller than their thread, so the two kinds of row still read apart. - **Important is a toggle**, like Delete and Toggle unread beside it. It only ever added the tag, so pressing it on something already important re-applied a tag that was already there, which changes nothing and looked like a dead key. Removing `flagged` previously meant opening the tag dialog. As with Delete, one direction is chosen for the whole selection: it unmarks only when every selected row is already important, so a single keystroke cannot leave a selection in two states. - The tagging rules window shows each rule's **Note** in the list. The field explaining why a rule is shaped the way it is was only visible after selecting that rule and reading the form, which is the wrong way round for the one column that says what a rule is for. Long notes are elided with the full text in the tooltip. ### Fixed - The message pane's right-click menu offered **Back, Forward, Reload** and **Save page**. The pane is not a browser: every message is rendered from memory with no history and no network, so all four were inert as well as meaningless. The menu now carries only what applies to a message: Copy, View source, and searching for the selected text. - Acting on a reply inside an expanded thread could read a different thread's state, because a reply's position is counted within its own thread and was being used as a position in the whole list. The first reply of any thread therefore answered as the first thread in the list. Three actions chose what to do from that wrong answer: **Delete** could undelete a thread that was never deleted, **Toggle unread** could go the wrong way, and **Edit tags** offered to remove tags the selected message did not carry, while showing the ones it did as unset. Every action that acts on a selection now resolves the thread through the row itself. - Acting on a reply gave no visual feedback at all: Delete and Toggle unread moved the unsynced-changes count and left the row looking exactly as it did before. A change scoped to one message now repaints that message's own row. A deleted or spam reply is filled and struck through, as a thread in the same state already was. The thread's own card deliberately does not change, since one deleted reply does not delete the conversation. - Marking an expanded thread read left its replies looking unread. The write reached every message, but the list only updated the thread's own card, so the replies stayed bold and undimmed until the next query corrected them. They now follow a thread-wide change as the card does. - Acting on a thread's own card did not repaint it, and emptied the message pane's chip row. A message-scoped write was only ever applied to a thread's loaded replies, and a thread's first message is not one of them: the card stands for it. So Delete or Toggle unread on a root card changed nothing on screen, and the pane's chips were replaced with the empty tag list the lookup returned. Both now find a root card's own message. - **A thread's card showed tags belonging to other messages in the thread**, and so did the message pane. notmuch reports a thread's tags as the union over its messages, so a four-message thread whose third message is signed read as signed everywhere, including on a card that stands for the first message and in a pane showing only that message. Opening a message now records what it really carries, and the card and the pane both use it. A card still shows the whole conversation's tags, since a card sits above a thread: its own message's tags come first at full size, and the ones only its siblings carry follow, smaller and muted. The split arrives with the query, so a row reads correctly before it has ever been selected. This is also what was stopping a card from repainting: with no per-message tags for a thread's first message, marking it read or deleting it had nothing to change, so the row stayed bold or unstruck while the write went through. - The message pane's tag chips did not follow an edit to the message on display. Tagging a reply repainted its row in the list and left the pane describing the message as it was, until you selected another message and came back. The pane now follows a message-scoped edit, as it already did a thread-scoped one. - **A tag change made on a single message during a sync was silently lost.** Edits made while a sync holds notmuch's write lock are held and sent when it finishes; that queue only ever re-sent whole-thread changes, so a message-scoped one was applied to the row, counted in the unsynced-changes indicator, and then dropped without ever being written. The change appeared to have been made and reported itself as pending right up until it vanished. - Delete and Toggle unread chose their direction from a reply's THREAD rather than from the reply itself, which made both one-way on a reply. A message-scoped write never changes the thread's tags, so the answer never moved: pressing Toggle unread on an unread reply re-added the tag it already had, and pressing Delete twice on a reply deleted it twice instead of putting it back. Re-applying a tag a message already carries changes nothing, which is why the key looked dead. Each row is now asked about what it stands for. ## [0.24.0] - 2026-08-15 Double-clicking a row now opens its thread on its own, expanded, with the row you clicked showing in the message pane. The rest of the release is three defects found by using the application: a card that would not open, an edit that came back undone after a sync, and an automatic sync that gave up instead of retrying. ### Added - Double-clicking a row opens that thread alone in the list, expanded, with the double-clicked row's own message in the pane. Double-clicking a reply opens its whole thread with that reply showing, rather than the reply by itself. There is no Back action: the filter buttons are how you leave the view. ### Fixed - A query whose result contains the thread already open left the message pane on the placeholder: the card selected, the status bar reporting one thread, and nothing rendered. Running a query blanked the pane but kept the ids naming what it had been showing, so selecting that thread again was read as "already displayed" and never loaded it. This is why an `id:` query copied out of a message's own details dialog produced a card that would not open. - An automatic sync skipped because another sync was already running gave up instead of trying again, so an edit the running sync had already passed sat pending until a manual sync or the next cron run. It now re-arms at the configured delay. Skipping a concurrent run is unchanged: two mbsync runs cannot share the lock. - A tag change made while a sync was running reappeared undone in the thread list when that sync finished. The change was held until the sync released notmuch's write lock, but the list was refreshed from the database before the held change was written to it, so the refresh painted the old tag back. The change itself was never lost, only the list was wrong. ## [0.23.0] - 2026-08-15 qtmaildir speaks Italian. Nothing loaded a translation before this release, so the interface was English whatever the locale said; all 355 strings are now translated, the language follows your environment, and a new `language` key overrides it either way. The audit that made this possible found eight labels in the tagging-rules dialog that could never have been translated into any language, in code that looked correct. The filter row also shows which of its four views you are in. ### Upgrading Nothing to do. The interface stays English unless your environment asks for Italian, and `language = en_US` pins it there permanently if you prefer. If you run an Italian desktop and have `startup_query` naming a built-in filter, you may keep it in English (`Inbox`): filter names are resolved by identity as well as by label, so the English spelling works in every language. The translated name works too. ### Added - **An Italian translation, and the machinery to load one.** Nothing read a translation before this: there was no `QTranslator`, no `.ts` file and no build rule, so every string was English whatever the locale said. The language follows the environment by default, `LANG=it_IT.UTF-8`, and any locale without a translation runs in English as before. All 355 strings are translated. - `ctest -R translations` guards the translation against the two ways it rots: a new string added without one, and a string `lupdate` cannot see at all. - **`language` under `[general]`**, choosing the interface language regardless of what the environment asks for. A short code or a full locale name both work (`it`, `it_IT`), `system` is the default and follows `$LANG`, and `en_US` forces English on a non-English desktop. A value that is not a locale name is reported rather than silently ignored, since `itallian` and `en_US` both load no translation and would otherwise look the same. - **The built-in filter matching the current view is drawn as a pressed button**, so the row shows where you are. It tracks the query rather than the last click, so editing the query clears the highlight and typing a filter's query lights it; changing the account recomputes it. ### Fixed - **Eight labels in the tagging-rules dialog could never be translated**, in any language. From, To, Cc, Subject, Tag, Folder, Attachment and Date were declared with `QT_TR_NOOP` inside an anonymous namespace, where `lupdate` extracts nothing while the dialog's own `tr()` reads them at runtime, so no translation file ever contained them. The source looked correct and only `lupdate` revealed it. - Twenty configuration and keybinding warnings were not translatable. They are user-facing: they appear in the status bar and the "Configuration problems" dialog. - **`startup_query` naming a built-in filter stopped working under a translated interface**, found in hand testing the Italian build. A filter's name is a translated label, so `startup_query = Inbox` matched nothing where the filter is shown as "In arrivo": the application opened a different view and reported the user's own working configuration as invalid. It now resolves on the generator as well, which is the same in every language, so a config written in English keeps working whatever `LANG` says. The translated name is still accepted. ## [0.22.0] - 2026-08-15 Follows 0.21.0's built-in filters with the parts that shipped wrong or missing. The filters carry icons and the flagged one is labelled Important, matching the action of that name. `startup_query` can name a filter as well as a saved query, which it could not, and a new `startup_account` says which account the dropdown opens on, so the application can start in "work - Inbox" rather than "All accounts - Inbox". ### Added - **`startup_account` under `[general]`**, naming the account the dropdown starts on by its `[account.]` suffix. With a `startup_query` naming a built-in filter, the application opens on that account's view of it: `startup_account = work` with `startup_query = Inbox` starts in "work - Inbox" rather than "All accounts - Inbox". It sets the starting scope, not a sticky one. Clicking a saved query that names no account still clears the selection, as it always has. A key naming no configured account is reported at startup and ignored. ### Changed - The built-in filters carry icons, like the Save button at the other end of the query row, with their text beside them. Important is a star, and Sent uses the folder icon rather than the envelope-in-flight some themes lack. ### Fixed - **`startup_query` can name a built-in filter**, and looks at both those and your saved queries. In 0.21.0 it searched saved queries only, so a `startup_query = Inbox` stopped matching once the duplicated Inbox entry was removed from `queries.json`, and the application opened on whichever query happened to be first in that file. A name matching nothing now falls back to the Unread filter rather than to an arbitrary saved query, and a saved query still wins a name collision with a filter. A `startup_query` naming a filter also **runs**. A filter composes its query from your accounts rather than storing one, and the startup path read the stored field directly, so it would have opened on an empty query bar. - The flagged filter is labelled **Important**, matching the action of the same name. It shipped in 0.21.0 as "Flagged", which put the same tag under two names in one window. ## [0.21.0] - 2026-08-15 The query row gains four filters the application ships: Unread, Inbox, Flagged and Sent. Unlike the buttons before them they cooperate with the account dropdown, so selecting an account and clicking Unread shows that account's unread mail rather than everyone's. Your own saved queries keep the behaviour they had and move below them. ### Added - **Four built-in filters on the query row: Unread, Inbox, Flagged and Sent.** They are part of the application rather than saved queries you happened to pin, and they **compose with the account dropdown**: select an account, hit Unread, and you get that account's unread mail instead of everyone's. With "All accounts" selected they span every account, as before. Changing the account still runs nothing on its own. The dropdown chooses the scope and the button is what queries, so you can pick an account and then decide what to look at. Your own saved queries are unchanged and keep the behaviour they had: one that names no account still clears the selection, because a saved query says exactly what it shows. - A third right-click search action, **Exclude from search**, wherever the other two are already offered: the message pane's header and body, a tag chip, and every row of the details dialog. It narrows the current query by everything that is *not* the value under the cursor, so a sender, a subject or a phrase can be dropped from a result list without retyping the query. The entry is greyed out when the query bar is empty, since there would be nothing to exclude from and running it would mean the whole Maildir minus one value. ### Removed - **The conversation view.** Selecting a thread used to render the whole conversation in the message pane, earlier messages as unexpandable stubs with the last two opened. Selecting any row, a thread root or a reply, now renders exactly one message. It was also inconsistent: a thread root rendered the conversation only until the thread had been expanded once, after which the identical click rendered a single message. Read a thread by expanding it and walking the reply rows. ### Fixed - **A saved query in the "More queries" menu could not be run.** Clicking one only opened its submenu of Edit and Delete actions. The entry now offers **Run** at the top of that submenu, above the editing actions. This was not new, but it was easy to miss while most queries lived on the row as buttons rather than in the menu. ### Changed - The status bar counts threads as they arrive instead of saying "Searching..." until the query is done. On a large query after a reboot, when the notmuch index is still being read from disk, the first rows appear seconds before the walk finishes; the bar used to go on claiming the query was running for all of that time, which made a slow query look like a frozen one. Nothing about the timing changes. - The dialog reporting configuration problems at startup now appears over the main window instead of before it. Which problems interrupt startup is unchanged: a keybinding that is being ignored does, a notice such as "no sync command configured" does not. ### Upgrading **The query row now starts with four buttons the application ships**, so your own pinned queries sit after them. If you had saved queries named Unread, Inbox, Flagged or Sent, you will see two buttons with the same name: yours and the built-in one. Right-click yours and choose **Move to menu** to keep the row readable. Your **Sent** button is a special case and is moved for you. Version 0.19.0 turned the old hardcoded Sent button into a saved query in `~/.config/qtmaildir/queries.json`; that entry now duplicates the built-in filter, so it is unpinned automatically on first launch. It keeps its name, it keeps working, and it is in the **More queries** menu. Nothing is deleted, and pinning it again restores it if you prefer it there. The difference worth knowing: the built-in filters **use** the account dropdown, while a saved query **sets** it. Selecting an account and clicking Unread shows that account's unread mail; selecting an account and clicking a saved query that names no account clears the selection first, as it always has. Reading a thread now means expanding it and clicking down its replies. Nothing in your config changes, and no habit built on the reply rows is affected, but the pane will show one message where it used to show a conversation. Automatic mark-read still applies to the **whole thread**, so opening a thread root marks its replies read as well, including ones you have not displayed. That was consistent while a root rendered the conversation and is not any more. Set `mark_read_delay_ms` to a negative value under `[general]` to turn the behaviour off entirely if that matters to you; narrowing it to one message is open work. ## [0.20.0] - 2026-08-14 Anything on screen in the message pane can now be searched for by right-clicking it, either replacing the current query or narrowing it. The message details dialog became labelled rows on the way, so each value in it can be searched for on its own. ### Added - Anything on screen in the message pane can be searched for by right-clicking it. The subject and the date in the header, the sender and recipients when the thread holds one message, a tag chip, a phrase selected in the message body, and every header of every message in the details dialog. Each offers **Search for this**, which replaces the query, and **Add to search**, which narrows what is already there. Narrowing is the half worth knowing about: a query returning a thousand threads can be cut down by adding a sender or a date to it, without retyping the query you started from. A search is only ever a search. Turning what you find into a tagging rule is still the existing road: save the query, then create a rule from it. ### Changed - The message details dialog shows labelled rows rather than one block of text, so each value can be searched for on its own. ## [0.19.0] - 2026-08-14 A saved query can become a tagging rule without retyping it, and a rule can no longer disappear because of what you called it. The name field accepts what a person types and cleans it into a valid id, saving says why when a rule would not survive being read back, and a rule already in the file with an unusable name is loaded so it can be repaired rather than silently ignored. ### Upgrading If a rule in `~/.config/mailrules/rules.json` has a name that is not lowercase letters, digits and dashes, it was being dropped by every reader: invisible in the dialog and never applied by the `post-new` hook. It now loads with its name repaired, and the dialog warns that the file still holds the old one. **Open the tagging rules dialog and press Save** to write the repair back. Until you do, the hook still ignores that rule. ### Added - A saved query can be turned into a tagging rule: right-click a stored query and choose **Create tagging rule...**. The rules dialog opens on a new rule carrying that query, with the tags left for you to fill in. Generated entries such as Sent are excluded, since their query is composed from your accounts and a rule would freeze a stale copy of it. ### Fixed - A tagging rule whose name contained a space, a capital or punctuation was written to `rules.json` and then dropped by everything that read it back: it was invisible in the dialog, never applied by the `post-new` hook, and would have been deleted outright by the next save. Names are now cleaned into a valid id as you type them, saving is refused with a reason when a rule could not be read back, and a rule already in the file with a bad name is loaded for repair rather than discarded. ### Changed - The tagging rules dialog reports problems in a red banner beside Save, rather than as a line of ordinary text under the heading where it read as more explanation. It can be dismissed, and comes back when there is something new to say. ## [0.18.0] - 2026-08-13 A query you have just written and are looking at the results of can now be kept, named and scoped, without opening a text editor. Saved queries move to a file of their own, which finally lets them be ordered: the row of buttons follows the file rather than the alphabet, the ones you use rarely go behind a menu, and Sent stops being a fixed button and becomes an entry you own like the rest. ### Upgrading Saved queries move out of the `[queries]` section of `qtmaildir.conf` and into `~/.config/qtmaildir/queries.json`. **The first launch migrates them for you**: the section is read, the JSON file is written from it, and every entry is marked pinned so your buttons stay where they were. Sent is appended as a `generated` entry, where its button already sat, provided an account configures a sent folder. Your config file is left byte-for-byte alone. The old `[queries]` section stays in it, ignored from then on, and can be deleted by hand whenever you like. It is not removed automatically because rewriting the file would discard your comments and reorder your keys. One behaviour changes with the move. Buttons used to appear in alphabetical order and now follow the file. If `[general] startup_query` names a query that does not exist, the fallback is likewise the first query in the file rather than the alphabetically first one, so a config that relied on that fallback may open on a different query than before. ### Added - A **Save query** button beside the query bar, also on the Edit menu and bound to `Ctrl+S`, keeps the query in the bar as a saved query: naming it, optionally scoping it to one account, and choosing whether it appears as a button or in a menu. Saved queries no longer have to be added by hand-editing the config file (item 23). - Saved queries live in `~/.config/qtmaildir/queries.json`, which carries their **order**, a `pinned` flag and an optional account scope. The order in the file is the order the buttons appear in, so rearranging them is a matter of moving lines. - **Right-click a saved query** to edit, pin, unpin or delete it. A saved query could previously be created and never changed: the only route to adjusting one field was to retype the whole query under the same name, and there was no way to delete one at all short of editing the file (item 82). Deleting asks first, since it writes your config and is not undoable. - **Sent is a saved query now**, carrying `"generated": "sent"` instead of a stored query. It is still composed from your accounts' `sent` keys every time you click it, so correcting a folder name still updates it with no edit, but it can now be reordered, renamed, unpinned or deleted like any other entry rather than being a fixed button you did not own. ### Changed - Saved queries have a **row of their own** beneath the query bar rather than sharing it, and the unpinned ones sit behind a **More queries** menu, so the query field is no longer squeezed by a long list of buttons. - Saved-query buttons follow the file's order instead of appearing alphabetically. - The Sent button is no longer hardcoded beside the saved queries, so the whole row now follows one rule instead of having one member that behaved differently from its neighbours. ## [0.17.0] - 2026-08-13 A rule is built from rows now instead of typed into four free-text fields: dropdowns for the field and the operator, buttons to add and remove conditions, and a separate block for the senders a rule should exclude. The stored file does not change, so the companion `mailctl` tool reads and writes exactly what it did before, and a rule too intricate for the rows still opens and still runs. This release also fixes a data-loss defect in 0.16.0: opening the tagging rules dialog and pressing Save destroyed the first rule in the list, with nothing edited. ### Upgrading **If you ran 0.16.0 and opened the tagging rules dialog, check your first rule.** Pressing Save there blanked its query, its tags and its note, and a rule with an empty query is dropped as malformed on the next load, so the rule disappears rather than reporting an error. Nothing else in the file was touched. ```bash mailctl rules list ``` A rule missing from that list, or one you remember writing and can no longer find, was lost this way and has to be entered again. There is no automatic repair: the blanked fields are not recoverable from the file itself, only from a backup or from a sibling rule that says the same thing. ### Added - The tagging rules dialog builds a rule from rows now: a field and operator dropdown per condition, `+`/`-` to add and remove them, a match all/any choice, and a separate "but not" block for exclusions. The notmuch query stays visible and is what gets saved, so a rule the builder cannot show opens as text and still works. Opening a rule without editing it leaves the stored query untouched. - A Folder condition picks from a list of every folder in your Maildir rather than being typed, Drafts and Sent included, not only the top of each account. A folder path with a typo matches nothing and notmuch reports no error, so the rule would simply never fire. The list is read from the tree on disk, so a folder that exists but has no mail in it yet is still offered. It stays editable, so a folder in the rules file that is no longer on disk still opens and still saves. - A **Preview in list** button in the tagging rules dialog runs the selected rule's query in the main window, so you can see which mail a rule collects rather than only how many messages it matches. The dialog stays open. The query runs exactly as stored, without the `tag:new` scope the hook adds, and the account selector is cleared first, since a rule query that names its own folder would otherwise be scoped twice and match nothing. - The rule list and the rule editor are now divided by a draggable splitter, and the condition rows scroll instead of growing without limit. A rule with eight senders used to squeeze the list to about one visible row, since the editor grew with every condition and the list gave up the space. Where you leave the divider is remembered. - The tagging rules window remembers the widths of the rule list's columns. A column you widen also survives adding or deleting a rule, which previously reset it. The window's own size is saved too, but a tiling window manager sizes the window itself, so there it opens at whatever size the tile gives it. ### Fixed - Opening the tagging rules dialog and saving destroyed the first rule in the list, even with nothing edited. The rule lost its query and its tags, then vanished on the next load, since a rule with an empty query is dropped as malformed. Populating the form emitted a change signal that wrote the form back over the rule before the query field had been filled. ## [0.16.0] - 2026-08-13 The rules that tag your mail on arrival move out of a shell script and into the application. They now live in a file the GUI and the companion `mailctl` tool both read, each rule keeping the note that explains why it is written the way it is, and the dialog counts how much mail a rule matches before the next sync applies it. A tag change also syncs itself out now, a couple of seconds after you stop making changes, and the panes draw their own marks rather than relying on whatever glyphs the desktop font happens to carry. ### Added - The rules that tag your mail as it arrives are now visible and editable from Message > Tagging rules, and each one shows how much mail it matches so you can judge a rule before the next sync applies it. They live in a shared file, `~/.config/mailrules/rules.json`, which the notmuch `post-new` hook reads to do the tagging and which the companion `mailctl` tool can read too. The rules previously lived inside the hook as shell, where nothing but a text editor could see them. Each rule keeps a note, so the reasoning behind it (which senders it deliberately excludes, and why) travels with the rule instead of being a comment only one program could read. - A tag change now syncs itself out, about two seconds after you stop making changes, instead of waiting for the Sync button or your cron job. The delay is a debounce, so tagging several threads in a row produces one sync rather than one per thread, and a sync already running is never interrupted or queued behind. Set `auto_sync_delay_ms` in `[general]` to change the delay, or to any negative value to turn the behaviour off and get the previous one back. - The panes draw their own marks instead of borrowing font glyphs. Flagged, attachment, forwarded, replied and the thread expander are six SVGs shipped with the application, recoloured from your palette, so they look the same on every desktop and cannot turn into a tofu box on a font that lacks a codepoint. Forwarded and replied were words in the tag strip and are now marks beside the subject, and the message pane shows the flagged and attachment marks next to the subject in its header. The toolbar and the menus are untouched and still follow your icon theme. ### Fixed - A sync you started no longer clears the message pane and the undo stack. It now reconciles the thread list the way a background sync already did, so a message stays on screen and open while the list updates around it. If the thread has stopped matching the current query, which is what happens when the message you are reading in Unread gets marked read, the pane keeps showing it and offers "Show it anyway" instead of going blank. ### Upgrading The tagging rules moved out of the notmuch `post-new` hook and into `~/.config/mailrules/rules.json`. The dialog reads and writes that file, but nothing applies the rules until the new hook is installed, so this needs two files copied from the companion `mailctl` project into your notmuch hooks directory: ```bash DB="$(notmuch config get database.path)" cp post-new mailrules.py "$DB/.notmuch/hooks/" chmod +x "$DB/.notmuch/hooks/post-new" ``` Keep a backup of your previous hook until a sync has run with the new one. Your existing rules do not convert themselves: each `notmuch tag` line becomes one entry in the JSON file, with the part after `tag:new and` as its query. Leave `tag:new` out of the stored query, the hook adds it, and do not carry over the final `notmuch tag -new` line, which the hook now does itself. Two behaviours of the new hook are worth knowing. It refuses to remove `unread` or `inbox`, since neither belongs in an unattended job that runs every ten minutes, and it will not consume the `tag:new` marker if the rules file fails to load, so a broken file delays tagging rather than losing it. ## [0.15.0] - 2026-08-11 Sent mail becomes a place you can go. A Sent button beside Inbox, Unread and Important shows what you sent across every account, as a flat list of recipients rather than as threads, because a message you sent is not a conversation you are following. The blank pane counts sent mail and drafts alongside unread, flagged and inbox, and the date on a thread card can now be given a format of your own. ### Upgrading Both new features read per-account folder names, and neither appears until you name them. Add `sent` and `drafts` to each `[account.*]` section that has them: ```ini [account.example] maildir = example sent = Sent drafts = Drafts ``` Providers that nest a localised folder under a bracketed parent take the full path, e.g. `sent = [Provider]/Posta inviata`. An account that keeps no sent or drafts folder locally simply omits the key: the Sent button and the counted lines are absent rather than empty, and nothing warns about it. `drafts` was already accepted and documented as having no effect. If you set it earlier, it now counts drafts on the blank pane. ### Added - **A Sent view.** A new `sent` key on each `[account.*]` names that account's sent folder, and a Sent button beside Inbox, Unread and Important shows what you sent across every account that configures one. Selecting an account narrows it to that account. The button is absent entirely when no account has the key. - Sent mail is shown as a flat list rather than as threads, and the cards name the **recipients** instead of the sender, which is you on every row. Selecting one opens what you sent, not the conversation your message started. - The blank message pane counts **sent mail and drafts** beside unread, flagged and inbox. Both are composed from each account's `sent` and `drafts` folder rather than from a tag, so they follow the same per-account configuration the Sent view uses. A line is absent entirely when no account configures that folder, rather than reading 0. The `drafts` key was already accepted and documented as unused; it now has an effect. - `[general] date_format`, an optional pattern for the date on a thread card. Absent or empty keeps the system locale's short format, which is unchanged and remains the default. A pattern containing no date or time field is refused with a message rather than printing the same fixed text on every card. ### Changed - The Sync button carries the refresh icon instead of a mailbox one. With the toolbar following the desktop's "icon only" style, the icon is the whole control, and a mailbox glyph read as "mail" rather than "fetch again". ## [0.14.0] - 2026-08-10 The thread list stops going stale. A sync running in the background now updates it directly: new mail appears, threads you have read leave, and whatever you were in the middle of stays where it was. Reading a thread out of a view no longer strands you either, since the message pane says when the thread it is showing has left the list and offers to bring it back. ### Upgrading Nothing to change. The "Background sync completed. Press Enter in the query bar to refresh." message is gone because there is nothing left to press Enter for; if that keystroke is in your fingers, it still re-runs the query and is now simply redundant. ### Changed - The thread list now follows a background sync on its own. New mail appears where the sort puts it, threads that stopped matching leave, and threads whose state changed repaint, with no keystroke. Previously the status bar asked you to press Enter, because refreshing meant re-running the query, which cleared the list, the selection and the message pane; the list is now reconciled instead, so an expanded thread stays expanded, the selection stays put and the message being read stays on screen. The "Background sync completed" message is gone: a refresh that changes nothing should be invisible. ### Added - A notice above the message when the thread being read no longer matches the current query, with a button that brings it back. Reading a thread to the end of an Unread view now removes it from the list as it should, and this is the way back to it: the whole thread is listed, and the message that was on screen is re-selected, so returning to reply four of eight lands on reply four. ## [0.13.0] - 2026-08-10 The thread list stops being a table. Each thread is a card of three lines, carrying its sender and date, its subject with the marks that apply to it, and its tags; expanding one shows its replies indented beneath it rather than as more rows of the same grid. The account colour moves from a chip in front of every subject to a bar down the card's edge, and threads can now be sorted oldest first. This replaces the presentation built for 0.12, which was finished and working and read as a table of records rather than as a list of conversations. The model, the reply walk and the keyboard navigation underneath it are unchanged. ### Changed - **The thread list is now a list of cards rather than a table of columns.** Each thread shows its sender and date, its subject with the flag, attachment and reply-count marks, and its tags, on three lines at one uniform height. Expanding a thread shows its replies indented under a continuous spine, carrying only the tags the thread itself does not have, and without the `Re:` prefix every reply used to repeat. - **Threads can be listed newest or oldest first**, from a new control beside the query bar. The choice is remembered between sessions. - **An account's colour now runs down the left edge of its threads**, and down the spine of their replies, replacing the account chip that used to sit in front of every subject. The account dropdown shows the same colours, so which colour means which account is readable in one place. - **Alt+Up and Alt+Down step between threads**, alongside the existing Ctrl+J and Ctrl+K. Plain Up and Down now step message by message through an expanded thread, which is the view's own behaviour rather than a binding. - **Selecting a thread now shows its first message**, not the whole conversation. The card at the head of a thread is that message, and the replies under it are the rest; a thread's opening message was previously unreachable, since the pane rendered every message at once and no row in the list offered it on its own. - **Dates follow the desktop's locale.** They were written in a fixed `yyyy-MM-dd hh:mm` regardless of locale, which is not the format most desktops use. - **The reply count reads as a control.** It is drawn as a rounded chip saying "3 replies", rather than as a bare number beside the subject that gave no hint it could be clicked. - **An out-of-range `message_zoom` now says so.** The documented 0.5 to 3.0 range was already enforced on the way to the web view, so a `message_zoom` of 500 rendered at 3.0 rather than unusably, but nothing reported that the value in the file was not the value on screen. It is now listed with the other configuration problems at startup. ### Fixed - **The message pane no longer comes back as a sliver.** A splitter position is saved in pixels, so one saved in a wide window did not fit a narrower one: the thread list kept its full saved width and the message pane got whatever was left, in one real case 29px. The pane now has a minimum width and cannot be collapsed, which covers the restore and the equivalent drag. - **Clicking a thread no longer scrolls the list sideways.** A card is exactly the width of the pane, so there is nowhere to scroll to. - **Next and previous thread no longer step onto a reply** when a thread is expanded. They skip message rows, so they keep meaning thread-to-thread. - **Threads without reply structure now expand.** A thread whose messages carry no usable `In-Reply-To` header, which notmuch reports as a flat list rather than a tree, advertised a reply count that opened onto nothing. Its replies now appear, indented under the same spine as any other thread's. - **The date is no longer clipped on unread threads.** Unread rows draw in bold, which is wider than the font the layout measured, so the leading digit of the year was cut off. - **The account colour down a card's edge is now visible.** It was drawn in a colour blended so far toward the pane's background that it matched it exactly on a dark theme, and account colours, which are chosen to be readable behind chip text, are muted enough that a few pixels of one barely registers. ### Upgrading - **Saved thread-list column widths are ignored.** There is one column now, so the `threadlist/header` and `threadlist/columns` entries in `~/.local/state/qtmaildir/uistate.conf` no longer do anything. Nothing needs to be done: they are read past and can be left in place or deleted. Window geometry, the splitter position and the message zoom are unaffected. ## [0.12.1] - 2026-08-09 A single fix for a defect 0.12.0 introduced. ### Fixed - **Archive and Mark all read no longer share an icon.** Both used the same one in 0.12.0, which was harmless while the toolbar showed text beside every icon and ambiguous once it follows a desktop set to icon-only. Archive now uses a distinct icon, and a test compares every action's icon against every other so the next duplicate fails the build rather than shipping. ## [0.12.0] - 2026-08-09 Syncing gets narrower and more honest: a sync fetches only the accounts you have actually edited, and one run from cron now clears the unsynced-changes indicator instead of leaving it claiming work that had already gone out. The toolbar gains icons throughout and defers to your desktop's own style. ### Upgrading Two things you may want to change in your own config, neither of which breaks if you leave it alone: - **Saved-query button labels are your own key names.** If you have `Flagged = tag:flagged` under `[queries]`, that button still reads "Flagged" after the action was renamed to "Important". Rename the key to `Important = tag:flagged` if you want the two to agree; the query is unchanged either way. - **The toolbar now honours your desktop's toolbar button style.** If that is set to "icon only" the toolbar loses its text labels, which it previously ignored. Icons are 24px by default; `[general] toolbar_icon_size` changes it. ### Added - **Threads expand in the list to show their replies.** A thread with more than one message carries an expander; opening it lists the replies as indented rows beneath it, marked with a thread line, a tinted background and smaller text. The replies are fetched when you expand, not with the query, so a large result still paints immediately. - **Selecting a reply opens that message on its own**, rather than the whole conversation, which is the point of having message rows at all. - **Actions follow what you selected, and the status bar says what they will touch.** A thread row acts on the whole thread and reports "1 thread selected (7 messages)" before and "(whole thread)" after; a reply row acts on that one message. Both are undoable. There is no confirmation dialog, deliberately: undo is this application's answer to a mistaken action, and naming the scope is what makes it usable. - **A sync now fetches only the accounts you have edited.** Tagging mail in one account and syncing no longer pulls every other account as well. A sync with nothing outstanding is a plain fetch and still covers everything, since narrowing that to wherever the last edit happened to be would quietly stop collecting mail everywhere else. - **An optional `channel` key per account**, naming the mbsync channel when it differs from the section key. It defaults to the key, so accounts whose two names already agree need no change. The two can genuinely diverge: a QSettings section key may carry dots that the channel does not, and mbsync treats an unknown channel as fatal rather than skipping it. - **`assets/mailsync.sh` takes channel names as arguments**, syncing all channels when given none. A replacement sync script that ignores its arguments still works, it just always syncs everything. - **Every action now carries an icon**, where before only eight of twenty-four did and adjacent menu entries disagreed with each other. Icons come from the desktop's icon theme; one the theme does not provide falls back to text alone. - **An optional `[sync] log` key**, naming the sync script's log file. It defaults to where `assets/mailsync.sh` writes, and only needs setting if you changed the script's `LOGFILE`. - **An optional `[general] toolbar_icon_size` key**, 16 to 64 pixels, defaulting to 24. Most styles report 16, which is a small target now that the toolbar can be icons only. Out-of-range values are clamped and reported rather than applied. ### Changed - **"Flag" is now "Important"**, on the menu entry, the undo history and the star column's tooltip. `Ctrl+I` is unchanged, and so is the `flagged` tag itself: neomutt, your saved queries and anything else reading the same Maildir keep working. The `flag` action name in `[keys]` is also unchanged, so existing bindings are untouched. - **The toolbar follows your desktop's toolbar button setting** instead of always showing text beside icons. If your desktop is set to "Icon only", the toolbar is now icons only; it previously ignored that. ### Fixed - **A sync run from cron now clears the unsynced-changes indicator.** Edits made in the application reach the mail store through any sync, but only a sync started from the window cleared the count, so the indicator kept reporting work that had already gone out and the quit prompt offered to sync for it. A failed sync, or one whose outcome cannot be read, still leaves the count standing. ## [0.11.0] - 2026-08-07 The empty message pane now carries the application's own identity and the counts worth knowing, and Escape finally does what it does everywhere else. ### Added - **The blank message pane shows a placeholder** instead of nothing: the wordmark over a soft grid, the number of unread, flagged and inbox threads, and a footer with the version and a link to the website. Each count is a link that runs its query. A sync line appears only when something needs attention, either that the last sync failed or that edits are waiting to go out, so the pane cannot turn into wallpaper that stops being read. It follows the desktop between a light and a dark version of the brand palette. - **A Maildir overview**, under Help. Total messages, threads and tags from notmuch, plus the configured accounts, since notmuch does not model accounts at all. It opens straight away and fills the counts in when they arrive, rather than making the window wait: counting every message in a large database is not instant. A count that cannot be answered reads as unknown, never as zero. - **Escape clears the selection as well as blanking the pane.** Blanking while the row stayed highlighted read as half an action. The narrower behaviour is still there on `Shift+Esc` for anyone who wants it, and either can be rebound in `[keys]` as `clear_selection` and `clear_pane`. - **The thread list shows each thread's tags**, as small coloured chips in a strip under the row, using the same colours as the message pane. Rows are taller to make room, and alternate in colour so one can be followed across the width. The strip spans the whole row rather than sitting inside the subject column, so a well-tagged thread does not lose its last tags off the edge. Tags the row already shows another way are left out: the account, the flag, the attachment, and read state. - **A star column for flagged threads**, beside the existing attachment paperclip. - **Mark all read**, on the toolbar, the Message menu and `Ctrl+Shift+U`. It acts on every thread in the current view rather than the selection, as one write and one undo entry, so a single `Ctrl+Z` puts back a view of 400 threads. It stays disabled until the query has reported its total: threads arrive in batches, and an action that says "all" must not run against whatever happened to have loaded. A view with nothing unread does nothing and says so, rather than pushing an undo entry that restores nothing. - **The status bar says which account is syncing**, then that notmuch is reindexing, instead of "Syncing..." for the whole run. The account name and the progress both come from mbsync's own output as it streams. ### Changed - **The sync script runs `mbsync -V`.** Without it mbsync prints nothing at all until it exits, then a single summary line, so a run of over a minute was silent and there was nothing for the status bar to report. This is not a buffering problem and `stdbuf` does not help. - **Read threads are dimmed in the list**, so unread mail stands out by colour as well as by weight. Bold alone was the only distinction, which leaves nothing to see when the desktop's own font is configured bold. Unread keeps the palette's text colour and read recedes toward the background; bold still applies on top. ### Fixed - **The message pane follows the desktop theme.** Its stylesheet hardcoded light-theme greys and set no background at all, so plain-text mail rendered as black on white inside a dark window. The colours now derive from the palette, with the secondary ones blended from it rather than fixed, since a grey chosen to read as subtle on white is nearly invisible on near-black. A message that brings its own HTML still brings its own colours: that styling is deliberately left alone. - **A message whose HTML body carries a `Content-Id` renders**, instead of opening blank with the app reporting no HTML part. A content id makes a part referenceable, not undisplayable, and setting one on the body is legal and common in bulk-sender output. - **Removing a tag suggests only the tags the selected threads carry**, rather than every tag in the database. Adding still reaches the whole vocabulary, since naming a tag that does not exist yet is what that field is for. - **A sync that finishes quickly no longer loses its own progress.** The per-run reset happened after the process launched, so a run that delivered its output before control returned wiped the state those lines had produced. ### Internal - The test suite pinned itself to the offscreen platform. `ctest` sets no platform of its own, so the result depended on how the suite was invoked: green when run by hand with `QT_QPA_PLATFORM=offscreen` and red under `ctest` in the same tree. The popup test at the centre of it also now checks its own geometry, because the compositor had been handing it a popup more than twice the width it exists to test, which would have passed for the wrong reason had the grab succeeded. ## [0.10.0] - 2026-08-06 Tag edits no longer stall the window when a background sync is running, and Sync is one control instead of two that disagreed. ### Changed - **A tag edit made while a background sync runs is held and sent when the sync finishes**, instead of being sent straight into a database open that blocks. The open never failed, it blocked and then succeeded, and because the worker is a single thread everything queued behind it waited too: the message pane froze on whichever thread was selected first and replayed the queue on release. The row keeps its tag in the meantime and the edit still counts as unsynced, so the quit prompt cannot let work leave silently. - **One Sync control.** The button beside the query bar is gone; Sync lives on the toolbar, the File menu and its shortcut. The two used to behave differently, and only the button showed the sync log, disabled itself, or reported that a sync was already running. - **The saved-query buttons moved onto the query row**, after the query field, so the bar is framed by the account selector on one side and the saved queries on the other. The row they occupied is gone and the thread list has that space. ### Added - **A clear button in the query field**, Qt's own, drawn inside the field and shown only when there is something to clear. ### Fixed - **Sync stayed clickable from the toolbar and the menu during a background sync.** 0.9.0 disabled the button beside the query bar and nothing else, so every other route to Sync could still start a run that could only be skipped. - **A rejected tag write no longer clears the whole undo stack**, only the edit that was rejected. ### Internal - Two tests depended on the machine they ran on: one read the live `/proc/locks` and failed whenever a real sync happened to be running, the other asserted a window wider than the offscreen platform's screen. Neither indicated a fault in the application. ## [0.9.0] - 2026-08-04 Small corrections from using 0.8.0, most of them things the application was saying that were not quite true. ### Added - **Escape blanks the message pane**, on `clear_pane`, rebindable like any other action. A view change only: the selection, the query and the undo stack are untouched. - **Delete is now a toggle.** Pressing it on a thread that is already deleted removes the tag instead, which is the natural way to say "no, put it back". Over several selected threads it picks one direction for all of them: it undeletes only when every selected thread is already deleted, so one keystroke can never leave the selection in two states. ### Changed - **Transient status messages expire** after a few seconds, leaving the thread count behind. Messages that describe a state rather than an event do not: "Syncing...", the selection count, and a sync failure, which must not vanish before it is read. - **The Sync button is disabled while a background sync holds the lock**, since starting one then could only produce a skip. It stays usable where the lock cannot be observed at all, because nothing is known there and a permanently dead button would be worse. - **The quit prompt names its default button.** The default was always set, and Qt agrees it is set, but the active style draws no visible mark, so the button now says so in words rather than fighting the theme. ### Fixed - **Unsynced changes are counted as net state rather than as writes.** Letting a thread be marked read automatically and then pressing Ctrl+U put it back reported two unsynced changes with the mail store exactly where it started. An edit and its inverse now cancel, per message and per tag, so two different tags on one message still count as two. ## [0.8.0] - 2026-08-04 Selecting more than one thread stops being a secret, and the window notices the syncs it did not start. ### Added - **Select all threads**, on **Ctrl+A** or Edit > Select all threads. Selecting several threads always worked with Ctrl+click and Shift+click, but nothing in the interface said so, and every tag action was reachable only from the keyboard. Rebindable as `select_all` like any other action. - **A right-click menu on the thread list**, holding archive, delete, spam, mark read/unread, flag, edit tags and select all. It is built from the same actions as the menu bar, so a rebinding in `[keys]` shows the new shortcut here too. Right-clicking inside a multi-thread selection keeps that selection rather than narrowing it to the row under the pointer. - **A selection count in the status bar** while a selection is being built, and a note in Help > Keyboard shortcuts describing Ctrl+click and Shift+click. Mouse gestures belong to the view rather than to any action, so they cannot appear in the generated shortcut table. - **Awareness of syncs started elsewhere.** A cron sync every ten minutes used to come and go unnoticed. The status bar now reports a background sync while it runs and when it finishes, and suggests refreshing. It deliberately does not refresh on its own: re-running the query clears the undo stack, the selection and the message pane, which is right for a query you typed and hostile for one a timer fired. - **`assets/mailsync.sh`**, the reference sync command, moved here from the companion `mailctl` project. It never belonged there: `mailctl` does not call it, while qtmaildir runs it as a subprocess and depends on how it behaves. Symlink it into `~/bin` rather than copying, so one script serves both cron and the application. ### Changed - **Selecting several threads no longer opens them.** The message pane blanks for a multi-thread selection instead of loading each row as the selection passes over it, and no thread selected that way is marked read. Selecting is not reading, and a selection gesture must never change what is in the Maildir. Narrowing back to a single thread opens it as before. ### Fixed - **The sync log pane stayed empty**, listed as a known limitation since 0.1.0. The reference `mailsync.sh` redirected all its output to a log file, so the subprocess printed nothing for the pane to show. It now writes to both. - **A failed sync reported success.** That script ended in an unconditional `exit 0`, so qtmaildir could not tell a clean sync from a broken one: it cleared the unsynced-changes count either way, and would have quit on a sync-on-exit that had not synced anything. It now exits with the real status. - **A sync you started reported itself as a background one**, replacing its own result a moment after it finished. Whether a sync was local was decided when its lock was released, by which time the process had already exited and the answer was always "not ours". ## [0.7.0] - 2026-08-04 Tagging stops being limited to the five tags someone chose in advance, and the application admits when your work has not reached the mail store yet. ### Added - **An Edit tags dialog**, on **Ctrl+T** or Message > Edit tags. Type tags to add or remove, separated by commas, or clear a checkbox to drop a tag already on the selection without retyping it. Until now archive, delete, spam, flag and toggle-unread were the only tags reachable from the UI, and applying any other one meant leaving for a terminal. - Both fields **complete against every tag in the database**, matching on substrings so `amazon` finds `shopping/amazon`. Completion is a guard against typing `shoppping` beside `shopping`, not a restriction: a tag that does not exist yet is exactly what the dialog is for. - With several threads selected, a tag on only some of them shows a partially checked box saying how many. **Leaving it alone changes nothing.** Check it to apply to all, clear it to remove from all. - Tag names are refused if empty, if they start with `-` (notmuch reads that as "remove this tag", so such a tag is a trap), or if they contain spaces or unprintable characters. Nothing is applied until the whole set is valid, since a half-applied change leaves you unable to tell which half landed. - **The status bar counts tag changes a sync has not carried over**, and clears the count when one succeeds. A failed sync leaves it standing. - **Quitting with changes outstanding asks what to do**, via the new `[general] sync_on_exit`: `ask` (the default) offers to sync, quit anyway or stay; `always` syncs without asking; `never` quits silently. A sync started at exit holds the window open until it finishes rather than being killed mid-run, and one that fails does not quit. ### Fixed - In the tag fields, only the first tag completed. `QLineEdit::setCompleter` matches against the widget's entire text, so once a field read `unread, fl` that whole string was matched against the tag names and nothing was offered again. The same defect the query bar hit in 0.5.0, in a second place. ### Notes The unsynced count is a lower bound rather than a guarantee: an external `notmuch new` from your own cron can carry changes over without the application noticing. The exit prompt is not a destructive-action confirmation of the kind this project avoids. Those cover tag mutations, which keep undo instead of a dialog. This asks about losing work at the one point where undo cannot help. ## [0.6.0] - 2026-08-04 Two things the app knew and would not say: who a message was addressed to, and whether you had read it. ### Added - **From, To and Cc in the message header.** All three were parsed on every message and then discarded before rendering. A thread holding one message now shows them under the subject. - A thread holding **several** messages still shows only the subject and the count. From, To and Cc differ per message, and once you have replied there is no single address the thread is addressed to, so naming one would be a guess presented as a fact. An empty Cc omits its row rather than printing a label with nothing after it. - **A details dialog**, behind a `Details...` button beside the subject or `Ctrl+Shift+D`, listing Subject, From, To, Cc, Date and Message-Id for every message in the thread, numbered. Read-only plain text: these values come from strangers, and the format that cannot interpret markup is the right one for showing them verbatim. - **An opened thread is marked read after a delay**, 2 seconds by default. Arrowing quickly through a list marks only the thread you stop on, never the ones you pass through. Configurable through `[general] mark_read_delay_ms`: zero marks read at once, and any negative value turns the behaviour off. ### Notes The automatic mark-read is deliberately **not** on the undo stack. Undoing an action you never took is worse than leaving a thread read, and `Ctrl+U` already puts it back. Marking a thread unread by hand cancels any pending timer, so the key cannot be silently reversed a moment later. **HTML messages already opened as HTML**, which a backlog item had doubted. Verified against real mail; no code changed. No preference was added for defaulting to plain text, since `Ctrl+H` already switches a thread by hand. ## [0.5.0] - 2026-08-04 Completion in the query bar. The point is not to save typing but to make the notmuch query language discoverable: every candidate carries a description, so the bar teaches the syntax to someone who has never written a notmuch query. ### Added - **Query prefixes** complete with a description each: `tag:`, `is:`, `from:`, `to:`, `subject:`, `date:`, `attachment:`, `mimetype:`, `folder:`, `path:`, `thread:`, `id:`, and the `and` / `or` / `not` operators. The list is hardcoded, since notmuch exposes no way to enumerate its own prefixes. - **Tag names** after `tag:` and `is:`, which notmuch treats as synonyms. The list is the real set of tags in the database, refreshed at startup, after a sync, and whenever a tag mutation introduces one that was not there before. - **Dates** after `date:`, symbolic and relative, completing each bound of a `..` range independently. Entries that are themselves open-ended ranges, like `1week..`, are withheld once a range is already underway, since they would produce malformed queries inside one. - **Content types** after `mimetype:`, from a built-in list extensible through the new `[completion] extra_mimetypes` key. Entries are appended to the built-ins rather than replacing them, so a typo cannot leave you with fewer completions than the defaults. - **Account directories** after `path:`, in both the plain and the recursive `/**` form. - `complete_query`, bound to **Ctrl+Space**, opens the popup on demand. - `[general] completion_on_focus`, off by default, opens it as soon as an empty query bar takes focus. - Accepting a prefix chains straight into its values, so taking `tag:` offers the tag list without a second keystroke. ### Fixed - Return in the query bar ran nothing and moved focus to the thread list. Return is bound to `open_thread` as a window shortcut, and a shortcut is dispatched before the focused widget sees the key; Qt withholds plain-letter shortcuts from editable widgets, but Return is not a letter and got no such protection. The query bar now claims the key back while it has focus. - Tab and the arrow keys crashed the application outright while the popup was open. `QCoreApplication::sendEvent` re-runs application-level event filters, so the filter forwarding a key to the popup was handed the same key straight back, recursing until the stack was exhausted. ### Notes The example configuration in the README had two keys that were live rather than commented, so copying the block activated a sync command and three mimetypes the reader never chose. Both are commented now. `from:` and `to:` complete no addresses: libnotmuch exposes no call to enumerate them. `folder:` completes nothing either, as a Maildir folder name is not something the configuration can enumerate. Saved query names are deliberately absent, a name not being valid notmuch syntax. ## [0.4.1] - 2026-08-03 Packaging only. No change to the application itself. ### Added - A SlackBuild under `assets/slackbuild/`, with the usual `.info`, `slack-desc`, `doinst.sh` and `README`. It follows SBo conventions except for the tag, `_danix` rather than `_SBo`, and the package type, `txz` rather than `tgz`, since it is not an SBo submission. - `QTMAILDIR_BUILD_TESTS`, on by default. Turning it off skips the test suite and its `Qt6::Test` dependency, which a packaging build has no use for. 0.4.0 accepted this flag but ignored it, because the option did not exist in that tarball; this release is the first where it takes effect. ## [0.4.0] - 2026-08-03 Attachments become reachable. They were parsed all along and there was simply no way to get at one, and no way to tell a message had any without opening it. ### Added - A paperclip column marks threads carrying an attachment, so it is visible without opening the thread. Driven by the `attachment` tag notmuch already applies, so it costs no extra query. - The message pane lists attachments behind one **Attachments (N)** button: a dialog with the message number, filename and size, a **Save** for each, and **Save all** when there is more than one. - **Save all** writes into a new subfolder named ` ` inside a folder you pick, so a thread with sixteen files does not scatter them among whatever is already there. The folder is named in the picker before you choose, and an existing folder of that name is never merged into. ### Fixed - **Attachments were unreachable.** The attachment bar had been created and added to the layout since it was written, and nothing ever populated it. - **Saving several attachments could destroy files.** Messages in one thread commonly attach the same filename, and each save overwrote the previous one while still reporting success: sixteen attachments produced ten files. Batch saves now add a numeric suffix instead, and keep a compound extension like `.tar.gz` whole. - **A `Date:` header carrying a timezone comment lost its date.** `+0200 (CEST)` is legal and common, but Qt rejects the whole header rather than the comment, so those messages got no date prefix on their folder. ### Changed - Thread-list column widths reset once on first launch after upgrading. The saved layout is from before the paperclip column existed, and applying it would have shifted every width onto the wrong column. ## [0.3.0] - 2026-08-03 The app remembers how you left it. Window, splitter and column sizes survive a restart, the message pane owns its zoom and keeps it, and startup opens the query you asked for rather than whichever one sorted first. ### Added - Window geometry, splitter position and thread-list column widths persist across restarts. Machine-written state lives in a separate file, `~/.local/state/qtmaildir/uistate.conf`, and never touches the hand-edited config: a base64 geometry blob does not belong in a file you edit, and rewriting that file on exit would drop its comments and key order. - The message pane owns its zoom, which is remembered across restarts. It was previously the web engine's own behavior, invisible to the application, which is why there was nothing to save. | Gesture | Does | |---|---| | `Ctrl++` | Zoom in | | `Ctrl+-` | Zoom out | | `Ctrl+0`, `Ctrl+=` | Actual size | | `Ctrl`+wheel | Zoom in and out | | `Ctrl`+middle-click | Actual size | All three actions appear in the View menu and are rebindable through `[keys]` as `zoom_in`, `zoom_out` and `zoom_reset`. The factor is clamped to 0.5 - 3.0. - `[general] message_zoom` sets the starting zoom for a profile that has never zoomed. Once you zoom, the state file remembers that instead. - `[general] startup_query` names the saved query to open at startup, and defaults to `Unread`. ### Changed - **Startup no longer opens `savedQueries().first()`.** `[queries]` is read through `childKeys()`, which sorts alphabetically, so the query that opened was whichever name sorted first rather than one you chose. It is now selected by name. If your `[queries]` has no entry named `Unread`, set `[general] startup_query` to the one you want, or you will keep getting the alphabetically first one. Saved-query button order is unchanged. ### Fixed - **`[general]` keys were never read.** They were looked up as `general/`, 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 been silently ignored since it was introduced. If you set it and wondered why nothing changed, it works now. The file format is unchanged. ## [0.2.0] - 2026-08-03 Menus, a toolbar and an in-app shortcut reference, so the app is usable without memorizing keys. Tags render as coloured chips rather than a column of text. Three default keybindings that had never worked now do. ### Added - Tags render as coloured chips instead of text in a column. The account tag sits in front of the subject in the thread list, and the functional tags fill a single row under the message pane, with anything that does not fit collapsing into a `+N` chip whose tooltip names the rest. - `[tagcolors]` config group. Colours resolve by exact tag first, then by top-level prefix, so one `shopping` entry covers `shopping/amazon` and `shopping/nike` while `shopping/amazon` can still override its own. Built-in defaults cover the usual state tags; anything unconfigured gets a stable colour derived from its name. - `color` and `label` keys in an account stanza, setting the account chip's fill and its text. `label` shortens a long key for display only and renames nothing in notmuch; unset falls back to the key. - The application icon is now used: window icon, a `.desktop` entry, and install rules placing both into `hicolor` and `share/applications`. - Toolbar and menu actions carry icons from the system theme, falling back to text where a theme lacks one. - Menu bar covering every action: File, Edit, Message, View and Help. - Toolbar with the frequent subset, Sync, Archive, Delete and Undo. - **Help > Keyboard shortcuts**, listing the current bindings. Generated from the actions themselves, so it shows configured overrides rather than a hand-written copy of the defaults. - **Help > About**. - Default bindings for `spam` and `load_remote`, which previously had none and were unreachable until bound by hand. ### Fixed - Acting on a thread now visibly changes its row. A thread tagged `deleted` or `spam` is filled dark red or orange, in white struck-through text, across every column. The tag change was already applied, but `Tags` sat after the stretching `Subject` column and was pushed off-screen, so Delete looked like it had done nothing. - Thread list columns are Date, From and Subject, all resizable. The tags column is gone: spelling out a dozen tags per row consumed most of the list's width. Widening past the viewport scrolls horizontally rather than squeezing the other columns. - Hierarchical tags in `[tagcolors]` were silently ignored. QSettings treats `/` in a key as a group separator, so `shopping/amazon` becomes a nested key that `childKeys()` never returns, and every tag containing a `/` fell through to its prefix. - Three default bindings never fired. Typing a capital sends `Shift`+the key, but `N`, `F` and `G` were stored as the unshifted key, which no keystroke produces, leaving `toggle_unread`, `flag` and `sync` dead. A bare capital in `[keys]` is now read as `Shift`+that letter. As a side effect `y` and `Y` are two distinct keys rather than a collision that silently dropped one. - Modifier shortcuts such as `Ctrl+Q` now work while the query bar has focus. The old event filter suppressed every binding there, not only the plain letters that would have interfered with typing. ### Changed - Default bindings moved to modifier shortcuts. **A `[keys]` section written for 0.1.0 keeps working and keeps the old keys**, which also means it hides every new default: delete the section to adopt them, or rebind individually. Single letters are still safe to bind, since Qt suppresses a plain-letter shortcut while the query bar has focus. | Action | 0.1.0 | 0.2.0 | |---|---|---| | `next_thread` | `j` | `Ctrl+J` | | `prev_thread` | `k` | `Ctrl+K` | | `open_thread` | `Return` | `Return` | | `archive` | `a` | `Ctrl+E` | | `delete` | `d` | `Ctrl+D` | | `spam` | *(unbound)* | `Ctrl+Shift+S` | | `toggle_unread` | `N` *(never fired)* | `Ctrl+U` | | `flag` | `F` *(never fired)* | `Ctrl+I` | | `focus_query` | `/` | `Ctrl+L` | | `toggle_html` | `h` | `Ctrl+H` | | `load_remote` | *(unbound)* | `Ctrl+M` | | `undo` | `u` | `Ctrl+Z` | | `sync` | `G` *(never fired)* | `Ctrl+G` | | `quit` | `Ctrl+Q` | `Ctrl+Q` | Action names are unchanged, so no existing binding becomes invalid. - Actions are `QAction`s dispatched by shortcut rather than a hash of callbacks behind an event filter, which is what lets them appear in menus. The hand-maintained list of registered action names is now derived from the actions, so it can no longer drift from them. ## [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.