diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 17:58:07 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 17:58:07 +0200 |
| commit | c4a38a5871250b9c952f3eb633ab4cf2ed3d717a (patch) | |
| tree | e3871be69d4688bbc4143bbcbf47930836aa5379 | |
| parent | 1e63b9127a34d0f3d396d5c4ad420074a04f5a55 (diff) | |
| download | qtmaildir-c4a38a5871250b9c952f3eb633ab4cf2ed3d717a.tar.gz qtmaildir-c4a38a5871250b9c952f3eb633ab4cf2ed3d717a.zip | |
fix: accept a bracketed Message-ID in the message selector
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>
| -rw-r--r-- | src/mainwindow.cpp | 23 | ||||
| -rw-r--r-- | src/mainwindow.h | 2 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 47 |
3 files changed, 69 insertions, 3 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6d2697b..c44e36d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5226,15 +5226,34 @@ void MainWindow::updateStaleThreadNotice() m_messageView->setStaleThread(threadId, messageId); } -void MainWindow::applySelectors(const LaunchSelectors &selectors) +void MainWindow::applySelectors(const LaunchSelectors &requested) { // Nothing asked for. A bare launch against a running window means "raise // yourself", which main() and the socket handler do around this call; from // here there is nothing to change, and re-running a query would take the // user off whatever they were reading. - if (selectors.isEmpty()) + if (requested.isEmpty()) return; + // A Message-ID is written <local@domain> in every header a caller copies + // it from, and notmuch stores it WITHOUT the angle brackets, so the + // bracketed form matched nothing. Stripped ONCE, here, so the resolve + // query and the recovery's selection target both see the bare id: fixing + // only the resolve would open the thread and then fail to find the + // message inside it. One enclosing pair only, as mimeparser.cpp does for a + // Content-Id; an id with brackets of its own inside is left as it is. + // + // No guard against `/.../`: notmuch reads a slashed value as a regex for + // `mid:` but NOT for `id:`, verified against notmuch 0.39, where + // id:"/.*/" counts 0 on an index where mid:"/.*/" counts every message. + LaunchSelectors selectors = requested; + selectors.messageId = selectors.messageId.trimmed(); + if (selectors.messageId.startsWith(QLatin1Char('<')) + && selectors.messageId.endsWith(QLatin1Char('>'))) { + selectors.messageId = + selectors.messageId.mid(1, selectors.messageId.size() - 2); + } + // The account FIRST, and the order matters: a built-in filter composes // with the dropdown, so a query run before the account moved would carry // the old scope. This is the same ordering the startup path uses. diff --git a/src/mainwindow.h b/src/mainwindow.h index 6b841fa..8167e74 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -151,7 +151,7 @@ public: /// A thread id that is not hex is such 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 argv. - void applySelectors(const LaunchSelectors &selectors); + void applySelectors(const LaunchSelectors &requested); /// Opens the list behind the unsynced-changes count. /// diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 66cc9b5..b9bd890 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -319,6 +319,7 @@ private slots: void aMessageSelectorOpensItsThread(); void aThreadSelectorThatIsNotHexIsRefused(); void anEmptySelectorSetChangesNothing(); + void aBracketedMessageSelectorOpensThatMessage(); void everyBuiltinFilterButtonCarriesAnIconAndItsText(); void theDraftsButtonIsAbsentWithoutADraftsFolder(); void aQueryInTheMenuCanActuallyBeRun(); @@ -18066,4 +18067,50 @@ void TestMainWindow::anEmptySelectorSetChangesNothing() QCOMPARE(queryEdit->text(), QStringLiteral("tag:flagged")); } +void TestMainWindow::aBracketedMessageSelectorOpensThatMessage() +{ + // A Message-ID is written <local@domain> in every header it appears in, so + // that is the form a caller copies. notmuch stores it WITHOUT the angle + // brackets, so the bracketed form matched nothing and the launch reported + // a miss for a message that was right there. + // + // The target is a REPLY, not the root. The recovery lands on the root + // whenever it cannot find the message it was given, so a root target + // would pass with the brackets stripped for the resolve and left on for + // the selection. + WorkerBackedWindow backed; + QVERIFY(backed.fixture().addMessage( + QStringLiteral("inbox"), QStringLiteral("one@example.org"), + QStringLiteral("First subject"), QStringLiteral("a@example.org"), + // Friday, verified with `date -d 2026-08-14 +%A`. + QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"), + QStringLiteral("Body one."))); + QVERIFY(backed.fixture().addMessage( + QStringLiteral("inbox"), QStringLiteral("two@example.org"), + QStringLiteral("Re: First subject"), QStringLiteral("b@example.org"), + // Saturday, verified with `date -d 2026-08-15 +%A`. + QStringLiteral("Sat, 15 Aug 2026 10:00:00 +0200"), + QStringLiteral("Body two."), true, + QStringLiteral("one@example.org"))); + QVERIFY2(backed.build(), qPrintable(backed.error())); + + MainWindow window(backed.config()); + auto *model = window.findChild<ThreadListModel *>(); + QVERIFY(model); + auto *view = window.findChild<ThreadListView *>(); + QVERIFY(view); + + LaunchSelectors selectors; + // Surrounding whitespace too, which a shell quote or a copy picks up. + selectors.messageId = QStringLiteral(" <two@example.org> "); + window.applySelectors(selectors); + + QTRY_VERIFY_WITH_TIMEOUT(view->currentIndex().isValid() + && model->isMessageRow(view->currentIndex()) + && model->messageAt(view->currentIndex()) + .messageId + == QStringLiteral("two@example.org"), + 15000); +} + #include "test_mainwindow.moc" |
