aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/notmuchworker.cpp77
-rw-r--r--src/notmuchworker.h38
2 files changed, 84 insertions, 31 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 &notmuchConfigPath, 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,
diff --git a/src/notmuchworker.h b/src/notmuchworker.h
index fa223e6..a175d7a 100644
--- a/src/notmuchworker.h
+++ b/src/notmuchworker.h
@@ -232,14 +232,24 @@ public slots:
const QStringList &remove,
const QString &description);
- /// Applies `+add -remove` to every message the change would move within
- /// `query` (item 212, a rule applied to existing mail). The query is
- /// narrowed with SearchTerm::changes() so already-correct messages are
- /// never written, then the ids go through applyTags(), which reports the
- /// effective set for undo. Emits nothing when nothing would change.
- void applyTagsToQuery(const QString &query, const QStringList &add,
- const QStringList &remove,
- const QString &description);
+ /// Applies a tagging rule to existing mail (item 212). Resolves the
+ /// messages the WHOLE rule would move once, with SearchTerm::changes(),
+ /// then writes one tag at a time over that fixed list: `+a` for each add,
+ /// `-r` for each remove.
+ ///
+ /// One write per tag because the effective set is per MESSAGE: a single
+ /// `+promo -inbox` write reports a message that already had promo (it lost
+ /// inbox), and the undo built from it would strip promo. One resolution
+ /// because a query may name a tag the rule removes, and re-resolving after
+ /// the first write would match nothing for the rest, writing half the
+ /// rule where the hook writes all of it.
+ ///
+ /// Answers with ruleApplied() ALWAYS, even when nothing moved, and never
+ /// with tagsApplied(): the window builds the undo from the answer rather
+ /// than from the shared confirmation queue.
+ void applyRule(const QString &query, const QStringList &add,
+ const QStringList &remove, const QString &description,
+ quint64 token);
/// Resolves whole threads to the message ids and file paths they contain.
///
@@ -386,6 +396,13 @@ signals:
void threadDigestLoaded(const ThreadDigest &digest, quint64 generation);
void tagsApplied(const TagChange &change);
+ /// applyRule()'s answer: one TagChange per single-tag operation, in the
+ /// order applyRule() documents, each carrying the ids that operation
+ /// actually moved (possibly none). `token` is echoed from the request.
+ /// Emitted even after a write error, carrying what was written before it,
+ /// so the window can still offer undo for that part.
+ void ruleApplied(quint64 token, const QVector<TagChange> &changes);
+
/// One file entered or left the index outside a query (item 192).
///
/// Emitted by indexDraftFile() and removeIndexedFile(), the two entry
@@ -483,6 +500,11 @@ private:
bool openReadOnly();
void close();
+ /// applyTags() without the signal: writes `change` and fills `changedIds`
+ /// with the messages whose tags really moved. False, with errorOccurred()
+ /// emitted, when the database could not be opened for writing.
+ bool writeTags(const TagChange &change, QStringList *changedIds);
+
QByteArray configPathArg() const;
QString m_configPath;