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: 79.0% 338 / 0 / 428
Functions: 87.5% 28 / 0 / 32
Branches: 70.8% 393 / 10 / 565

controller/MailBodyTransport.cpp
Line Branch Exec Source
1 #include "controller/MailBodyTransport.h"
2
3 #include <QDeadlineTimer>
4 #include "service/MailDisplayReadService.h"
5 #include "service/MailBodyWriteService.h"
6 #include "service/MailSelectionMutationService.h"
7 #include <QFileInfo>
8 #include <QFutureWatcher>
9 #include <QtConcurrentRun>
10 #include <QLocale>
11 #include <QLoggingCategory>
12 #include <QPointer>
13 #include <algorithm>
14 #include <limits>
15 #include <utility>
16 #include "data/MailCache.h"
17 #include "performance/BackgroundPerformanceTracker.h"
18 #include "performance/FolderPerformanceTracker.h"
19 #include "performance/ContentPerformanceTracker.h"
20 #include "performance/MailOpenPerformanceTracker.h"
21 #include "performance/MutationPerformanceTracker.h"
22 #include "util/AttachmentFileSecurity.h"
23 #include "data/Models.h"
24 #include "controller/UndoManager.h"
25 #include "service/ImapService.h"
26 #include "service/ConnectionHealthMonitor.h"
27 #include "service/MimeParser.h"
28 #include "service/NetworkLifecycleCoordinator.h"
29 #include "ui/MailListModel.h"
30 #include "ui/MailThreadModel.h"
31 #include "util/MessageId.h"
32 #include "util/Utf8ByteCount.h"
33
34 // Same category as MailController (defined there), see docs/LOGGING.md.
35 Q_DECLARE_LOGGING_CATEGORY(lcController)
36
37 1050 MailBodyTransport::MailBodyTransport(
38 MailCache *cache, MailListModel *model, MailDisplayPort *const &view,
39 const FolderContext &folder, ImapService *const &bodyImap,
40 QString &bodyImapSelectedFolder, QTimer *const &loadingPlaceholderTimer,
41 const bool &networkEnabled, const bool &fetchInProgress,
42 1050 const QSet<QString> &unavailableFolders, Deps deps, QObject *parent)
43 1050 : QObject(parent), m_cache(cache), m_model(model), m_view(view), m_folder(folder),
44 1050 m_bodyImap(bodyImap), m_bodyImapSelectedFolder(bodyImapSelectedFolder),
45 1050 m_loadingPlaceholderTimer(loadingPlaceholderTimer), m_networkEnabled(networkEnabled),
46 1050 m_fetchInProgress(fetchInProgress), m_unavailableFolders(unavailableFolders),
47
1/1
✓ Branch 7 → 8 taken 1050 times.
1050 m_deps(std::move(deps)) {
48 // A demand MIME parse must be able to start while one speculative payload
49 // parses. Dispatch admission below keeps at most one prefetch in flight
50 // through the bounded parse/persist pipeline.
51
1/1
✓ Branch 11 → 12 taken 1050 times.
1050 m_bodyPreparationPool.setMaxThreadCount(2);
52 1050 }
53
54 1216 void MailBodyTransport::noteDemandActivity() {
55 1216 m_lastDemandBodyActivityMs = QDeadlineTimer::current().deadline();
56 1216 }
57
58 17 void MailBodyTransport::dropReconnectRetries(qint64 folderId) {
59 17 m_bodyReconnectRetries.removeIf([this, folderId](const PendingBodyRetry &retry) {
60 ✗ return retry.request.mailId.accountId == m_folder.accountId &&
61 ✗ retry.request.mailId.folderId == folderId;
62 });
63 17 }
64
65 2 void MailBodyTransport::showMessageAboveDownloadLimit(const MailHeader &header,
66 qint64 maxBytes) {
67 2 const QString actual = header.size > 0
68
1/3
✓ Branch 3 → 4 taken 1 time.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
2 ? QLocale().formattedDataSize(header.size)
69
6/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
3 : tr("an unknown size");
70
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
4 const QString maximum = QLocale().formattedDataSize(maxBytes);
71
72 2 MailBody notice;
73 2 notice.uid = header.uid;
74 notice.textPlain =
75
1/1
✓ Branch 13 → 14 taken 2 times.
4 tr("This message was not downloaded because its size (%1) exceeds "
76 "the configured limit (%2). You can change the limit under "
77 "Settings > General > Message Downloads.")
78
1/1
✓ Branch 14 → 15 taken 2 times.
2 .arg(actual, maximum);
79
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 2 times.
2 if (m_view)
80 ✗ m_deps.presentMail(header, notice);
81
1/1
✓ Branch 22 → 23 taken 2 times.
2 m_deps.statusMessage(
82
1/1
✓ Branch 20 → 21 taken 2 times.
4 tr("Message not downloaded: size %1 exceeds limit %2")
83
1/1
✓ Branch 21 → 22 taken 2 times.
4 .arg(actual, maximum));
84 2 }
85
86 127 bool MailBodyTransport::rejectMessageAboveDownloadLimit(
87 const MailHeader &header, bool showFeedback) {
88 127 const qint64 limit = ImapService::configuredMaxMessageBytes();
89
4/4
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 5 taken 117 times.
✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 6 taken 1 time.
127 if (header.size <= 0 || header.size <= limit)
90 126 return false;
91
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
1 if (showFeedback)
92 1 showMessageAboveDownloadLimit(header, limit);
93 1 return true;
94 }
95
96 18 quint64 MailBodyTransport::requestBodyForConsumer(const MailId &mailId,
97 BodyPurpose purpose,
98 quint64 consumerId,
99 bool workerConfirmedMissing) {
100
3/6
✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 18 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 18 times.
18 if (!mailId.isValid() || mailId.accountId != m_folder.accountId)
101 ✗ return 0;
102
7/7
✓ Branch 10 → 11 taken 15 times.
✓ Branch 10 → 14 taken 3 times.
✓ Branch 11 → 12 taken 15 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 11 times.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 15 → 33 taken 14 times.
18 if (!workerConfirmedMissing && m_cache->hasBody(mailId.folderId, mailId.uid)) {
103 // Prefetch may commit after the reader's missing-body snapshot but before
104 // its GUI callback requests transport. Re-read instead of leaving the
105 // loading placeholder without a display consumer or a network request.
106
7/8
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 21 taken 3 times.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 3 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 3 times.
4 if (purpose == BodyPurpose::Display && m_bodyRequests.selectedMailId() == mailId)
107
1/1
✓ Branch 25 → 26 taken 1 time.
1 m_deps.displayMailId(mailId);
108 // A speculative caller needs no completion event for an already cached
109 // body; it only needs to avoid unnecessary transport.
110
2/2
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 1 time.
4 if (purpose == BodyPurpose::Prefetch) return 0;
111
1/1
✓ Branch 28 → 29 taken 1 time.
1 m_deps.bodyLoaded(mailId.uid, mailId.folderId);
112
1/1
✓ Branch 30 → 31 taken 1 time.
1 m_deps.bodyRequestFinished(mailId, BodyOutcome::Loaded, {});
113 1 return 0;
114 }
115 // The selected mailbox path is already bound to this account/folder ID.
116 // Only cross-folder requests need a cache lookup to resolve their path.
117 27 const QString folderPath = mailId.folderId == m_folder.currentFolderId &&
118
3/4
✓ Branch 33 → 34 taken 13 times.
✓ Branch 33 → 37 taken 1 time.
✓ Branch 35 → 36 taken 13 times.
✗ Branch 35 → 37 not taken.
14 !m_folder.currentFolder.isEmpty() ? m_folder.currentFolder
119
1/1
✓ Branch 37 → 38 taken 1 time.
14 : m_cache->folderPath(mailId.folderId);
120 // S106-192: a folder that no longer resolves (removed meanwhile) answers
121 // like an unavailable one — the loading placeholder needs an outcome.
122
5/6
✓ Branch 39 → 40 taken 13 times.
✓ Branch 39 → 42 taken 1 time.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 13 times.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 52 taken 13 times.
14 if (folderPath.isEmpty() || m_unavailableFolders.contains(folderPath)) {
123
1/1
✓ Branch 48 → 49 taken 1 time.
2 m_deps.bodyRequestFinished(mailId, BodyOutcome::Failed, QStringLiteral("Mailbox unavailable"));
124 1 return 0;
125 }
126 const auto ticket =
127
1/1
✓ Branch 52 → 53 taken 13 times.
13 m_bodyRequests.request(mailId, folderPath, purpose, consumerId);
128
1/2
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 58 taken 13 times.
13 if (ticket.requestId == 0) {
129 ✗ if (purpose == BodyPurpose::Display)
130 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance().fail(mailId);
131 ✗ return 0;
132 }
133
1/1
✓ Branch 58 → 59 taken 13 times.
13 m_deps.ensureBodyConnection();
134
1/1
✓ Branch 59 → 60 taken 13 times.
13 dispatchNextBodyRequest();
135 13 return ticket.requestId;
136 14 }
137
138 4 void MailBodyTransport::removeBodyConsumer(BodyPurpose purpose,
139 quint64 consumerId) {
140 4 m_bodyRequests.removeConsumer(purpose, consumerId);
141
4/4
✓ Branch 3 → 4 taken 4 times.
✓ Branch 4 → 5 taken 4 times.
✓ Branch 10 → 6 taken 6 times.
✓ Branch 10 → 11 taken 4 times.
10 for (PendingBodyRetry &pending : m_bodyReconnectRetries) {
142
1/1
✓ Branch 7 → 8 taken 6 times.
6 pending.consumers.removeIf(
143 11 [purpose, consumerId](const BodyConsumer &consumer) {
144
2/2
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 5 taken 3 times.
19 return consumer.purpose == purpose &&
145
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 4 times.
19 consumer.consumerId == consumerId;
146 });
147 }
148 4 m_bodyReconnectRetries.removeIf(
149 6 [](const PendingBodyRetry &pending) { return pending.consumers.isEmpty(); });
150 4 }
151
152 // S91.0.6: body transport consumers of the body connection.
153 19 void MailBodyTransport::connectBodyTransport(ImapService *bodyImap) {
154 19 connect(bodyImap, &ImapService::bodyRequestCompleted, this,
155
1/1
✓ Branch 2 → 3 taken 19 times.
19 &MailBodyTransport::onBodyRequestCompleted);
156 19 connect(bodyImap, &ImapService::bodyMailboxSelected, this,
157
1/1
✓ Branch 4 → 5 taken 19 times.
19 [this](quint64 requestId, bool success) {
158 ✗ const auto mailId = m_bodyRequests.mailIdForRequest(requestId);
159 ✗ if (mailId) {
160 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance()
161 ✗ .transportSelectionFinished(*mailId, success);
162 }
163 ✗ });
164 19 connect(bodyImap, &ImapService::stateChanged, this,
165
1/1
✓ Branch 6 → 7 taken 19 times.
19 [this](ImapService::State s) {
166
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 5 taken 2 times.
4 if (s == ImapService::State::Authenticated) {
167 2 retryBodiesAfterReconnect();
168 2 dispatchNextBodyRequest();
169 }
170 4 });
171 19 }
172
173 2 bool MailBodyTransport::demandBodyIdle() const {
174
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (m_bodyRequests.hasQueuedDemand()) return false;
175
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
2 if (m_lastDemandBodyActivityMs == 0) return true;
176 2 const qint64 now = QDeadlineTimer::current().deadline();
177 2 return now - m_lastDemandBodyActivityMs >= PrefetchIdleWindowMs;
178 }
179
180 61 void MailBodyTransport::dispatchNextBodyRequest() {
181
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 61 times.
61 if (!m_networkEnabled)
182 61 return;
183
2/2
✓ Branch 5 → 6 taken 7 times.
✓ Branch 5 → 12 taken 43 times.
50 if (m_bodyTransportBusy || !m_bodyImap ||
184
1/2
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 12 not taken.
7 m_bodyPreparations >= MaximumBodyPreparations ||
185
5/8
✓ Branch 4 → 5 taken 50 times.
✓ Branch 4 → 12 taken 11 times.
✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 12 not taken.
✓ Branch 9 → 10 taken 7 times.
✗ Branch 9 → 13 not taken.
✓ Branch 14 → 15 taken 61 times.
✗ Branch 14 → 16 not taken.
125 m_bodyPreparationBytes >= MaximumBodyPreparationBytes ||
186
1/2
✓ Branch 11 → 12 taken 7 times.
✗ Branch 11 → 13 not taken.
14 (m_bodyImap->state() != ImapService::State::Authenticated &&
187 7 m_bodyImap->state() != ImapService::State::Selected)) {
188 61 return;
189 }
190 // P90.9: never commit the single transport slot to a speculative fetch
191 // while the user is actively reading; retry when the idle window opens.
192 ✗ const bool nextPrefetch = m_bodyRequests.nextDispatchIsPrefetch();
193 ✗ if (nextPrefetch && !demandBodyIdle()) {
194 ✗ if (!m_prefetchRetryQueued) {
195 ✗ m_prefetchRetryQueued = true;
196 ✗ const qint64 elapsed = QDeadlineTimer::current().deadline() -
197 ✗ m_lastDemandBodyActivityMs;
198 ✗ const int remainingMs = int(qBound<qint64>(1,
199 ✗ PrefetchIdleWindowMs - elapsed, PrefetchIdleWindowMs));
200 ✗ QTimer::singleShot(remainingMs, this, [this] {
201 ✗ m_prefetchRetryQueued = false;
202 ✗ dispatchNextBodyRequest();
203 ✗ });
204 }
205 ✗ return;
206 }
207 // Keep one of the two preparation slots free for a newly arriving demand.
208 // Release schedules another dispatch, so a queued prefetch is revisited
209 // after the current payload has finished parse and durable persistence.
210 ✗ if (nextPrefetch && m_bodyPreparations > 0)
211 ✗ return;
212 ✗ const auto request = m_bodyRequests.takeNextForDispatch(/*allowPrefetch=*/true);
213 ✗ if (!request)
214 ✗ return;
215 ✗ m_bodyImapSelectedFolder = request->folderPath;
216 ✗ m_bodyTransportBusy = true;
217 ✗ m_activeBodyTransportRequestId = request->requestId;
218 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance().transportStarted(
219 ✗ request->mailId);
220 ✗ m_bodyImap->requestBodyInFolder(request->requestId, request->folderPath,
221 ✗ request->mailId.uid);
222 ✗ }
223
224 34 void MailBodyTransport::onBodyRequestCompleted(const BodyResult &result) {
225
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 15 taken 29 times.
34 if (result.requestId != m_activeBodyTransportRequestId) {
226
5/5
✓ Branch 3 → 4 taken 5 times.
✓ Branch 7 → 8 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 13 → 5 taken 5 times.
✓ Branch 13 → 14 taken 5 times.
10 qCInfo(lcController) << "Ignoring completed unknown body request"
227
1/1
✓ Branch 9 → 10 taken 5 times.
5 << result.requestId;
228 23 return;
229 }
230 29 m_bodyTransportBusy = false;
231 29 m_activeBodyTransportRequestId = 0;
232
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 18 taken 28 times.
29 if (!result.dispatched) {
233
1/1
✓ Branch 16 → 17 taken 1 time.
1 requeueUndispatchedBody(result);
234 1 return;
235 }
236
1/1
✓ Branch 18 → 19 taken 28 times.
28 const auto mailId = m_bodyRequests.mailIdForRequest(result.requestId);
237
7/8
✓ Branch 20 → 21 taken 26 times.
✓ Branch 20 → 26 taken 2 times.
✓ Branch 23 → 24 taken 26 times.
✗ Branch 23 → 26 not taken.
✓ Branch 24 → 25 taken 17 times.
✓ Branch 24 → 26 taken 9 times.
✓ Branch 27 → 28 taken 17 times.
✓ Branch 27 → 65 taken 11 times.
28 if (mailId && mailId->accountId == m_folder.accountId && result.outcome == BodyOutcome::Loaded) {
238 17 constexpr qsizetype maximumPreparationBytes = 64LL * 1024 * 1024;
239
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 38 taken 16 times.
17 if (result.bytes.size() > maximumPreparationBytes) {
240 1 BodyResult oversized = result;
241
1/1
✓ Branch 31 → 32 taken 1 time.
1 oversized.bytes.clear();
242 1 oversized.outcome = BodyOutcome::PreparationLimited;
243 1 oversized.maxBytes = maximumPreparationBytes;
244
1/1
✓ Branch 34 → 35 taken 1 time.
1 finishPreparedBody(std::move(oversized));
245 1 return;
246 1 }
247
1/1
✓ Branch 38 → 39 taken 16 times.
16 auto &tracker = MailJD::Performance::MailOpenPerformanceTracker::instance();
248 // Own the payload once and move it through parse and persistence: the
249 // raw bytes are the single largest allocation of an open (T-87.C2).
250 16 BodyResult owned = result;
251
1/1
✓ Branch 42 → 43 taken 16 times.
16 tracker.transportFinished(*mailId, true, owned.bytes.size());
252
1/1
✓ Branch 44 → 45 taken 16 times.
16 tracker.mimeParseStarted(*mailId);
253 // P90.9: bounded pipeline admission. Keep the registry entry alive during
254 // parsing so new consumers coalesce and final display eligibility is
255 // fresh; the next transport may already dispatch while this payload
256 // parses and persists (slot and byte caps above).
257 16 ++m_bodyPreparations;
258 16 m_bodyPreparationBytes += owned.bytes.size();
259
1/1
✓ Branch 47 → 48 taken 16 times.
16 m_bodyPreparationBytesByRequest.insert(owned.requestId, owned.bytes.size());
260
1/1
✓ Branch 48 → 49 taken 16 times.
16 QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
261 struct PreparedBody {
262 MimeMessage message;
263 BodyResult result;
264 };
265
3/6
✓ Branch 49 → 50 taken 16 times.
✓ Branch 50 → 51 taken 16 times.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 53 taken 16 times.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
16 auto *watcher = new QFutureWatcher<PreparedBody>(this);
266
1/1
✓ Branch 54 → 55 taken 16 times.
16 connect(watcher, &QFutureWatcher<PreparedBody>::finished, this,
267
1/1
✓ Branch 53 → 54 taken 16 times.
32 [this, watcher, mailId] {
268 // This future has exactly one parser result. Take its MIME tree and
269 // attachment payloads instead of copying the completed result back
270 // through the GUI callback.
271
2/2
✓ Branch 2 → 3 taken 15 times.
✓ Branch 3 → 4 taken 15 times.
15 auto prepared = watcher->future().takeResult();
272
1/1
✓ Branch 5 → 6 taken 15 times.
15 watcher->deleteLater();
273
2/2
✓ Branch 6 → 7 taken 15 times.
✓ Branch 8 → 9 taken 15 times.
15 MailJD::Performance::MailOpenPerformanceTracker::instance().mimeParseFinished(*mailId);
274
1/1
✓ Branch 11 → 12 taken 15 times.
15 finishPreparedBody(std::move(prepared.result), &prepared.message);
275 15 });
276
2/2
✓ Branch 59 → 60 taken 16 times.
✓ Branch 60 → 61 taken 16 times.
16 watcher->setFuture(QtConcurrent::run(&m_bodyPreparationPool,
277 32 [result = std::move(owned)]() mutable {
278
1/1
✓ Branch 2 → 3 taken 16 times.
16 auto message = MimeParser::parse(result.bytes);
279 32 return PreparedBody{std::move(message), std::move(result)};
280 16 }));
281 16 return;
282 16 }
283
1/1
✓ Branch 66 → 67 taken 11 times.
11 finishPreparedBody(result);
284 28 }
285
286 28 void MailBodyTransport::releaseBodyPreparation(quint64 requestId) {
287 28 const auto it = m_bodyPreparationBytesByRequest.constFind(requestId);
288
2/2
✓ Branch 5 → 6 taken 13 times.
✓ Branch 5 → 7 taken 15 times.
28 if (it == m_bodyPreparationBytesByRequest.constEnd()) return;
289 15 m_bodyPreparationBytes = qMax(0, m_bodyPreparationBytes - it.value());
290
1/1
✓ Branch 9 → 10 taken 15 times.
15 m_bodyPreparationBytesByRequest.erase(it);
291 15 m_bodyPreparations = qMax(0, m_bodyPreparations - 1);
292
1/1
✓ Branch 11 → 12 taken 15 times.
15 QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
293 }
294
295 // finishPreparedBody step: hand an admitted, parsed payload to the body
296 // writer; the writer's completion re-enters finishPreparedBody.
297 12 void MailBodyTransport::persistPreparedBody(BodyResult result, MimeMessage *prepared,
298 const MailId &pendingId) {
299
2/2
✓ Branch 2 → 3 taken 12 times.
✓ Branch 3 → 4 taken 12 times.
12 MailJD::Performance::MailOpenPerformanceTracker::instance().storeIndexStarted(pendingId);
300 // Move the payload into the writer and keep a byte-free terminal copy for
301 // the completion callback.
302 12 BodyResult terminal = std::move(result);
303 12 const QString databasePath = m_cache->databasePath();
304
1/1
✓ Branch 7 → 8 taken 12 times.
12 const auto writePriority = m_bodyRequests.hasDemandConsumer(terminal.requestId)
305
1/2
✓ Branch 8 → 9 taken 12 times.
✗ Branch 8 → 10 not taken.
12 ? MailBodyWriteService::Priority::Foreground
306 12 : MailBodyWriteService::Priority::Speculative;
307 // SELECT already established the current folder's authoritative epoch.
308 // The ordinary visible-body path need not enter the GUI SQLite writer
309 // connection just to rediscover it before the worker's identity-checked
310 // commit. Cross-folder and pre-SELECT requests still read the cache.
311 23 const quint32 uidValidity = pendingId.folderId == m_folder.currentFolderId &&
312
1/2
✓ Branch 12 → 13 taken 11 times.
✗ Branch 12 → 14 not taken.
11 m_folder.activeFolderUidValidity != 0
313
3/3
✓ Branch 11 → 12 taken 11 times.
✓ Branch 11 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
23 ? m_folder.activeFolderUidValidity : m_cache->uidValidity(pendingId.folderId);
314 // The callback must capture a byte-free terminal result regardless of
315 // C++ function-argument evaluation order. Moving bytes as another
316 // argument of write() could occur after the lambda copies terminal.
317 12 QByteArray raw = std::move(terminal.bytes);
318 12 MimeMessage message = std::move(*prepared);
319
2/2
✓ Branch 20 → 21 taken 12 times.
✓ Branch 28 → 29 taken 12 times.
36 MailBodyWriteService::shared()->write(this, databasePath, pendingId,
320 24 uidValidity, std::move(raw), std::move(message),
321
1/3
✓ Branch 23 → 24 taken 12 times.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
24 [this, terminal, databasePath](MailBodyWriteResult stored) mutable {
322
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 23 taken 12 times.
12 if (databasePath != m_cache->databasePath()) {
323 // P90.9: the payload is finished even when the namespace changed.
324 ✗ releaseBodyPreparation(terminal.requestId);
325 ✗ BodyResult cancelled = terminal;
326 ✗ cancelled.outcome = BodyOutcome::Cancelled;
327 ✗ const auto resolution = m_bodyRequests.complete(cancelled);
328 ✗ if (resolution)
329 ✗ m_deps.bodyRequestFinished(resolution->request.mailId, BodyOutcome::Cancelled,
330 ✗ QStringLiteral("Cache namespace changed during persistence"));
331 ✗ QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
332 ✗ return;
333 ✗ }
334
1/1
✓ Branch 25 → 26 taken 12 times.
12 finishPreparedBody(std::move(terminal), nullptr, &stored);
335 }, writePriority);
336 12 }
337
338 // finishPreparedBody step: present a failed/limited/cancelled body to the
339 // display consumer and queue one reconnect retry for transient failures.
340 // `persisted`: the payload reached the body writer (a local save failed).
341 14 void MailBodyTransport::finishFailedBody(const BodyRequestResolution &resolution,
342 bool persisted) {
343 14 const MailId mailId = resolution.request.mailId;
344
4/4
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 6 taken 6 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 7 times.
14 if (resolution.displayEligible && m_loadingPlaceholderTimer)
345
1/1
✓ Branch 5 → 6 taken 1 time.
1 m_loadingPlaceholderTimer->stop();
346
2/2
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 55 taken 6 times.
14 if (resolution.displayEligible) {
347
1/1
✓ Branch 7 → 8 taken 8 times.
8 const auto header = m_cache->header(mailId.folderId, mailId.uid);
348
4/8
✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 13 taken 3 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 13 taken 5 times.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 8 times.
8 if (header && m_view && resolution.result.outcome == BodyOutcome::PreparationLimited) {
349 ✗ m_deps.presentError(*header,
350 ✗ tr("Message content exceeds the supported processing limits. It has not been saved."), false);
351
4/8
✓ Branch 21 → 22 taken 5 times.
✓ Branch 21 → 25 taken 3 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 5 times.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 32 taken 8 times.
8 } else if (header && m_view && persisted) {
352 ✗ m_deps.presentError(*header, tr("Message content could not be saved locally. Retry."), true);
353
6/6
✓ Branch 33 → 34 taken 5 times.
✓ Branch 33 → 36 taken 3 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 4 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 40 taken 7 times.
8 } else if (header && resolution.result.outcome == BodyOutcome::TooLarge) {
354
1/1
✓ Branch 39 → 53 taken 1 time.
1 showMessageAboveDownloadLimit(*header, resolution.result.maxBytes);
355
4/6
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 44 taken 3 times.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 4 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 53 taken 7 times.
7 } else if (header && m_view) {
356 const QString reason =
357 ✗ resolution.result.outcome == BodyOutcome::Cancelled
358 ✗ ? tr("Loading was interrupted. Check the connection and retry.")
359 : tr("Message content could not be loaded. Retry when the "
360 ✗ "connection is available.");
361 ✗ m_deps.presentError(*header, reason, true);
362 ✗ }
363 8 }
364 14 m_deps.bodyRequestFinished(mailId, resolution.result.outcome,
365
1/1
✓ Branch 55 → 56 taken 14 times.
14 resolution.result.error);
366
367 const bool protocolFailure =
368
3/4
✓ Branch 65 → 66 taken 12 times.
✓ Branch 65 → 71 taken 1 time.
✓ Branch 83 → 84 taken 14 times.
✗ Branch 83 → 85 not taken.
41 resolution.result.error == QStringLiteral("Mailbox selection failed") ||
369
7/8
✓ Branch 60 → 61 taken 13 times.
✓ Branch 60 → 71 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 70 → 72 taken 11 times.
✓ Branch 79 → 80 taken 13 times.
✓ Branch 79 → 81 taken 1 time.
✓ Branch 81 → 82 taken 14 times.
✗ Branch 81 → 83 not taken.
68 resolution.result.error == QStringLiteral("Body fetch failed") ||
370
6/6
✓ Branch 73 → 74 taken 12 times.
✓ Branch 73 → 75 taken 2 times.
✓ Branch 75 → 76 taken 12 times.
✓ Branch 75 → 77 taken 2 times.
✓ Branch 77 → 78 taken 13 times.
✓ Branch 77 → 79 taken 1 time.
26 resolution.result.error == QStringLiteral("Body response was empty");
371 14 QList<BodyConsumer> retryableConsumers;
372
2/2
✓ Branch 101 → 87 taken 14 times.
✓ Branch 101 → 102 taken 14 times.
28 for (const BodyConsumer &consumer : resolution.consumers) {
373 const bool activeDisplay =
374
3/4
✓ Branch 88 → 89 taken 8 times.
✓ Branch 88 → 93 taken 6 times.
✓ Branch 91 → 92 taken 8 times.
✗ Branch 91 → 93 not taken.
22 consumer.purpose == BodyPurpose::Display &&
375
2/2
✓ Branch 94 → 95 taken 8 times.
✓ Branch 94 → 96 taken 6 times.
22 m_bodyRequests.selectedMailId() == resolution.request.mailId;
376
4/4
✓ Branch 96 → 97 taken 6 times.
✓ Branch 96 → 98 taken 8 times.
✓ Branch 97 → 98 taken 5 times.
✓ Branch 97 → 99 taken 1 time.
14 if (activeDisplay || consumer.purpose == BodyPurpose::Tab)
377
1/1
✓ Branch 98 → 99 taken 13 times.
13 retryableConsumers.append(consumer);
378 }
379 14 const bool transientFailure =
380
2/2
✓ Branch 102 → 103 taken 5 times.
✓ Branch 102 → 104 taken 9 times.
19 resolution.result.outcome == BodyOutcome::Failed ||
381
2/2
✓ Branch 103 → 104 taken 1 time.
✓ Branch 103 → 105 taken 4 times.
5 resolution.result.outcome == BodyOutcome::Cancelled;
382
4/4
✓ Branch 107 → 108 taken 7 times.
✓ Branch 107 → 114 taken 3 times.
✓ Branch 108 → 109 taken 4 times.
✓ Branch 108 → 114 taken 3 times.
10 if (transientFailure && !persisted && !protocolFailure &&
383
6/6
✓ Branch 106 → 107 taken 10 times.
✓ Branch 106 → 114 taken 4 times.
✓ Branch 110 → 111 taken 3 times.
✓ Branch 110 → 114 taken 1 time.
✓ Branch 115 → 116 taken 2 times.
✓ Branch 115 → 122 taken 12 times.
27 !m_bodyAutoRetried.contains(mailId) &&
384
2/2
✓ Branch 112 → 113 taken 2 times.
✓ Branch 112 → 114 taken 1 time.
3 !retryableConsumers.isEmpty()) {
385 4 m_bodyReconnectRetries.append(
386
1/3
✓ Branch 118 → 119 taken 2 times.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
2 PendingBodyRetry{resolution.request, retryableConsumers});
387
1/1
✓ Branch 120 → 121 taken 2 times.
2 m_bodyAutoRetried.insert(mailId);
388 }
389 14 }
390
391 39 void MailBodyTransport::finishPreparedBody(BodyResult result, MimeMessage *prepared,
392 const MailBodyWriteResult *persisted) {
393
1/1
✓ Branch 2 → 3 taken 39 times.
39 const auto pendingId = m_bodyRequests.mailIdForRequest(result.requestId);
394
5/6
✓ Branch 4 → 5 taken 14 times.
✓ Branch 4 → 14 taken 1 time.
✓ Branch 5 → 6 taken 14 times.
✗ Branch 5 → 14 not taken.
✓ Branch 7 → 8 taken 13 times.
✓ Branch 7 → 14 taken 1 time.
29 if (prepared && !prepared->limitExceeded && result.outcome == BodyOutcome::Loaded &&
395
8/9
✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 14 taken 24 times.
✓ Branch 10 → 11 taken 12 times.
✓ Branch 10 → 14 taken 1 time.
✓ Branch 11 → 12 taken 12 times.
✓ Branch 12 → 13 taken 12 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 12 times.
✓ Branch 15 → 22 taken 27 times.
68 pendingId && pendingId->accountId == m_folder.accountId && m_bodyRequests.matchesResult(result)) {
396
1/1
✓ Branch 19 → 20 taken 12 times.
12 persistPreparedBody(std::move(result), prepared, *pendingId);
397 12 return;
398 }
399 27 BodyResult completed = std::move(result);
400
4/4
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 34 taken 24 times.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 34 taken 2 times.
27 if (prepared && prepared->limitExceeded) {
401 1 completed.outcome = BodyOutcome::PreparationLimited;
402
1/1
✓ Branch 26 → 27 taken 1 time.
1 completed.bytes.clear();
403 1 completed.error = QStringLiteral("MIME preparation limit exceeded");
404 }
405
4/4
✓ Branch 34 → 35 taken 12 times.
✓ Branch 34 → 40 taken 15 times.
✓ Branch 35 → 36 taken 3 times.
✓ Branch 35 → 40 taken 9 times.
27 if (persisted && persisted->outcome != MailBodyWriteResult::Outcome::Stored) {
406 6 completed.outcome = persisted->outcome == MailBodyWriteResult::Outcome::Limited
407
1/2
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 3 times.
3 ? BodyOutcome::PreparationLimited : BodyOutcome::Failed;
408 3 completed.error = persisted->error;
409 }
410
1/1
✓ Branch 40 → 41 taken 27 times.
27 const auto resolution = m_bodyRequests.complete(completed);
411 // P90.9: every converged payload releases its pipeline accounting —
412 // admitted or not (the lookup is idempotent for never-admitted results).
413
1/1
✓ Branch 41 → 42 taken 27 times.
27 releaseBodyPreparation(completed.requestId);
414
2/2
✓ Branch 43 → 44 taken 3 times.
✓ Branch 43 → 57 taken 24 times.
27 if (!resolution) {
415
5/5
✓ Branch 44 → 45 taken 3 times.
✓ Branch 48 → 49 taken 3 times.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 54 → 46 taken 3 times.
✓ Branch 54 → 55 taken 3 times.
6 qCInfo(lcController) << "Ignoring completed unknown body request"
416
1/1
✓ Branch 50 → 51 taken 3 times.
3 << result.requestId;
417
1/1
✓ Branch 55 → 56 taken 3 times.
3 QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
418 3 return;
419 }
420
421 24 const MailId mailId = resolution->request.mailId;
422
2/2
✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 63 taken 23 times.
24 if (mailId.accountId != m_folder.accountId) {
423
1/1
✓ Branch 61 → 62 taken 1 time.
1 QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
424 1 return;
425 }
426 auto &performanceTracker =
427
1/1
✓ Branch 63 → 64 taken 23 times.
23 MailJD::Performance::MailOpenPerformanceTracker::instance();
428
4/4
✓ Branch 64 → 65 taken 22 times.
✓ Branch 64 → 70 taken 1 time.
✓ Branch 65 → 66 taken 10 times.
✓ Branch 65 → 70 taken 12 times.
23 if (!prepared && !persisted)
429 10 performanceTracker.transportFinished(
430
1/1
✓ Branch 69 → 70 taken 10 times.
10 mailId, resolution->result.outcome == BodyOutcome::Loaded,
431 10 resolution->result.bytes.size());
432
2/2
✓ Branch 71 → 72 taken 14 times.
✓ Branch 71 → 73 taken 9 times.
23 if (resolution->result.outcome != BodyOutcome::Loaded)
433
1/1
✓ Branch 72 → 73 taken 14 times.
14 performanceTracker.fail(mailId);
434
2/2
✓ Branch 74 → 75 taken 9 times.
✓ Branch 74 → 130 taken 14 times.
23 if (resolution->result.outcome == BodyOutcome::Loaded) {
435
1/2
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 85 taken 9 times.
9 if (!persisted) {
436 ✗ performanceTracker.fail(mailId);
437 ✗ m_deps.bodyRequestFinished(mailId, BodyOutcome::Failed,
438 ✗ QStringLiteral("Body response was not admitted for persistence"));
439 ✗ QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
440 ✗ return;
441 }
442 9 const auto &header = persisted->header;
443
1/2
✗ Branch 86 → 87 not taken.
✓ Branch 86 → 95 taken 9 times.
9 if (!header) {
444 ✗ m_deps.bodyRequestFinished(mailId, BodyOutcome::Cancelled,
445 ✗ QStringLiteral("Message no longer exists after preparation"));
446 ✗ QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
447 ✗ return;
448 }
449 9 const MailBody &body = persisted->body;
450
1/1
✓ Branch 95 → 96 taken 9 times.
9 m_model->setHasAttachments(mailId.uid, mailId.folderId, persisted->hasVisibleAttachment);
451
1/1
✓ Branch 97 → 98 taken 9 times.
9 performanceTracker.storeIndexFinished(mailId, body.attachments.size(), persisted->inlineImageCount);
452
453
2/2
✓ Branch 99 → 100 taken 7 times.
✓ Branch 99 → 114 taken 2 times.
9 if (resolution->displayEligible) {
454
1/2
✗ Branch 100 → 101 not taken.
✓ Branch 100 → 102 taken 7 times.
7 if (m_loadingPlaceholderTimer)
455 ✗ m_loadingPlaceholderTimer->stop();
456
1/2
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 105 taken 7 times.
7 if (m_view) {
457 ✗ m_deps.presentMail(*header, body);
458 }
459
1/1
✓ Branch 105 → 106 taken 7 times.
7 const auto loadedFlags = m_model->flagsFor(mailId.uid, mailId.folderId);
460
1/1
✓ Branch 112 → 113 taken 7 times.
7 m_deps.markMailAsSeenInFolder(mailId.uid, mailId.folderId,
461
2/2
✓ Branch 107 → 108 taken 4 times.
✓ Branch 107 → 111 taken 3 times.
14 !loadedFlags ? std::optional<bool>(header->isSeen()) : std::nullopt);
462 }
463
1/1
✓ Branch 114 → 115 taken 9 times.
9 m_deps.bodyLoaded(mailId.uid, mailId.folderId);
464
1/1
✓ Branch 116 → 117 taken 9 times.
9 m_deps.bodyRequestFinished(mailId, BodyOutcome::Loaded, {});
465
1/1
✓ Branch 122 → 123 taken 9 times.
18 const bool prefetchConsumer = std::any_of(
466 18 resolution->consumers.cbegin(), resolution->consumers.cend(),
467 9 [](const BodyConsumer &consumer) {
468 9 return consumer.purpose == BodyPurpose::Prefetch;
469 });
470
2/6
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 127 taken 9 times.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 128 → 129 not taken.
✓ Branch 128 → 132 taken 9 times.
9 if (prefetchConsumer && !resolution->displayEligible)
471 ✗ performanceTracker.rememberPrefetch(mailId);
472 } else {
473
1/1
✓ Branch 131 → 132 taken 14 times.
14 finishFailedBody(*resolution, persisted != nullptr);
474 }
475
476
1/1
✓ Branch 132 → 133 taken 23 times.
23 QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
477 48 }
478
479 // S92.5.2: the body connection failed before the FETCH was written (probe
480 // before use found a closed socket). Nothing reached the server and the
481 // connection is reconnecting: queue the same consumers again without a
482 // failure presentation; the request dispatches once the body connection is
483 // authenticated again.
484 1 void MailBodyTransport::requeueUndispatchedBody(const BodyResult &result) {
485
1/1
✓ Branch 2 → 3 taken 1 time.
1 const auto resolution = m_bodyRequests.complete(result);
486
1/1
✓ Branch 3 → 4 taken 1 time.
1 releaseBodyPreparation(result.requestId);
487
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
1 if (!resolution)
488 ✗ return;
489
6/6
✓ Branch 7 → 8 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 19 → 9 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
2 qCInfo(lcController) << "Requeueing body request" << result.requestId
490
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
1 << "not dispatched:" << result.error;
491
2/2
✓ Branch 32 → 23 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
2 for (const BodyConsumer &consumer : resolution->consumers) {
492
1/4
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 1 time.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
1 if (consumer.purpose == BodyPurpose::Prefetch && m_prefetchSuspended)
493 ✗ continue;
494
1/1
✓ Branch 29 → 30 taken 1 time.
1 m_bodyRequests.request(resolution->request.mailId, resolution->request.folderPath,
495 1 consumer.purpose, consumer.consumerId);
496 }
497
1/1
✓ Branch 33 → 34 taken 1 time.
1 QTimer::singleShot(0, this, &MailBodyTransport::dispatchNextBodyRequest);
498 1 }
499
500 3 void MailBodyTransport::retryBodiesAfterReconnect() {
501 const QList<PendingBodyRetry> retries =
502 3 std::exchange(m_bodyReconnectRetries, {});
503
2/2
✓ Branch 29 → 7 taken 1 time.
✓ Branch 29 → 30 taken 3 times.
4 for (const PendingBodyRetry &pending : retries) {
504
2/2
✓ Branch 26 → 10 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
2 for (const BodyConsumer &consumer : pending.consumers) {
505
1/2
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 16 not taken.
2 if (consumer.purpose == BodyPurpose::Display &&
506
3/6
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 19 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 1 time.
2 m_bodyRequests.selectedMailId() != pending.request.mailId) {
507 ✗ continue;
508 }
509
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 1 time.
1 if (consumer.purpose == BodyPurpose::Prefetch)
510 ✗ continue;
511 1 m_bodyRequests.request(pending.request.mailId,
512 1 pending.request.folderPath, consumer.purpose,
513
1/1
✓ Branch 23 → 24 taken 1 time.
1 consumer.consumerId);
514 }
515 }
516 3 }
517
518 3 void MailBodyTransport::setPrefetchSuspended(bool suspended) {
519
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
3 if (m_prefetchSuspended == suspended) return;
520 3 m_prefetchSuspended = suspended;
521
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 1 time.
3 if (suspended)
522 2 m_bodyRequests.removeConsumer(BodyPurpose::Prefetch, 0);
523 }
524
525 3 void MailBodyTransport::setPrefetchConservative(bool conservative) {
526 3 m_prefetchConservative = conservative;
527 3 }
528
529 41 void MailBodyTransport::prefetchAdjacent(int currentRow) {
530 // T-119: Don't prefetch during active sync (avoids unnecessary body fetches
531 // that compete with header streaming for IMAP bandwidth)
532
2/2
✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 4 taken 9 times.
41 if (m_fetchInProgress)
533 33 return;
534 // T-87.G2: hidden windows drop queued speculation and issue nothing.
535
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 8 times.
9 if (m_prefetchSuspended)
536 1 return;
537
538 // Weight the few candidates by the current navigation direction so the
539 // likely next messages are requested first; conservative mode keeps only
540 // the immediate neighbours.
541 8 int direction = 1;
542
4/4
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 12 taken 4 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 12 taken 1 time.
8 if (m_lastNavigationRow >= 0 && currentRow != m_lastNavigationRow)
543
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 1 time.
3 direction = currentRow > m_lastNavigationRow ? 1 : -1;
544 8 m_lastNavigationRow = currentRow;
545
546 // Queued speculative work belongs to the previous adjacency window. An
547 // already-dispatched fetch may finish and populate cache, but it no longer
548 // has a prefetch consumer and can never affect display or Seen state.
549
1/1
✓ Branch 12 → 13 taken 8 times.
8 m_bodyRequests.removeConsumer(BodyPurpose::Prefetch, 0);
550
551 8 const QList<int> offsets = m_prefetchConservative
552
2/2
✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 15 taken 3 times.
8 ? QList<int>{direction, -direction}
553
2/2
✓ Branch 14 → 16 taken 5 times.
✓ Branch 15 → 16 taken 3 times.
8 : QList<int>{direction, -direction, 2 * direction};
554
2/2
✓ Branch 68 → 18 taken 19 times.
✓ Branch 68 → 69 taken 8 times.
27 for (int offset : offsets) {
555 19 int r = currentRow + offset;
556
6/7
✓ Branch 19 → 20 taken 16 times.
✓ Branch 19 → 23 taken 3 times.
✓ Branch 21 → 22 taken 16 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 16 times.
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 27 taken 16 times.
19 if (r < 0 || r >= m_model->rowCount())
557 6 continue;
558
559
1/1
✓ Branch 27 → 28 taken 16 times.
16 auto *h = m_model->headerAt(r);
560
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 16 times.
16 if (!h)
561 ✗ continue;
562 // Speculation carries a tighter byte budget than demand fetches: an
563 // oversized message is served on explicit open, never prefetched.
564
2/2
✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 45 taken 13 times.
16 if (h->size > PrefetchMaximumBytes) {
565 6 qCInfo(lcController) << "Skipping speculative prefetch beyond byte budget for UID" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; size guard remains measured.
566
3/3
✓ Branch 37 → 38 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 39 → 40 taken 3 times.
3 << h->uid << "size" << h->size;
567 3 continue;
568 3 }
569
2/3
✓ Branch 45 → 46 taken 13 times.
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 61 taken 13 times.
13 if (rejectMessageAboveDownloadLimit(*h, false)) {
570 ✗ qCInfo(lcController) << "Skipping oversized body prefetch for UID" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; size guard remains measured.
571 ✗ << h->uid << "size" << h->size;
572 ✗ continue;
573 ✗ }
574
575 13 const MailId mailId{m_folder.accountId, m_folder.currentFolderId, h->uid};
576
1/1
✓ Branch 62 → 63 taken 13 times.
13 requestBodyForConsumer(mailId, BodyPurpose::Prefetch);
577 13 }
578 8 }
579
580 ✗ void MailBodyTransport::prefetchIndexedAdjacent(const MailId &mailId) {
581 ✗ if (m_fetchInProgress || m_prefetchSuspended || !m_indexedAdjacentHeaders ||
582 ✗ mailId.accountId != m_folder.accountId || mailId.folderId != m_folder.currentFolderId) return;
583 ✗ const auto candidates = m_indexedAdjacentHeaders(mailId);
584 ✗ m_bodyRequests.removeConsumer(BodyPurpose::Prefetch, 0);
585 ✗ const int limit = m_prefetchConservative ? 2 : 3;
586 ✗ for (int i = 0; i < candidates.size() && i < limit; ++i) {
587 ✗ const auto &header = candidates.at(i);
588 ✗ if (header.folderId != mailId.folderId || header.uid <= 0 ||
589 ✗ header.size > PrefetchMaximumBytes ||
590 ✗ rejectMessageAboveDownloadLimit(header, false)) continue;
591 ✗ requestBodyForConsumer({m_folder.accountId, header.folderId, header.uid},
592 BodyPurpose::Prefetch);
593 }
594 ✗ }
595
596 5 bool MailBodyTransport::downloadAttachment(qint64 attachmentId,
597 const QString &savePath,
598 bool overwriteExisting) {
599 auto &performanceTracker =
600
1/1
✓ Branch 2 → 3 taken 5 times.
5 MailJD::Performance::ContentPerformanceTracker::instance();
601
1/1
✓ Branch 3 → 4 taken 5 times.
5 performanceTracker.attachmentFileStarted(0);
602
1/1
✓ Branch 5 → 6 taken 5 times.
5 QByteArray data = m_cache->attachments().attachmentData(attachmentId);
603
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 24 taken 4 times.
5 if (data.isEmpty()) {
604
1/1
✓ Branch 8 → 9 taken 1 time.
1 performanceTracker.attachmentFileFinished(false, 0);
605
6/6
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 19 → 11 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
2 qCWarning(lcController) << "No data for attachment ID" << attachmentId;
606
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
1 m_deps.statusMessage(tr("Failed to save attachment"));
607 1 return false;
608 }
609
610 4 QString writeError;
611 4 const auto policy =
612 overwriteExisting
613
2/2
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 26 taken 1 time.
4 ? AttachmentFileSecurity::ExistingFilePolicy::Replace
614 : AttachmentFileSecurity::ExistingFilePolicy::FailIfExists;
615
3/3
✓ Branch 27 → 28 taken 4 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 55 taken 2 times.
4 if (AttachmentFileSecurity::writeAtomically(savePath, data, &writeError,
616 policy)) {
617
1/1
✓ Branch 30 → 31 taken 2 times.
2 performanceTracker.attachmentFileFinished(true, data.size());
618
7/7
✓ Branch 31 → 32 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 38 → 39 taken 2 times.
✓ Branch 43 → 33 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
4 qCInfo(lcController) << "Saved attachment" << attachmentId << "to"
619
1/1
✓ Branch 39 → 40 taken 2 times.
2 << savePath;
620
1/1
✓ Branch 49 → 50 taken 2 times.
2 m_deps.statusMessage(
621
4/4
✓ Branch 44 → 45 taken 2 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
✓ Branch 48 → 49 taken 2 times.
6 QString("Attachment saved: %1").arg(QFileInfo(savePath).fileName()));
622 2 return true;
623 }
624
625
6/6
✓ Branch 55 → 56 taken 2 times.
✓ Branch 59 → 60 taken 2 times.
✓ Branch 60 → 61 taken 2 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 67 → 57 taken 2 times.
✓ Branch 67 → 68 taken 2 times.
4 qCWarning(lcController) << "Failed to write attachment to" << savePath
626
2/2
✓ Branch 62 → 63 taken 2 times.
✓ Branch 63 → 64 taken 2 times.
2 << ":" << writeError;
627
1/1
✓ Branch 69 → 70 taken 2 times.
2 performanceTracker.attachmentFileFinished(false, data.size());
628
2/2
✓ Branch 70 → 71 taken 2 times.
✓ Branch 71 → 72 taken 2 times.
2 m_deps.statusMessage(tr("Failed to save attachment"));
629 2 return false;
630 5 }
631