aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/notmuchworker.cpp24
-rw-r--r--src/notmuchworker.h28
-rw-r--r--tests/test_notmuchworker.cpp59
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