| Age | Commit message (Collapse) | Author | Files | Lines |
|
--message naming the first message of a multi-message conversation
selected the conversation row, so the pane showed the dashboard rather
than the message. The recovery matched the conversation row on its own
message id, which is the root's. The rule is that --message always
targets the message, shown inside its expanded thread, and that
--thread is the way to ask for the conversation.
Since item 177 setThreadMessages keeps a conversation's first message as
child 0, so the root already has a row of its own. applyPendingRecovery()
now lets the thread row answer for a named message only when that row is
not a conversation, which leaves a thread of one opening on its message
as before, and finds the root among the children like any reply. The
provisional pass still selects the thread while the tree loads and is
refined to the root's row when the children arrive.
The same recovery serves the stale notice and a double-click. A stale
notice raised while reading the root's row now reopens that row, which
is the same request. A double-click on a conversation row asks for the
conversation, so it no longer names the root and still lands on the
dashboard. recoveryOnTheFirstMessageSelectsTheThreadRow asserted the old
behaviour on the pre-177 premise that the root has no child row; it is
retargeted as recoveryOnTheFirstMessageSelectsItsOwnRow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
A --message launch handed to a window already in use switched the list
to the conversation and made the reply row current, and the message
pane stayed empty. The same launch into a fresh window worked.
selectRowAt() selected the row and then called the view's
setCurrentIndex(). QAbstractItemView::setCurrentIndex() asks
selectionCommand() what to do with the selection, and with no event to
read it answers from QGuiApplication::keyboardModifiers(), which is the
modifier state of the last input event the application received rather
than anything the user is doing now. With Control in that state the
command is a Toggle: the row selected a line earlier was deselected
again, onThreadSelected() refused a current row that is not selected,
and nothing was loaded. The provisional thread row the recovery selects
first went the same way, so the pane never left the placeholder. A
fresh process has received no input at all, which is why only a window
in use was affected.
The current index is now moved through the selection model with
NoUpdate, so the selection stays exactly what select() made it. Every
caller of selectRowAt() shared the defect (the stale-thread recovery,
double-click, the dashboard's message entries and the launch selectors)
and every one of them means "select exactly this row", so all of them
take the fix.
The test puts a window into use, leaves Control as the last modifier
the application saw, launches --message for a reply in another thread,
and asserts the reply is current, selected, and rendered in the pane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
recoverStaleThread() runs thread:<id> in the account dropdown's scope, so a
--thread or --message for a conversation in another account than the one
the window was left on came back with no rows, and the list went blank with
nothing said.
A thread or message selector now switches the dropdown to All accounts
first, unless the launch named an account with --account, in which case
that scope is kept. For a message the switch waits until the id has
resolved, so a message that does not exist leaves the dropdown alone.
When the selector's own thread:<id> query returns no rows, the miss is
named in the status bar, reusing the existing "No thread matched" and "No
message matched" strings, and the view that was on screen when the launch
arrived comes back: the dropdown, the bar's text and the list, re-run in
the scope it was built in. The notice waits for that query to land, since
the query writes its row count to the bar and would cover it. A refused
thread id and an unresolved message restore the dropdown the same way, and
re-run nothing, since the list never changed.
The judgement is keyed to a flag the selector paths set after the query
and runQuery() clears, so the stale-thread notice and double-click, which
share recoverStaleThread(), behave as before.
README and CHANGELOG describe the selectors as they now behave: --account
alone opens its startup view, the other two look in every account unless
narrowed, brackets are accepted, and a miss keeps the view the user had.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
The account dropdown deliberately does not re-run the query: by hand the
user picks a filter next. A launch from another program has no next click,
so `--account work` on its own moved the dropdown and changed nothing the
user could see.
When the account selector applies and no thread or message was given, the
startup view now runs again in the new account. The constructor's startup
path moves into runStartupView() and both callers use it, so the view is
resolved the same way in each: a generated filter is asked for the
account's own query rather than having its all-accounts query wrapped in
the account's path.
The test starts on the trash view because its per-account query is the
account's own trash path; for a tag filter the generated and the wrapped
queries are the same string, and a test on one passed against the wrap.
It asserts on the generated string and on the rows, and fails with the
wrap put back.
anEmptySelectorSetChangesNothing also asserts that no query ran, which the
bar and the dropdown alone cannot show.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
A Message-ID is written <local@domain> in every header a caller copies it
from, and notmuch stores it without the angle brackets, so
`--message '<abc@example.org>'`, the form the README itself shows, matched
nothing and was reported as a miss. applySelectors() now trims the value
and strips one enclosing pair of brackets before anything else sees it, so
the worker's resolve and the recovery's selection target both use the bare
id. Stripping for the resolve alone would open the right thread and then
fail to select the message inside it, which is why the test targets a reply
rather than a thread root.
No refusal of a slashed id was added: notmuch 0.39 reads `/.../` as a regex
for mid: but not for id:, measured as 0 matches for id:"/.*/" on an index
where mid:"/.*/" matches every message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
A launch selector that matches nothing is echoed into the status label so
the user can see which value missed. That value comes from another
program's command line or from the single-instance socket, and a QLabel
left at Qt::AutoText guesses whether a string is markup, so a selector
shaped like HTML would have been rendered rather than shown. The format is
now stated rather than guessed, the same reasoning AGENTS.md records for
MessageDetailsDialog. No existing status message carries markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
One entry point, called both at startup and by the socket handler when a later
launch arrives. Two paths would drift, which is the lesson this file has
already learned from every other pair.
The account moves first, because a built-in filter composes with the dropdown
and a query run before it would carry the old scope. The thread case reuses
recoverStaleThread() whole, as item 91's double-click already does. A message
id resolves to its thread first through the worker: opening a message means
opening its conversation with that message selected.
A thread id must be hex or it is treated as a miss and never reaches notmuch.
recoverStaleThread() builds thread:<id> unquoted, which is safe for ids notmuch
handed out and not for ones from another program's command line, where
"0000 or tag:inbox" would widen the query to the whole inbox. The check uses
anchoredPattern() because a PCRE $ also matches before a trailing newline.
An empty selector set changes nothing. A bare launch against a running window
means raise yourself, and a raise is not a navigation.
Also drops an empty public: label left in notmuchworker.h by the previous
commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
For --message (item 200), which knows an id and needs the conversation:
opening a message means opening its thread with that message selected, never
an id: query showing one card out of a conversation (item 91).
The id is quoted before it reaches notmuch, unlike every other id in this
class. Those came out of notmuch; this one comes off another program's command
line, and notmuch parses garbage happily while matching nothing, so an id
carrying query syntax would be read AS syntax with no error anywhere. The
quoting is local rather than SearchTerm::quote(): that escapes an embedded
quote with a backslash, which notmuch does not honour inside a phrase, so the
quote closed the phrase and the injected query matched a real thread (the new
test failed against it). notmuch escapes a quote by doubling it.
threadIdForTesting() keeps its name and gains a shared helper rather than being
promoted: it is documented as not being a slot, and the new entry point has to
answer asynchronously.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
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>
|
|
--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>
|
|
aSuccessfulCronSyncDrainsTheEditedAccounts named a log but no status
file, so Config fell back to ~/.local/state/qtmaildir/syncstatus.json
and the test passed or failed on the developer's last cron run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
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>
|
|
|
|
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.
|
|
|
|
save be retried
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|