diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-31 12:24:32 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-31 12:24:32 +0100 |
| commit | ef42103bb1639006c5558eae5f857cd8f9ad2418 (patch) | |
| tree | 33aada792086d58d6a97690f52fdfd4e84f6f33e /src/notmuchworker.cpp | |
| parent | aa5fc733c768f8779af897c0cdcaf836b2afcefb (diff) | |
| download | qtmaildir-ef42103bb1639006c5558eae5f857cd8f9ad2418.tar.gz qtmaildir-ef42103bb1639006c5558eae5f857cd8f9ad2418.zip | |
feat: a worker entry point that applies a rule to existing mail
applyRule() replaces applyTagsToQuery(). It resolves the messages the
whole rule would move once, then writes one tag at a time over that
fixed list, and answers with ruleApplied(token, changes): one TagChange
per operation carrying the ids it actually moved. Always answered, even
when nothing moved, and never through tagsApplied(), so the window can
build the undo entry from the answer instead of the shared confirmation
queue that user gestures also use.
Resolving once matters when the query names a tag the rule removes:
re-resolving after the first write matched nothing and wrote half the
rule. Writing per tag keeps each effective set exact, so the undo never
strips a tag a message had before.
applyTags() keeps its behaviour; its body is now writeTags(), which
returns the changed ids without emitting.
Item 212.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/notmuchworker.cpp')
| -rw-r--r-- | src/notmuchworker.cpp | 77 |
1 files changed, 54 insertions, 23 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index d41b54b..e5f5afa 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -362,12 +362,19 @@ static const int kSenderCountsMetaType = static const int kThreadDigestMetaType = qRegisterMetaType<ThreadDigest>("ThreadDigest"); +/// And for applyRule()'s answer, which crosses the queued ruleApplied +/// connection as a container. An unregistered one is dropped with only a +/// warning, and the window would then never build the rule's undo entry. +static const int kTagChangesMetaType = + qRegisterMetaType<QVector<TagChange>>("QVector<TagChange>"); + NotmuchWorker::NotmuchWorker(const QString ¬muchConfigPath, QObject *parent) : QObject(parent), m_configPath(notmuchConfigPath) { Q_UNUSED(kSortOrderMetaType); Q_UNUSED(kSenderCountsMetaType); Q_UNUSED(kThreadDigestMetaType); + Q_UNUSED(kTagChangesMetaType); } NotmuchWorker::~NotmuchWorker() @@ -1083,25 +1090,39 @@ void NotmuchWorker::applyTagsToThreads(const QStringList &threadIds, applyTags(TagChange{ messageIds, add, remove, description }); } -void NotmuchWorker::applyTagsToQuery(const QString &query, - const QStringList &add, - const QStringList &remove, - const QString &description) +void NotmuchWorker::applyRule(const QString &query, const QStringList &add, + const QStringList &remove, + const QString &description, quint64 token) { - if (!openReadOnly()) + QVector<TagChange> changes; + if (!openReadOnly()) { + emit ruleApplied(token, changes); return; + } QStringList messageIds; if (!collectMessageIds(m_db, SearchTerm::changes(query, add, remove), &messageIds)) { emit errorOccurred(QStringLiteral("Cannot resolve the rule's query")); + emit ruleApplied(token, changes); return; } - // Empty is not an error: the rule has nothing left to do. applyTags() - // returns at once on an empty list, and stays silent, as it must - // (CLAUDE.md, item 176). - applyTags(TagChange{ messageIds, add, remove, description }); + QList<TagChange> ops; + for (const QString &tag : add) + ops.append(TagChange{ messageIds, { tag }, {}, description }); + for (const QString &tag : remove) + ops.append(TagChange{ messageIds, {}, { tag }, description }); + + for (TagChange &op : ops) { + QStringList changed; + // An empty list writes nothing; writeTags() is not even asked to open. + if (!messageIds.isEmpty() && !writeTags(op, &changed)) + break; // errorOccurred() already emitted; report what landed + op.messageIds = changed; + changes.append(op); + } + emit ruleApplied(token, changes); } void NotmuchWorker::applyTags(const TagChange &change) @@ -1109,6 +1130,25 @@ void NotmuchWorker::applyTags(const TagChange &change) if (change.messageIds.isEmpty()) return; + QStringList changedIds; + if (!writeTags(change, &changedIds)) + return; + + // Nothing moved, so there is nothing to record as pending, nothing to sync + // and nothing to undo. Emitting an empty change would push an undo entry + // whose inverse adds a tag no message ever carried, which is item 176 one + // step further along. The same silence as the empty-id early return above, + // for the same reason. + if (changedIds.isEmpty()) + return; + + emit tagsApplied(TagChange{ changedIds, change.added, change.removed, + change.description }); +} + +bool NotmuchWorker::writeTags(const TagChange &change, QStringList *changedIds) +{ + // The read-only handle must be closed first: notmuch allows only one open // handle per process. close(); @@ -1139,7 +1179,7 @@ void NotmuchWorker::applyTags(const TagChange &change) .arg(QString::fromUtf8(error ? error : notmuch_status_to_string(status)))); free(error); - return; + return false; } // Item 176. What the caller ASKED for is not what the write does: a @@ -1149,8 +1189,8 @@ void NotmuchWorker::applyTags(const TagChange &change) // unread", measured on real mail as 2 unread becoming 43. With // maildir.synchronize_flags on, that rewrites filenames and reaches the // server. - QStringList changedIds; - changedIds.reserve(change.messageIds.size()); + changedIds->clear(); + changedIds->reserve(change.messageIds.size()); for (const QString &id : change.messageIds) { notmuch_message_t *raw = nullptr; @@ -1196,7 +1236,7 @@ void NotmuchWorker::applyTags(const TagChange &change) moves = moves || !before.contains(tag); moves = moves || !strippedOrigins.isEmpty(); if (moves) - changedIds.append(id); + changedIds->append(id); notmuch_message_freeze(message.get()); for (const QString &tag : change.removed) @@ -1215,16 +1255,7 @@ void NotmuchWorker::applyTags(const TagChange &change) notmuch_database_close(db); notmuch_database_destroy(db); - // Nothing moved, so there is nothing to record as pending, nothing to sync - // and nothing to undo. Emitting an empty change would push an undo entry - // whose inverse adds a tag no message ever carried, which is item 176 one - // step further along. The same silence as the empty-id early return above, - // for the same reason. - if (changedIds.isEmpty()) - return; - - emit tagsApplied(TagChange{ changedIds, change.added, change.removed, - change.description }); + return true; } void NotmuchWorker::moveMessages(const QStringList &messageIds, |
