diff options
| -rw-r--r-- | src/notmuchworker.cpp | 24 | ||||
| -rw-r--r-- | src/notmuchworker.h | 28 | ||||
| -rw-r--r-- | tests/test_notmuchworker.cpp | 59 |
3 files changed, 111 insertions, 0 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index 2672f6a..2588f4e 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -764,6 +764,30 @@ void NotmuchWorker::loadThreadTree(const QString &threadId, QString NotmuchWorker::threadIdForTesting(const QString &query) { + return firstThreadIdMatching(query); +} + +void NotmuchWorker::resolveThreadForMessage(const QString &messageId) +{ + if (messageId.isEmpty()) { + emit threadForMessageResolved(messageId, QString()); + return; + } + // This id came off another program's command line, and notmuch reads + // unquoted query syntax AS syntax. Quoted here rather than through + // SearchTerm::quote(): that escapes an embedded quote with a backslash, + // which notmuch's (Xapian's) parser does not honour inside a phrase, so + // the quote closes the phrase and the rest is parsed as query. notmuch + // escapes a quote by DOUBLING it. + QString escaped = messageId; + escaped.replace(QLatin1Char('"'), QStringLiteral("\"\"")); + emit threadForMessageResolved( + messageId, + firstThreadIdMatching(QStringLiteral("id:\"%1\"").arg(escaped))); +} + +QString NotmuchWorker::firstThreadIdMatching(const QString &query) +{ if (!openReadOnly()) return QString(); diff --git a/src/notmuchworker.h b/src/notmuchworker.h index b4e9707..3183729 100644 --- a/src/notmuchworker.h +++ b/src/notmuchworker.h @@ -138,6 +138,13 @@ public: /// be reached across the thread boundary by accident. QString threadIdForTesting(const QString &query); +private: + /// The first thread id matching \p query, or empty. Shared by + /// threadIdForTesting() and resolveThreadForMessage(). + QString firstThreadIdMatching(const QString &query); + +public: + public slots: /// Loads ONE message, for a message row selected in the list. @@ -147,6 +154,21 @@ public slots: /// reporting. void loadMessage(const QString &messageId, quint64 generation); + /// Answers which thread a Message-ID belongs to (item 200). + /// + /// For `--message`, 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). + /// + /// Answers with an EMPTY thread id when the message is unknown rather than + /// staying silent, since the window reports the miss and a slot that never + /// replies would leave it waiting forever. + /// + /// **The id is quoted before it reaches notmuch.** Unlike every other id in + /// this class, this one came from argv rather than from notmuch itself, and + /// notmuch parses garbage happily while matching nothing. + void resolveThreadForMessage(const QString &messageId); + /// Applies tag changes. Opens the database read-write, applies, and closes /// immediately: notmuch's write lock is exclusive process-wide, so holding /// it would block the user's cron `notmuch new`. @@ -346,6 +368,12 @@ signals: quint64 generation); void messageLoaded(const QVector<MessageRef> &messages, quint64 generation); + /// The answer to resolveThreadForMessage(). The message id is echoed back + /// so a caller can tell which request this answers; the thread id is empty + /// when nothing matched. + void threadForMessageResolved(const QString &messageId, + const QString &threadId); + /// The dashboard's digest. `generation` is the dashboard's own counter, /// echoed back so a stale answer can be discarded. void threadDigestLoaded(const ThreadDigest &digest, quint64 generation); diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index b2810d1..10f8dbc 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -70,6 +70,9 @@ private slots: void loadMessageReturnsOnlyThatMessage(); void loadMessageOnAnUnknownIdReturnsNothing(); + void resolvingAMessageIdAnswersItsThreadId(); + void resolvingAnUnknownMessageIdAnswersEmpty(); + void resolvingAMessageIdQuotesTheId(); void aQueryCarriesEachThreadsFirstMessageId(); void aSentQueryCarriesTheMatchedMessageNotTheThreadsFirst(); void queryCarriesTheFirstMessageSender(); @@ -459,6 +462,62 @@ void TestNotmuchWorker::loadMessageOnAnUnknownIdReturnsNothing() QCOMPARE(errors.count(), 0); } +void TestNotmuchWorker::resolvingAMessageIdAnswersItsThreadId() +{ + // What --message needs (item 200): the CLI knows a Message-ID and the + // window needs the thread id, because opening the message means opening + // its conversation with that message selected. + NotmuchWorker worker(m_fixture.configPath()); + QSignalSpy resolved(&worker, &NotmuchWorker::threadForMessageResolved); + + worker.resolveThreadForMessage(QStringLiteral("a2@example.org")); + + QCOMPARE(resolved.count(), 1); + QCOMPARE(resolved.first().at(0).toString(), QStringLiteral("a2@example.org")); + + // a2 is a REPLY, so its thread id is the thread's, not its own. It must + // agree with the thread a1 resolves to. + const QString threadId = resolved.first().at(1).toString(); + QVERIFY(!threadId.isEmpty()); + QCOMPARE(threadId, + worker.threadIdForTesting(QStringLiteral("id:a1@example.org"))); +} + +void TestNotmuchWorker::resolvingAnUnknownMessageIdAnswersEmpty() +{ + // Answers rather than staying silent: the window shows the miss in the + // status bar, and a slot that never replies would leave it waiting. + NotmuchWorker worker(m_fixture.configPath()); + QSignalSpy resolved(&worker, &NotmuchWorker::threadForMessageResolved); + QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred); + + worker.resolveThreadForMessage(QStringLiteral("nonexistent@example.org")); + + QCOMPARE(resolved.count(), 1); + QCOMPARE(resolved.first().at(0).toString(), + QStringLiteral("nonexistent@example.org")); + QVERIFY(resolved.first().at(1).toString().isEmpty()); + QCOMPARE(errors.count(), 0); +} + +void TestNotmuchWorker::resolvingAMessageIdQuotesTheId() +{ + // The id comes from argv, not from notmuch, and notmuch's parser rejects + // almost nothing: an unquoted id carrying query syntax would be PARSED as + // syntax. It must stay a miss, with no error. + NotmuchWorker worker(m_fixture.configPath()); + QSignalSpy resolved(&worker, &NotmuchWorker::threadForMessageResolved); + QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred); + + worker.resolveThreadForMessage( + QStringLiteral("a2@example.org\" or from:alice or \"x")); + + QCOMPARE(resolved.count(), 1); + QVERIFY2(resolved.first().at(1).toString().isEmpty(), + "an id carrying query syntax resolved to a thread: it was not quoted"); + QCOMPARE(errors.count(), 0); +} + void TestNotmuchWorker::aQueryCarriesEachThreadsFirstMessageId() { // The root card IS the thread's first message, so selecting it must be |
