aboutsummaryrefslogtreecommitdiffstats
path: root/src
AgeCommit message (Collapse)AuthorFilesLines
47 hoursfeat: add the single-instance socketDanilo M.3-0/+270
A QLocalServer under the state directory. The first launch listens; a later one connects, hands over its selectors and exits. Connect-first ordering, and on Qt 6.11 the probe is the ONLY guard for a live instance: with UserAccessOption, listen() binds in a private directory and renames the socket onto the path, which replaces whatever is there, a stale file and a live socket alike. Measured: with the probe disabled, a second launch takes the first one's socket and both handover tests fail. The same rename is what reclaims a stale file after a crash; the removeServer() retry on AddressInUse is kept for a listen that binds in place. The server reads each connection asynchronously and parses on disconnect, rather than blocking in waitForReadyRead() on the UI thread. The client's one write followed by a disconnect is what marks the payload complete, a reader past the payload cap is aborted, and a connection that never hangs up is dropped after two seconds. A connection that writes nothing at all is the other launch's probe and is ignored without a warning. A socket that cannot be created does NOT stop the window opening. A read-only state directory costs single-instance behaviour, which is a degradation; it must not cost the user their mail client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 hoursfeat: parse the launch selectors as a value typeDanilo M.3-0/+272
--account, --thread and --message, plus the payload that crosses the socket. A value type with no GUI dependency: it is parsed before QApplication exists and both halves need tests no window has to be built for. QDataStream rather than a line-based payload, because a Message-ID may contain a newline. Every read is status-checked, which is what catches a truncated payload: a short read otherwise leaves the fields default-constructed and a half-written id would be applied as an empty one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 hoursbuild: link Qt6::Network for the single-instance socketDanilo M.1-1/+2
A unix domain socket between two copies of this program, for item 200. Not network protocol work: the rule in AGENTS.md is about IMAP and SMTP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat: close the calendar's event pane with EscapeDanilo M.2-5/+20
Escape was scoped to the pane and only cancelled an edit, so once an event was selected nothing closed the pane again. It is a window shortcut now, labelled Close event: with no edit open it clears the selection and the pane hides. With an edit open it cancels only when focus is inside the pane, so a stray Escape after clicking the grid cannot discard the form. That guard needed the month grid to take focus on a click, which it never did; the view-scoped PgUp/PgDn, Delete and Ctrl+Z were likewise reachable only by Tab until now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat: hide the calendar's event pane while nothing is selectedDanilo M.1-0/+4
The pane reserved a quarter of the window even when empty. It now shows only on the details or edit page, so the grid takes the full width until an event is selected or a new one is started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfix: stop a chip's colour flooding the next calendar day cellDanilo M.1-0/+3
The month grid drew each cell's outline with drawRect while the brush was still set from the previous cell's all-day chip or today pill, so every day after one carrying an event was filled with that event's colour. Reset the brush before the outline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfix: drop orphaned overrides when a series stops, plus review minorsDanilo M.5-2/+38
7 daysdocs: document the calendar, translate it, file its follow-upsDanilo M.2-5/+10
Give RepeatRule its own Q_DECLARE_TR_FUNCTIONS context so lupdate extracts describe() and the ordinal words, mirroring the array's QT_TRANSLATE_NOOP; without it lupdate warned and RepeatRule/last was never extractable.
7 daysfeat: open the calendar from the View menu and the toolbarDanilo M.3-0/+61
7 daysfix: add the toolbar New button, sync the month controls, and let a stale ↵Danilo M.2-4/+32
save be retried
7 daysfeat: the calendar window, with undo and a stale-safe saveDanilo M.3-0/+866
7 daysfix: show the collection swatch and translate the attendee line in the event ↵Danilo M.2-3/+16
pane
7 daysfeat: show and edit a calendar event in the side paneDanilo M.3-0/+621
7 daysfix: swallow the calendar more-line double-click and clear the agenda selectionDanilo M.2-2/+14
7 daysfeat: paint the calendar's month grid and agenda listDanilo M.5-0/+387
7 daysfeat: lay out the calendar's month grid without a painterDanilo M.3-0/+139
7 daystest: a write during a run queues one more calendar syncDanilo M.1-1/+5
7 daysfeat: debounce the calendar sync command after writesDanilo M.3-0/+126
7 daysfeat: read the calendar's config keysDanilo M.2-0/+50
7 daysfeat: write calendar files atomically, refusing stale onesDanilo M.3-0/+108
7 daysfeat: edit or delete one occurrence of a repeating eventDanilo M.1-3/+48
7 daysfix: compare sameMeaning overrides by recurrence id and check the uidDanilo M.1-2/+12
7 daysfeat: edit a calendar event in place and write new onesDanilo M.1-3/+260
7 daysfeat: an event is editable only when the user organises itDanilo M.1-1/+9
7 daysfix: expand a long dense series completely and flag overrides' own all-dayDanilo M.1-4/+6
7 daysfeat: expand calendar occurrences in each event's own zoneDanilo M.1-2/+112
7 daysfix: an empty RRULE means does-not-repeat, not a custom ruleDanilo M.1-0/+3
7 daysfeat: model the calendar's repeat control as RRULE textDanilo M.1-4/+251
7 daysfeat: read every time form and load a calendar vdirDanilo M.1-1/+72
7 daysfeat: parse one calendar event with libical (item 206)Danilo M.7-1/+518
13 daysfix: sanitise untrusted contact names on insertionDanilo M.1-16/+24
A vCard FN is untrusted and ContactStore faithfully decodes `\n` to a real newline, so `Evil\nBcc: x` was inserted raw into a recipient field and flowed through splitRecipients() to MessageBuilder. contactInsertionText() only quoted a name carrying a comma or a double quote, so every other RFC 5322 special (<, >, ;, @) and any control character reached the header unguarded. The name now has control characters and whitespace runs replaced by single spaces, and every non-empty name is quoted, with `\` escaped before `"`. Quoting contains all the specials in one step. The parser is left faithful; this is fixed at the consumer/trust boundary. Tests: a name with a decoded newline inserts no control character and yields one recipient; a name with <, >, ;, @ is quoted and yields one recipient. The three tests that expected an unquoted plain name now expect the quoted form.
13 daysfeat: load the contact store once and feed both consumersDanilo M.2-0/+30
MainWindow reads contactsDir() once while building its UI, holds the result in m_contacts, and hands it to the query bar's completer right after that is constructed and to every ComposeWindow as it opens. An empty contactsDir() skips the call, so a machine with no address book pays nothing and warns about nothing. The directory is not watched: a restart picks up a vdirsyncer update, and a QFileSystemWatcher would be a live-index feature nobody asked for. No action is added, so none of the five places in "Adding an action is FIVE places" applies: there is no name in KeyMap::knownActions(), no default binding, no icon-table entry and no menu entry to add.
13 daysfeat: complete contacts in the query barDanilo M.2-4/+34
from: and to: now offer the vCard store's addresses, each quoted via SearchTerm::quote() with the contact's name as the description. The store is the enumerator libnotmuch does not expose, which is what the old complete-nothing comment said was missing; it keeps that role for folder:, subject:, attachment:, thread: and id:. With no store configured the branch returns {} and the behaviour is unchanged.
13 daysfeat: complete contacts in the composer recipientsDanilo M.2-5/+259
One shared QCompleter serves To, Cc and Bcc, attached with setWidget and never setCompleter, which resets the prefix to the whole field and stops matching after the first comma. The prefix is the comma-delimited token under the cursor, set by hand from textEdited; accepting replaces only that token and leaves the rest of the field alone. Candidates match the name and the address case-insensitively. A display name containing a comma is quoted on insertion, and splitRecipients() is now quote-aware so the quoted name survives as one recipient. Contacts reach the composer through setContacts() rather than a fourth constructor argument, so every existing three-argument construction and test stays as it was. An empty list leaves the fields behaving exactly as before completion existed.
13 daysfeat: add the contacts_dir config keyDanilo M.2-0/+50
Adds Config::contactsDir(), the [general] key Task 5 will read to locate the ContactStore. Empty means the feature is off. The key is read WITHOUT the general/ prefix, like notmuch_config, because QSettings' INI backend strips a section literally named [general]. Absent or empty is silent; a set path that does not exist is reported through addProblem(). A leading ~ is expanded by a local helper, since config.cpp expands no other path and this is the first one to need it.
13 daysfeat: add ContactStore, the vCard address-book parseDanilo M.3-0/+295
Reads a vdirsyncer contacts directory of vCard 3.0 files into a QList<Contact> for the completion work that follows. Pure over values, no widget and no QCompleter, so the parse is testable without a window. unfold() joins folded lines before any field is looked at; parseCard() splits property from value on the first colon outside a quoted parameter, unescapes FN, and yields one contact per EMAIL line; loadDirectory() walks recursively, skips unreadable or addressless cards, de-duplicates on the address case-insensitively, and sorts by name then address. N, PHOTO, ADR and TEL are deliberately not used. 23 new tests. No user-facing strings, so no tr() change.
2026-09-14fix: keep the message bar populated on a spam replyDanilo M.1-0/+7
The spam branch of populateMessageBar() was keyed on the path predicate alone, but its only action is hidden on a reply, so a reply inside an expanded spam conversation lost Reply, Forward and Star. Skip the branch when the reply guard is set, so the ordinary branch populates instead. Extend notSpamIsOfferedInTheSpamView with the message-bar assertions the QAction-only check missed, waiting for the reply row to load first, and mark Task 9's step checkboxes done in the plan.
2026-09-14fix: the Not spam review findingsDanilo M.1-7/+9
Close the two Important test gaps and fold in the minor notes. notSpamIsAbsentOnAReplyRow passed for the wrong reason: the reply node had no filePath and the selected child was the first message, so the predicate answered false on the empty path and hid the action with or without the reply guard. Give the reply a real spam path and select the actual reply child; mutation-checked that removing the guard now fails the test. Add notSpamThreadMovesEveryMessageHome, the thread-scoped coverage notSpamThreads()/m_pendingThreadScope/wholeThreadIds had none of, and assert the folded thread-scoped trash Restore re-adds the inbox tag. Rename the label Not junk -> Not spam (no free mnemonic in the Message menu) to match the rest of the UI, with the Italian translation updated, and add a changelog line for the thread-scoped Restore inbox-tag fix.
2026-09-14feat: a Not spam actionDanilo M.3-84/+249
Backlog item 201. A message in the Spam view had no way back out: spam is one-way and Restore is hidden outside the trash. not_spam moves each message to the folder its moved-from: origin names, falling back to the account's inbox (reported) for provider-caught mail with no origin. The action is labelled "Not junk" on the free Alt+J: every letter of "Not spam" is taken in the Message menu, and Alt+P (Re&ply) and Alt+S (Mark &spam, frozen) are unavailable. restoreResolvedMessages() and the undelete_thread branch are parameterised with the cleared tag and undo description rather than copied, so Delete and Not spam cannot drift.
2026-09-14fix: gate spam like delete, and keep one origin in the modelDanilo M.3-9/+67
2026-09-14feat: find stranded spam mailDanilo M.3-0/+62
2026-09-14feat: empty spam moves mail to the trash, per accountDanilo M.3-2/+122
2026-09-14feat: spam on the message bar, with a bug icon and junk fallbackDanilo M.2-92/+124
2026-09-13feat: mark spam moves mail to the account's spam folderDanilo M.2-5/+141
Mark spam was a tag-only action that added the spam tag and removed inbox, so a message marked as spam stayed in the inbox on disk. It now MOVES the file into the account's configured spam folder, exactly mirroring Delete: the account-relative spam key is the destination, the move records moved-from: with the origin, and unread and inbox are stripped in the same confirmed write so one undo returns the folder and the tags together. NotmuchWorker::moveMessages already handled a folder generically and applyTags already overwrote an older moved-from: tag, so the worker needed no change; the tests pin that behaviour for the spam destination. Five existing tests used spam as a worker-free, tag-only stand-in for the old Delete. Since spam is now a move too, they are retargeted to flag, the remaining selection-scoped tag-only action.
2026-09-13refactor: rename the origin tag to moved-from: with overwrite semanticsDanilo M.7-26/+55
2026-09-13feat(config): a threaded, path-based spam filterDanilo M.2-1/+21
2026-09-13feat(config): per-account spam folder and queriesDanilo M.2-0/+51
2026-09-08feat: make Sync follow the account being looked atDanilo M.2-10/+54
Item 101. The sync run was already account-aware for pending edits and consulted the account dropdown for nothing, so looking at one account and pressing Sync collected every one of them. pendingSyncChannels() now reads the dropdown as well as m_editedAccounts. The two are a UNION rather than one replacing the other, which is the whole safety property: looking at one account while having edited another is ordinary, and a run that dropped the edited account's channel would strand that write with nothing on screen to say so. All accounts is the empty key and narrows nothing, so a full fetch stays what an unselected window asks for. The existing fallback is untouched: an account whose section names no channel still widens the run rather than being silently skipped. syncStartedText() is the visibility half. A run opened with "Syncing..." whether it covered one account or all of them; it now names what it covers. The exit paths overwrite the label with their own wording immediately, so the parameter is defaulted rather than threaded through them. Four tests. The narrowing one asserts the channel list is EMPTY before the gesture, since empty is what a full fetch looks like and a test starting from a narrowed state could not tell the fix from a window that had never widened. Mutation-checked twice: ignoring the selection fails three of them, and replacing the pending set instead of unioning with it fails the union test alone, which is the version that looks correct and loses a write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
2026-09-07fix: refresh the current query when the index changesDanilo M.3-0/+36
Item 192's second half, answered by the user: indexing is not repainting. The sent copy became findable the moment it was sent and a Sent view already on screen still did not show it, because nothing re-ran the query. The model cannot insert the row optimistically either, since item 170's constraint applies: the query never returned that thread. NotmuchWorker::indexChanged() is emitted at the end of both indexDraftFile() and removeIndexedFile(), the only two entry points that change what a path query would return without any query having run. MainWindow connects it to refreshCurrentQuery(), which covers all three gestures a path view can miss: a sent copy indexed, a draft saved, a draft's entry dropped on send. Wiring only the indexing half would have left a ghost draft row visible in a Drafts view after a send. The signal carries nothing, so it cannot invite an optimistic insert. It is emitted after the database closes, so a refresh reaching notmuch on the next turn of the event loop cannot race the write handle. refreshCurrentQuery() rather than runCurrentQuery(): a send must not clear the selection, the expansions or the undo stack of the window behind the composer. aSentMessageAppearsInASentViewAlreadyOnScreen drives a real send through a worker-backed window, asserts the Sent view is empty first, and asserts the row arrives with no second returnPressed() and no sync. Mutation-checked by disabling the connection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWeBw3UqxpBktSc1AkZ6ir
2026-09-06fix: index the sent copy so it appears in the Sent view at onceDanilo M.3-0/+40
ComposeWindow filed the sent copy into the account's sent folder and discarded the path DraftStore::write() returned, using the result only to test for failure. Nothing announced the file, so notmuch never learned it existed, and the Sent view is a path query over the index rather than a listing of the folder: the message was on disk and invisible until the next notmuch new, which here is a cron tick up to ten minutes away. Measured immediately after a send: 65 files in the account's Sent folder, 64 messages indexed for the same path. This is item 158's defect one path over. That item established the rule for drafts, and MainWindow wires both halves of it, so the send path was already telling the worker to DROP the draft's entry while never telling it to add the sent copy's. The missing half is the one the user sees. A sentCopyFiled signal, emitted only where the write succeeded, connected to the worker's existing indexDraftFile. That slot is generic despite its name: it calls notmuch_database_index_file, applies nothing draft-specific, and its previousPath already defaults to empty, which is right for a copy that replaces nothing. The connection is a lambda whose context object is m_worker, and that is load-bearing. indexDraftFile takes two arguments where the signal carries one, so a direct slot connection does not compile; the context object is what queues the call onto the worker's thread and keeps notmuch off the GUI thread. Simplifying it to a plain call reads as tidier and would cross that boundary, so the comment says so. The test asserts on the signal, on the file existing, and on it being inside the Sent folder. Indexing itself is already covered against a real database in test_notmuchworker; what was unproven was that anything ever called it for a sent copy. Announcing a path that was never written is the ghost entry removeIndexedFile exists to undo, which is why the existence check is there. Indexing is not repainting: a Sent view already on screen does not gain the row from this, and whether it should refresh after a send is left as a separate question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq9gXquUo9W4KXDagJXMmn