diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-20 09:47:11 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-20 09:47:11 +0200 |
| commit | 98914f2c184539c9be9cac7c3ae8def6820868b8 (patch) | |
| tree | c87a7fe3ecd4b90e3222ba41e95bb5dc6eecf51b /src/notmuchworker.cpp | |
| parent | 40059782a5f5658ba02b47c4ab42fcfa4234989d (diff) | |
| download | qtmaildir-98914f2c184539c9be9cac7c3ae8def6820868b8.tar.gz qtmaildir-98914f2c184539c9be9cac7c3ae8def6820868b8.zip | |
fix(worker): read the mail root, not the index directory
notmuch can be configured with `mail_root` and `path` as separate keys,
which puts the Xapian index outside the Maildir. Under that layout
notmuch_database_get_path() returns the INDEX directory, and the worker
treated it as the mail root at four sites.
The consequences are not symmetric. Message paths resolved to `../..`
escapes that match no account prefix, which is a display defect. But
moveMessages() composes its destination from the same root, so Delete
would have written into the Xapian tree: outside the Maildir, invisible
to mbsync, and gone from every other client. That is the stranded-mail
failure of item 103 with a new cause.
notmuch_config_get(NOTMUCH_CONFIG_MAIL_ROOT) is correct under both
layouts, so no conditional is needed. Verified against the live database:
with only `path` set it returns the same string as get_path(), making
this a no-op for the current configuration.
The fixture gains an opt-in splitIndex(). That is load-bearing rather
than convenience: in the ordinary layout the index lives inside the mail
root and both accessors return the same string, so a test written
against it passes whichever one the code uses. All three new tests fail
against the old accessor, confirmed by mutation.
Also records the finding as backlog item 124, and corrects item 121's
timings, which had been copied from item 74 rather than measured. A cold
run seven minutes after boot, with the index verifiably unread, gives
2008 ms to the first rows and 38618 ms to a complete list, against the
642 ms and 5714 ms recorded there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
Diffstat (limited to 'src/notmuchworker.cpp')
| -rw-r--r-- | src/notmuchworker.cpp | 30 |
1 files changed, 26 insertions, 4 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index 66b408c..d0274cd 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -35,6 +35,28 @@ namespace { +/// Where the MAIL lives, which is not always where the index lives. +/// +/// Item 124. notmuch can be configured with `mail_root` and `path` as separate +/// keys, which is how the Xapian index moves to faster storage while the +/// Maildir stays put. Under that layout `notmuch_database_get_path()` returns +/// the INDEX directory, so every path composed from it lands in the wrong tree: +/// message paths resolve to `../..` escapes that match no account, and a move +/// writes into the index instead of the Maildir, where mbsync never sees it. +/// +/// `NOTMUCH_CONFIG_MAIL_ROOT` is correct under BOTH layouts. With only `path` +/// set it returns that same directory, so this is not a special case for split +/// configurations but the right question to ask in every one. Measured against +/// notmuch 0.39: legacy config, `get_path()` and `MAIL_ROOT` agree; split +/// config, only `MAIL_ROOT` names the Maildir. +/// +/// The string is owned by notmuch and must not be freed (notmuch.h:2585). +QString mailRootOf(notmuch_database_t *db) +{ + const char *root = notmuch_config_get(db, NOTMUCH_CONFIG_MAIL_ROOT); + return root ? QString::fromUtf8(root) : QString(); +} + QStringList tagsOf(notmuch_message_t *message) { QStringList result; @@ -295,7 +317,7 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation, // database-relative prefix: comparing the two never matched and left every // row resolving to no account at all. const QString dbRoot = - QDir(QString::fromUtf8(notmuch_database_get_path(m_db))).absolutePath(); + QDir(mailRootOf(m_db)).absolutePath(); QVector<ThreadSummary> batch; batch.reserve(kBatchSize); @@ -752,7 +774,7 @@ void NotmuchWorker::moveMessages(const QStringList &messageIds, return; } - const QString root = QString::fromUtf8(notmuch_database_get_path(db)); + const QString root = mailRootOf(db); const QString destDir = root + QLatin1Char('/') + destFolder + QStringLiteral("/cur"); @@ -898,7 +920,7 @@ void NotmuchWorker::resolveQuery(const QString &query, // ThreadSummary::firstMessagePath: the UI knows accounts only by their // maildir, itself a database-relative prefix. const QString dbRoot = - QDir(QString::fromUtf8(notmuch_database_get_path(m_db))).absolutePath(); + QDir(mailRootOf(m_db)).absolutePath(); QStringList messageIds; QStringList paths; @@ -1059,7 +1081,7 @@ void NotmuchWorker::requestFolders() if (!openReadOnly()) return; - const QString root = QString::fromUtf8(notmuch_database_get_path(m_db)); + const QString root = mailRootOf(m_db); if (root.isEmpty()) { emit errorOccurred( QStringLiteral("notmuch reports no database path.")); |
