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: 98.6% 137 / 0 / 139
Functions: 100.0% 16 / 0 / 16
Branches: 75.2% 164 / 0 / 218

service/SieveDiscovery.cpp
Line Branch Exec Source
1 #include "SieveDiscovery.h"
2
3 #include "service/ManageSieveClient.h"
4 #include "util/HostMatch.h"
5
6 #include <QDnsLookup>
7 #include <QLoggingCategory>
8 #include <QPointer>
9 #include <QTimer>
10 #include <QUrl>
11
12 #include <algorithm>
13 #include <memory>
14
15
4/7
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 8 taken 102 times.
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 8 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
110 Q_LOGGING_CATEGORY(lcSieveDiscovery, "mailjd.sieve.discovery")
16
17 struct SieveDiscovery::Run {
18 AccountConfig account;
19 QList<Candidate> candidates;
20 int index = 0;
21 Result result;
22 Callback callback;
23 };
24
25 namespace {
26
27 17 void defaultSrvLookup(QObject *owner, const QString &name, SieveDiscovery::SrvCallback callback) {
28
2/5
✓ Branch 3 → 4 taken 17 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 17 times.
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
17 auto *lookup = new QDnsLookup(QDnsLookup::SRV, name, owner);
29
2/2
✓ Branch 6 → 7 taken 17 times.
✓ Branch 7 → 8 taken 17 times.
17 QObject::connect(lookup, &QDnsLookup::finished, owner, [lookup, callback] {
30 14 QList<SieveDiscovery::SrvRecord> records;
31
3/3
✓ Branch 2 → 3 taken 14 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 20 taken 11 times.
14 if (lookup->error() == QDnsLookup::NoError)
32
4/5
✓ Branch 4 → 5 taken 3 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 17 → 8 not taken.
✓ Branch 17 → 18 taken 3 times.
3 for (const QDnsServiceRecord &record : lookup->serviceRecords())
33 3 records.append({record.target(), record.port(), record.priority(), record.weight()});
34
1/1
✓ Branch 20 → 21 taken 14 times.
14 lookup->deleteLater();
35
1/1
✓ Branch 21 → 22 taken 14 times.
14 callback(records);
36
0/7
✗ Branch 9 → 10 not taken.
✗ Branch 10 → 11 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 12 → 13 not taken.
✗ Branch 13 → 14 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
14 });
37 17 lookup->lookup();
38 17 }
39
40 } // namespace
41
42 874 SieveDiscovery::SieveDiscovery(QObject *parent) : QObject(parent) {
43 1765 m_srvLookup = [this](const QString &name, SrvCallback callback) {
44
1/1
✓ Branch 4 → 5 taken 17 times.
17 defaultSrvLookup(this, name, std::move(callback));
45 891 };
46 874 }
47
48 std::optional<SieveDiscovery::Candidate>
49 24 SieveDiscovery::imapSieveCandidate(const QStringList &imapCapabilities) {
50
2/2
✓ Branch 48 → 4 taken 8 times.
✓ Branch 48 → 49 taken 18 times.
26 for (const QString &capability : imapCapabilities) {
51
3/3
✓ Branch 6 → 7 taken 8 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 7 times.
8 if (!capability.startsWith(QLatin1String("IMAPSIEVE="), Qt::CaseInsensitive))
52 2 continue;
53
2/2
✓ Branch 9 → 10 taken 7 times.
✓ Branch 10 → 11 taken 7 times.
7 const QUrl url(capability.mid(10));
54
10/16
✓ Branch 12 → 13 taken 7 times.
✓ Branch 15 → 16 taken 6 times.
✓ Branch 15 → 20 taken 1 time.
✓ Branch 17 → 18 taken 6 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
✓ Branch 22 → 24 taken 1 time.
✓ Branch 24 → 25 taken 7 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 28 taken 6 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
13 if (url.scheme().compare(QLatin1String("sieve"), Qt::CaseInsensitive) != 0 || url.host().isEmpty())
55 1 continue;
56
1/1
✓ Branch 28 → 29 taken 6 times.
6 Candidate candidate;
57
1/1
✓ Branch 30 → 31 taken 6 times.
6 candidate.host = url.host();
58
1/1
✓ Branch 33 → 34 taken 6 times.
6 candidate.port = static_cast<quint16>(url.port(4190));
59 6 candidate.source = QStringLiteral("imapsieve");
60 6 return candidate;
61 13 }
62 18 return std::nullopt;
63 }
64
65 100 bool SieveDiscovery::candidateNeedsConfirmation(const AccountConfig &account, const Candidate &candidate) {
66
1/1
✓ Branch 2 → 3 taken 100 times.
100 const QString host = HostMatch::canonicalName(candidate.host);
67
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 99 times.
100 if (host.isEmpty()) return true;
68
1/2
✓ Branch 11 → 12 taken 81 times.
✗ Branch 11 → 14 not taken.
180 if (account.sieve.mode == QLatin1String("manual") &&
69
8/11
✓ Branch 8 → 9 taken 81 times.
✓ Branch 8 → 14 taken 18 times.
✓ Branch 9 → 10 taken 81 times.
✓ Branch 12 → 13 taken 81 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 81 times.
✓ Branch 15 → 17 taken 18 times.
✓ Branch 17 → 18 taken 81 times.
✓ Branch 17 → 19 taken 18 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
279 host == HostMatch::canonicalName(account.sieve.host) && candidate.port == account.sieve.port)
70 81 return false;
71
3/3
✓ Branch 19 → 20 taken 18 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 16 times.
18 if (host == HostMatch::canonicalName(account.imap.host)) return false;
72
2/2
✓ Branch 24 → 25 taken 16 times.
✓ Branch 25 → 26 taken 16 times.
16 return !HostMatch::isHostOrSubdomain(host, mailDomain(account));
73 100 }
74
75 38 QString SieveDiscovery::mailDomain(const AccountConfig &account) {
76 38 const int at = account.email.lastIndexOf(QLatin1Char('@'));
77
2/2
✓ Branch 5 → 6 taken 37 times.
✓ Branch 5 → 11 taken 1 time.
38 if (at >= 0)
78
2/2
✓ Branch 6 → 7 taken 37 times.
✓ Branch 7 → 8 taken 37 times.
37 return HostMatch::canonicalName(account.email.mid(at + 1));
79 1 return HostMatch::canonicalName(account.imap.host);
80 }
81
82 105 void SieveDiscovery::discover(const AccountConfig &account, const QStringList &imapCapabilities,
83 Callback callback) {
84
1/1
✓ Branch 2 → 3 taken 105 times.
105 auto run = std::make_shared<Run>();
85 105 run->account = account;
86 105 run->callback = std::move(callback);
87
1/1
✓ Branch 8 → 9 taken 105 times.
105 run->result.checkedAt = QDateTime::currentDateTimeUtc();
88
1/1
✓ Branch 12 → 13 taken 105 times.
105 QPointer<SieveDiscovery> guard(this);
89
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 21 taken 103 times.
105 if (account.sieve.mode == QLatin1String("off")) {
90 2 run->result.state = State::Disabled;
91
1/1
✓ Branch 18 → 19 taken 2 times.
4 QTimer::singleShot(0, this, [run] { run->callback(run->result); });
92 2 return;
93 }
94
2/2
✓ Branch 23 → 24 taken 82 times.
✓ Branch 23 → 38 taken 21 times.
103 if (account.sieve.mode == QLatin1String("manual")) {
95 164 run->candidates.append({account.sieve.host, account.sieve.port, account.sieve.security,
96 82 QStringLiteral("manual")});
97
1/3
✓ Branch 35 → 36 taken 82 times.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
82 QTimer::singleShot(0, this, [this, guard, run] {
98
1/2
✓ Branch 3 → 4 taken 82 times.
✗ Branch 3 → 5 not taken.
82 if (guard)
99 82 probeNext(run);
100 82 });
101 82 return;
102 }
103
3/3
✓ Branch 38 → 39 taken 21 times.
✓ Branch 40 → 41 taken 4 times.
✓ Branch 40 → 44 taken 17 times.
21 if (const auto fromImap = imapSieveCandidate(imapCapabilities))
104
1/1
✓ Branch 43 → 44 taken 4 times.
21 run->candidates.append(*fromImap);
105
2/2
✓ Branch 45 → 46 taken 21 times.
✓ Branch 49 → 50 taken 21 times.
42 const QString srvName = QStringLiteral("_sieve._tcp.") + mailDomain(account);
106
2/4
✓ Branch 55 → 56 taken 21 times.
✓ Branch 56 → 57 taken 21 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
21 m_srvLookup(srvName, [this, guard, run](const QList<SrvRecord> &records) {
107
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 18 times.
18 if (!guard)
108 ✗ return;
109 18 QList<SrvRecord> sorted = records;
110
3/3
✓ Branch 6 → 7 taken 18 times.
✓ Branch 7 → 8 taken 18 times.
✓ Branch 8 → 9 taken 18 times.
18 std::sort(sorted.begin(), sorted.end(), [](const SrvRecord &a, const SrvRecord &b) {
111
1/2
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
2 return a.priority != b.priority ? a.priority < b.priority : a.weight > b.weight;
112 });
113
4/4
✓ Branch 9 → 10 taken 18 times.
✓ Branch 10 → 11 taken 18 times.
✓ Branch 48 → 12 taken 3 times.
✓ Branch 48 → 49 taken 18 times.
21 for (const SrvRecord &record : sorted) {
114 3 QString target = record.target;
115
3/3
✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 2 times.
3 if (target.endsWith(QLatin1Char('.')))
116
1/1
✓ Branch 18 → 19 taken 1 time.
1 target.chop(1);
117
3/6
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 24 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 3 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 3 times.
3 if (target.isEmpty() || target == QLatin1String("."))
118 ✗ continue; // "." = service explicitly not offered
119 6 run->candidates.append({target, record.port, QStringLiteral("starttls"), QStringLiteral("srv")});
120 3 }
121 36 run->candidates.append({run->account.imap.host, m_fallbackPort, QStringLiteral("starttls"),
122 18 QStringLiteral("imap-host")});
123 // Drop duplicates (same host and port), keeping the first source.
124 18 QList<Candidate> unique;
125
4/4
✓ Branch 63 → 64 taken 18 times.
✓ Branch 64 → 65 taken 18 times.
✓ Branch 74 → 66 taken 25 times.
✓ Branch 74 → 75 taken 18 times.
43 for (const Candidate &candidate : run->candidates) {
126
1/1
✓ Branch 69 → 70 taken 25 times.
25 const bool seen = std::any_of(unique.cbegin(), unique.cend(), [&](const Candidate &other) {
127
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 6 taken 5 times.
11 return other.host.compare(candidate.host, Qt::CaseInsensitive) == 0 &&
128
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
11 other.port == candidate.port;
129 });
130
1/2
✓ Branch 70 → 71 taken 25 times.
✗ Branch 70 → 72 not taken.
25 if (!seen)
131
1/1
✓ Branch 71 → 72 taken 25 times.
25 unique.append(candidate);
132 }
133 18 run->candidates = unique;
134
1/1
✓ Branch 77 → 78 taken 18 times.
18 probeNext(run);
135
2/10
✓ Branch 36 → 37 taken 3 times.
✓ Branch 58 → 59 taken 18 times.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 116 not taken.
81 });
136 353 }
137
138 113 void SieveDiscovery::probeNext(const std::shared_ptr<Run> &run) {
139
2/2
✓ Branch 5 → 6 taken 11 times.
✓ Branch 5 → 25 taken 102 times.
113 if (run->index >= run->candidates.size()) {
140 11 run->result.state = State::NotAvailable;
141
6/6
✓ Branch 7 → 8 taken 11 times.
✓ Branch 11 → 12 taken 11 times.
✓ Branch 12 → 13 taken 11 times.
✓ Branch 15 → 16 taken 11 times.
✓ Branch 20 → 9 taken 11 times.
✓ Branch 20 → 21 taken 11 times.
22 qCInfo(lcSieveDiscovery) << "No rule server found after" << run->candidates.size()
142
1/1
✓ Branch 16 → 17 taken 11 times.
11 << "candidates";
143
1/1
✓ Branch 23 → 24 taken 11 times.
11 run->callback(run->result);
144 11 return;
145 }
146 102 const Candidate candidate = run->candidates.at(run->index++);
147
3/6
✓ Branch 29 → 30 taken 102 times.
✓ Branch 30 → 31 taken 102 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 102 times.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
102 auto *client = new ManageSieveClient(this);
148
1/1
✓ Branch 33 → 34 taken 102 times.
102 client->setCaCertificates(m_ca);
149 102 ManageSieveClient::Timeouts timeouts;
150 102 timeouts.connectMs = m_probeTimeoutMs;
151 102 timeouts.commandMs = m_probeTimeoutMs;
152 102 client->setTimeouts(timeouts);
153
1/1
✓ Branch 35 → 36 taken 102 times.
102 SieveServerConfig config;
154 102 config.host = candidate.host;
155 102 config.port = candidate.port;
156 102 config.security = candidate.security;
157
1/1
✓ Branch 38 → 39 taken 102 times.
102 client->setServer(config);
158
1/1
✓ Branch 39 → 40 taken 102 times.
102 QPointer<SieveDiscovery> guard(this);
159
2/8
✓ Branch 43 → 44 taken 102 times.
✓ Branch 44 → 45 taken 102 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
✗ Branch 67 → 68 not taken.
✗ Branch 67 → 69 not taken.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
102 client->probe([this, guard, run, candidate, client](const ManageSieveClient::Result &probe) {
160
1/1
✓ Branch 2 → 3 taken 99 times.
99 client->deleteLater();
161
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 99 times.
99 if (!guard)
162 13 return;
163
3/3
✓ Branch 10 → 11 taken 99 times.
✓ Branch 12 → 13 taken 99 times.
✓ Branch 14 → 15 taken 99 times.
396 const QString label = QStringLiteral("%1:%2 (%3)").arg(candidate.host).arg(candidate.port).arg(candidate.source);
164
2/2
✓ Branch 19 → 20 taken 13 times.
✓ Branch 19 → 49 taken 86 times.
99 if (!probe.ok) {
165
3/3
✓ Branch 24 → 25 taken 13 times.
✓ Branch 25 → 26 taken 13 times.
✓ Branch 26 → 27 taken 13 times.
26 run->result.tried.append(label + QStringLiteral(": ") + probe.message);
166
7/7
✓ Branch 31 → 32 taken 13 times.
✓ Branch 35 → 36 taken 13 times.
✓ Branch 36 → 37 taken 13 times.
✓ Branch 37 → 38 taken 13 times.
✓ Branch 38 → 39 taken 13 times.
✓ Branch 46 → 33 taken 13 times.
✓ Branch 46 → 47 taken 13 times.
26 qCInfo(lcSieveDiscovery) << "Candidate" << candidate.source << "failed, error"
167
4/4
✓ Branch 39 → 40 taken 13 times.
✓ Branch 40 → 41 taken 13 times.
✓ Branch 41 → 42 taken 13 times.
✓ Branch 42 → 43 taken 13 times.
13 << static_cast<int>(probe.error) << "in" << probe.elapsedMs << "ms";
168
1/1
✓ Branch 47 → 48 taken 13 times.
13 probeNext(run);
169 13 return;
170 }
171 86 const ManageSieve::Capabilities &capabilities = client->capabilities();
172 86 run->result.state = State::Available;
173 86 run->result.server = candidate;
174 86 run->result.implementation = capabilities.implementation;
175 86 run->result.extensions = capabilities.sieveExtensions;
176 86 run->result.extensionCount = capabilities.sieveExtensions.size();
177
1/1
✓ Branch 60 → 61 taken 86 times.
86 run->result.needsConfirmation = candidateNeedsConfirmation(run->account, candidate);
178
2/2
✓ Branch 66 → 67 taken 86 times.
✓ Branch 67 → 68 taken 86 times.
172 run->result.tried.append(label + QStringLiteral(": OK"));
179
7/7
✓ Branch 71 → 72 taken 86 times.
✓ Branch 75 → 76 taken 86 times.
✓ Branch 76 → 77 taken 86 times.
✓ Branch 77 → 78 taken 86 times.
✓ Branch 78 → 79 taken 86 times.
✓ Branch 87 → 73 taken 86 times.
✓ Branch 87 → 88 taken 86 times.
172 qCInfo(lcSieveDiscovery) << "Rule server found via" << candidate.source << "in"
180
3/3
✓ Branch 79 → 80 taken 86 times.
✓ Branch 80 → 81 taken 86 times.
✓ Branch 82 → 83 taken 86 times.
86 << probe.elapsedMs << "ms," << run->result.extensionCount
181
1/1
✓ Branch 83 → 84 taken 86 times.
86 << "extensions";
182
1/1
✓ Branch 90 → 91 taken 86 times.
86 run->callback(run->result);
183
2/2
✓ Branch 93 → 94 taken 86 times.
✓ Branch 93 → 96 taken 13 times.
99 });
184 102 }
185