MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 32 / 0 / 32
Functions: 100.0% 26 / 0 / 26
Branches: 80.0% 4 / 0 / 5

presentation/QuickRulesBackend.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QHash>
4 #include <QMap>
5 #include <QObject>
6 #include <QPointer>
7 #include <QTimer>
8 #include <QUrl>
9 #include <QVariantList>
10 #include <QVariantMap>
11
12 #include <functional>
13 #include <optional>
14 #include <utility>
15
16 #include "QuickRuleModel.h"
17 #include "RuleMatcher.h"
18 #include "RuleSuggestions.h"
19 #include "SieveSyntaxHighlighter.h"
20 #include "app/core/SieveSession.h"
21 #include "data/AccountConfig.h"
22 #include "data/Models.h"
23 #include "data/SieveMailboxName.h"
24 #include "data/SieveRuleGuards.h"
25 #include "service/SieveDiscovery.h"
26
27 // QML facade of server rules (Sprint 95, SIEVE-MANAGER/-EDITOR/-SCRIPT/
28 // -HISTORY/-HEALTH/-VACATION/-SETUP). Exposed as `backend.rules`.
29 //
30 // Owns one SieveSession per account (created lazily), the discovery state
31 // of the active account and the rule list model. Discovery never runs
32 // during cold start (G16): it is armed by foldersReady() and delayed, and
33 // a result younger than 7 days is reused from
34 // <profile>/sieve/<account-id>/discovery.json.
35 //
36 // Every edit — editor, sheet, console, quick actions, health fixes,
37 // restores — becomes new script bytes through SieveRuleCodec and goes
38 // through SieveSession::save (checked, conflict-aware). Quick actions push
39 // the replaced bytes on an undo stack (D-6). User-facing texts are plain
40 // words; technical detail goes to `details` and the log (G13).
41 class QuickRulesBackend final : public QObject {
42 5904 Q_OBJECT
43 Q_PROPERTY(QString state READ state NOTIFY stateChanged)
44 Q_PROPERTY(bool available READ available NOTIFY stateChanged)
45 Q_PROPERTY(QString serverSummary READ serverSummary NOTIFY stateChanged)
46 Q_PROPERTY(QString confirmHost READ confirmHost NOTIFY stateChanged)
47 Q_PROPERTY(bool managerOpen READ managerOpen WRITE setManagerOpen NOTIFY managerOpenChanged)
48 Q_PROPERTY(int matchRevision READ matchRevision NOTIFY matchesChanged)
49 Q_PROPERTY(QString sessionStatus READ sessionStatus NOTIFY sessionChanged)
50 Q_PROPERTY(QString statusLine READ statusLine NOTIFY sessionChanged)
51 Q_PROPERTY(QString errorText READ errorText NOTIFY sessionChanged)
52 Q_PROPERTY(QString errorDetails READ errorDetails NOTIFY sessionChanged)
53 Q_PROPERTY(bool readOnly READ readOnly NOTIFY sessionChanged)
54 Q_PROPERTY(bool busy READ busy NOTIFY sessionChanged)
55 Q_PROPERTY(QAbstractItemModel *rules READ rules CONSTANT)
56 Q_PROPERTY(int ruleCount READ ruleCount NOTIFY documentChanged)
57 Q_PROPERTY(int enabledRuleCount READ enabledRuleCount NOTIFY documentChanged)
58 Q_PROPERTY(int problemCount READ problemCount NOTIFY documentChanged)
59 Q_PROPERTY(int offCount READ offCount NOTIFY documentChanged)
60 Q_PROPERTY(QStringList targetFolders READ targetFolders NOTIFY documentChanged)
61 Q_PROPERTY(QVariantMap capabilities READ capabilities NOTIFY sessionChanged)
62 Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterChanged)
63 Q_PROPERTY(QString folderFilter READ folderFilter WRITE setFolderFilter NOTIFY filterChanged)
64 Q_PROPERTY(bool problemsOnly READ problemsOnly WRITE setProblemsOnly NOTIFY filterChanged)
65 Q_PROPERTY(bool offOnly READ offOnly WRITE setOffOnly NOTIFY filterChanged)
66 Q_PROPERTY(bool conflictActive READ conflictActive NOTIFY conflictChanged)
67 Q_PROPERTY(QVariantList conflictBlocks READ conflictBlocks NOTIFY conflictChanged)
68 Q_PROPERTY(bool canUndo READ canUndo NOTIFY undoChanged)
69 Q_PROPERTY(QVariantMap vacation READ vacation NOTIFY documentChanged)
70 Q_PROPERTY(QVariantList scripts READ scripts NOTIFY scriptsChanged)
71 Q_PROPERTY(QString activeScript READ activeScript NOTIFY scriptsChanged)
72 Q_PROPERTY(QStringList inactiveScripts READ inactiveScripts NOTIFY scriptsChanged)
73
74 public:
75 struct Deps {
76 std::function<std::optional<AccountConfig>()> account; // active account, with passwords
77 std::function<std::optional<AccountConfig>(const QString &)> configuration; // any account
78 std::function<QStringList()> imapCapabilities;
79 std::function<QList<FolderInfo>()> folders;
80 std::function<QString(const QString &)> decodeMailbox; // modified UTF-7 → UTF-8
81 QString profileDirectory;
82 QList<QSslCertificate> trustedCertificates; // tests: fixture CA; empty = system CAs
83 // Mail flow (QuickRuleMailFlow.cpp, Phase 4).
84 struct DisplayedMail {
85 qint64 folderId = -1;
86 qint64 uid = -1;
87 QString folderPath; // raw IMAP path
88 QMap<QString, QString> headers; // lower-case names; empty when unknown
89 QString predictedFolder; // raw IMAP path or empty
90 };
91 std::function<DisplayedMail()> displayedMail;
92 std::function<QList<MailHeader>(const QString &imapPath)> cachedHeaders;
93 std::function<QMap<QString, QString>(qint64 folderId, qint64 uid)> cachedHeaderFields;
94 std::function<void(const QString &sourcePath, const QList<qint64> &uids, const QString &targetPath)> moveMails;
95 std::function<void(const QString &sourcePath, qint64 uid, const QString &kind, const QString &value)> flagMail;
96 std::function<bool()> suggestionsEnabled; // General → "Suggest a rule after …" (D-12)
97 };
98
99 explicit QuickRulesBackend(Deps deps, QObject *parent = nullptr);
100 ~QuickRulesBackend() override;
101
102 2746 QString state() const { return m_state; }
103 10630 bool available() const { return m_state == QLatin1String("available"); }
104 QString serverSummary() const;
105
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 41 times.
41 QString confirmHost() const { return m_discovery.needsConfirmation ? m_discovery.server.host : QString(); }
106 1025 bool managerOpen() const { return m_managerOpen; }
107 void setManagerOpen(bool open);
108 QString sessionStatus() const;
109 QString statusLine() const;
110 65 QString errorText() const { return m_errorText; }
111 127 QString errorDetails() const { return m_errorDetails; }
112 bool readOnly() const;
113 bool busy() const;
114 61 QAbstractItemModel *rules() { return &m_model; }
115 int ruleCount() const;
116 int enabledRuleCount() const;
117 89 int problemCount() const { return m_model.problemCount(); }
118 44 int offCount() const { return m_model.offCount(); }
119 263 QStringList targetFolders() const { return m_model.targets(); }
120 QVariantMap capabilities() const;
121 54 QString filterText() const { return m_filterText; }
122 void setFilterText(const QString &text);
123 61 QString folderFilter() const { return m_folderFilter; }
124 void setFolderFilter(const QString &folder);
125 66 bool problemsOnly() const { return m_problemsOnly; }
126 void setProblemsOnly(bool on);
127 53 bool offOnly() const { return m_offOnly; }
128 void setOffOnly(bool on);
129 962 bool conflictActive() const { return !m_conflictMine.isNull(); }
130 1 QVariantList conflictBlocks() const { return m_conflictBlocks; }
131 2 bool canUndo() const { return !m_undo.isEmpty(); }
132 1 qsizetype undoDepth() const { return m_undo.size(); } // tests (S106-320)
133 QVariantMap vacation() const;
134 QVariantList scripts() const;
135 QString activeScript() const;
136 QStringList inactiveScripts() const;
137
138 // Lifecycle (called by QuickMailBackend).
139 void foldersReady();
140 void accountChanged();
141 void configurationChanged(const QString &accountId);
142
143 // Tests: trust a fixture CA for discovery and sessions (production keeps
144 // the system CAs; Deps::trustedCertificates is empty there).
145 void setTrustedCertificates(const QList<QSslCertificate> &certificates);
146
147 Q_INVOKABLE void openManager(const QString &folder = {});
148 15 Q_INVOKABLE void closeManager() { setManagerOpen(false); }
149 // Opens the manager with a panel: "vacation", "script", "scripts", "new".
150 Q_INVOKABLE void openPanel(const QString &panel);
151 // The panel requested before the manager page existed (read once).
152 21 Q_INVOKABLE QString takePendingPanel() { return std::exchange(m_pendingPanel, QString()); }
153 // Opens the manager with the editor on a prepared draft ("More options…").
154 1 Q_INVOKABLE void openDraft(const QVariantMap &draft) {
155 1 m_pendingDraft = draft;
156
1/1
✓ Branch 6 → 7 taken 1 time.
1 openPanel(QStringLiteral("draft"));
157 1 }
158 2 Q_INVOKABLE QVariantMap takePendingDraft() { return std::exchange(m_pendingDraft, QVariantMap()); }
159 // Opens the manager with the editor on an existing rule.
160 1 Q_INVOKABLE void openRule(int blockIndex) {
161
1/1
✓ Branch 2 → 3 taken 1 time.
1 m_pendingDraft = draftFor(blockIndex);
162
1/1
✓ Branch 8 → 9 taken 1 time.
1 openPanel(QStringLiteral("draft"));
163 1 }
164 // Removes the out-of-office block (offered after its last day).
165 Q_INVOKABLE QString removeVacation();
166 Q_INVOKABLE void reload();
167 Q_INVOKABLE void rediscover();
168 Q_INVOKABLE void confirmServer();
169
170 // Editing (QuickRuleEdits.cpp). Results arrive as editFinished(requestId, result).
171 Q_INVOKABLE QVariantMap draftFor(int blockIndex) const;
172 Q_INVOKABLE QVariantMap newDraft() const;
173 Q_INVOKABLE QString draftSentence(const QVariantMap &draft) const;
174 Q_INVOKABLE QString draftSieve(const QVariantMap &draft) const;
175 Q_INVOKABLE QVariantList draftIssues(int blockIndex, const QVariantMap &draft,
176 const QVariantMap &confirmations) const;
177 Q_INVOKABLE QString saveDraft(int blockIndex, const QVariantMap &draft, const QVariantMap &confirmations);
178 Q_INVOKABLE QString setEnabled(int blockIndex, bool enabled);
179 Q_INVOKABLE QString moveRule(int blockIndex, int targetBlockIndex);
180 Q_INVOKABLE QString moveRuleBy(int blockIndex, int delta);
181 Q_INVOKABLE QString deleteRule(int blockIndex);
182 // `alternative` applies the second solution ("Only reject").
183 Q_INVOKABLE QString applyHealthFix(int blockIndex, bool alternative = false);
184 Q_INVOKABLE QString undo();
185 Q_INVOKABLE int positionOf(int blockIndex) const;
186
187 // Script editor and scripts panel.
188 Q_INVOKABLE QStringList folderChoices() const;
189 // Installs Sieve highlighting on a TextArea's QQuickTextDocument with the
190 // palette's sieve* colors; call again after a theme change.
191 Q_INVOKABLE void attachHighlighter(QObject *quickTextDocument, const QVariantMap &palette);
192 Q_INVOKABLE void setHighlightedErrorLine(int line);
193 Q_INVOKABLE void openManagerForFolder(const QString &imapPath);
194 Q_INVOKABLE QString scriptText() const;
195 Q_INVOKABLE QString checkScript(const QString &text);
196 // expectedServer: the server version a reviewed merge was made with.
197 Q_INVOKABLE QString saveScript(const QString &text, const QString &expectedServer = {});
198 Q_INVOKABLE void resolveConflict(const QString &choice); // "mine" | "theirs"
199 // Sprint 107 (S107.3.4, QuickRuleConflict.cpp): the held conflict as
200 // three scripts for the resolver; empty unless conflictActive().
201 struct ConflictInputs {
202 QByteArray base; // the version the edit started from
203 QByteArray mine; // the upload that met the conflict
204 QByteArray theirs; // the server version it met
205 };
206 11 ConflictInputs conflictInputs() const { return {m_conflictBase, m_conflictMine, m_conflictTheirs}; }
207 // Uploads a resolution through the checked pipeline against `theirs`
208 // (D-9: a newer server version yields a new conflict, never an overwrite).
209 // `done(ok, kind, message, errorLines)`: kind is the outcome kind
210 // ("saved", "merged", "conflict", "rejected", …).
211 using ResolveCallback = std::function<void(bool ok, const QString &kind, const QString &message,
212 const QVariantList &errors)>;
213 void resolveWith(const QByteArray &resolved, const QByteArray &theirs, ResolveCallback done);
214 Q_INVOKABLE QString activateScript(const QString &name);
215 Q_INVOKABLE QString renameScript(const QString &from, const QString &to);
216 Q_INVOKABLE QString deleteScript(const QString &name);
217 Q_INVOKABLE QVariantList historyEntries();
218 // Review before upload (S95.3.10): {identical, wholeScript, summary,
219 // changes: [{kind, name, lines: [{op, text}]}]} against the server version.
220 Q_INVOKABLE QVariantMap reviewScript(const QString &text) const;
221 Q_INVOKABLE QVariantMap reviewVersion(const QString &id);
222 // Script files (Sprint 95 Could): a .sieve file into the script editor
223 // (uploaded only through Save + review) and the editor text to a file.
224 // {ok, text?, message}; at most 1 MiB of valid UTF-8.
225 Q_INVOKABLE QVariantMap readScriptFile(const QUrl &file) const;
226 Q_INVOKABLE QVariantMap writeScriptFile(const QUrl &file, const QString &text) const;
227 Q_INVOKABLE QString restoreVersion(const QString &id);
228
229 // Mail flow (S95.4.1, S95.4.4, S95.4.6, S95.4.9 — QuickRuleMailFlow.cpp).
230 Q_INVOKABLE QVariantMap mailRuleContext();
231 Q_INVOKABLE QVariantMap searchRuleContext(const QString &query);
232 Q_INVOKABLE QVariantMap evaluateDraft(const QVariantMap &draft, const QString &scopePath);
233 Q_INVOKABLE QString applyToExisting(const QVariantMap &draft, const QString &scopePath);
234 // F100-46: one saved rule applied to the inbox's cached mail now; the
235 // result (also "none") is announced as a toast.
236 Q_INVOKABLE QString applyRuleNow(int blockIndex);
237 Q_INVOKABLE QString addToRule(int blockIndex, const QVariantMap &condition);
238 Q_INVOKABLE QVariantList matchingRulesForMail();
239 // F95-14: whether an enabled rule matches a mail-list row (definite
240 // matches only: Cc/List-Id are known for rows synced since schema v24).
241 // Cached per mail until the rules change (matchesChanged).
242 bool matchesHeader(const MailHeader &header);
243 180 int matchRevision() const { return m_matchRevision; }
244 // Console rule mode (S95.4.3).
245 Q_INVOKABLE QVariantMap parseConsoleRule(const QString &text);
246 Q_INVOKABLE QString completeConsoleRule(const QString &text) const;
247 Q_INVOKABLE QVariantMap saveConsoleRule(const QString &text, bool confirmed);
248 Q_INVOKABLE QString consoleSeed();
249
250 // Suggestions from manual moves (S95.4.5, RuleSuggestions). The mail
251 // backend reports every user move; rule moves are not reported.
252 // Mailing-list mail (List-Id cached since schema v24) counts per list.
253 void recordManualMove(const QString &senderAddress, const QString &listId, const QString &imapPath);
254 Q_INVOKABLE QVariantMap suggestionContext(const QString &key, const QString &imapPath);
255 Q_INVOKABLE void silenceSuggestions(const QString &key);
256 // "Create rule" in the suggestion toast: sender → folder, saved at once
257 // (RJ5, one interaction; the saved toast offers Undo).
258 Q_INVOKABLE QString acceptSuggestion(const QString &key, const QString &imapPath);
259 // Junk rules (S95.5.2): offered after "Mark as junk"; also from the mail
260 // context menu. The rule files the sender's mail into Junk.
261 void offerJunkRule(const QString &sender);
262 Q_INVOKABLE QString acceptJunkRule(const QString &sender);
263
264 // Folders keep their rules (S95.4.7).
265 void folderRenamed(const QString &oldImapPath, const QString &newImapPath);
266 Q_INVOKABLE QStringList rulesUsingFolder(const QString &imapPath) const;
267 Q_INVOKABLE QString disableRulesForFolder(const QString &imapPath);
268
269 // Out of office (S95.5.1).
270 Q_INVOKABLE QString saveVacation(const QVariantMap &draft);
271 Q_INVOKABLE QString setVacationEnabled(bool enabled);
272
273 // Settings section: test with the form's fields (any account).
274 Q_INVOKABLE QString testConnection(const QVariantMap &fields);
275
276 // Plain-words message for a client error (G13), shared with tests.
277 static QString messageFor(ManageSieveClient::ErrorKind error, const QString &host);
278
279 signals:
280 void stateChanged();
281 void managerOpenChanged();
282 void sessionChanged();
283 void documentChanged();
284 void filterChanged();
285 void conflictChanged();
286 void undoChanged();
287 void scriptsChanged();
288 void editFinished(const QString &requestId, const QVariantMap &result);
289 void connectionTested(const QString &requestId, const QVariantMap &result);
290 void toast(const QString &message, bool undoable);
291 // F100-68: a rule made from a suggestion toast was saved; `blockIndex`
292 // opens it in the editor (openRule). Undoable like any rule save.
293 void ruleCreated(const QString &message, int blockIndex);
294 void folderRulesRequested(const QString &folder);
295 void panelRequested(const QString &panel);
296 void ruleSuggested(const QString &message, const QString &sender, const QString &imapPath);
297 void junkRuleOffered(const QString &message, const QString &sender);
298 void matchesChanged();
299
300 private:
301 struct Edit {
302 QByteArray bytes;
303 QString reason;
304 QString successText;
305 bool undoable = false;
306 // Set for rules created from a suggestion: announced with ruleCreated.
307 QString createdRule;
308 int createdBlock = -1; // its index in the submitted document
309
310 };
311 QString saveRule(int blockIndex, const QVariantMap &draft, const QVariantMap &confirmations,
312 bool announceCreated);
313
314 // QuickRulesBackend.cpp
315 void setState(const QString &state);
316 QString accountId() const;
317 QString storageDirectory(const QString &accountId) const;
318 void readDiscoveryCache();
319 void writeDiscoveryCache();
320 void startDiscovery();
321 void applyDiscovery(const SieveDiscovery::Result &result);
322 SieveSession *ensureSession();
323 SieveSession *session() const;
324 void wireSession(SieveSession *session);
325 void refreshModel();
326 SieveMailboxDirectory mailboxDirectory() const;
327 // message, fix label and an optional second fix label per block index
328 struct HealthFinding { QString message; QString fix; QString alternative; };
329 QHash<int, HealthFinding> healthFindings(const Sieve::Document &document) const;
330 static constexpr qsizetype MaxUndoScripts = 20;
331 void pushUndoScript(const QByteArray &previous);
332 void runConnectionTest(const AccountConfig &config, const QByteArray &typedSieve, const QString &requestId);
333 // S106-314: rule pairs compared per health run (about 300 enabled rules in full).
334 static constexpr qint64 MaxHealthPairs = 50000;
335 void provideCredentials(SieveSession::PasswordCallback done);
336 // QuickRuleMailFlow.cpp
337 QVariantMap reviewMap(const QByteArray &before, const QByteArray &after) const;
338 RuleMatcher::Message messageFor(const MailHeader &header, const QMap<QString, QString> &fields) const;
339 QString sieveNameFor(const QString &imapPath) const;
340 QString imapPathFor(const QString &sieveName) const;
341 // QuickRuleEdits.cpp
342 QString submit(const Edit &edit);
343 QVariantMap outcomeMap(const SieveSession::SaveOutcome &outcome) const;
344 Sieve::GuardContext guardContext(int blockIndex, const QVariantMap &confirmations) const;
345 QString refuseEdit(const QString &message);
346 void finishLater(const QString &requestId, const QVariantMap &result);
347 Sieve::Rule prepareRule(const QVariantMap &draft) const;
348 // S106-087: did the user confirm this (foreign) rule server for account `id`?
349 bool userConfirmedServer(const QString &id, const SieveDiscovery::Candidate &server) const;
350 QString nextRequestId();
351
352 Deps m_deps;
353 QuickRuleModel m_model;
354 SieveDiscovery m_discoverer;
355 SieveDiscovery::Result m_discovery;
356 QString m_state = QStringLiteral("unknown");
357 QString m_discoveredAccount;
358 RuleSuggestions m_suggestions;
359 QHash<QString, SieveSession *> m_sessions;
360 bool m_managerOpen = false;
361 QString m_filterText;
362 QString m_folderFilter;
363 bool m_problemsOnly = false;
364 bool m_offOnly = false;
365 QString m_errorText;
366 QString m_errorDetails;
367 void holdConflict(const QByteArray &mine, const QByteArray &base, const SieveSession::SaveOutcome &outcome);
368 void clearConflict();
369 QByteArray m_conflictMine;
370 QByteArray m_conflictBase;
371 QByteArray m_conflictTheirs;
372 QVariantList m_conflictBlocks;
373 QList<QByteArray> m_undo;
374 QTimer m_discoveryDelay;
375 quint64 m_requestCounter = 0;
376 bool m_announceNextApply = false; // F100-46: the next apply also announces "no match"
377 QPointer<SieveSyntaxHighlighter> m_highlighter;
378 QString m_pendingPanel;
379 QVariantMap m_pendingDraft;
380 QHash<QPair<qint64, qint64>, bool> m_matchCache;
381 static constexpr qsizetype MaxMatchCache = 20000;
382 int m_matchRevision = 0;
383 bool m_matchesPossible = false; // a rule document could match (server available)
384 void invalidateMatches();
385 };
386