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: 96.5% 83 / 0 / 86
Functions: 100.0% 5 / 0 / 5
Branches: 81.0% 179 / 0 / 221

util/SpamHeaders.h
Line Branch Exec Source
1 #pragma once
2 // F100-56: what a spam filter on the server wrote into a message's header —
3 // SpamAssassin (X-Spam-Status/-Flag/-Score/-Level) and Rspamd (X-Spam,
4 // X-Spamd-Result, X-Rspamd-Score/-Action). Header-only and pure: the display
5 // read extracts the relevant lines from a bounded raw prefix (worker thread),
6 // the reader turns them into a verdict. Never trusted for actions — display only.
7
8 #include <QByteArray>
9 #include <QRegularExpression>
10 #include <QString>
11 #include <QStringList>
12 #include <cmath>
13 #include <optional>
14
15 namespace SpamHeaders {
16
17 // Lower-case header names this module reads.
18 611 inline const QStringList &fieldNames() {
19 static const QStringList names{"x-spam-status", "x-spam-flag", "x-spam-score", "x-spam-level", "x-spam",
20
5/10
✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 20 taken 600 times.
✓ Branch 4 → 5 taken 11 times.
✗ Branch 4 → 20 not taken.
✓ Branch 17 → 18 taken 88 times.
✓ Branch 17 → 19 taken 11 times.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
782 "x-spamd-result", "x-rspamd-score", "x-rspamd-action"};
21 611 return names;
22 11 }
23
24 // The relevant fields of a raw message prefix as unfolded "name: value" lines
25 // (name lower-case, value trimmed and at most 1024 characters). Reads only the
26 // header block of at most `maximumBytes`.
27 933 inline QString extract(const QByteArray &raw, qsizetype maximumBytes = 16384) {
28
1/1
✓ Branch 4 → 5 taken 933 times.
933 const QByteArray prefix = raw.first(qMin(raw.size(), maximumBytes));
29 933 qsizetype end = prefix.indexOf("\r\n\r\n");
30 933 const qsizetype lfEnd = prefix.indexOf("\n\n");
31
5/6
✓ Branch 9 → 10 taken 79 times.
✓ Branch 9 → 13 taken 854 times.
✓ Branch 10 → 11 taken 45 times.
✓ Branch 10 → 12 taken 34 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 45 times.
933 if (lfEnd >= 0 && (end < 0 || lfEnd < end)) end = lfEnd;
32
3/3
✓ Branch 13 → 14 taken 132 times.
✓ Branch 13 → 15 taken 801 times.
✓ Branch 14 → 16 taken 132 times.
933 const QByteArray block = end >= 0 ? prefix.first(end) : prefix;
33 933 QStringList lines;
34 933 QString name, value;
35 2666 const auto flush = [&] {
36 // Trimmed again after the cut: a value cut right after a blank would
37 // otherwise change when its stored line is read again (fuzz oracle).
38
6/6
✓ Branch 3 → 4 taken 611 times.
✓ Branch 3 → 8 taken 2055 times.
✓ Branch 6 → 7 taken 25 times.
✓ Branch 6 → 8 taken 586 times.
✓ Branch 9 → 10 taken 25 times.
✓ Branch 9 → 27 taken 2641 times.
2666 if (!name.isEmpty() && fieldNames().contains(name))
39
6/6
✓ Branch 10 → 11 taken 25 times.
✓ Branch 11 → 12 taken 25 times.
✓ Branch 12 → 13 taken 25 times.
✓ Branch 16 → 17 taken 25 times.
✓ Branch 17 → 18 taken 25 times.
✓ Branch 18 → 19 taken 25 times.
50 lines << name + QStringLiteral(": ") + value.trimmed().left(1024).trimmed();
40 2666 name.clear();
41 2666 value.clear();
42 2666 };
43
5/5
✓ Branch 16 → 17 taken 933 times.
✓ Branch 17 → 18 taken 933 times.
✓ Branch 18 → 19 taken 933 times.
✓ Branch 71 → 20 taken 1814 times.
✓ Branch 71 → 72 taken 933 times.
2747 for (QByteArray line : block.split('\n')) {
44
4/4
✓ Branch 22 → 23 taken 1814 times.
✓ Branch 23 → 24 taken 514 times.
✓ Branch 23 → 25 taken 1300 times.
✓ Branch 24 → 25 taken 514 times.
1814 if (line.endsWith('\r')) line.chop(1);
45
8/8
✓ Branch 26 → 27 taken 1035 times.
✓ Branch 26 → 32 taken 779 times.
✓ Branch 28 → 29 taken 959 times.
✓ Branch 28 → 31 taken 76 times.
✓ Branch 30 → 31 taken 5 times.
✓ Branch 30 → 32 taken 954 times.
✓ Branch 33 → 34 taken 81 times.
✓ Branch 33 → 47 taken 1733 times.
1814 if (!line.isEmpty() && (line.at(0) == ' ' || line.at(0) == '\t')) {
46
5/6
✓ Branch 35 → 36 taken 81 times.
✗ Branch 35 → 46 not taken.
✓ Branch 36 → 37 taken 81 times.
✓ Branch 37 → 38 taken 81 times.
✓ Branch 40 → 41 taken 81 times.
✓ Branch 41 → 42 taken 81 times.
81 if (!name.isEmpty()) value += QLatin1Char(' ') + QString::fromLatin1(line).trimmed();
47 81 continue;
48 }
49
1/1
✓ Branch 47 → 48 taken 1733 times.
1733 flush();
50 1733 const qsizetype colon = line.indexOf(':');
51
2/2
✓ Branch 49 → 50 taken 1122 times.
✓ Branch 49 → 51 taken 611 times.
1733 if (colon <= 0) continue;
52
4/4
✓ Branch 51 → 52 taken 611 times.
✓ Branch 52 → 53 taken 611 times.
✓ Branch 53 → 54 taken 611 times.
✓ Branch 54 → 55 taken 611 times.
611 name = QString::fromLatin1(line.first(colon)).trimmed().toLower();
53
2/2
✓ Branch 60 → 61 taken 611 times.
✓ Branch 61 → 62 taken 611 times.
611 value = QString::fromLatin1(line.mid(colon + 1));
54 2747 }
55
1/1
✓ Branch 73 → 74 taken 933 times.
933 flush();
56
1/1
✓ Branch 76 → 77 taken 933 times.
1866 return lines.join(QLatin1Char('\n'));
57 933 }
58
59 struct Verdict {
60 bool known = false; // a spam filter wrote something
61 bool spam = false; // it classified the message as spam
62 std::optional<double> score;
63 std::optional<double> threshold;
64 QString tool; // "SpamAssassin", "Rspamd" or empty
65 QStringList tests; // rule/symbol names, at most 40
66 };
67
68 // A finite number of the header ("nan", "inf" and overflow are no scores).
69 21 inline std::optional<double> number(const QString &text) {
70 21 bool ok = false;
71
2/2
✓ Branch 2 → 3 taken 21 times.
✓ Branch 3 → 4 taken 21 times.
21 const double value = text.trimmed().toDouble(&ok);
72
3/4
✓ Branch 5 → 6 taken 20 times.
✓ Branch 5 → 9 taken 1 time.
✓ Branch 7 → 8 taken 20 times.
✗ Branch 7 → 9 not taken.
42 return ok && std::isfinite(value) ? std::optional<double>(value) : std::nullopt;
73 }
74
75 // The verdict of extracted lines (see extract()).
76 4010 inline Verdict parse(const QString &lines) {
77 4010 Verdict verdict;
78
5/5
✓ Branch 5 → 6 taken 4010 times.
✓ Branch 6 → 7 taken 4010 times.
✓ Branch 7 → 8 taken 4010 times.
✓ Branch 274 → 9 taken 28 times.
✓ Branch 274 → 275 taken 4010 times.
4038 for (const QString &line : lines.split(QLatin1Char('\n'), Qt::SkipEmptyParts)) {
79 28 const qsizetype colon = line.indexOf(QLatin1Char(':'));
80
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 28 times.
28 if (colon <= 0) continue;
81
1/1
✓ Branch 15 → 16 taken 28 times.
28 const QString name = line.left(colon);
82
2/2
✓ Branch 16 → 17 taken 28 times.
✓ Branch 17 → 18 taken 28 times.
28 const QString value = line.mid(colon + 1).trimmed();
83 28 verdict.known = true;
84
2/2
✓ Branch 21 → 22 taken 9 times.
✓ Branch 21 → 118 taken 19 times.
28 if (name == QLatin1String("x-spam-status")) {
85 // "Yes, score=7.2 required=5.0 tests=BAYES_99,URIBL_BLOCKED autolearn=no"
86 9 verdict.tool = QStringLiteral("SpamAssassin");
87
5/5
✓ Branch 28 → 29 taken 6 times.
✓ Branch 28 → 32 taken 3 times.
✓ Branch 30 → 31 taken 6 times.
✓ Branch 31 → 32 taken 4 times.
✓ Branch 31 → 33 taken 2 times.
9 verdict.spam = verdict.spam || value.startsWith(QLatin1String("yes"), Qt::CaseInsensitive);
88
4/7
✓ Branch 34 → 35 taken 4 times.
✓ Branch 34 → 47 taken 5 times.
✓ Branch 36 → 37 taken 4 times.
✗ Branch 36 → 47 not taken.
✓ Branch 41 → 42 taken 4 times.
✗ Branch 292 → 293 not taken.
✗ Branch 292 → 294 not taken.
13 static const QRegularExpression score(QStringLiteral(R"(\bscore=(-?[0-9.]+))"));
89
4/7
✓ Branch 47 → 48 taken 4 times.
✓ Branch 47 → 60 taken 5 times.
✓ Branch 49 → 50 taken 4 times.
✗ Branch 49 → 60 not taken.
✓ Branch 54 → 55 taken 4 times.
✗ Branch 302 → 303 not taken.
✗ Branch 302 → 304 not taken.
13 static const QRegularExpression required(QStringLiteral(R"(\brequired=(-?[0-9.]+))"));
90
4/7
✓ Branch 60 → 61 taken 4 times.
✓ Branch 60 → 73 taken 5 times.
✓ Branch 62 → 63 taken 4 times.
✗ Branch 62 → 73 not taken.
✓ Branch 67 → 68 taken 4 times.
✗ Branch 312 → 313 not taken.
✗ Branch 312 → 314 not taken.
13 static const QRegularExpression tests(QStringLiteral(R"(\btests=([A-Za-z0-9_,\s]+?)(?:\s+\w+=|$))"));
91
6/6
✓ Branch 74 → 75 taken 9 times.
✓ Branch 75 → 76 taken 9 times.
✓ Branch 76 → 77 taken 6 times.
✓ Branch 76 → 81 taken 3 times.
✓ Branch 77 → 78 taken 6 times.
✓ Branch 78 → 79 taken 6 times.
9 if (const auto m = score.match(value); m.hasMatch()) verdict.score = number(m.captured(1));
92
6/6
✓ Branch 83 → 84 taken 9 times.
✓ Branch 84 → 85 taken 9 times.
✓ Branch 85 → 86 taken 5 times.
✓ Branch 85 → 90 taken 4 times.
✓ Branch 86 → 87 taken 5 times.
✓ Branch 87 → 88 taken 5 times.
9 if (const auto m = required.match(value); m.hasMatch()) verdict.threshold = number(m.captured(1));
93
4/4
✓ Branch 92 → 93 taken 9 times.
✓ Branch 93 → 94 taken 9 times.
✓ Branch 94 → 95 taken 5 times.
✓ Branch 94 → 116 taken 4 times.
9 if (const auto m = tests.match(value); m.hasMatch())
94
6/6
✓ Branch 95 → 96 taken 5 times.
✓ Branch 99 → 100 taken 5 times.
✓ Branch 101 → 102 taken 5 times.
✓ Branch 102 → 103 taken 5 times.
✓ Branch 113 → 104 taken 6 times.
✓ Branch 113 → 114 taken 5 times.
11 for (const QString &test : m.captured(1).split(QLatin1Char(','), Qt::SkipEmptyParts))
95
3/4
✓ Branch 106 → 107 taken 6 times.
✗ Branch 106 → 111 not taken.
✓ Branch 107 → 108 taken 6 times.
✓ Branch 108 → 109 taken 6 times.
20 if (verdict.tests.size() < 40) verdict.tests << test.trimmed();
96
6/6
✓ Branch 120 → 121 taken 13 times.
✓ Branch 120 → 124 taken 6 times.
✓ Branch 123 → 124 taken 4 times.
✓ Branch 123 → 125 taken 9 times.
✓ Branch 126 → 127 taken 10 times.
✓ Branch 126 → 134 taken 9 times.
19 } else if (name == QLatin1String("x-spam-flag") || name == QLatin1String("x-spam")) {
97
4/5
✓ Branch 127 → 128 taken 10 times.
✗ Branch 127 → 131 not taken.
✓ Branch 129 → 130 taken 10 times.
✓ Branch 130 → 131 taken 8 times.
✓ Branch 130 → 132 taken 2 times.
10 verdict.spam = verdict.spam || value.startsWith(QLatin1String("yes"), Qt::CaseInsensitive);
98
5/6
✓ Branch 136 → 137 taken 2 times.
✓ Branch 136 → 140 taken 7 times.
✓ Branch 138 → 139 taken 2 times.
✗ Branch 138 → 140 not taken.
✓ Branch 141 → 142 taken 2 times.
✓ Branch 141 → 143 taken 7 times.
9 } else if (name == QLatin1String("x-spam-score") && !verdict.score) {
99
1/1
✓ Branch 142 → 268 taken 2 times.
2 verdict.score = number(value);
100
5/8
✓ Branch 145 → 146 taken 1 time.
✓ Branch 145 → 156 taken 6 times.
✓ Branch 147 → 148 taken 1 time.
✗ Branch 147 → 156 not taken.
✓ Branch 149 → 150 taken 1 time.
✗ Branch 149 → 156 not taken.
✓ Branch 154 → 155 taken 1 time.
✗ Branch 154 → 156 not taken.
8 } else if (name == QLatin1String("x-spam-level") && !verdict.score && !value.isEmpty() &&
101
3/3
✓ Branch 152 → 153 taken 1 time.
✓ Branch 157 → 158 taken 1 time.
✓ Branch 157 → 161 taken 6 times.
8 value.count(QLatin1Char('*')) == value.size()) {
102 1 verdict.score = value.size();
103
2/2
✓ Branch 163 → 164 taken 4 times.
✓ Branch 163 → 232 taken 2 times.
6 } else if (name == QLatin1String("x-spamd-result")) {
104 // "default: False [1.20 / 15.00]; R_SPF_ALLOW(-0.20)[+ip4]; MIME_GOOD(-0.10)[text/plain]"
105 4 verdict.tool = QStringLiteral("Rspamd");
106
3/5
✓ Branch 170 → 171 taken 4 times.
✗ Branch 170 → 174 not taken.
✓ Branch 172 → 173 taken 4 times.
✗ Branch 173 → 174 not taken.
✓ Branch 173 → 175 taken 4 times.
4 verdict.spam = verdict.spam || value.contains(QLatin1String(": true"), Qt::CaseInsensitive);
107
4/7
✓ Branch 176 → 177 taken 2 times.
✓ Branch 176 → 189 taken 2 times.
✓ Branch 178 → 179 taken 2 times.
✗ Branch 178 → 189 not taken.
✓ Branch 183 → 184 taken 2 times.
✗ Branch 357 → 358 not taken.
✗ Branch 357 → 359 not taken.
6 static const QRegularExpression range(QStringLiteral(R"(\[\s*(-?[0-9.]+)\s*/\s*(-?[0-9.]+)\s*\])"));
108
3/4
✓ Branch 190 → 191 taken 4 times.
✓ Branch 191 → 192 taken 4 times.
✓ Branch 192 → 193 taken 4 times.
✗ Branch 192 → 200 not taken.
4 if (const auto m = range.match(value); m.hasMatch()) {
109
2/2
✓ Branch 193 → 194 taken 4 times.
✓ Branch 194 → 195 taken 4 times.
4 verdict.score = number(m.captured(1));
110
2/2
✓ Branch 196 → 197 taken 4 times.
✓ Branch 197 → 198 taken 4 times.
4 verdict.threshold = number(m.captured(2));
111 4 }
112
4/7
✓ Branch 201 → 202 taken 2 times.
✓ Branch 201 → 214 taken 2 times.
✓ Branch 203 → 204 taken 2 times.
✗ Branch 203 → 214 not taken.
✓ Branch 208 → 209 taken 2 times.
✗ Branch 377 → 378 not taken.
✗ Branch 377 → 379 not taken.
6 static const QRegularExpression symbol(QStringLiteral(R"(;\s*([A-Z0-9_]+)\()"));
113
7/8
✓ Branch 215 → 216 taken 4 times.
✓ Branch 223 → 224 taken 61 times.
✓ Branch 224 → 225 taken 57 times.
✓ Branch 224 → 228 taken 4 times.
✓ Branch 226 → 227 taken 57 times.
✗ Branch 226 → 228 not taken.
✓ Branch 229 → 217 taken 57 times.
✓ Branch 229 → 230 taken 4 times.
61 for (auto it = symbol.globalMatch(value); it.hasNext() && verdict.tests.size() < 40;)
114
3/3
✓ Branch 217 → 218 taken 57 times.
✓ Branch 218 → 219 taken 57 times.
✓ Branch 219 → 220 taken 57 times.
61 verdict.tests << it.next().captured(1);
115
1/2
✗ Branch 234 → 235 not taken.
✓ Branch 234 → 244 taken 2 times.
2 } else if (name == QLatin1String("x-rspamd-score")) {
116 ✗ verdict.tool = QStringLiteral("Rspamd");
117 ✗ if (!verdict.score) verdict.score = number(value);
118
1/2
✓ Branch 246 → 247 taken 2 times.
✗ Branch 246 → 268 not taken.
2 } else if (name == QLatin1String("x-rspamd-action")) {
119 2 verdict.tool = QStringLiteral("Rspamd");
120
1/1
✓ Branch 253 → 254 taken 2 times.
2 const QString action = value.toLower();
121
3/4
✓ Branch 257 → 258 taken 1 time.
✓ Branch 257 → 264 taken 1 time.
✓ Branch 260 → 261 taken 1 time.
✗ Branch 260 → 264 not taken.
3 verdict.spam = verdict.spam || action == QLatin1String("add header") ||
122
2/4
✓ Branch 254 → 255 taken 2 times.
✗ Branch 254 → 264 not taken.
✗ Branch 263 → 264 not taken.
✓ Branch 263 → 265 taken 1 time.
5 action == QLatin1String("rewrite subject") || action == QLatin1String("reject");
123 2 }
124 4038 }
125 4010 return verdict;
126 ✗ }
127
128 } // namespace SpamHeaders
129