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: 86.6% 330 / 0 / 381
Functions: 93.5% 29 / 0 / 31
Branches: 67.7% 434 / 0 / 641

presentation/MailReaderState.cpp
Line Branch Exec Source
1 #include "MailReaderState.h"
2 #include "util/SpamHeaders.h"
3 #include <QRegularExpression>
4 #include "service/NativeDocumentService.h"
5 #include "service/NativeResourceDocument.h"
6 #include "data/Models.h"
7 #include <QQuickItem>
8 #include <QTextCursor>
9 #include <QTextTable>
10 #include <QFontMetricsF>
11 #include <QLoggingCategory>
12
13
3/7
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
1 Q_LOGGING_CATEGORY(lcReaderState, "mailjd.quick.reader")
14
15 14804 QVariantList MailReaderState::attachments() const {
16 14804 QVariantList list;
17
2/2
✓ Branch 29 → 4 taken 110 times.
✓ Branch 29 → 30 taken 14804 times.
14914 for (const auto &attachment : m_body.attachments) {
18 // Inline-only parts already render inside the sanitized body; an
19 // explicit attachment disposition always wins (Widgets parity).
20
2/2
✓ Branch 5 → 6 taken 21 times.
✓ Branch 5 → 8 taken 89 times.
110 if (attachment.presentation != AttachmentPresentation::Regular &&
21
2/2
✓ Branch 6 → 7 taken 20 times.
✓ Branch 6 → 8 taken 1 time.
21 attachment.presentation != AttachmentPresentation::ExplicitAttachmentWithContentId)
22 20 continue;
23
1/1
✓ Branch 18 → 19 taken 90 times.
90 list.append(QVariantMap{
24 90 {"id", attachment.id},
25 90 {"name", attachment.filename},
26 90 {"contentType", attachment.contentType},
27
3/5
✓ Branch 14 → 15 taken 90 times.
✓ Branch 22 → 23 taken 360 times.
✓ Branch 22 → 24 taken 90 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
540 {"sizeText", QLocale().formattedDataSize(std::max<qint64>(0, attachment.size))}});
28 }
29 14804 return list;
30 180 }
31
32 838 MailReaderState::MailReaderState(QObject *parent, NativeDocumentService *service)
33
4/4
✓ Branch 22 → 23 taken 838 times.
✓ Branch 24 → 25 taken 838 times.
✓ Branch 25 → 26 taken 838 times.
✓ Branch 26 → 27 taken 838 times.
3352 : QObject(parent), m_service(service), m_document(std::make_unique<NativeResourceDocument>()) {
34
1/1
✓ Branch 30 → 31 taken 838 times.
838 m_preparePlaceholderTimer.setSingleShot(true);
35
1/1
✓ Branch 31 → 32 taken 838 times.
838 m_preparePlaceholderTimer.setInterval(200);
36
1/1
✓ Branch 32 → 33 taken 838 times.
838 connect(&m_preparePlaceholderTimer, &QTimer::timeout, this, [this] {
37 ✗ if (m_status != QLatin1String("preparing") || !m_bodyReady ||
38 ✗ !m_visible || m_loadingPlaceholderActive || !m_document->isEmpty()) return;
39 ✗ replaceDocument(tr("Preparing message…"));
40 ✗ m_loadingPlaceholderActive = true;
41 ✗ emit changed();
42 });
43
1/1
✓ Branch 34 → 35 taken 838 times.
838 m_plainSliceTimer.setInterval(0);
44
1/1
✓ Branch 35 → 36 taken 838 times.
838 connect(&m_plainSliceTimer, &QTimer::timeout, this, &MailReaderState::appendPlainSlice);
45 838 }
46
47 842 MailReaderState::~MailReaderState() {
48 838 cancelPreparation();
49 838 attachDocument(nullptr);
50 842 }
51
52 2376 void MailReaderState::attachDocument(QQuickTextDocument *target) {
53
2/2
✓ Branch 3 → 4 taken 836 times.
✓ Branch 3 → 5 taken 1540 times.
2376 if (m_target == target) return;
54 // A surviving control must never retain a pointer to our destroyed document.
55
5/8
✓ Branch 6 → 7 taken 769 times.
✓ Branch 6 → 17 taken 771 times.
✓ Branch 11 → 12 taken 769 times.
✓ Branch 12 → 13 taken 769 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 769 times.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
1540 if (m_target) m_target->setTextDocument(new NativeResourceDocument(m_target));
56 1540 m_target = target;
57
2/2
✓ Branch 19 → 20 taken 771 times.
✓ Branch 19 → 32 taken 769 times.
1540 if (m_target) {
58
3/3
✓ Branch 22 → 23 taken 771 times.
✓ Branch 23 → 24 taken 771 times.
✓ Branch 24 → 25 taken 771 times.
771 m_document->setDefaultFont(m_target->textDocument()->defaultFont());
59 771 m_target->setTextDocument(m_document.get());
60 771 applyViewportCulling(m_document->characterCount());
61 }
62 }
63
64 7523 void MailReaderState::cancelPreparation() {
65 7523 ++m_renderGeneration;
66 7523 m_preparePlaceholderTimer.stop();
67
2/2
✓ Branch 4 → 5 taken 69 times.
✓ Branch 4 → 7 taken 7454 times.
7523 if (m_service) m_service->cancel(this);
68 7523 }
69
70 131 QString MailReaderState::plainText() const {
71 // The Quick reader consumes the attached document directly. Exporting its
72 // complete text during every selection duplicates large bodies on the GUI
73 // thread; do it only for an explicit plainText consumer.
74 131 return m_document->toPlainText();
75 }
76
77 28 void MailReaderState::deferPreparationPlaceholder() {
78
7/8
✓ Branch 2 → 3 taken 26 times.
✓ Branch 2 → 8 taken 2 times.
✓ Branch 5 → 6 taken 11 times.
✓ Branch 5 → 8 taken 15 times.
✓ Branch 6 → 7 taken 11 times.
✗ Branch 6 → 8 not taken.
✓ Branch 9 → 10 taken 11 times.
✓ Branch 9 → 11 taken 17 times.
28 if (!m_loadingPlaceholderActive && m_document->isEmpty() && m_visible)
79 11 m_preparePlaceholderTimer.start();
80 28 }
81
82 6179 void MailReaderState::replaceDocument(const QString &text, const NativeMailDocument *prepared) {
83 // Rapid navigation often supersedes a cache read before it returns. Once
84 // the mounted document is already neutral, another selection only changes
85 // identity/status; rebuilding the same empty QTextDocument would still
86 // force a QQuickTextDocument swap before the first content frame.
87
1/1
✓ Branch 2 → 3 taken 6179 times.
6179 m_plainSliceTimer.stop();
88 6179 m_plainShown = m_plainLimit = 0;
89
10/11
✓ Branch 3 → 4 taken 6162 times.
✓ Branch 3 → 11 taken 17 times.
✓ Branch 5 → 6 taken 5326 times.
✓ Branch 5 → 11 taken 836 times.
✓ Branch 6 → 7 taken 4993 times.
✓ Branch 6 → 11 taken 333 times.
✓ Branch 8 → 9 taken 4993 times.
✓ Branch 9 → 10 taken 4993 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 4993 times.
✓ Branch 12 → 14 taken 1186 times.
6179 if (!prepared && text.isEmpty() && m_documentIsNeutral && m_document->isEmpty())
90 4993 return;
91 ✗ auto document = std::make_unique<NativeResourceDocument>(nullptr,
92
3/3
✓ Branch 14 → 15 taken 17 times.
✓ Branch 14 → 16 taken 1169 times.
✓ Branch 17 → 18 taken 1186 times.
1186 prepared ? prepared->images : QHash<QUrl, QImage>{});
93
2/2
✓ Branch 21 → 22 taken 1186 times.
✓ Branch 22 → 23 taken 1186 times.
1186 document->setDefaultFont(m_document->defaultFont());
94
1/1
✓ Branch 25 → 26 taken 1186 times.
1186 document->setUndoRedoEnabled(false);
95
5/6
✓ Branch 26 → 27 taken 17 times.
✓ Branch 26 → 30 taken 1169 times.
✓ Branch 28 → 29 taken 17 times.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 17 times.
✓ Branch 31 → 38 taken 1169 times.
1186 if (prepared && prepared->fragment) {
96
1/1
✓ Branch 33 → 34 taken 17 times.
17 QTextCursor cursor(document.get());
97
1/1
✓ Branch 35 → 36 taken 17 times.
17 cursor.insertFragment(*prepared->fragment);
98
1/3
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 41 taken 1169 times.
✗ Branch 40 → 43 not taken.
1186 } else if (prepared) document->setHtml(prepared->html);
99
1/1
✓ Branch 42 → 43 taken 1169 times.
1169 else document->setPlainText(text);
100
4/6
✓ Branch 43 → 44 taken 17 times.
✓ Branch 43 → 47 taken 1169 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 17 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 75 taken 1186 times.
1186 if (prepared && !prepared->fragment) {
101 // Normalized tables retain cell identity, but have no authored layout CSS.
102 // Keep adjacent headings readable without adding a grid of visual borders.
103 ✗ auto frames = document->rootFrame()->childFrames();
104 ✗ const auto padding = qMax(4.0, QFontMetricsF(document->defaultFont()).height() / 3.0);
105 ✗ while (!frames.isEmpty()) {
106 ✗ auto *frame = frames.takeLast();
107 ✗ frames.append(frame->childFrames());
108 ✗ if (auto *table = qobject_cast<QTextTable *>(frame)) {
109 ✗ auto format = table->format();
110 ✗ format.setCellPadding(padding);
111 ✗ table->setFormat(format);
112 ✗ }
113 }
114 ✗ }
115
3/3
✓ Branch 76 → 77 taken 1151 times.
✓ Branch 76 → 80 taken 35 times.
✓ Branch 79 → 80 taken 1151 times.
1186 if (m_target) m_target->setTextDocument(document.get());
116 1186 m_document = std::move(document);
117
4/4
✓ Branch 82 → 83 taken 1169 times.
✓ Branch 82 → 86 taken 17 times.
✓ Branch 84 → 85 taken 333 times.
✓ Branch 84 → 86 taken 836 times.
1186 m_documentIsNeutral = !prepared && text.isEmpty();
118
2/2
✓ Branch 88 → 89 taken 1186 times.
✓ Branch 89 → 90 taken 1186 times.
1186 applyViewportCulling(m_document->characterCount());
119 1186 }
120
121 2622 void MailReaderState::applyViewportCulling(qsizetype characters) {
122 // P90.20: TextEdit limits its scene-graph nodes to the visible viewport
123 // only while it observes the viewport (it enables that itself only for
124 // text set through its text property). Without it, a document swapped in
125 // here builds glyph nodes for every block on each sync: 64 ms per sync for
126 // 256 Ki characters. Observing costs an extra frame after viewport moves,
127 // so only documents large enough to benefit observe it.
128
2/2
✓ Branch 3 → 4 taken 2573 times.
✓ Branch 3 → 7 taken 49 times.
2622 auto *item = m_target ? qobject_cast<QQuickItem *>(m_target->parent()) : nullptr;
129
2/2
✓ Branch 8 → 9 taken 2573 times.
✓ Branch 8 → 10 taken 49 times.
2622 if (item) item->setFlag(QQuickItem::ItemObservesViewport, characters >= CulledDocumentChars);
130 2622 }
131
132 25 void MailReaderState::appendPlainSlice() {
133 // Invariant: every body/mode change renders or replaces the document,
134 // which resets the slice state; the checks only guard that invariant.
135
4/8
✓ Branch 3 → 4 taken 25 times.
✗ Branch 3 → 9 not taken.
✓ Branch 6 → 7 taken 25 times.
✗ Branch 6 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 25 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 25 times.
50 if (!plainLayoutPending() || m_effectiveMode != QLatin1String("text") ||
136 25 m_plainLimit > m_body.textPlain.size()) {
137 ✗ m_plainSliceTimer.stop();
138 ✗ m_plainShown = m_plainLimit = 0;
139 ✗ return;
140 }
141 25 const qsizetype count = qMin(m_plainLimit - m_plainShown, PlainSliceChars);
142
1/1
✓ Branch 16 → 17 taken 25 times.
25 QTextCursor cursor(m_document.get());
143
1/1
✓ Branch 17 → 18 taken 25 times.
25 cursor.movePosition(QTextCursor::End);
144
2/2
✓ Branch 18 → 19 taken 25 times.
✓ Branch 19 → 20 taken 25 times.
25 cursor.insertText(m_body.textPlain.mid(m_plainShown, count));
145 25 m_plainShown += count;
146
3/3
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 23 times.
✓ Branch 23 → 24 taken 2 times.
25 if (!plainLayoutPending()) m_plainSliceTimer.stop();
147 25 }
148
149 768 void MailReaderState::setCachePath(const QString &path) {
150
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 767 times.
768 if (m_cachePath == path) return;
151 767 m_cachePath = path;
152 767 clearDisplay();
153 }
154
155 868 void MailReaderState::setReadingMode(const QString &mode) {
156
4/4
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 866 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 866 times.
870 const QString canonical = mode == "html-light" ? QStringLiteral("light") : mode;
157
10/10
✓ Branch 11 → 12 taken 46 times.
✓ Branch 11 → 16 taken 822 times.
✓ Branch 13 → 14 taken 8 times.
✓ Branch 13 → 16 taken 38 times.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 15 → 18 taken 1 time.
✓ Branch 17 → 18 taken 796 times.
✓ Branch 17 → 19 taken 71 times.
✓ Branch 20 → 21 taken 797 times.
✓ Branch 20 → 22 taken 71 times.
868 if ((canonical != "text" && canonical != "light" && canonical != "html") || canonical == m_mode) return;
158 71 m_mode = canonical;
159
3/3
✓ Branch 23 → 24 taken 62 times.
✓ Branch 23 → 25 taken 9 times.
✓ Branch 24 → 27 taken 62 times.
71 if (m_bodyReady) render();
160 else {
161 9 m_effectiveMode = m_mode;
162
1/1
✓ Branch 26 → 27 taken 9 times.
9 emit changed();
163 }
164 868 }
165
166 1482 void MailReaderState::setVisible(bool visible) {
167
2/2
✓ Branch 2 → 3 taken 768 times.
✓ Branch 2 → 4 taken 714 times.
1482 if (m_visible == visible) return;
168 714 m_visible = visible;
169
2/2
✓ Branch 4 → 5 taken 517 times.
✓ Branch 4 → 6 taken 197 times.
714 if (!visible) cancelPreparation();
170
9/10
✓ Branch 6 → 7 taken 196 times.
✓ Branch 6 → 12 taken 1 time.
✓ Branch 8 → 9 taken 195 times.
✓ Branch 8 → 13 taken 1 time.
✓ Branch 9 → 10 taken 49 times.
✓ Branch 9 → 13 taken 146 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 49 times.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 196 times.
197 else if (m_needsPreparation || (m_htmlRenderer && m_bodyReady && m_effectiveMode == "html")) render();
171 714 emit changed();
172 }
173
174 724 void MailReaderState::render() {
175
1/1
✓ Branch 2 → 3 taken 724 times.
724 cancelPreparation();
176 724 m_retryable = false;
177 724 m_notice.clear();
178 724 m_needsPreparation = false;
179 // P90.8: a mode projection can leave html legitimately empty. Falling
180 // back to text is only correct when that emptiness is authoritative;
181 // otherwise request a reprojection instead of silently degrading the
182 // user's chosen mode.
183
4/4
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 16 taken 667 times.
✓ Branch 11 → 12 taken 34 times.
✓ Branch 11 → 16 taken 23 times.
1454 if ((m_mode == QLatin1String("light") || m_mode == QLatin1String("html")) &&
184 57 m_body.textHtml.isEmpty() &&
185
7/8
✓ Branch 6 → 7 taken 673 times.
✓ Branch 6 → 10 taken 51 times.
✓ Branch 12 → 13 taken 29 times.
✓ Branch 12 → 16 taken 5 times.
✓ Branch 14 → 15 taken 29 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 29 times.
✓ Branch 17 → 33 taken 695 times.
1477 m_body.projection == MailBody::Projection::TextOnly &&
186 29 m_identity.isValid()) {
187 29 m_effectiveMode = QStringLiteral("text");
188 29 m_status = QStringLiteral("loading");
189
1/1
✓ Branch 30 → 31 taken 29 times.
29 emit reprojectionRequested(true);
190
1/1
✓ Branch 31 → 32 taken 29 times.
29 emit changed();
191 698 return;
192 }
193 // P90.8 (symmetric case): text mode needs the plain variant an html
194 // projection skipped; request the reprojection instead of showing an
195 // empty body. A truly plain-less mail loads both variants (projection
196 // Full), so the emptiness there is authoritative and the native html
197 // fallback applies.
198
2/2
✓ Branch 37 → 38 taken 7 times.
✓ Branch 37 → 42 taken 660 times.
1362 if (m_mode == QLatin1String("text") && m_body.textPlain.isEmpty() &&
199
4/8
✓ Branch 35 → 36 taken 667 times.
✓ Branch 35 → 42 taken 28 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 42 taken 7 times.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 59 taken 695 times.
1362 m_body.projection == MailBody::Projection::HtmlOnly &&
200 ✗ m_identity.isValid()) {
201 ✗ m_effectiveMode = QStringLiteral("text");
202 ✗ m_status = QStringLiteral("loading");
203 ✗ emit reprojectionRequested(false);
204 ✗ emit changed();
205 ✗ return;
206 }
207
4/4
✓ Branch 60 → 61 taken 651 times.
✓ Branch 60 → 64 taken 44 times.
✓ Branch 67 → 68 taken 651 times.
✓ Branch 67 → 69 taken 44 times.
1346 m_effectiveMode = m_body.textHtml.isEmpty() ? QStringLiteral("text") : m_mode;
208
2/2
✓ Branch 70 → 71 taken 2 times.
✓ Branch 70 → 94 taken 693 times.
695 if (m_effectiveMode == "html") {
209
3/6
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 76 taken 2 times.
✓ Branch 81 → 82 taken 2 times.
✗ Branch 81 → 83 not taken.
✗ Branch 83 → 84 not taken.
✓ Branch 83 → 85 taken 2 times.
4 m_status = m_htmlRenderer ? QStringLiteral("preparing") : QStringLiteral("unavailable");
210
1/2
✗ Branch 86 → 87 not taken.
✓ Branch 86 → 88 taken 2 times.
2 if (m_htmlRenderer) {
211 // P90.11: keep an already-visible loading placeholder stable; the
212 // preparing phase stays an accessible status, never a second
213 // placeholder text swap for the same selection.
214 ✗ deferPreparationPlaceholder();
215 } else {
216 2 m_loadingPlaceholderActive = false;
217
2/2
✓ Branch 88 → 89 taken 2 times.
✓ Branch 89 → 90 taken 2 times.
2 replaceDocument(tr("Full HTML is not connected in this Quick reader yet. Select Text or HTML-light."));
218 }
219
1/1
✓ Branch 92 → 93 taken 2 times.
2 emit changed();
220 2 return;
221 }
222
7/8
✓ Branch 95 → 96 taken 672 times.
✓ Branch 95 → 101 taken 21 times.
✓ Branch 97 → 98 taken 7 times.
✓ Branch 97 → 100 taken 665 times.
✗ Branch 99 → 100 not taken.
✓ Branch 99 → 101 taken 7 times.
✓ Branch 102 → 103 taken 665 times.
✓ Branch 102 → 145 taken 28 times.
693 if (m_effectiveMode == "text" && (!m_body.textPlain.isEmpty() || m_body.textHtml.isEmpty())) {
223 665 m_status = QStringLiteral("ready");
224 // Show the message; no fallback commentary (user feedback 2026-09-08).
225 665 m_loadingPlaceholderActive = false;
226 // Sprint 90 (P90.20): the document is laid out on the GUI thread; a
227 // 50 MiB plain-text body froze the window for 11 s. Very large bodies
228 // show their beginning, and long bodies arrive in slices after the first
229 // frame instead of one layout pass.
230 665 const qsizetype limit = qMin(m_body.textPlain.size(), MaxShownPlainChars);
231
2/2
✓ Branch 112 → 113 taken 2 times.
✓ Branch 112 → 132 taken 663 times.
665 if (m_body.textPlain.size() > MaxShownPlainChars)
232
1/1
✓ Branch 113 → 114 taken 2 times.
2 m_notice = tr("This message is very large. Only its beginning is shown (%1 of %2 characters).")
233
3/3
✓ Branch 115 → 116 taken 2 times.
✓ Branch 116 → 117 taken 2 times.
✓ Branch 117 → 118 taken 2 times.
4 .arg(QLocale().toString(qint64(MaxShownPlainChars)))
234
3/3
✓ Branch 119 → 120 taken 2 times.
✓ Branch 121 → 122 taken 2 times.
✓ Branch 122 → 123 taken 2 times.
4 .arg(QLocale().toString(qint64(m_body.textPlain.size())));
235 665 const qsizetype first = qMin(limit, PlainSliceChars);
236
4/4
✓ Branch 134 → 135 taken 662 times.
✓ Branch 134 → 136 taken 3 times.
✓ Branch 136 → 137 taken 3 times.
✓ Branch 137 → 138 taken 665 times.
665 replaceDocument(first == m_body.textPlain.size() ? m_body.textPlain : m_body.textPlain.left(first));
237 665 m_plainShown = first;
238 665 m_plainLimit = limit;
239
1/1
✓ Branch 139 → 140 taken 665 times.
665 applyViewportCulling(limit);
240
3/3
✓ Branch 141 → 142 taken 3 times.
✓ Branch 141 → 143 taken 662 times.
✓ Branch 142 → 143 taken 3 times.
665 if (plainLayoutPending()) m_plainSliceTimer.start();
241
1/1
✓ Branch 143 → 144 taken 665 times.
665 emit changed();
242 665 return;
243 }
244 28 m_status = QStringLiteral("preparing");
245 28 m_needsPreparation = true;
246
1/1
✓ Branch 151 → 152 taken 28 times.
28 deferPreparationPlaceholder();
247 28 const auto lease = m_renderGeneration;
248
1/1
✓ Branch 152 → 153 taken 28 times.
28 const QPointer<MailReaderState> owner(this);
249
1/1
✓ Branch 153 → 154 taken 28 times.
28 emit changed();
250
7/8
✓ Branch 155 → 156 taken 28 times.
✗ Branch 155 → 158 not taken.
✓ Branch 156 → 157 taken 27 times.
✓ Branch 156 → 158 taken 1 time.
✓ Branch 157 → 158 taken 1 time.
✓ Branch 157 → 159 taken 26 times.
✓ Branch 160 → 161 taken 2 times.
✓ Branch 160 → 162 taken 26 times.
28 if (!owner || lease != m_renderGeneration || !m_visible) return;
251
4/4
✓ Branch 163 → 164 taken 10 times.
✓ Branch 163 → 166 taken 16 times.
✓ Branch 164 → 165 taken 10 times.
✓ Branch 165 → 166 taken 10 times.
26 if (!m_service) m_service = NativeDocumentService::shared();
252 26 const auto identity = m_identity;
253 26 const auto generation = m_generation;
254 26 const auto mode = m_effectiveMode;
255 52 NativeImageContext images{m_cachePath, identity.folderId, identity.uid,
256 26 m_body.cacheUidValidity, {}, mode == "light"};
257 20 auto ready = [this, lease, identity, generation, mode](NativeMailDocument result) {
258
2/4
✓ Branch 4 → 5 taken 20 times.
✗ Branch 4 → 9 not taken.
✓ Branch 5 → 6 taken 20 times.
✗ Branch 5 → 9 not taken.
20 if (lease != m_renderGeneration || identity != m_identity || generation != m_generation ||
259
4/8
✓ Branch 2 → 3 taken 20 times.
✗ Branch 2 → 9 not taken.
✓ Branch 7 → 8 taken 20 times.
✗ Branch 7 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 20 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 20 times.
40 mode != m_effectiveMode || !m_visible) return;
260 20 m_preparePlaceholderTimer.stop();
261 20 m_needsPreparation = false;
262
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 44 taken 19 times.
20 if (result.outcome != NativeMailDocument::Outcome::Ready) {
263 2 m_status = result.outcome == NativeMailDocument::Outcome::Limited
264
3/6
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 19 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 28 not taken.
2 ? QStringLiteral("limited") : QStringLiteral("error");
265 1 m_loadingPlaceholderActive = false;
266
1/1
✓ Branch 37 → 38 taken 1 time.
1 replaceDocument(result.outcome == NativeMailDocument::Outcome::Limited
267
4/13
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 36 not taken.
✗ Branch 34 → 37 not taken.
✓ Branch 35 → 37 taken 1 time.
✗ Branch 36 → 37 not taken.
✓ Branch 39 → 40 taken 1 time.
✗ Branch 39 → 41 not taken.
✓ Branch 41 → 42 taken 1 time.
✗ Branch 41 → 43 not taken.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 80 not taken.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
4 ? (mode == QStringLiteral("light")
268
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 1 time.
1 ? tr("This message is too large or complex for HTML-light. Choose HTML or Text in the message actions.")
269 : tr("This message is too large or complex to show as text. Choose HTML in the message actions."))
270 : tr("The native document could not be prepared. Retry."));
271 1 m_retryable = result.outcome != NativeMailDocument::Outcome::Limited;
272 } else {
273 19 m_status = QStringLiteral("ready");
274 19 m_loadingPlaceholderActive = false;
275
3/3
✓ Branch 50 → 51 taken 16 times.
✓ Branch 50 → 55 taken 3 times.
✓ Branch 51 → 52 taken 16 times.
19 if (result.simplified) m_notice = tr("Simplified layout. Unavailable images are shown as descriptions.");
276
4/4
✓ Branch 55 → 56 taken 19 times.
✓ Branch 56 → 57 taken 19 times.
✓ Branch 67 → 58 taken 16 times.
✓ Branch 67 → 68 taken 8 times.
24 for (const auto &reference : result.imageReferences) {
277
3/3
✓ Branch 59 → 60 taken 16 times.
✓ Branch 60 → 61 taken 11 times.
✓ Branch 60 → 65 taken 5 times.
16 if (!reference.remoteUrl.isEmpty()) {
278
1/1
✓ Branch 61 → 62 taken 11 times.
11 m_notice = tr("External images are blocked in this reader.");
279 11 break;
280 }
281 }
282
3/3
✓ Branch 69 → 70 taken 17 times.
✓ Branch 69 → 71 taken 2 times.
✓ Branch 72 → 73 taken 19 times.
19 replaceDocument(result.plainText, mode == "light" ? &result : nullptr);
283 }
284 20 emit changed();
285 26 };
286 // The snapshot/write worker already supplied the HTML digest. Both short
287 // and large HTML-light bodies can therefore check the prepared cache
288 // without encoding the complete body in the Quick interaction frame.
289 // On a miss the native worker encodes it once for parsing/CID admission.
290
3/3
✓ Branch 176 → 177 taken 26 times.
✓ Branch 180 → 181 taken 26 times.
✓ Branch 181 → 182 taken 26 times.
78 m_service->prepareDecodedHtml(this, m_body.textHtml, std::move(ready),
291
1/1
✓ Branch 175 → 176 taken 26 times.
78 std::move(images), m_document->defaultFont(),
292 26 m_body.htmlDigest);
293 28 }
294
295 1 void MailReaderState::finishHtml(quint64 revision, const QString &error) {
296
3/6
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 5 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
1 if (revision != m_renderGeneration || !htmlRequested()) return;
297 ✗ m_preparePlaceholderTimer.stop();
298 ✗ m_error = error;
299 ✗ m_status = error.isEmpty() ? QStringLiteral("ready") : QStringLiteral("error");
300 ✗ m_retryable = !error.isEmpty();
301 // A quick HTML load can go from the neutral document straight to WebEngine.
302 // Do not allocate/swap another empty QTextDocument for that success path.
303 // A delayed loading/preparing document still has content and is cleared.
304 ✗ if (!error.isEmpty() || !m_document->isEmpty()) replaceDocument(error);
305 ✗ m_loadingPlaceholderActive = false;
306 ✗ emit changed();
307 }
308
309 1 void MailReaderState::prepareHtml(quint64 revision) {
310
3/6
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 5 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
1 if (revision != m_renderGeneration || !htmlRequested()) return;
311 ✗ m_status = QStringLiteral("preparing");
312 ✗ m_error.clear();
313 ✗ m_retryable = false;
314 // P90.11: no visible placeholder swap after the loading state.
315 ✗ deferPreparationPlaceholder();
316 ✗ emit changed();
317 }
318
319 1256 void MailReaderState::presentLoading(const MailId &identity, quint64 generation,
320 const MailHeader &header, const QString &message, bool showPlaceholder) {
321
7/8
✓ Branch 3 → 4 taken 1255 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 4 → 5 taken 1254 times.
✓ Branch 4 → 6 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1254 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 1254 times.
1256 if (!identity.isValid() || identity.uid != header.uid || identity.folderId != header.folderId) return;
322
7/8
✓ Branch 11 → 12 taken 56 times.
✓ Branch 11 → 16 taken 1198 times.
✓ Branch 12 → 13 taken 50 times.
✓ Branch 12 → 16 taken 6 times.
✓ Branch 14 → 15 taken 50 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 50 times.
✓ Branch 17 → 30 taken 1204 times.
1254 if (identity == m_identity && generation == m_generation && m_status == "loading") {
323
9/11
✓ Branch 18 → 19 taken 49 times.
✓ Branch 18 → 23 taken 1 time.
✓ Branch 19 → 20 taken 49 times.
✓ Branch 21 → 22 taken 48 times.
✓ Branch 21 → 23 taken 1 time.
✓ Branch 24 → 25 taken 49 times.
✓ Branch 24 → 26 taken 1 time.
✓ Branch 26 → 27 taken 48 times.
✓ Branch 26 → 29 taken 2 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
50 if (showPlaceholder && plainText() != message) {
324 48 replaceDocument(message);
325 48 m_loadingPlaceholderActive = true;
326 48 emit changed();
327 }
328 50 return;
329 }
330 1204 cancelPreparation();
331 1204 m_identity = identity;
332 1204 m_generation = generation;
333 1204 m_header = header;
334 1204 m_body = {};
335 1204 m_bodyReady = m_needsPreparation = false;
336 1204 m_error.clear();
337 1204 m_notice.clear();
338 1204 m_retryable = false;
339 1204 m_status = QStringLiteral("loading");
340 1204 m_effectiveMode = m_mode;
341 1204 m_loadingPlaceholderActive = false;
342 // Publish the new identity with a neutral body. Keeping the previous body
343 // would misattribute its content; displaying the placeholder here would
344 // ignore the controller's delay and flash on every local cache hit.
345
3/3
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 1203 times.
✓ Branch 47 → 48 taken 1204 times.
1204 replaceDocument(showPlaceholder ? message : QString{});
346
2/2
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 1203 times.
1204 if (showPlaceholder) m_loadingPlaceholderActive = true;
347 1204 emit changed();
348 }
349
350 19752 QVariantMap MailReaderState::identity() const {
351 ✗ return {{"accountId", m_identity.accountId}, {"folderId", QString::number(m_identity.folderId)},
352
2/4
✓ Branch 9 → 10 taken 59256 times.
✓ Branch 9 → 11 taken 19752 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
79008 {"uid", QString::number(m_identity.uid)}};
353 19752 }
354
355 646 void MailReaderState::presentMail(const MailId &identity, quint64 generation,
356 const MailHeader &header, const MailBody &body) {
357
9/10
✓ Branch 3 → 4 taken 646 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 645 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 5 → 6 taken 644 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 643 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 643 times.
646 if (!identity.isValid() || identity.uid != header.uid || identity.folderId != header.folderId || body.uid != identity.uid) return;
358 643 m_identity = identity;
359 643 m_generation = generation;
360 643 m_header = header;
361 643 m_body = body;
362 643 m_error.clear();
363 643 m_retryable = false;
364 643 m_bodyReady = true;
365 643 render();
366 }
367
368 118 void MailReaderState::presentError(const MailId &identity, quint64 generation,
369 const MailHeader &header, const QString &reason, bool retryable) {
370
6/8
✓ Branch 3 → 4 taken 118 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 118 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 117 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 117 times.
118 if (!identity.isValid() || identity.uid != header.uid || identity.folderId != header.folderId) return;
371 117 cancelPreparation();
372 117 m_identity = identity;
373 117 m_generation = generation;
374 117 m_header = header;
375 117 m_body = {};
376 117 m_error = reason;
377 117 m_retryable = retryable;
378 117 m_bodyReady = m_needsPreparation = false;
379 117 m_notice.clear();
380 117 m_status = QStringLiteral("error");
381 117 m_loadingPlaceholderActive = false;
382 117 replaceDocument(reason);
383 117 emit changed();
384 }
385
386 19 void MailReaderState::presentBodyVariant(const MailId &identity, const MailBody &variant) {
387 // P90.8: merge only the authoritative fields of the fetched variant; the
388 // skipped field of a projection stays untouched instead of clobbering the
389 // already-displayed one. Both variants become authoritative afterwards.
390
4/8
✓ Branch 3 → 4 taken 19 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 19 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 19 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 19 times.
19 if (!identity.isValid() || identity != m_identity || !m_bodyReady) return;
391 // S106-162: the identity carries no mailbox epoch — after a UIDVALIDITY
392 // reset with UID reuse the variant may belong to another mail under the
393 // same UID; it merges only into a body of the same cached epoch.
394
3/4
✓ Branch 11 → 12 taken 19 times.
✗ Branch 11 → 27 not taken.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 27 taken 18 times.
19 if (m_body.cacheUidValidity != 0 && variant.cacheUidValidity != m_body.cacheUidValidity) {
395
6/6
✓ Branch 13 → 14 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 25 → 15 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
2 qCInfo(lcReaderState) << "Body variant dropped: epoch" << variant.cacheUidValidity
396
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
1 << "is not the displayed" << m_body.cacheUidValidity;
397 1 return;
398 }
399
2/4
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 16 times.
✓ Branch 27 → 33 taken 2 times.
✗ Branch 27 → 37 not taken.
18 switch (variant.projection) {
400 ✗ case MailBody::Projection::TextOnly:
401 ✗ m_body.textPlain = variant.textPlain;
402 ✗ break;
403 16 case MailBody::Projection::HtmlOnly:
404 16 m_body.textHtml = variant.textHtml;
405 16 m_body.htmlDigest = variant.htmlDigest;
406 16 break;
407 2 case MailBody::Projection::Full:
408 2 m_body.textPlain = variant.textPlain;
409 2 m_body.textHtml = variant.textHtml;
410 2 m_body.htmlDigest = variant.htmlDigest;
411 2 break;
412 }
413
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 18 times.
18 if (!variant.spamHeaders.isEmpty()) m_body.spamHeaders = variant.spamHeaders;
414 18 m_body.projection = MailBody::Projection::Full;
415 18 render();
416 }
417
418 4123 void MailReaderState::clearDisplay() {
419 4123 cancelPreparation();
420 4123 m_identity = {};
421 4123 m_generation = 0;
422 4123 m_header = {};
423 4123 m_body = {};
424 4123 m_error.clear();
425 4123 m_retryable = false;
426 4123 m_bodyReady = m_needsPreparation = false;
427 4123 m_notice.clear();
428 4123 m_status = QStringLiteral("empty");
429 4123 m_effectiveMode = QStringLiteral("text");
430 4123 m_loadingPlaceholderActive = false;
431
1/1
✓ Branch 25 → 26 taken 4123 times.
4123 replaceDocument({});
432 4123 emit changed();
433 8246 }
434
435 3971 QVariantMap MailReaderState::spam() const {
436 // Sprint 105 S105.2.4: the verdict stored at header sync comes first (it
437 // is there with the first frame, also without a cached raw source); the
438 // raw-source lines add the rule names for the tooltip. A body that
439 // arrived from the network carries its raw source instead of the lines
440 // the display read extracts.
441
1/2
✓ Branch 5 → 6 taken 3970 times.
✗ Branch 5 → 7 not taken.
7941 const QString lines = !m_body.spamHeaders.isEmpty() || m_body.rawSource.isEmpty()
442
2/3
✓ Branch 3 → 4 taken 3970 times.
✓ Branch 3 → 6 taken 1 time.
✗ Branch 7 → 8 not taken.
7941 ? m_body.spamHeaders : SpamHeaders::extract(m_body.rawSource);
443
1/1
✓ Branch 8 → 9 taken 3971 times.
3971 SpamHeaders::Verdict verdict = SpamHeaders::parse(lines);
444
5/6
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 13 taken 3967 times.
✓ Branch 11 → 12 taken 4 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 19 taken 3967 times.
3971 if (m_header.spamKnown && !m_header.spamTool.isEmpty()) {
445 4 verdict.known = true;
446 4 verdict.spam = m_header.isSpam;
447 4 verdict.score = m_header.spamScore;
448 4 verdict.threshold = m_header.spamThreshold;
449
1/2
✓ Branch 17 → 18 taken 4 times.
✗ Branch 17 → 19 not taken.
4 if (m_header.spamTool != QLatin1String("other")) verdict.tool = m_header.spamTool;
450 }
451
1/1
✓ Branch 19 → 20 taken 3971 times.
7942 return spamOfVerdict(verdict);
452 3971 }
453
454 ✗ void MailReaderState::presentSpamVerdict(const MailId &identity, const MailHeader &header) {
455 ✗ if (!(identity == m_identity) || header.uid != m_header.uid || !header.spamKnown) return;
456 ✗ m_header.spamKnown = true;
457 ✗ m_header.isSpam = header.isSpam;
458 ✗ m_header.spamScore = header.spamScore;
459 ✗ m_header.spamThreshold = header.spamThreshold;
460 ✗ m_header.spamTool = header.spamTool;
461 ✗ emit changed();
462 }
463
464 3 QVariantMap MailReaderState::spamOf(const QString &spamHeaderLines) {
465
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 3 → 4 taken 3 times.
3 return spamOfVerdict(SpamHeaders::parse(spamHeaderLines));
466 }
467
468 3976 QVariantMap MailReaderState::spamOfVerdict(const SpamHeaders::Verdict &verdict) {
469
4/6
✓ Branch 2 → 3 taken 3967 times.
✓ Branch 2 → 9 taken 9 times.
✓ Branch 6 → 7 taken 3967 times.
✓ Branch 6 → 8 taken 3967 times.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
11910 if (!verdict.known) return {{"known", false}};
470
1/1
✓ Branch 9 → 10 taken 9 times.
9 const QLocale locale;
471
2/3
✓ Branch 11 → 12 taken 9 times.
✗ Branch 11 → 14 not taken.
✓ Branch 13 → 15 taken 9 times.
9 const QString score = verdict.score ? locale.toString(*verdict.score, 'f', 1) : QString();
472
2/3
✓ Branch 16 → 17 taken 9 times.
✗ Branch 16 → 19 not taken.
✓ Branch 18 → 20 taken 9 times.
9 const QString threshold = verdict.threshold ? locale.toString(*verdict.threshold, 'f', 1) : QString();
473 9 const QString text = verdict.spam
474
2/7
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 3 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 29 taken 3 times.
✗ Branch 26 → 27 not taken.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
3 ? (score.isEmpty() ? tr("Marked as spam") : threshold.isEmpty() ? tr("Spam · score %1").arg(score)
475
1/3
✓ Branch 29 → 30 taken 3 times.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 194 not taken.
6 : tr("Spam · score %1 of %2").arg(score, threshold))
476
4/9
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 6 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 39 taken 6 times.
✗ Branch 36 → 37 not taken.
✓ Branch 45 → 46 taken 3 times.
✓ Branch 45 → 47 taken 6 times.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
15 : (score.isEmpty() ? tr("Not spam") : threshold.isEmpty() ? tr("Not spam · score %1").arg(score)
477
9/17
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 31 taken 6 times.
✗ Branch 23 → 41 not taken.
✗ Branch 28 → 41 not taken.
✓ Branch 30 → 41 taken 3 times.
✗ Branch 33 → 41 not taken.
✗ Branch 38 → 41 not taken.
✓ Branch 39 → 40 taken 6 times.
✓ Branch 40 → 41 taken 6 times.
✓ Branch 41 → 42 taken 6 times.
✓ Branch 41 → 43 taken 3 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 9 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 9 times.
✗ Branch 183 → 184 not taken.
✗ Branch 183 → 185 not taken.
27 : tr("Not spam · score %1 of %2").arg(score, threshold));
478 // The compact item of the header's date line (ux.md §12): "Spam 8.4 / 5.0"
479 // or a quiet "Score 1.2 / 5.0"; only "Spam" is red.
480
7/18
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 9 times.
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 9 times.
✓ Branch 58 → 59 taken 9 times.
✓ Branch 59 → 60 taken 9 times.
✓ Branch 60 → 61 taken 9 times.
✗ Branch 60 → 62 not taken.
✓ Branch 62 → 63 taken 9 times.
✗ Branch 62 → 64 not taken.
✓ Branch 64 → 65 taken 9 times.
✗ Branch 64 → 66 not taken.
✗ Branch 201 → 202 not taken.
✗ Branch 201 → 203 not taken.
✗ Branch 205 → 206 not taken.
✗ Branch 205 → 207 not taken.
✗ Branch 209 → 210 not taken.
✗ Branch 209 → 211 not taken.
18 const QString numbers = score.isEmpty() ? QString() : threshold.isEmpty() ? score : score + QStringLiteral(" / ") + threshold;
481
2/5
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 70 taken 3 times.
✓ Branch 70 → 71 taken 3 times.
✗ Branch 219 → 220 not taken.
✗ Branch 219 → 221 not taken.
6 const QString shortText = verdict.spam ? (numbers.isEmpty() ? tr("Spam") : tr("Spam %1").arg(numbers))
482
10/15
✓ Branch 66 → 67 taken 3 times.
✓ Branch 66 → 73 taken 6 times.
✗ Branch 69 → 79 not taken.
✓ Branch 72 → 79 taken 3 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 6 times.
✗ Branch 75 → 79 not taken.
✓ Branch 76 → 77 taken 6 times.
✓ Branch 78 → 79 taken 6 times.
✓ Branch 79 → 80 taken 6 times.
✓ Branch 79 → 81 taken 3 times.
✓ Branch 81 → 82 taken 3 times.
✓ Branch 81 → 83 taken 6 times.
✗ Branch 214 → 215 not taken.
✗ Branch 214 → 216 not taken.
12 : (numbers.isEmpty() ? tr("No spam") : tr("Score %1").arg(numbers));
483
4/4
✓ Branch 83 → 84 taken 3 times.
✓ Branch 83 → 85 taken 6 times.
✓ Branch 84 → 86 taken 3 times.
✓ Branch 85 → 86 taken 6 times.
9 const QString judgement = verdict.spam ? tr("spam") : tr("not spam");
484
0/3
✗ Branch 88 → 89 not taken.
✗ Branch 232 → 233 not taken.
✗ Branch 232 → 234 not taken.
9 const QString accessible = score.isEmpty() ? tr("Spam filter: %1").arg(judgement)
485
1/5
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 95 taken 9 times.
✗ Branch 93 → 94 not taken.
✗ Branch 227 → 228 not taken.
✗ Branch 227 → 229 not taken.
9 : threshold.isEmpty() ? tr("Spam filter: %1, score %2").arg(judgement, score)
486
6/14
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 91 taken 9 times.
✗ Branch 90 → 97 not taken.
✗ Branch 94 → 97 not taken.
✓ Branch 95 → 96 taken 9 times.
✓ Branch 96 → 97 taken 9 times.
✓ Branch 97 → 98 taken 9 times.
✗ Branch 97 → 99 not taken.
✗ Branch 99 → 100 not taken.
✓ Branch 99 → 101 taken 9 times.
✗ Branch 101 → 102 not taken.
✓ Branch 101 → 103 taken 9 times.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 225 not taken.
18 : tr("Spam filter: %1, score %2 of %3").arg(judgement, score, threshold);
487 9 QStringList tooltip;
488
2/4
✗ Branch 104 → 105 not taken.
✓ Branch 104 → 106 taken 9 times.
✗ Branch 105 → 107 not taken.
✓ Branch 107 → 108 taken 9 times.
9 tooltip << (verdict.tool.isEmpty() ? tr("The server's spam filter") : verdict.tool);
489
3/6
✓ Branch 110 → 111 taken 9 times.
✗ Branch 110 → 114 not taken.
✓ Branch 112 → 113 taken 9 times.
✗ Branch 112 → 114 not taken.
✓ Branch 115 → 116 taken 9 times.
✗ Branch 115 → 130 not taken.
9 if (!score.isEmpty() && !threshold.isEmpty())
490
5/7
✓ Branch 118 → 119 taken 3 times.
✓ Branch 118 → 121 taken 6 times.
✓ Branch 119 → 120 taken 3 times.
✓ Branch 120 → 123 taken 3 times.
✓ Branch 122 → 123 taken 6 times.
✗ Branch 245 → 246 not taken.
✗ Branch 245 → 247 not taken.
33 tooltip << (verdict.score >= verdict.threshold ? tr("%1 of %2 — above the server's limit").arg(score, threshold)
491
6/8
✓ Branch 121 → 122 taken 6 times.
✓ Branch 123 → 124 taken 9 times.
✓ Branch 125 → 126 taken 6 times.
✓ Branch 125 → 127 taken 3 times.
✓ Branch 127 → 128 taken 3 times.
✓ Branch 127 → 129 taken 6 times.
✗ Branch 241 → 242 not taken.
✗ Branch 241 → 243 not taken.
24 : tr("%1 of %2 — below the server's limit").arg(score, threshold));
492
5/5
✓ Branch 131 → 132 taken 2 times.
✓ Branch 131 → 143 taken 7 times.
✓ Branch 132 → 133 taken 2 times.
✓ Branch 136 → 137 taken 2 times.
✓ Branch 137 → 138 taken 2 times.
11 if (!verdict.tests.isEmpty()) tooltip << verdict.tests.mid(0, 6).join(QStringLiteral(", "));
493 9 return {{"known", true}, {"spam", verdict.spam}, {"score", score}, {"threshold", threshold},
494 18 {"tool", verdict.tool}, {"tests", verdict.tests}, {"text", text}, {"short", shortText},
495
3/5
✓ Branch 154 → 155 taken 9 times.
✓ Branch 158 → 159 taken 90 times.
✓ Branch 158 → 160 taken 9 times.
✗ Branch 263 → 264 not taken.
✗ Branch 263 → 265 not taken.
108 {"accessible", accessible}, {"tooltip", tooltip.join(QLatin1Char('\n'))}};
496 7961 }
497
498 7927 QStringList MailReaderState::remoteHosts() const {
499
4/4
✓ Branch 3 → 4 taken 7927 times.
✓ Branch 6 → 7 taken 7927 times.
✓ Branch 7 → 8 taken 7927 times.
✓ Branch 8 → 9 taken 7927 times.
7927 const QString key = QString::number(m_generation) + QLatin1Char('/') + QString::number(m_body.textHtml.size());
500
2/2
✓ Branch 13 → 14 taken 2184 times.
✓ Branch 13 → 19 taken 5743 times.
7927 if (key != m_remoteHostsKey) {
501 2184 m_remoteHostsKey = key;
502
1/1
✓ Branch 15 → 16 taken 2184 times.
2184 m_remoteHosts = remoteHostsOf(m_body.textHtml);
503 }
504 15854 return m_remoteHosts;
505 7927 }
506
507 2188 QStringList MailReaderState::remoteHostsOf(const QString &html) {
508 // S106-242: the attribute name must not continue a longer name —
509 // data-src, data-srcset, data-background, data-poster (lazy-load markup)
510 // load nothing, '\b' took the '-' for a word boundary.
511 static const QRegularExpression attribute(
512 44 QStringLiteral(R"re((?<![\w-])(?:src|srcset|background|poster)\s*=\s*["']?\s*(?:https?:)?//([^/"'\s>?#]+)|url\(\s*["']?\s*(?:https?:)?//([^/"')\s?#]+))re"),
513
4/7
✓ Branch 2 → 3 taken 22 times.
✓ Branch 2 → 15 taken 2166 times.
✓ Branch 4 → 5 taken 22 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 22 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
2232 QRegularExpression::CaseInsensitiveOption);
514 2188 QStringList hosts;
515
2/2
✓ Branch 16 → 17 taken 2168 times.
✓ Branch 16 → 18 taken 20 times.
2188 if (html.isEmpty()) return hosts;
516
1/1
✓ Branch 19 → 20 taken 20 times.
20 auto matches = attribute.globalMatch(html);
517
6/7
✓ Branch 52 → 53 taken 29 times.
✓ Branch 53 → 54 taken 9 times.
✓ Branch 53 → 57 taken 20 times.
✓ Branch 55 → 56 taken 9 times.
✗ Branch 55 → 57 not taken.
✓ Branch 58 → 21 taken 9 times.
✓ Branch 58 → 59 taken 20 times.
29 while (matches.hasNext() && hosts.size() < 20) {
518
1/1
✓ Branch 21 → 22 taken 9 times.
9 const auto match = matches.next();
519
6/6
✓ Branch 22 → 23 taken 9 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 8 times.
✓ Branch 25 → 27 taken 1 time.
✓ Branch 26 → 27 taken 8 times.
✓ Branch 27 → 28 taken 9 times.
9 QString host = (match.captured(1).isEmpty() ? match.captured(2) : match.captured(1)).toLower();
520
2/2
✓ Branch 33 → 34 taken 9 times.
✓ Branch 37 → 38 taken 9 times.
18 host = host.section(QLatin1Char('@'), -1).section(QLatin1Char(':'), 0, 0);
521
6/7
✓ Branch 42 → 43 taken 9 times.
✗ Branch 42 → 46 not taken.
✓ Branch 44 → 45 taken 8 times.
✓ Branch 44 → 46 taken 1 time.
✓ Branch 47 → 48 taken 8 times.
✓ Branch 47 → 49 taken 1 time.
✓ Branch 48 → 49 taken 8 times.
9 if (!host.isEmpty() && !hosts.contains(host)) hosts.append(host);
522 9 }
523 20 return hosts;
524 20 }
525
526