From c7a2444ed5b9bb30b9f10d3e8dff8a6d6b6bb16a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 13 Sep 2026 20:32:47 +0200 Subject: feat: mark spam moves mail to the account's spam folder Mark spam was a tag-only action that added the spam tag and removed inbox, so a message marked as spam stayed in the inbox on disk. It now MOVES the file into the account's configured spam folder, exactly mirroring Delete: the account-relative spam key is the destination, the move records moved-from: with the origin, and unread and inbox are stripped in the same confirmed write so one undo returns the folder and the tags together. NotmuchWorker::moveMessages already handled a folder generically and applyTags already overwrote an older moved-from: tag, so the worker needed no change; the tests pin that behaviour for the spam destination. Five existing tests used spam as a worker-free, tag-only stand-in for the old Delete. Since spam is now a move too, they are retargeted to flag, the remaining selection-scoped tag-only action. --- src/mainwindow.h | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'src/mainwindow.h') diff --git a/src/mainwindow.h b/src/mainwindow.h index 97d90e5..14bb06f 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -226,6 +226,15 @@ public: refreshTrashActions(); } + /// The trash predicate, exposed because `spam` is a DIFFERENT folder and + /// the predicate must not answer for it. A test seam rather than a + /// behavioural one: asserting on Delete's visibility would prove the same + /// thing only through the label refresh. + bool everySelectedRowIsInATrashFolderForTesting() const + { + return everySelectedRowIsInATrashFolder(); + } + /// Runs a purge without the confirmation, which a test cannot drive: a /// modal blocks the thread it is shown on (item 84). What this exists to /// cover is what happens AFTER the user confirms. @@ -1233,6 +1242,29 @@ private: /// back where it came from. void untrashThreads(const QStringList &threadIds); + /// Moves each selected row's message to its account's spam folder, tagging + /// it `spam` and recording where it came from. Delete's sibling. + void spamSelected(); + + /// The half of spamSelected() that does the work, given the messages and + /// their paths. + /// + /// Shaped exactly like trashMessages(): paths are passed in rather than + /// looked up, because the thread-scoped caller has messages the MODEL has + /// never seen. + void spamMessages(const QStringList &messageIds, + const QHash &pathById, + int messageCount, + const QStringList &wholeThreadIds = {}); + + /// Moves every message of the named THREADS to their accounts' spam + /// folder. + /// + /// Asynchronous like trashThreads(), and for the same reason: the ids and + /// paths of an unexpanded thread's messages live only in the database, so + /// this asks the worker and finishes in onThreadMessagesResolved(). + void spamThreads(const QStringList &threadIds); + /// Runs the thread-scoped delete once the worker has resolved the /// threads to messages. void onThreadMessagesResolved(const QStringList &messageIds, -- cgit v1.2.3