diff options
| -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" |
