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: 91.0% 566 / 0 / 622
Functions: 96.9% 62 / 0 / 64
Branches: 65.3% 682 / 0 / 1045

presentation/QuickRulesBackend.cpp
Line Branch Exec Source
1 // State, discovery, sessions, scripts, history and the settings test of
2 // QuickRulesBackend. Editing lives in QuickRuleEdits.cpp.
3 #include "QuickRulesBackend.h"
4
5 #include "RuleMatcher.h"
6 #include "RuleSentence.h"
7 #include "data/CredentialStore.h"
8
9 #include <QDir>
10 #include <QFile>
11 #include <QJsonArray>
12 #include <QQuickTextDocument>
13 #include <QJsonDocument>
14 #include <QJsonObject>
15 #include <QLocale>
16 #include <QLoggingCategory>
17 #include <QTimer>
18 #include <QUuid>
19
20
4/7
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 8 taken 208 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 9 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
217 Q_LOGGING_CATEGORY(lcRulesBackend, "mailjd.sieve.ui")
21
22 namespace {
23 constexpr qint64 kDiscoveryMaxAgeSeconds = 7 * 24 * 3600;
24 constexpr int kDiscoveryDelayMs = 10000;
25 } // namespace
26
27 864 QuickRulesBackend::QuickRulesBackend(Deps deps, QObject *parent)
28
4/4
✓ Branch 5 → 6 taken 864 times.
✓ Branch 6 → 7 taken 864 times.
✓ Branch 7 → 8 taken 864 times.
✓ Branch 24 → 25 taken 864 times.
1728 : QObject(parent), m_deps(std::move(deps)) {
29 864 m_discoverer.setCaCertificates(m_deps.trustedCertificates);
30
1/1
✓ Branch 30 → 31 taken 864 times.
864 m_discoveryDelay.setSingleShot(true);
31
1/1
✓ Branch 31 → 32 taken 864 times.
864 m_discoveryDelay.setInterval(kDiscoveryDelayMs);
32
1/1
✓ Branch 32 → 33 taken 864 times.
864 connect(&m_discoveryDelay, &QTimer::timeout, this, [this] {
33 1 readDiscoveryCache();
34
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (m_discovery.checkedAt.isValid())
35 ✗ applyDiscovery(m_discovery);
36 else
37 1 startDiscovery();
38 1 });
39 // F95-14: the mail-list rule markers follow the rules and their state.
40
1/1
✓ Branch 34 → 35 taken 864 times.
864 connect(this, &QuickRulesBackend::documentChanged, this, &QuickRulesBackend::invalidateMatches);
41
1/1
✓ Branch 36 → 37 taken 864 times.
864 connect(this, &QuickRulesBackend::stateChanged, this, &QuickRulesBackend::invalidateMatches);
42 864 }
43
44 931 QuickRulesBackend::~QuickRulesBackend() = default;
45
46 // --- state ------------------------------------------------------------------------
47
48 982 void QuickRulesBackend::setState(const QString &state) {
49
2/2
✓ Branch 3 → 4 taken 791 times.
✓ Branch 3 → 5 taken 191 times.
982 if (m_state == state)
50 791 return;
51 191 m_state = state;
52
6/6
✓ Branch 6 → 7 taken 191 times.
✓ Branch 10 → 11 taken 191 times.
✓ Branch 11 → 12 taken 191 times.
✓ Branch 12 → 13 taken 191 times.
✓ Branch 16 → 8 taken 191 times.
✓ Branch 16 → 17 taken 191 times.
382 qCInfo(lcRulesBackend) << "Rule server state" << state;
53 191 emit stateChanged();
54 }
55
56 14429 QString QuickRulesBackend::accountId() const {
57
2/3
✓ Branch 3 → 4 taken 14429 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 14429 times.
14429 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
58
2/2
✓ Branch 7 → 8 taken 14413 times.
✓ Branch 7 → 10 taken 16 times.
28858 return account ? account->id : QString();
59 14429 }
60
61 375 QString QuickRulesBackend::storageDirectory(const QString &id) const {
62
3/3
✓ Branch 2 → 3 taken 375 times.
✓ Branch 6 → 7 taken 375 times.
✓ Branch 7 → 8 taken 375 times.
750 return QDir(m_deps.profileDirectory).filePath(QStringLiteral("sieve/") + id);
63 }
64
65 2 QString QuickRulesBackend::serverSummary() const {
66
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (m_discovery.server.host.isEmpty())
67 ✗ return {};
68
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
6 QString text = QStringLiteral("%1:%2").arg(m_discovery.server.host).arg(m_discovery.server.port);
69
1/2
✓ Branch 16 → 17 taken 2 times.
✗ Branch 16 → 26 not taken.
2 if (!m_discovery.implementation.isEmpty())
70
2/2
✓ Branch 20 → 21 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
4 text += QStringLiteral(" · ") + m_discovery.implementation;
71 2 return text;
72 2 }
73
74
1/1
✓ Branch 2 → 3 taken 14282 times.
14282 SieveSession *QuickRulesBackend::session() const { return m_sessions.value(accountId()); }
75
76 3482 QString QuickRulesBackend::sessionStatus() const {
77 3482 const SieveSession *s = session();
78
2/2
✓ Branch 3 → 4 taken 1217 times.
✓ Branch 3 → 9 taken 2265 times.
3482 if (!s)
79 1217 return QStringLiteral("idle");
80
5/7
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 16 taken 1981 times.
✓ Branch 10 → 21 taken 242 times.
✓ Branch 10 → 26 taken 36 times.
✓ Branch 10 → 31 taken 3 times.
✓ Branch 10 → 36 taken 3 times.
✗ Branch 10 → 41 not taken.
2265 switch (s->status()) {
81 ✗ case SieveSession::Status::Idle: return QStringLiteral("idle");
82 3962 case SieveSession::Status::Loading: return QStringLiteral("loading");
83 484 case SieveSession::Status::Ready: return QStringLiteral("ready");
84 72 case SieveSession::Status::Saving: return QStringLiteral("saving");
85 6 case SieveSession::Status::Offline: return QStringLiteral("offline");
86 6 case SieveSession::Status::Error: return QStringLiteral("error");
87 }
88 ✗ return QStringLiteral("idle");
89 }
90
91 70 QString QuickRulesBackend::statusLine() const {
92
1/1
✓ Branch 2 → 3 taken 70 times.
70 const SieveSession *s = session();
93
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 9 taken 60 times.
70 if (!s)
94
1/3
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 10 times.
✗ Branch 6 → 8 not taken.
10 return available() ? tr("Rules are not loaded yet.") : QString();
95
1/1
✓ Branch 9 → 10 taken 60 times.
60 const QLocale locale;
96
4/4
✓ Branch 11 → 12 taken 8 times.
✓ Branch 11 → 14 taken 1 time.
✓ Branch 11 → 26 taken 1 time.
✓ Branch 11 → 27 taken 50 times.
60 switch (s->status()) {
97 8 case SieveSession::Status::Loading:
98
1/1
✓ Branch 12 → 13 taken 8 times.
8 return tr("Loading rules…");
99 1 case SieveSession::Status::Offline:
100
1/1
✓ Branch 14 → 15 taken 1 time.
2 return tr("Offline — showing the rules as of %1")
101
3/3
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
1 .arg(locale.toString(s->loadedAt().toLocalTime(), QLocale::ShortFormat));
102 1 case SieveSession::Status::Error:
103 1 return m_errorText;
104 50 default:
105 50 break;
106 }
107
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 40 taken 49 times.
50 if (s->activeScript().isEmpty())
108
3/3
✓ Branch 32 → 33 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
2 return tr("No active script yet — the first rule creates „%1“.").arg(SieveSession::newScriptName());
109
3/3
✓ Branch 40 → 41 taken 49 times.
✓ Branch 41 → 42 taken 49 times.
✓ Branch 44 → 45 taken 49 times.
147 QString line = tr("Active script „%1“ · rules: %n", nullptr, ruleCount()).arg(s->activeScript());
110
3/3
✓ Branch 48 → 49 taken 49 times.
✓ Branch 50 → 51 taken 23 times.
✓ Branch 50 → 72 taken 26 times.
49 if (s->savedAt().isValid())
111 46 line += QStringLiteral(" · ") +
112
7/7
✓ Branch 51 → 52 taken 23 times.
✓ Branch 54 → 55 taken 23 times.
✓ Branch 55 → 56 taken 23 times.
✓ Branch 56 → 57 taken 23 times.
✓ Branch 57 → 58 taken 23 times.
✓ Branch 61 → 62 taken 23 times.
✓ Branch 62 → 63 taken 23 times.
92 tr("saved at %1").arg(locale.toString(s->savedAt().toLocalTime().time(), QLocale::ShortFormat));
113 49 return line;
114 60 }
115
116 331 bool QuickRulesBackend::readOnly() const {
117 331 const SieveSession *s = session();
118
6/6
✓ Branch 3 → 4 taken 314 times.
✓ Branch 3 → 10 taken 17 times.
✓ Branch 5 → 6 taken 313 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 7 → 8 taken 312 times.
✓ Branch 7 → 10 taken 1 time.
643 return !s || s->status() == SieveSession::Status::Offline || s->status() == SieveSession::Status::Error ||
119
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 312 times.
643 s->document().readOnly;
120 }
121
122 9 bool QuickRulesBackend::busy() const {
123 9 const SieveSession *s = session();
124
3/4
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 5 times.
9 return s && s->busy();
125 }
126
127 108 int QuickRulesBackend::ruleCount() const {
128 108 const SieveSession *s = session();
129
1/2
✓ Branch 3 → 4 taken 108 times.
✗ Branch 3 → 7 not taken.
108 return s ? s->document().ruleCount() : 0;
130 }
131
132 2447 int QuickRulesBackend::enabledRuleCount() const {
133 2447 const SieveSession *s = session();
134
2/2
✓ Branch 3 → 4 taken 70 times.
✓ Branch 3 → 7 taken 2377 times.
2447 return s ? s->document().enabledRuleCount() : 0;
135 }
136
137 216 QVariantMap QuickRulesBackend::capabilities() const {
138 216 const SieveSession *s = session();
139
5/6
✓ Branch 3 → 4 taken 215 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 6 → 7 taken 215 times.
✗ Branch 6 → 8 not taken.
✓ Branch 9 → 10 taken 215 times.
✓ Branch 9 → 12 taken 1 time.
216 if (s && !s->capabilities().isEmpty())
140 215 return s->capabilities().toVariantMap();
141 2 return {{QStringLiteral("extensions"), m_discovery.extensions},
142
2/4
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 1 time.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
5 {QStringLiteral("implementation"), m_discovery.implementation}};
143 3 }
144
145 908 void QuickRulesBackend::setManagerOpen(bool open) {
146
2/2
✓ Branch 2 → 3 taken 807 times.
✓ Branch 2 → 4 taken 101 times.
908 if (m_managerOpen == open)
147 807 return;
148 101 m_managerOpen = open;
149 101 emit managerOpenChanged();
150 }
151
152 2 void QuickRulesBackend::setFilterText(const QString &text) {
153
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (m_filterText == text)
154 ✗ return;
155 2 m_filterText = text;
156 2 m_model.setFilter(m_filterText, m_folderFilter, m_problemsOnly, m_offOnly);
157 2 emit filterChanged();
158 }
159
160 103 void QuickRulesBackend::setFolderFilter(const QString &folder) {
161
2/2
✓ Branch 3 → 4 taken 97 times.
✓ Branch 3 → 5 taken 6 times.
103 if (m_folderFilter == folder)
162 97 return;
163 6 m_folderFilter = folder;
164 6 m_model.setFilter(m_filterText, m_folderFilter, m_problemsOnly, m_offOnly);
165 6 emit filterChanged();
166 }
167
168 2 void QuickRulesBackend::setProblemsOnly(bool on) {
169
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (m_problemsOnly == on)
170 ✗ return;
171 2 m_problemsOnly = on;
172 2 m_model.setFilter(m_filterText, m_folderFilter, m_problemsOnly, m_offOnly);
173 2 emit filterChanged();
174 }
175
176 2 void QuickRulesBackend::setOffOnly(bool on) {
177
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (m_offOnly == on)
178 ✗ return;
179 2 m_offOnly = on;
180 2 m_model.setFilter(m_filterText, m_folderFilter, m_problemsOnly, m_offOnly);
181 2 emit filterChanged();
182 }
183
184 17 void QuickRulesBackend::setTrustedCertificates(const QList<QSslCertificate> &certificates) {
185 17 m_deps.trustedCertificates = certificates;
186 17 m_discoverer.setCaCertificates(certificates);
187
1/2
✗ Branch 12 → 7 not taken.
✓ Branch 12 → 13 taken 17 times.
17 for (SieveSession *s : std::as_const(m_sessions))
188 ✗ s->client()->setCaCertificates(certificates);
189 17 }
190
191 // --- discovery --------------------------------------------------------------------
192
193 40 void QuickRulesBackend::foldersReady() {
194
1/1
✓ Branch 2 → 3 taken 40 times.
40 const QString id = accountId();
195
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 40 times.
40 if (id.isEmpty())
196 ✗ return;
197
2/2
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 10 taken 33 times.
40 if (id == m_discoveredAccount) {
198
1/1
✓ Branch 8 → 9 taken 7 times.
7 refreshModel(); // folders changed: health findings may change
199 7 return;
200 }
201 33 m_discoveredAccount = id;
202
1/1
✓ Branch 11 → 12 taken 33 times.
33 readDiscoveryCache();
203
3/3
✓ Branch 12 → 13 taken 33 times.
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 40 taken 29 times.
33 if (m_discovery.checkedAt.isValid()) {
204 // Show the node at once from the cached result; connect only later.
205
1/1
✓ Branch 29 → 30 taken 4 times.
4 setState(m_discovery.state == SieveDiscovery::State::Available
206
5/10
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 22 not taken.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 19 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 39 taken 2 times.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 60 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
16 ? (m_discovery.needsConfirmation ? QStringLiteral("confirm") : QStringLiteral("available"))
207
2/6
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 26 not taken.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 37 taken 2 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
4 : m_discovery.state == SieveDiscovery::State::Disabled ? QStringLiteral("disabled")
208
2/6
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 4 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 4 times.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
4 : QStringLiteral("unavailable"));
209 }
210
1/1
✓ Branch 40 → 41 taken 33 times.
33 m_discoveryDelay.start(); // G16: never during cold start
211 40 }
212
213 795 void QuickRulesBackend::accountChanged() {
214 795 m_discoveryDelay.stop();
215 795 m_discoveredAccount.clear();
216 795 m_discovery = {};
217 795 m_undo.clear();
218 795 m_conflictMine.clear();
219 795 m_conflictBlocks.clear();
220 // S106-317: the previous account's rule-server error (with its host) is
221 // not shown under the new account.
222 795 m_errorText.clear();
223 795 m_errorDetails.clear();
224 795 setManagerOpen(false);
225
1/1
✓ Branch 20 → 21 taken 795 times.
795 setState(QStringLiteral("unknown"));
226 795 refreshModel();
227 795 emit sessionChanged();
228 795 emit undoChanged();
229 795 emit conflictChanged();
230 2385 }
231
232 11 void QuickRulesBackend::configurationChanged(const QString &changedId) {
233
3/3
✓ Branch 2 → 3 taken 11 times.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 7 times.
11 if (changedId != accountId())
234 4 return;
235
4/4
✓ Branch 7 → 8 taken 7 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 12 → 13 taken 7 times.
✓ Branch 13 → 14 taken 7 times.
14 QFile::remove(QDir(storageDirectory(changedId)).filePath(QStringLiteral("discovery.json")));
236 7 m_discovery = {};
237 7 startDiscovery();
238 14 }
239
240 99 void QuickRulesBackend::readDiscoveryCache() {
241 99 m_discovery = {};
242
2/3
✓ Branch 10 → 11 taken 99 times.
✗ Branch 10 → 12 not taken.
✓ Branch 11 → 13 taken 99 times.
99 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
243
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 99 times.
99 if (!account)
244 ✗ return;
245
4/4
✓ Branch 17 → 18 taken 99 times.
✓ Branch 18 → 19 taken 99 times.
✓ Branch 22 → 23 taken 99 times.
✓ Branch 23 → 24 taken 99 times.
198 QFile file(QDir(storageDirectory(account->id)).filePath(QStringLiteral("discovery.json")));
246
3/3
✓ Branch 30 → 31 taken 99 times.
✓ Branch 31 → 32 taken 94 times.
✓ Branch 31 → 33 taken 5 times.
99 if (!file.open(QIODevice::ReadOnly))
247 94 return;
248
3/3
✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
✓ Branch 35 → 36 taken 5 times.
5 const QJsonObject o = QJsonDocument::fromJson(file.readAll()).object();
249
3/3
✓ Branch 41 → 42 taken 5 times.
✓ Branch 42 → 43 taken 5 times.
✓ Branch 43 → 44 taken 5 times.
5 const QDateTime checked = QDateTime::fromString(o.value(QStringLiteral("checkedAt")).toString(), Qt::ISODate);
250
5/16
✓ Branch 52 → 53 taken 5 times.
✓ Branch 53 → 54 taken 5 times.
✓ Branch 120 → 121 taken 5 times.
✗ Branch 120 → 122 not taken.
✓ Branch 122 → 123 taken 5 times.
✗ Branch 122 → 124 not taken.
✓ Branch 124 → 125 taken 5 times.
✗ Branch 124 → 126 not taken.
✗ Branch 378 → 379 not taken.
✗ Branch 378 → 380 not taken.
✗ Branch 382 → 383 not taken.
✗ Branch 382 → 384 not taken.
✗ Branch 386 → 387 not taken.
✗ Branch 386 → 388 not taken.
✗ Branch 390 → 391 not taken.
✗ Branch 390 → 392 not taken.
15 const bool sameSetup = o.value(QStringLiteral("imapHost")).toString() == account->imap.host &&
251
10/18
✓ Branch 59 → 60 taken 4 times.
✓ Branch 60 → 61 taken 4 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 61 → 70 taken 1 time.
✓ Branch 69 → 70 taken 3 times.
✗ Branch 69 → 87 not taken.
✓ Branch 114 → 115 taken 4 times.
✓ Branch 114 → 116 taken 1 time.
✓ Branch 116 → 117 taken 4 times.
✓ Branch 116 → 118 taken 1 time.
✓ Branch 118 → 119 taken 5 times.
✗ Branch 118 → 120 not taken.
✗ Branch 366 → 367 not taken.
✗ Branch 366 → 368 not taken.
✗ Branch 370 → 371 not taken.
✗ Branch 370 → 372 not taken.
✗ Branch 374 → 375 not taken.
✗ Branch 374 → 376 not taken.
12 (o.value(QStringLiteral("policyVersion")).toInt() < 2 ||
252
11/20
✓ Branch 66 → 67 taken 3 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 77 → 78 taken 4 times.
✗ Branch 77 → 87 not taken.
✓ Branch 106 → 107 taken 3 times.
✓ Branch 106 → 108 taken 2 times.
✓ Branch 108 → 109 taken 3 times.
✓ Branch 108 → 110 taken 2 times.
✓ Branch 110 → 111 taken 3 times.
✓ Branch 110 → 112 taken 2 times.
✓ Branch 112 → 113 taken 4 times.
✓ Branch 112 → 114 taken 1 time.
✗ Branch 350 → 351 not taken.
✗ Branch 350 → 352 not taken.
✗ Branch 354 → 355 not taken.
✗ Branch 354 → 356 not taken.
✗ Branch 358 → 359 not taken.
✗ Branch 358 → 360 not taken.
✗ Branch 362 → 363 not taken.
✗ Branch 362 → 364 not taken.
15 o.value(QStringLiteral("email")).toString() == account->email) &&
253
13/22
✓ Branch 55 → 56 taken 4 times.
✓ Branch 55 → 87 taken 1 time.
✓ Branch 74 → 75 taken 4 times.
✓ Branch 75 → 76 taken 4 times.
✓ Branch 85 → 86 taken 4 times.
✗ Branch 85 → 87 not taken.
✓ Branch 98 → 99 taken 4 times.
✓ Branch 98 → 100 taken 1 time.
✓ Branch 100 → 101 taken 4 times.
✓ Branch 100 → 102 taken 1 time.
✓ Branch 102 → 103 taken 4 times.
✓ Branch 102 → 104 taken 1 time.
✓ Branch 104 → 105 taken 3 times.
✓ Branch 104 → 106 taken 2 times.
✗ Branch 334 → 335 not taken.
✗ Branch 334 → 336 not taken.
✗ Branch 338 → 339 not taken.
✗ Branch 338 → 340 not taken.
✗ Branch 342 → 343 not taken.
✗ Branch 342 → 344 not taken.
✗ Branch 346 → 347 not taken.
✗ Branch 346 → 348 not taken.
27 o.value(QStringLiteral("mode")).toString() == account->sieve.mode &&
254
12/20
✓ Branch 82 → 83 taken 4 times.
✓ Branch 83 → 84 taken 4 times.
✓ Branch 88 → 89 taken 4 times.
✓ Branch 88 → 90 taken 1 time.
✓ Branch 90 → 91 taken 4 times.
✓ Branch 90 → 92 taken 1 time.
✓ Branch 92 → 93 taken 4 times.
✓ Branch 92 → 94 taken 1 time.
✓ Branch 94 → 95 taken 4 times.
✓ Branch 94 → 96 taken 1 time.
✓ Branch 96 → 97 taken 4 times.
✓ Branch 96 → 98 taken 1 time.
✗ Branch 318 → 319 not taken.
✗ Branch 318 → 320 not taken.
✗ Branch 322 → 323 not taken.
✗ Branch 322 → 324 not taken.
✗ Branch 326 → 327 not taken.
✗ Branch 326 → 328 not taken.
✗ Branch 330 → 331 not taken.
✗ Branch 330 → 332 not taken.
13 o.value(QStringLiteral("manualHost")).toString() == account->sieve.host;
255
4/5
✓ Branch 126 → 127 taken 5 times.
✓ Branch 127 → 128 taken 5 times.
✗ Branch 127 → 132 not taken.
✓ Branch 128 → 129 taken 4 times.
✓ Branch 128 → 132 taken 1 time.
9 if (!checked.isValid() || !sameSetup ||
256
7/10
✓ Branch 129 → 130 taken 4 times.
✓ Branch 130 → 131 taken 4 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 133 taken 4 times.
✓ Branch 134 → 135 taken 4 times.
✓ Branch 134 → 136 taken 1 time.
✓ Branch 136 → 137 taken 1 time.
✓ Branch 136 → 138 taken 4 times.
✗ Branch 394 → 395 not taken.
✗ Branch 394 → 396 not taken.
9 checked.secsTo(QDateTime::currentDateTimeUtc()) > kDiscoveryMaxAgeSeconds)
257 1 return;
258 4 m_discovery.checkedAt = checked;
259
2/2
✓ Branch 142 → 143 taken 4 times.
✓ Branch 143 → 144 taken 4 times.
4 const QString state = o.value(QStringLiteral("state")).toString();
260
1/2
✓ Branch 149 → 150 taken 4 times.
✗ Branch 149 → 151 not taken.
4 m_discovery.state = state == QLatin1String("available") ? SieveDiscovery::State::Available
261
0/2
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
4 : state == QLatin1String("disabled") ? SieveDiscovery::State::Disabled
262 : SieveDiscovery::State::NotAvailable;
263
2/2
✓ Branch 160 → 161 taken 4 times.
✓ Branch 161 → 162 taken 4 times.
4 m_discovery.server.host = o.value(QStringLiteral("host")).toString();
264
2/2
✓ Branch 170 → 171 taken 4 times.
✓ Branch 171 → 172 taken 4 times.
4 m_discovery.server.port = static_cast<quint16>(o.value(QStringLiteral("port")).toInt(4190));
265
2/2
✓ Branch 178 → 179 taken 4 times.
✓ Branch 182 → 183 taken 4 times.
8 m_discovery.server.security = o.value(QStringLiteral("security")).toString(QStringLiteral("starttls"));
266
2/2
✓ Branch 193 → 194 taken 4 times.
✓ Branch 194 → 195 taken 4 times.
4 m_discovery.server.source = o.value(QStringLiteral("source")).toString();
267
2/2
✓ Branch 203 → 204 taken 4 times.
✓ Branch 204 → 205 taken 4 times.
4 m_discovery.implementation = o.value(QStringLiteral("implementation")).toString();
268
7/7
✓ Branch 213 → 214 taken 4 times.
✓ Branch 214 → 215 taken 4 times.
✓ Branch 218 → 219 taken 4 times.
✓ Branch 219 → 220 taken 4 times.
✓ Branch 222 → 223 taken 6 times.
✓ Branch 229 → 221 taken 6 times.
✓ Branch 229 → 230 taken 4 times.
10 for (const QJsonValue &value : o.value(QStringLiteral("extensions")).toArray())
269
2/2
✓ Branch 223 → 224 taken 6 times.
✓ Branch 224 → 225 taken 6 times.
10 m_discovery.extensions.append(value.toString());
270 4 m_discovery.extensionCount = m_discovery.extensions.size();
271
6/14
✓ Branch 235 → 236 taken 4 times.
✓ Branch 236 → 237 taken 4 times.
✓ Branch 237 → 238 taken 3 times.
✓ Branch 237 → 245 taken 1 time.
✓ Branch 254 → 255 taken 4 times.
✗ Branch 254 → 256 not taken.
✓ Branch 256 → 257 taken 4 times.
✗ Branch 256 → 258 not taken.
✗ Branch 493 → 494 not taken.
✗ Branch 493 → 495 not taken.
✗ Branch 497 → 498 not taken.
✗ Branch 497 → 499 not taken.
✗ Branch 501 → 502 not taken.
✗ Branch 501 → 503 not taken.
15 m_discovery.confirmedByUser = o.value(QStringLiteral("policyVersion")).toInt() == 2 &&
272
11/18
✓ Branch 241 → 242 taken 3 times.
✓ Branch 242 → 243 taken 3 times.
✓ Branch 243 → 244 taken 1 time.
✓ Branch 243 → 245 taken 2 times.
✓ Branch 246 → 247 taken 3 times.
✓ Branch 246 → 248 taken 1 time.
✓ Branch 248 → 249 taken 3 times.
✓ Branch 248 → 250 taken 1 time.
✓ Branch 250 → 251 taken 3 times.
✓ Branch 250 → 252 taken 1 time.
✓ Branch 252 → 253 taken 4 times.
✗ Branch 252 → 254 not taken.
✗ Branch 481 → 482 not taken.
✗ Branch 481 → 483 not taken.
✗ Branch 485 → 486 not taken.
✗ Branch 485 → 487 not taken.
✗ Branch 489 → 490 not taken.
✗ Branch 489 → 491 not taken.
7 o.value(QStringLiteral("confirmedByUser")).toBool();
273
3/3
✓ Branch 259 → 260 taken 4 times.
✓ Branch 260 → 261 taken 3 times.
✓ Branch 260 → 263 taken 1 time.
7 m_discovery.needsConfirmation = SieveDiscovery::candidateNeedsConfirmation(*account, m_discovery.server) &&
274
2/2
✓ Branch 261 → 262 taken 2 times.
✓ Branch 261 → 263 taken 1 time.
3 !m_discovery.confirmedByUser;
275 394 }
276
277 89 void QuickRulesBackend::writeDiscoveryCache() {
278
2/3
✓ Branch 3 → 4 taken 89 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 89 times.
89 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
279
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 89 times.
89 if (!account)
280 ✗ return;
281
3/3
✓ Branch 10 → 11 taken 89 times.
✓ Branch 12 → 13 taken 89 times.
✓ Branch 13 → 14 taken 89 times.
89 QDir().mkpath(storageDirectory(account->id));
282 78 const QString state = m_discovery.state == SieveDiscovery::State::Available ? QStringLiteral("available")
283
4/4
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 25 taken 10 times.
✓ Branch 32 → 33 taken 78 times.
✓ Branch 32 → 34 taken 11 times.
100 : m_discovery.state == SieveDiscovery::State::Disabled ? QStringLiteral("disabled")
284
6/6
✓ Branch 17 → 18 taken 78 times.
✓ Branch 17 → 21 taken 11 times.
✓ Branch 28 → 29 taken 10 times.
✓ Branch 28 → 30 taken 79 times.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 32 taken 88 times.
178 : QStringLiteral("unavailable");
285
1/1
✓ Branch 37 → 38 taken 89 times.
178 QJsonObject o{{QStringLiteral("checkedAt"), m_discovery.checkedAt.toString(Qt::ISODate)},
286 178 {QStringLiteral("policyVersion"), 2},
287 178 {QStringLiteral("email"), account->email},
288 178 {QStringLiteral("confirmedByUser"), m_discovery.confirmedByUser},
289 178 {QStringLiteral("state"), state},
290 178 {QStringLiteral("host"), m_discovery.server.host},
291 178 {QStringLiteral("port"), m_discovery.server.port},
292 178 {QStringLiteral("security"), m_discovery.server.security},
293 178 {QStringLiteral("source"), m_discovery.server.source},
294 178 {QStringLiteral("implementation"), m_discovery.implementation},
295
1/1
✓ Branch 79 → 80 taken 89 times.
178 {QStringLiteral("extensions"), QJsonArray::fromStringList(m_discovery.extensions)},
296 178 {QStringLiteral("needsConfirmation"), m_discovery.needsConfirmation},
297 178 {QStringLiteral("imapHost"), account->imap.host},
298 178 {QStringLiteral("mode"), account->sieve.mode},
299
2/4
✓ Branch 102 → 103 taken 1335 times.
✓ Branch 102 → 104 taken 89 times.
✗ Branch 180 → 181 not taken.
✗ Branch 180 → 182 not taken.
1513 {QStringLiteral("manualHost"), account->sieve.host}};
300
4/4
✓ Branch 137 → 138 taken 89 times.
✓ Branch 138 → 139 taken 89 times.
✓ Branch 142 → 143 taken 89 times.
✓ Branch 143 → 144 taken 89 times.
178 QFile file(QDir(storageDirectory(account->id)).filePath(QStringLiteral("discovery.json")));
301
2/3
✓ Branch 150 → 151 taken 89 times.
✓ Branch 151 → 152 taken 89 times.
✗ Branch 151 → 160 not taken.
89 if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
302
1/1
✓ Branch 153 → 154 taken 89 times.
89 file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
303
3/3
✓ Branch 154 → 155 taken 89 times.
✓ Branch 155 → 156 taken 89 times.
✓ Branch 156 → 157 taken 89 times.
89 file.write(QJsonDocument(o).toJson());
304 }
305 1513 }
306
307 21 void QuickRulesBackend::rediscover() {
308 21 m_discovery = {};
309 21 startDiscovery();
310 42 }
311
312 94 void QuickRulesBackend::startDiscovery() {
313
1/1
✓ Branch 2 → 3 taken 94 times.
94 m_discoveryDelay.stop();
314
2/3
✓ Branch 4 → 5 taken 94 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 7 taken 94 times.
94 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
315
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 94 times.
94 if (!account)
316 ✗ return;
317
1/1
✓ Branch 13 → 14 taken 94 times.
94 setState(QStringLiteral("checking"));
318 94 const QString id = account->id;
319
4/5
✓ Branch 19 → 20 taken 94 times.
✓ Branch 21 → 22 taken 94 times.
✗ Branch 21 → 23 not taken.
✓ Branch 22 → 24 taken 94 times.
✓ Branch 25 → 26 taken 94 times.
94 m_discoverer.discover(*account, m_deps.imapCapabilities ? m_deps.imapCapabilities() : QStringList{},
320 188 [this, id](const SieveDiscovery::Result &result) {
321
2/3
✓ Branch 2 → 3 taken 88 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 88 times.
88 if (id != accountId())
322 ✗ return;
323 88 applyDiscovery(result);
324 });
325 94 }
326
327 88 void QuickRulesBackend::applyDiscovery(const SieveDiscovery::Result &result) {
328 88 m_discovery = result;
329 88 writeDiscoveryCache();
330
3/3
✓ Branch 4 → 5 taken 77 times.
✓ Branch 4 → 28 taken 1 time.
✓ Branch 4 → 35 taken 10 times.
88 switch (result.state) {
331 77 case SieveDiscovery::State::Available:
332
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 13 taken 76 times.
77 if (result.needsConfirmation) {
333
1/1
✓ Branch 9 → 10 taken 1 time.
1 setState(QStringLiteral("confirm"));
334 1 return;
335 }
336
1/1
✓ Branch 16 → 17 taken 76 times.
76 setState(QStringLiteral("available"));
337
5/6
✓ Branch 20 → 21 taken 76 times.
✗ Branch 20 → 24 not taken.
✓ Branch 22 → 23 taken 75 times.
✓ Branch 22 → 24 taken 1 time.
✓ Branch 25 → 26 taken 75 times.
✓ Branch 25 → 27 taken 1 time.
76 if (SieveSession *s = ensureSession(); s && s->status() == SieveSession::Status::Idle)
338 75 s->load();
339 76 return;
340 1 case SieveDiscovery::State::Disabled:
341
1/1
✓ Branch 31 → 32 taken 1 time.
1 setState(QStringLiteral("disabled"));
342 1 return;
343 10 default:
344
1/1
✓ Branch 38 → 39 taken 10 times.
10 setState(QStringLiteral("unavailable"));
345 }
346 }
347
348 2 void QuickRulesBackend::confirmServer() {
349
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
2 if (m_discovery.state != SieveDiscovery::State::Available)
350 1 return;
351 1 m_discovery.needsConfirmation = false;
352 1 m_discovery.confirmedByUser = true;
353 1 writeDiscoveryCache();
354
1/1
✓ Branch 8 → 9 taken 1 time.
1 setState(QStringLiteral("available"));
355
1/2
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 14 not taken.
1 if (SieveSession *s = ensureSession())
356 1 s->load();
357 }
358
359 // --- sessions ---------------------------------------------------------------------
360
361 193 void QuickRulesBackend::provideCredentials(SieveSession::PasswordCallback done) {
362
2/3
✓ Branch 3 → 4 taken 193 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 193 times.
193 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
363
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 12 taken 193 times.
193 if (!account) {
364 ✗ done(false, {});
365 ✗ return;
366 }
367
1/2
✓ Branch 14 → 15 taken 193 times.
✗ Branch 14 → 20 not taken.
193 if (!account->sieve.usesSeparateLogin()) {
368
1/1
✓ Branch 18 → 19 taken 193 times.
193 done(!account->imap.password.isEmpty(), account->imap.password);
369 193 return;
370 }
371 ✗ auto *store = new CredentialStore(this);
372 ✗ store->readPassword(QStringLiteral("sieve"), account->sieve.credentialId,
373 ✗ [store, done](bool ok, const QByteArray &password) {
374 ✗ store->deleteLater();
375 ✗ done(ok, password);
376 ✗ });
377 193 }
378
379 119 SieveSession *QuickRulesBackend::ensureSession() {
380
2/3
✓ Branch 3 → 4 taken 119 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 119 times.
119 const auto account = m_deps.account ? m_deps.account() : std::nullopt;
381
7/8
✓ Branch 7 → 8 taken 119 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 118 times.
✓ Branch 8 → 10 taken 1 time.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 116 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 14 taken 116 times.
119 if (!account || m_discovery.state != SieveDiscovery::State::Available || m_discovery.needsConfirmation)
382 3 return nullptr;
383
1/1
✓ Branch 14 → 15 taken 116 times.
116 SieveServerConfig server;
384 116 server.host = m_discovery.server.host;
385 116 server.port = m_discovery.server.port;
386 116 server.security = m_discovery.server.security;
387
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 22 taken 116 times.
116 server.username = account->sieve.usesSeparateLogin() ? account->sieve.username : account->imap.username;
388 116 SieveSession *s = m_sessions.value(account->id);
389
2/2
✓ Branch 27 → 28 taken 76 times.
✓ Branch 27 → 50 taken 40 times.
116 if (!s) {
390
1/1
✓ Branch 28 → 29 taken 76 times.
76 SieveSession::Config config;
391 76 config.accountId = account->id;
392 76 config.server = server;
393
1/1
✓ Branch 33 → 34 taken 76 times.
76 config.storageDirectory = storageDirectory(account->id);
394
3/6
✓ Branch 36 → 37 taken 76 times.
✓ Branch 37 → 38 taken 76 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 76 times.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
76 s = new SieveSession(config, this);
395
1/1
✓ Branch 41 → 42 taken 76 times.
76 s->client()->setCaCertificates(m_deps.trustedCertificates);
396
1/1
✓ Branch 4 → 5 taken 193 times.
269 s->setCredentialProvider([this](SieveSession::PasswordCallback done) { provideCredentials(std::move(done)); });
397
1/1
✓ Branch 46 → 47 taken 76 times.
76 m_sessions.insert(account->id, s);
398
1/1
✓ Branch 47 → 48 taken 76 times.
76 wireSession(s);
399 76 } else {
400 40 s->setServer(server);
401 }
402 116 return s;
403 119 }
404
405 76 void QuickRulesBackend::wireSession(SieveSession *s) {
406
1/1
✓ Branch 2 → 3 taken 76 times.
76 connect(s, &SieveSession::statusChanged, this, [this, s] {
407
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 372 times.
372 if (s != session())
408 ✗ return;
409
6/6
✓ Branch 6 → 7 taken 371 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 370 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 20 taken 370 times.
372 if (s->status() == SieveSession::Status::Error || s->status() == SieveSession::Status::Offline) {
410
1/1
✓ Branch 13 → 14 taken 2 times.
2 m_errorText = messageFor(s->lastError(), m_discovery.server.host);
411 2 m_errorDetails = s->statusMessage();
412 } else {
413 370 m_errorText.clear();
414 370 m_errorDetails.clear();
415 }
416 372 emit sessionChanged();
417 });
418
1/1
✓ Branch 4 → 5 taken 76 times.
76 connect(s, &SieveSession::documentChanged, this, [this, s] {
419
1/2
✓ Branch 3 → 4 taken 162 times.
✗ Branch 3 → 5 not taken.
162 if (s == session())
420 162 refreshModel();
421 162 });
422
1/1
✓ Branch 6 → 7 taken 76 times.
76 connect(s, &SieveSession::scriptsChanged, this, [this, s] {
423
1/2
✓ Branch 3 → 4 taken 90 times.
✗ Branch 3 → 5 not taken.
90 if (s == session())
424 90 emit scriptsChanged();
425 90 });
426 76 }
427
428 98 void QuickRulesBackend::openManager(const QString &folder) {
429 98 setFolderFilter(folder);
430 98 setManagerOpen(true);
431
2/2
✓ Branch 6 → 7 taken 65 times.
✓ Branch 6 → 13 taken 33 times.
98 if (m_state == QLatin1String("unknown")) {
432 65 readDiscoveryCache();
433
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 65 times.
65 if (m_discovery.checkedAt.isValid())
434 ✗ applyDiscovery(m_discovery);
435 else
436 65 startDiscovery();
437 65 return;
438 }
439
4/6
✓ Branch 14 → 15 taken 30 times.
✓ Branch 14 → 18 taken 3 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 30 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 33 times.
33 if (SieveSession *s = ensureSession(); s && s->status() == SieveSession::Status::Idle)
440 ✗ s->load();
441 }
442
443 12 void QuickRulesBackend::openPanel(const QString &panel) {
444 12 m_pendingPanel = panel;
445
1/1
✓ Branch 4 → 5 taken 12 times.
12 openManager();
446 12 emit panelRequested(panel);
447 12 }
448
449 9 void QuickRulesBackend::reload() {
450
1/2
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 5 not taken.
9 if (SieveSession *s = ensureSession())
451 9 s->load();
452 9 }
453
454 964 void QuickRulesBackend::refreshModel() {
455
1/1
✓ Branch 2 → 3 taken 964 times.
964 const SieveSession *s = session();
456
2/2
✓ Branch 3 → 4 taken 800 times.
✓ Branch 3 → 9 taken 164 times.
964 if (!s) {
457
1/1
✓ Branch 5 → 6 taken 800 times.
800 m_model.setRows({});
458
1/1
✓ Branch 7 → 8 taken 800 times.
800 emit documentChanged();
459 800 return;
460 }
461
1/1
✓ Branch 11 → 12 taken 164 times.
164 QList<QuickRuleModel::Row> rows = QuickRuleModel::rowsFor(s->document(), s->capabilities().sieveExtensions);
462
1/1
✓ Branch 13 → 14 taken 164 times.
164 const auto findings = healthFindings(s->document());
463
4/4
✓ Branch 14 → 15 taken 164 times.
✓ Branch 15 → 16 taken 164 times.
✓ Branch 25 → 17 taken 1167 times.
✓ Branch 25 → 26 taken 164 times.
1331 for (QuickRuleModel::Row &row : rows) {
464 1167 const auto finding = findings.value(row.blockIndex);
465 1167 row.health = finding.message;
466 1167 row.healthFix = finding.fix;
467 1167 row.healthAlternative = finding.alternative;
468 1167 }
469
1/1
✓ Branch 26 → 27 taken 164 times.
164 m_model.setRows(rows);
470
1/1
✓ Branch 27 → 28 taken 164 times.
164 emit documentChanged();
471 164 }
472
473 1216 SieveMailboxDirectory QuickRulesBackend::mailboxDirectory() const {
474 1216 SieveMailboxDirectory directory;
475
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1216 times.
1216 if (!m_deps.folders)
476 ✗ return directory;
477
5/5
✓ Branch 5 → 6 taken 1216 times.
✓ Branch 6 → 7 taken 1216 times.
✓ Branch 7 → 8 taken 1216 times.
✓ Branch 18 → 9 taken 6566 times.
✓ Branch 18 → 19 taken 1216 times.
7782 for (const FolderInfo &folder : m_deps.folders()) {
478
2/3
✓ Branch 11 → 12 taken 6566 times.
✗ Branch 11 → 13 not taken.
✓ Branch 12 → 14 taken 6566 times.
6566 const QString decoded = m_deps.decodeMailbox ? m_deps.decodeMailbox(folder.path) : folder.path;
479
1/1
✓ Branch 14 → 15 taken 6566 times.
6566 directory.addFolder(folder.path, decoded, folder.delimiter, folder.flags);
480 7782 }
481 1216 return directory;
482 ✗ }
483
484 // S95.3.9: missing target folders, reject combined with delivery, rules
485 // that never run, duplicates.
486 164 QHash<int, QuickRulesBackend::HealthFinding> QuickRulesBackend::healthFindings(const Sieve::Document &document) const {
487 164 QHash<int, HealthFinding> findings;
488
1/1
✓ Branch 2 → 3 taken 164 times.
164 const SieveMailboxDirectory directory = mailboxDirectory();
489 164 QList<int> earlier;
490 // S106-314: duplicate and "never runs" checks compare rule pairs on the
491 // UI thread; a script of thousands of rules stops them at a pair budget
492 // (the per-rule checks above still run for every rule).
493 164 qint64 pairBudget = MaxHealthPairs;
494
2/2
✓ Branch 130 → 4 taken 1332 times.
✓ Branch 130 → 131 taken 164 times.
1496 for (int i = 0; i < document.blocks.size(); ++i) {
495 1332 const Sieve::Block &block = document.blocks.at(i);
496
2/2
✓ Branch 5 → 6 taken 262 times.
✓ Branch 5 → 7 taken 1070 times.
1332 if (block.kind != Sieve::BlockKind::Rule)
497 262 continue;
498 1070 const Sieve::Rule &rule = block.rule;
499
7/7
✓ Branch 7 → 8 taken 1024 times.
✓ Branch 7 → 11 taken 46 times.
✓ Branch 8 → 9 taken 1024 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 1022 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 19 taken 1068 times.
1070 if (rule.enabled && Sieve::rejectConflicts(rule))
500 2 findings.insert(i, {tr("The server ignores this rule: reject can't be combined with moving or forwarding. "
501 "Mail stays in Inbox."),
502 tr("Remove reject"),
503 // Which half was meant is the user's call: keep the
504 // move (first fix) or keep the rejection.
505 tr("Only reject")});
506
2/2
✓ Branch 56 → 21 taken 1117 times.
✓ Branch 56 → 57 taken 811 times.
1928 for (const Sieve::Action &action : rule.actions) {
507
2/2
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 1115 times.
1117 if (findings.contains(i))
508 259 break;
509
2/2
✓ Branch 27 → 28 taken 1056 times.
✓ Branch 27 → 33 taken 1 time.
1057 if (action.kind != Sieve::Action::Kind::FileInto || !action.specialUse.isEmpty() ||
510
9/11
✓ Branch 25 → 26 taken 1057 times.
✓ Branch 25 → 33 taken 58 times.
✓ Branch 28 → 29 taken 1056 times.
✗ Branch 28 → 33 not taken.
✓ Branch 30 → 31 taken 1056 times.
✗ Branch 30 → 33 not taken.
✓ Branch 31 → 32 taken 1056 times.
✓ Branch 32 → 33 taken 799 times.
✓ Branch 32 → 34 taken 257 times.
✓ Branch 35 → 36 taken 858 times.
✓ Branch 35 → 37 taken 257 times.
2172 action.create || directory.isEmpty() || directory.exists(action.target))
511 858 continue;
512
1/1
✓ Branch 37 → 38 taken 257 times.
257 const QString suggestion = directory.suggestionFor(action.target);
513 257 findings.insert(i, {tr("Folder „%1“ does not exist.").arg(action.target),
514
1/3
✓ Branch 44 → 45 taken 114 times.
✗ Branch 150 → 151 not taken.
✗ Branch 150 → 152 not taken.
228 suggestion.isEmpty() ? QString() : tr("Use „%1“").arg(suggestion), QString()});
515 257 break;
516 257 }
517
8/8
✓ Branch 58 → 59 taken 811 times.
✓ Branch 58 → 62 taken 259 times.
✓ Branch 59 → 60 taken 767 times.
✓ Branch 59 → 62 taken 44 times.
✓ Branch 60 → 61 taken 684 times.
✓ Branch 60 → 62 taken 83 times.
✓ Branch 63 → 64 taken 684 times.
✓ Branch 63 → 126 taken 386 times.
1070 if (!findings.contains(i) && rule.enabled && pairBudget > 0) {
518 684 pairBudget -= earlier.size();
519
2/2
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 81 taken 683 times.
684 if (pairBudget <= 0)
520
8/8
✓ Branch 66 → 67 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 72 → 73 taken 1 time.
✓ Branch 73 → 74 taken 1 time.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 79 → 68 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
2 qCInfo(lcRulesBackend) << "Rule health: pair checks stopped at rule" << i << "of" << document.blocks.size();
521
4/4
✓ Branch 81 → 82 taken 684 times.
✓ Branch 82 → 83 taken 684 times.
✓ Branch 124 → 84 taken 1092 times.
✓ Branch 124 → 125 taken 296 times.
1388 for (const int j : earlier) {
522 1092 const Sieve::Block &previous = document.blocks.at(j);
523
1/3
✓ Branch 86 → 87 taken 1092 times.
✗ Branch 86 → 88 not taken.
✗ Branch 88 → 89 not taken.
1092 const QString name = previous.named ? previous.name : RuleSentence::generatedName(previous.rule);
524
7/8
✓ Branch 89 → 90 taken 1092 times.
✓ Branch 90 → 91 taken 317 times.
✓ Branch 90 → 94 taken 775 times.
✓ Branch 91 → 92 taken 317 times.
✓ Branch 92 → 93 taken 317 times.
✗ Branch 92 → 94 not taken.
✓ Branch 95 → 96 taken 317 times.
✓ Branch 95 → 104 taken 775 times.
1092 if (previous.rule.conditions == rule.conditions && previous.rule.actions == rule.actions) {
525 317 findings.insert(i, {tr("Same as rule „%1“.").arg(name), tr("Delete this rule"), QString()});
526 317 break;
527 }
528
3/3
✓ Branch 104 → 105 taken 775 times.
✓ Branch 105 → 106 taken 71 times.
✓ Branch 105 → 117 taken 704 times.
775 if (RuleMatcher::shadows(previous.rule, rule)) {
529 71 findings.insert(i, {tr("Never runs: rule „%1“ above catches the same messages and stops.").arg(name),
530 ✗ tr("Move above „%1“").arg(name), QString()});
531 71 break;
532 }
533 1092 }
534 }
535
2/2
✓ Branch 126 → 127 taken 1024 times.
✓ Branch 126 → 128 taken 46 times.
1070 if (rule.enabled)
536
1/1
✓ Branch 127 → 128 taken 1024 times.
1024 earlier.append(i);
537 }
538 164 return findings;
539 2400 }
540
541 778 QStringList QuickRulesBackend::folderChoices() const {
542
2/2
✓ Branch 2 → 3 taken 778 times.
✓ Branch 3 → 4 taken 778 times.
778 QStringList names = mailboxDirectory().sieveNames();
543
1/1
✓ Branch 5 → 6 taken 778 times.
778 names.sort(Qt::CaseInsensitive);
544 778 return names;
545 ✗ }
546
547 1 void QuickRulesBackend::openManagerForFolder(const QString &imapPath) {
548
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 const QString name = mailboxDirectory().sieveName(imapPath);
549
2/3
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
1 openManager(name.isEmpty() ? imapPath : name);
550 1 }
551
552 8 void QuickRulesBackend::attachHighlighter(QObject *quickTextDocument, const QVariantMap &palette) {
553
1/1
✓ Branch 2 → 3 taken 8 times.
8 auto *document = qobject_cast<QQuickTextDocument *>(quickTextDocument);
554
4/7
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 8 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 8 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 8 times.
8 if (!document || !document->textDocument())
555 ✗ return;
556
2/8
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 16 taken 8 times.
✗ Branch 13 → 14 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
✓ Branch 18 → 19 taken 8 times.
✗ Branch 18 → 26 not taken.
8 if (!m_highlighter || m_highlighter->document() != document->textDocument())
557
5/8
✓ Branch 19 → 20 taken 8 times.
✓ Branch 20 → 21 taken 8 times.
✓ Branch 21 → 22 taken 8 times.
✓ Branch 22 → 23 taken 8 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 8 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
8 m_highlighter = new SieveSyntaxHighlighter(document->textDocument());
558 8 SieveSyntaxHighlighter::Colors colors;
559
2/2
✓ Branch 30 → 31 taken 8 times.
✓ Branch 31 → 32 taken 8 times.
16 colors.keyword = QColor(palette.value(QStringLiteral("sieveKeyword")).toString());
560
2/2
✓ Branch 42 → 43 taken 8 times.
✓ Branch 43 → 44 taken 8 times.
16 colors.string = QColor(palette.value(QStringLiteral("sieveString")).toString());
561
2/2
✓ Branch 54 → 55 taken 8 times.
✓ Branch 55 → 56 taken 8 times.
16 colors.comment = QColor(palette.value(QStringLiteral("sieveComment")).toString());
562
2/2
✓ Branch 66 → 67 taken 8 times.
✓ Branch 67 → 68 taken 8 times.
16 colors.tag = QColor(palette.value(QStringLiteral("sieveTag")).toString());
563
2/2
✓ Branch 78 → 79 taken 8 times.
✓ Branch 79 → 80 taken 8 times.
16 colors.error = QColor(palette.value(QStringLiteral("danger")).toString());
564
1/1
✓ Branch 87 → 88 taken 8 times.
8 m_highlighter->setColors(colors);
565 }
566
567 2 void QuickRulesBackend::setHighlightedErrorLine(int line) {
568
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
2 if (m_highlighter)
569 2 m_highlighter->setErrorLine(line);
570 2 }
571
572 // --- scripts, history -------------------------------------------------------------
573
574 2 QVariantList QuickRulesBackend::scripts() const {
575 2 QVariantList result;
576
2/3
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 34 not taken.
2 if (const SieveSession *s = session())
577
4/4
✓ Branch 5 → 6 taken 2 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 31 → 8 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
4 for (const ManageSieve::ScriptEntry &entry : s->scripts())
578
3/5
✓ Branch 19 → 20 taken 2 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 23 → 25 taken 2 times.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
12 result.append(QVariantMap{{QStringLiteral("name"), entry.name}, {QStringLiteral("active"), entry.active}});
579 2 return result;
580 6 }
581
582 41 QString QuickRulesBackend::activeScript() const {
583 41 const SieveSession *s = session();
584
1/2
✓ Branch 3 → 4 taken 41 times.
✗ Branch 3 → 5 not taken.
41 return s ? s->activeScript() : QString();
585 }
586
587 1 QStringList QuickRulesBackend::inactiveScripts() const {
588 1 QStringList result;
589
2/3
✓ Branch 2 → 3 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 16 taken 1 time.
1 if (const SieveSession *s = session())
590 ✗ for (const ManageSieve::ScriptEntry &entry : s->scripts())
591 ✗ if (!entry.active)
592 ✗ result.append(entry.name);
593 1 return result;
594 ✗ }
595
596 7 QVariantList QuickRulesBackend::historyEntries() {
597 7 QVariantList result;
598
1/1
✓ Branch 2 → 3 taken 7 times.
7 SieveSession *s = session();
599
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 6 times.
7 if (!s)
600 1 return result;
601
1/1
✓ Branch 5 → 6 taken 6 times.
6 const QLocale locale;
602
5/5
✓ Branch 7 → 8 taken 6 times.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
✓ Branch 67 → 11 taken 25 times.
✓ Branch 67 → 68 taken 6 times.
31 for (const SieveHistory::Entry &entry : s->history().entries()) {
603
2/2
✓ Branch 13 → 14 taken 25 times.
✓ Branch 14 → 15 taken 25 times.
25 const Sieve::Document document = Sieve::decode(s->history().read(entry.id));
604
1/1
✓ Branch 44 → 45 taken 25 times.
25 result.append(QVariantMap{
605 50 {QStringLiteral("id"), entry.id},
606
2/2
✓ Branch 23 → 24 taken 25 times.
✓ Branch 24 → 25 taken 25 times.
50 {QStringLiteral("when"), locale.toString(entry.savedAt.toLocalTime(), QLocale::ShortFormat)},
607 50 {QStringLiteral("script"), entry.scriptName},
608
1/1
✓ Branch 34 → 35 taken 25 times.
75 {QStringLiteral("size"), locale.formattedDataSize(entry.size)},
609
4/6
✓ Branch 39 → 40 taken 25 times.
✓ Branch 40 → 41 taken 25 times.
✓ Branch 48 → 49 taken 125 times.
✓ Branch 48 → 50 taken 25 times.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
175 {QStringLiteral("summary"), tr("Rules: %n", nullptr, document.ruleCount())}});
610 31 }
611 6 return result;
612 156 }
613
614 // --- plain words ----------------------------------------------------------------------
615
616 20 QString QuickRulesBackend::messageFor(ManageSieveClient::ErrorKind error, const QString &host) {
617 using E = ManageSieveClient::ErrorKind;
618
11/13
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 10 taken 1 time.
✓ Branch 2 → 11 taken 1 time.
✓ Branch 2 → 12 taken 3 times.
✓ Branch 2 → 13 taken 3 times.
✓ Branch 2 → 14 taken 1 time.
✓ Branch 2 → 15 taken 5 times.
✓ Branch 2 → 16 taken 1 time.
✓ Branch 2 → 17 taken 1 time.
✓ Branch 2 → 18 taken 1 time.
✗ Branch 2 → 19 not taken.
20 switch (error) {
619 ✗ case E::None: return {};
620 2 case E::Network:
621 2 case E::Timeout: return tr("The rule server could not be reached.");
622
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
2 case E::Certificate: return tr("The rule server's certificate is not valid for %1.").arg(host);
623 1 case E::NoStartTls:
624 1 return tr("The rule server offers no encrypted connection. MailJD does not send the password unencrypted.");
625 1 case E::NoMechanism: return tr("The rule server offers no supported way to log in.");
626 3 case E::NoCredentials: return tr("No password is available for the rule server.");
627 3 case E::AuthFailed: return tr("The rule server did not accept the user name or password.");
628 1 case E::Referral: return tr("The rule server refers to another server. Enter that server manually.");
629 5 case E::Bye: return tr("The rule server closed the connection. Try again later.");
630 1 case E::Server: return tr("The rule server refused the request.");
631 1 case E::Protocol: return tr("The rule server sent an answer MailJD does not understand.");
632 1 case E::Cancelled: return tr("The request was cancelled.");
633 }
634 ✗ return {};
635 }
636
637 // --- settings: connection test -----------------------------------------------------
638
639 5 QString QuickRulesBackend::testConnection(const QVariantMap &fields) {
640
1/1
✓ Branch 2 → 3 taken 5 times.
5 const QString requestId = nextRequestId();
641
2/2
✓ Branch 7 → 8 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
10 const QString id = fields.value(QStringLiteral("id")).toString();
642
2/3
✓ Branch 14 → 15 taken 5 times.
✗ Branch 14 → 16 not taken.
✓ Branch 15 → 17 taken 5 times.
5 std::optional<AccountConfig> stored = m_deps.configuration ? m_deps.configuration(id) : std::nullopt;
643
2/8
✓ Branch 17 → 18 taken 5 times.
✓ Branch 18 → 19 taken 5 times.
✗ Branch 249 → 250 not taken.
✗ Branch 249 → 251 not taken.
✗ Branch 252 → 253 not taken.
✗ Branch 252 → 254 not taken.
✗ Branch 255 → 256 not taken.
✗ Branch 255 → 257 not taken.
25 AccountConfig config = stored.value_or(AccountConfig{});
644 5 config.id = id;
645
2/2
✓ Branch 42 → 43 taken 5 times.
✓ Branch 43 → 44 taken 5 times.
10 config.email = fields.value(QStringLiteral("email"), config.email).toString();
646
3/3
✓ Branch 54 → 55 taken 5 times.
✓ Branch 55 → 56 taken 5 times.
✓ Branch 56 → 57 taken 5 times.
10 config.imap.host = fields.value(QStringLiteral("imapHost"), config.imap.host).toString().trimmed();
647
3/3
✓ Branch 68 → 69 taken 5 times.
✓ Branch 69 → 70 taken 5 times.
✓ Branch 70 → 71 taken 5 times.
10 config.imap.username = fields.value(QStringLiteral("imapUsername"), config.imap.username).toString().trimmed();
648
3/3
✓ Branch 82 → 83 taken 5 times.
✓ Branch 83 → 84 taken 5 times.
✓ Branch 84 → 85 taken 5 times.
10 const QByteArray typedImap = fields.value(QStringLiteral("imapPassword")).toString().toUtf8();
649
2/2
✓ Branch 91 → 92 taken 1 time.
✓ Branch 91 → 93 taken 4 times.
5 if (!typedImap.isEmpty())
650 1 config.imap.password = typedImap;
651
2/2
✓ Branch 97 → 98 taken 5 times.
✓ Branch 98 → 99 taken 5 times.
10 config.sieve.mode = fields.value(QStringLiteral("sieveMode"), config.sieve.mode).toString();
652
3/3
✓ Branch 109 → 110 taken 5 times.
✓ Branch 110 → 111 taken 5 times.
✓ Branch 111 → 112 taken 5 times.
10 config.sieve.host = fields.value(QStringLiteral("sieveHost"), config.sieve.host).toString().trimmed();
653
2/2
✓ Branch 123 → 124 taken 5 times.
✓ Branch 124 → 125 taken 5 times.
10 config.sieve.port = static_cast<quint16>(fields.value(QStringLiteral("sievePort"), config.sieve.port).toInt());
654
2/2
✓ Branch 133 → 134 taken 5 times.
✓ Branch 134 → 135 taken 5 times.
10 config.sieve.security = fields.value(QStringLiteral("sieveSecurity"), config.sieve.security).toString();
655
2/2
✓ Branch 145 → 146 taken 5 times.
✓ Branch 146 → 147 taken 5 times.
10 config.sieve.login = fields.value(QStringLiteral("sieveLogin"), config.sieve.login).toString();
656
2/2
✓ Branch 157 → 158 taken 5 times.
✓ Branch 158 → 159 taken 5 times.
10 config.sieve.username = fields.value(QStringLiteral("sieveUsername"), config.sieve.username).toString();
657
3/3
✓ Branch 169 → 170 taken 5 times.
✓ Branch 170 → 171 taken 5 times.
✓ Branch 171 → 172 taken 5 times.
10 const QByteArray typedSieve = fields.value(QStringLiteral("sievePassword")).toString().toUtf8();
658 // S106-315: the stored configuration carries no passwords. "Same as
659 // incoming" needs the IMAP password: the active account's own, else the
660 // keyring's — the test used to sign in with an empty one and fail.
661
5/6
✓ Branch 178 → 179 taken 5 times.
✗ Branch 178 → 182 not taken.
✓ Branch 180 → 181 taken 1 time.
✓ Branch 180 → 182 taken 4 times.
✓ Branch 183 → 184 taken 1 time.
✓ Branch 183 → 200 taken 4 times.
5 if (!config.sieve.usesSeparateLogin() && config.imap.password.isEmpty()) {
662
6/12
✓ Branch 184 → 185 taken 1 time.
✓ Branch 186 → 187 taken 1 time.
✗ Branch 186 → 191 not taken.
✓ Branch 188 → 189 taken 1 time.
✗ Branch 188 → 191 not taken.
✓ Branch 189 → 190 taken 1 time.
✓ Branch 192 → 193 taken 1 time.
✗ Branch 192 → 194 not taken.
✓ Branch 195 → 196 taken 1 time.
✗ Branch 195 → 198 not taken.
✗ Branch 414 → 415 not taken.
✗ Branch 414 → 416 not taken.
2 if (const auto active = id == accountId() && m_deps.account ? m_deps.account() : std::nullopt)
663 1 config.imap.password = active->imap.password;
664 }
665
4/8
✓ Branch 201 → 202 taken 5 times.
✗ Branch 201 → 207 not taken.
✓ Branch 203 → 204 taken 5 times.
✗ Branch 203 → 207 not taken.
✗ Branch 205 → 206 not taken.
✓ Branch 205 → 207 taken 5 times.
✗ Branch 208 → 209 not taken.
✓ Branch 208 → 228 taken 5 times.
5 if (stored && !config.sieve.usesSeparateLogin() && config.imap.password.isEmpty()) {
666 ✗ auto *store = new CredentialStore(this);
667 ✗ store->readPassword(QStringLiteral("imap"), AccountConfigLoader::imapCredentialName(config),
668 ✗ [this, store, config, typedSieve, requestId](bool ok, const QByteArray &password) mutable {
669 ✗ store->deleteLater();
670 ✗ if (ok)
671 ✗ config.imap.password = password;
672 ✗ qCInfo(lcRulesBackend) << "Connection test: IMAP password from the keyring:" << ok;
673 ✗ runConnectionTest(config, typedSieve, requestId);
674 ✗ });
675 ✗ return requestId;
676 }
677
1/1
✓ Branch 228 → 229 taken 5 times.
5 runConnectionTest(config, typedSieve, requestId);
678 5 return requestId;
679 5 }
680
681 5 void QuickRulesBackend::runConnectionTest(const AccountConfig &config, const QByteArray &typedSieve,
682 const QString &requestId) {
683 5 const QString id = config.id;
684
3/6
✓ Branch 3 → 4 taken 5 times.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 5 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
5 auto *discovery = new SieveDiscovery(this);
685 5 discovery->setCaCertificates(m_deps.trustedCertificates);
686
3/8
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 15 not taken.
✓ Branch 12 → 13 taken 5 times.
✗ Branch 12 → 15 not taken.
✓ Branch 16 → 17 taken 5 times.
✗ Branch 16 → 18 not taken.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
10 const QStringList capabilities = id == accountId() && m_deps.imapCapabilities ? m_deps.imapCapabilities()
687
2/2
✓ Branch 8 → 9 taken 5 times.
✓ Branch 13 → 14 taken 5 times.
10 : QStringList{};
688
2/6
✓ Branch 21 → 22 taken 5 times.
✓ Branch 22 → 23 taken 5 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
5 discovery->discover(config, capabilities, [this, discovery, config, typedSieve, requestId](
689 const SieveDiscovery::Result &found) {
690
1/1
✓ Branch 2 → 3 taken 5 times.
5 discovery->deleteLater();
691
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 37 taken 5 times.
5 if (found.state != SieveDiscovery::State::Available) {
692 ✗ emit connectionTested(requestId, {{QStringLiteral("ok"), false},
693 ✗ {QStringLiteral("message"),
694 ✗ found.state == SieveDiscovery::State::Disabled
695 ✗ ? tr("Server rules are turned off for this account.")
696 : tr("No rule server was found for this account.")},
697 ✗ {QStringLiteral("details"), found.tried.join(QLatin1Char('\n'))}});
698 1 return;
699 }
700 // S106-087: the SRV/IMAPSIEVE trust model holds here too — a server
701 // outside the IMAP host and mail domain gets no password before the user
702 // confirmed it once in the rules area (SieveDiscovery.h). The test used to
703 // sign in first and report needsConfirmation afterwards.
704
7/7
✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 41 taken 3 times.
✓ Branch 38 → 39 taken 2 times.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 41 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 106 taken 4 times.
5 if (found.needsConfirmation && !userConfirmedServer(config.id, found.server)) {
705
2/2
✓ Branch 47 → 48 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
3 const QString where = QStringLiteral("%1:%2").arg(found.server.host).arg(found.server.port);
706
5/5
✓ Branch 53 → 54 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 62 → 55 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
2 qCInfo(lcRulesBackend) << "Connection test stopped before sign-in: unconfirmed rule server";
707
3/5
✓ Branch 87 → 88 taken 1 time.
✓ Branch 90 → 91 taken 5 times.
✓ Branch 90 → 92 taken 1 time.
✗ Branch 193 → 194 not taken.
✗ Branch 193 → 195 not taken.
8 emit connectionTested(requestId, {{QStringLiteral("ok"), false},
708 2 {QStringLiteral("needsConfirmation"), true},
709 2 {QStringLiteral("server"), where},
710 2 {QStringLiteral("implementation"), found.implementation},
711 2 {QStringLiteral("message"),
712
1/1
✓ Branch 82 → 83 taken 1 time.
1 tr("The rule server %1 is outside your mail domain. Confirm it under "
713
1/1
✓ Branch 84 → 85 taken 1 time.
2 "Server rules before MailJD sends your password there.").arg(where)}});
714 1 return;
715 1 }
716
3/6
✓ Branch 106 → 107 taken 4 times.
✓ Branch 107 → 108 taken 4 times.
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 110 taken 4 times.
✗ Branch 245 → 246 not taken.
✗ Branch 245 → 247 not taken.
4 auto *client = new ManageSieveClient(this);
717
1/1
✓ Branch 110 → 111 taken 4 times.
4 client->setCaCertificates(m_deps.trustedCertificates);
718 8 SieveServerConfig server{found.server.host, found.server.port, found.server.security,
719 4 config.sieve.usesSeparateLogin() ? config.sieve.username : config.imap.username,
720
2/4
✗ Branch 114 → 115 not taken.
✓ Branch 114 → 116 taken 4 times.
✗ Branch 119 → 120 not taken.
✓ Branch 119 → 121 taken 4 times.
4 config.sieve.usesSeparateLogin() ? typedSieve : config.imap.password};
721
1/1
✓ Branch 123 → 124 taken 4 times.
4 client->setServer(server);
722
2/4
✓ Branch 126 → 127 taken 4 times.
✓ Branch 127 → 128 taken 4 times.
✗ Branch 252 → 253 not taken.
✗ Branch 252 → 254 not taken.
4 client->listScripts([this, client, found, requestId](const ManageSieveClient::Result &result) {
723
1/1
✓ Branch 2 → 3 taken 4 times.
4 client->deleteLater();
724 4 QString active;
725
2/2
✓ Branch 10 → 5 taken 3 times.
✓ Branch 10 → 11 taken 4 times.
7 for (const ManageSieve::ScriptEntry &entry : result.scripts)
726
1/2
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 8 not taken.
3 if (entry.active)
727 3 active = entry.name;
728
2/2
✓ Branch 15 → 16 taken 4 times.
✓ Branch 17 → 18 taken 4 times.
12 const QString where = QStringLiteral("%1:%2").arg(found.server.host).arg(found.server.port);
729 8 QVariantMap map{{QStringLiteral("ok"), result.ok},
730 8 {QStringLiteral("server"), where},
731 8 {QStringLiteral("implementation"), found.implementation},
732 8 {QStringLiteral("activeScript"), active},
733 8 {QStringLiteral("needsConfirmation"), found.needsConfirmation},
734
2/4
✓ Branch 47 → 48 taken 24 times.
✓ Branch 47 → 49 taken 4 times.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
32 {QStringLiteral("details"), result.message}};
735
1/1
✓ Branch 73 → 74 taken 4 times.
8 map.insert(QStringLiteral("message"),
736
3/8
✓ Branch 61 → 62 taken 3 times.
✓ Branch 61 → 68 taken 1 time.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 66 taken 3 times.
✗ Branch 64 → 65 not taken.
✗ Branch 65 → 69 not taken.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
8 result.ok ? (active.isEmpty() ? tr("Connected to %1 · %2 · no active script").arg(where, found.implementation)
737
4/7
✓ Branch 66 → 67 taken 3 times.
✓ Branch 78 → 79 taken 3 times.
✓ Branch 78 → 80 taken 1 time.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 82 taken 4 times.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
7 : tr("Connected to %1 · %2 · active script „%3“")
738
1/1
✓ Branch 67 → 69 taken 3 times.
3 .arg(where, found.implementation, active))
739
1/1
✓ Branch 68 → 69 taken 1 time.
1 : messageFor(result.error, found.server.host));
740
1/1
✓ Branch 82 → 83 taken 4 times.
4 emit connectionTested(requestId, map);
741
1/5
✓ Branch 45 → 46 taken 4 times.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 142 not taken.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
32 });
742
1/10
✗ Branch 22 → 23 not taken.
✓ Branch 86 → 87 taken 1 time.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 171 not taken.
✗ Branch 169 → 170 not taken.
✗ Branch 169 → 171 not taken.
✗ Branch 235 → 236 not taken.
✗ Branch 235 → 239 not taken.
✗ Branch 237 → 238 not taken.
✗ Branch 237 → 239 not taken.
12 });
743 5 }
744
745 2 bool QuickRulesBackend::userConfirmedServer(const QString &id, const SieveDiscovery::Candidate &server) const {
746
5/8
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 10 not taken.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 1 time.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
5 if (id == accountId() && m_discovery.confirmedByUser &&
747
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 10 not taken.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 13 not taken.
5 m_discovery.server.host == server.host && m_discovery.server.port == server.port)
748 1 return true;
749 // Another account: its stored discovery decision (policy version 2 only).
750
4/4
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
2 QFile file(QDir(storageDirectory(id)).filePath(QStringLiteral("discovery.json")));
751
4/7
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 32 not taken.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 33 not taken.
✓ Branch 34 → 35 taken 1 time.
✗ Branch 34 → 36 not taken.
1 if (id.isEmpty() || !file.open(QIODevice::ReadOnly))
752 1 return false;
753 ✗ const QJsonObject o = QJsonDocument::fromJson(file.readAll()).object();
754 ✗ return o.value(QStringLiteral("policyVersion")).toInt() == 2 &&
755 ✗ o.value(QStringLiteral("confirmedByUser")).toBool() &&
756 ✗ o.value(QStringLiteral("host")).toString() == server.host &&
757 ✗ o.value(QStringLiteral("port")).toInt() == server.port;
758 1 }
759
760 139 QString QuickRulesBackend::nextRequestId() {
761
1/1
✓ Branch 6 → 7 taken 139 times.
278 return QStringLiteral("rules-%1").arg(++m_requestCounter);
762 }
763
764 // --- rule markers of the mail list (F95-14) ---------------------------------------
765
766 25407 bool QuickRulesBackend::matchesHeader(const MailHeader &header) {
767
1/1
✓ Branch 2 → 3 taken 25407 times.
25407 const auto key = qMakePair(header.folderId, header.uid);
768
2/2
✓ Branch 6 → 7 taken 20705 times.
✓ Branch 6 → 9 taken 4702 times.
25407 if (const auto cached = m_matchCache.constFind(key); cached != m_matchCache.cend())
769 20705 return cached.value();
770
3/3
✓ Branch 10 → 12 taken 409 times.
✓ Branch 10 → 14 taken 4293 times.
✓ Branch 12 → 13 taken 409 times.
4702 const SieveSession *s = available() ? session() : nullptr;
771 // matchingBlocks only reports enabled rules that definitely match.
772
12/18
✓ Branch 15 → 16 taken 409 times.
✓ Branch 15 → 23 taken 4293 times.
✓ Branch 17 → 18 taken 409 times.
✓ Branch 19 → 20 taken 409 times.
✓ Branch 21 → 22 taken 110 times.
✓ Branch 21 → 23 taken 299 times.
✓ Branch 24 → 25 taken 409 times.
✓ Branch 24 → 26 taken 4293 times.
✓ Branch 26 → 27 taken 409 times.
✓ Branch 26 → 28 taken 4293 times.
✓ Branch 28 → 29 taken 409 times.
✓ Branch 28 → 30 taken 4293 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
4702 const bool matched = s && !RuleMatcher::matchingBlocks(s->document(), messageFor(header, {})).isEmpty();
773 // S106-318: one entry per shown row; a long session over many folders
774 // starts the cache again instead of growing it without end.
775
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 4702 times.
4702 if (m_matchCache.size() >= MaxMatchCache)
776 ✗ m_matchCache.clear();
777
1/1
✓ Branch 33 → 34 taken 4702 times.
4702 m_matchCache.insert(key, matched);
778 4702 return matched;
779 }
780
781 1155 void QuickRulesBackend::invalidateMatches() {
782 // Without a rule server every row stays "no rule"; state changes of the
783 // discovery (checking → unavailable at start-up) must not refresh every
784 // list row.
785
4/4
✓ Branch 3 → 4 taken 242 times.
✓ Branch 3 → 7 taken 913 times.
✓ Branch 5 → 6 taken 163 times.
✓ Branch 5 → 7 taken 79 times.
1155 const bool possible = available() && session() != nullptr;
786
4/4
✓ Branch 8 → 9 taken 992 times.
✓ Branch 8 → 11 taken 163 times.
✓ Branch 9 → 10 taken 991 times.
✓ Branch 9 → 11 taken 1 time.
1155 if (!possible && !m_matchesPossible)
787 991 return;
788 164 m_matchesPossible = possible;
789 164 m_matchCache.clear();
790 164 ++m_matchRevision;
791 164 emit matchesChanged();
792 }
793