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: 95.7% 337 / 0 / 352
Functions: 97.4% 37 / 0 / 38
Branches: 86.1% 384 / 0 / 446

service/SettingsSyncService.cpp
Line Branch Exec Source
1 #include "service/SettingsSyncService.h"
2
3 #include <QLoggingCategory>
4
5 #include <algorithm>
6 #include <functional>
7 #include <QPointer>
8 #include <utility>
9
10 #include "service/ConnectionHealthMonitor.h"
11 #include "service/ImapService.h"
12
13
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 317 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
321 Q_LOGGING_CATEGORY(lcSync, "mailjd.sync")
14
15 static constexpr const char *SYNC_SUBJECT = "X-MailJD-Settings-Sync";
16
17 // ═══════════════════════════════════════════════════════
18 // Construction / Configuration
19 // ═══════════════════════════════════════════════════════
20
21
1/1
✓ Branch 3 → 4 taken 32 times.
32 SettingsSyncService::SettingsSyncService(QObject *parent) : QObject(parent) {
22 // T-720: The reconnect timer/handleDisconnect() plumbing is gone —
23 // ConnectionHealthMonitor owns both the liveness probe + the
24 // exponential-backoff reconnect. The service owns the monitor.
25 32 }
26
27 44 SettingsSyncService::~SettingsSyncService() { shutdown(); }
28
29 23 void SettingsSyncService::configure(const ImapConfig &config,
30 const QString &syncFolder) {
31 23 retireTransport(ImapService::DisconnectReason::AccountReload);
32 23 m_config = config;
33 23 m_syncFolder = syncFolder;
34
35
2/5
✓ Branch 6 → 7 taken 23 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 23 times.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
23 m_imap = new ImapService(this);
36 23 m_imap->setAutoIdle(false); // We manage IDLE ourselves
37
38 // Wire up signals
39 23 connect(m_imap, &ImapService::stateChanged, this,
40
1/1
✓ Branch 10 → 11 taken 23 times.
23 &SettingsSyncService::onStateChanged);
41 23 connect(m_imap, &ImapService::folderListReceived, this,
42
1/1
✓ Branch 12 → 13 taken 23 times.
23 &SettingsSyncService::onFolderListReceived);
43 23 connect(m_imap, &ImapService::folderSelected, this,
44
1/1
✓ Branch 14 → 15 taken 23 times.
23 &SettingsSyncService::onFolderSelected);
45 23 connect(m_imap, &ImapService::folderCreated, this,
46
1/1
✓ Branch 16 → 17 taken 23 times.
23 &SettingsSyncService::onFolderCreated);
47 23 connect(m_imap, &ImapService::searchResultReceived, this,
48
1/1
✓ Branch 18 → 19 taken 23 times.
23 &SettingsSyncService::onSearchResultReceived);
49 23 connect(m_imap, &ImapService::rawBodyReceived, this,
50
1/1
✓ Branch 20 → 21 taken 23 times.
23 &SettingsSyncService::onRawBodyReceived);
51 23 connect(m_imap, &ImapService::messageAppended, this,
52
1/1
✓ Branch 22 → 23 taken 23 times.
23 &SettingsSyncService::onMessageAppended);
53 23 connect(m_imap, &ImapService::appendError, this,
54
1/1
✓ Branch 24 → 25 taken 23 times.
23 &SettingsSyncService::onAppendError);
55 23 connect(m_imap, &ImapService::folderOperationError, this,
56
1/1
✓ Branch 26 → 27 taken 23 times.
23 &SettingsSyncService::onFolderOperationError);
57 23 connect(m_imap, &ImapService::idleNewMessages, this,
58
1/1
✓ Branch 28 → 29 taken 23 times.
23 &SettingsSyncService::onIdleNewMessages);
59 23 connect(m_imap, &ImapService::expungeComplete, this,
60
1/1
✓ Branch 30 → 31 taken 23 times.
23 &SettingsSyncService::onExpungeComplete);
61 23 connect(m_imap, &ImapService::expungeFailed, this,
62
1/1
✓ Branch 32 → 33 taken 23 times.
23 &SettingsSyncService::onExpungeFailed);
63 // T-720: errorOccurred still resets the phase, but reconnect scheduling
64 // is delegated to the monitor.
65 23 connect(m_imap, &ImapService::errorOccurred, this,
66
1/1
✓ Branch 34 → 35 taken 23 times.
23 [this](const QString &err) {
67 ✗ qCWarning(lcSync) << "IMAP error:" << err;
68 ✗ if (m_phase == Phase::CleaningUp) {
69 ✗ onExpungeFailed(err);
70 ✗ return;
71 }
72 ✗ m_phase = Phase::Idle;
73 ✗ emit syncError(err);
74 });
75
76 // T-720: (Re)create the monitor for the new service and install the
77 // reconnect config. Activation follows m_enabled — see setEnabled().
78 // systemWatchEnabled=false: secondary monitor — the main connection
79 // (MainWindow) owns the single set of system-wide suspend/network hooks.
80
2/2
✓ Branch 36 → 37 taken 21 times.
✓ Branch 36 → 42 taken 2 times.
23 if (!m_health)
81
2/5
✓ Branch 38 → 39 taken 21 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 21 times.
✗ Branch 77 → 78 not taken.
✗ Branch 77 → 79 not taken.
21 m_health = new ConnectionHealthMonitor(false, this);
82 23 m_health->attach(m_imap);
83 23 m_health->setReconnectConfig(m_config);
84 23 m_health->setActive(m_enabled);
85
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 23 times.
23 if (m_enabled)
86 ✗ m_imap->connectToServer(m_config);
87 23 }
88
89 18 void SettingsSyncService::setEnabled(bool enabled) {
90
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 17 times.
18 if (m_enabled == enabled)
91 1 return;
92
6/8
✓ Branch 4 → 5 taken 14 times.
✓ Branch 4 → 9 taken 3 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 13 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 17 times.
17 if (enabled && !m_imap && !m_config.host.isEmpty())
93 ✗ configure(m_config, m_syncFolder);
94 17 m_enabled = enabled;
95
96
4/4
✓ Branch 12 → 13 taken 14 times.
✓ Branch 12 → 26 taken 3 times.
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 26 taken 1 time.
17 if (enabled && m_imap) {
97
5/5
✓ Branch 14 → 15 taken 13 times.
✓ Branch 18 → 19 taken 13 times.
✓ Branch 19 → 20 taken 13 times.
✓ Branch 23 → 16 taken 13 times.
✓ Branch 23 → 24 taken 13 times.
26 qCInfo(lcSync) << "Settings sync enabled, connecting...";
98 13 m_imap->connectToServer(m_config);
99
2/2
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 38 taken 1 time.
17 } else if (!enabled) {
100
5/5
✓ Branch 27 → 28 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 36 → 29 taken 3 times.
✓ Branch 36 → 37 taken 3 times.
6 qCInfo(lcSync) << "Settings sync disabled";
101 3 retireTransport(ImapService::DisconnectReason::UserRequested);
102 }
103 // T-720: Reflect the new enabled state on the monitor so reconnect is
104 // only active while the user has sync turned on.
105
2/2
✓ Branch 38 → 39 taken 15 times.
✓ Branch 38 → 40 taken 2 times.
17 if (m_health)
106 15 m_health->setActive(m_enabled);
107 }
108
109 37 void SettingsSyncService::shutdown() {
110 37 m_enabled = false;
111 37 retireTransport(ImapService::DisconnectReason::ApplicationShutdown);
112 37 }
113
114 63 void SettingsSyncService::retireTransport(ImapService::DisconnectReason reason) {
115 63 ++m_generation;
116 63 m_phase = Phase::Idle;
117 63 m_pendingUpload.clear();
118 63 m_currentSettingsUid = -1;
119 63 m_newAppendedUid = -1;
120 63 m_validatedSettingsUid = -1;
121 63 m_validatedUidValidity = 0;
122 63 m_previousSettingsUid = -1;
123 63 m_uploadUidValidity = 0;
124 63 m_messageCount = 0;
125
2/2
✓ Branch 3 → 4 taken 28 times.
✓ Branch 3 → 6 taken 35 times.
63 if (m_health) {
126 28 m_health->setActive(false);
127 28 m_health->detach();
128 }
129
2/2
✓ Branch 7 → 8 taken 25 times.
✓ Branch 7 → 12 taken 38 times.
63 if (auto *retired = std::exchange(m_imap, nullptr)) {
130 // deleteLater keeps this socket alive until the event loop drains. Cut
131 // every service callback as well as the monitor before disconnecting it.
132 25 QObject::disconnect(retired, nullptr, this, nullptr);
133 25 retired->clearDeferredCommands();
134 25 retired->disconnect(reason);
135 25 retired->deleteLater();
136 }
137 63 }
138
139 51 void SettingsSyncService::afterIdle(std::function<void()> command) {
140
2/4
✓ Branch 2 → 3 taken 51 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 51 times.
51 if (!m_imap || !m_enabled)
141 ✗ return;
142
1/1
✓ Branch 5 → 6 taken 51 times.
51 QPointer<SettingsSyncService> guard(this);
143 51 const auto generation = m_generation;
144
2/4
✓ Branch 9 → 10 taken 51 times.
✓ Branch 10 → 11 taken 51 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
51 m_imap->executeAfterIdle([guard, generation, command = std::move(command)] {
145
4/8
✓ Branch 3 → 4 taken 50 times.
✗ Branch 3 → 11 not taken.
✓ Branch 5 → 6 taken 50 times.
✗ Branch 5 → 11 not taken.
✓ Branch 7 → 8 taken 50 times.
✗ Branch 7 → 11 not taken.
✓ Branch 12 → 13 taken 50 times.
✗ Branch 12 → 14 not taken.
100 if (guard && guard->m_generation == generation && guard->m_enabled &&
146
1/2
✓ Branch 9 → 10 taken 50 times.
✗ Branch 9 → 11 not taken.
50 guard->m_imap)
147 50 command();
148 50 });
149 51 }
150
151 // ═══════════════════════════════════════════════════════
152 // IMAP State Machine
153 // ═══════════════════════════════════════════════════════
154
155 89 void SettingsSyncService::onStateChanged(ImapService::State state) {
156
2/2
✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 15 taken 76 times.
89 if (state == ImapService::State::Authenticated) {
157
5/5
✓ Branch 3 → 4 taken 13 times.
✓ Branch 7 → 8 taken 13 times.
✓ Branch 8 → 9 taken 13 times.
✓ Branch 12 → 5 taken 13 times.
✓ Branch 12 → 13 taken 13 times.
26 qCInfo(lcSync) << "Sync IMAP authenticated, checking folder...";
158 13 emit connected();
159 13 checkOrCreateFolder();
160
4/4
✓ Branch 15 → 16 taken 75 times.
✓ Branch 15 → 17 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 74 times.
76 } else if (state == ImapService::State::Disconnected ||
161 state == ImapService::State::Error) {
162 // T-720: Keep the phase reset so a stale WatchingIdle phase cannot
163 // receive push notifications on the next (reconnected) session.
164 // Reconnect scheduling itself moved to ConnectionHealthMonitor.
165 2 m_phase = Phase::Idle;
166 2 m_validatedSettingsUid = -1;
167 2 m_validatedUidValidity = 0;
168
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 2 times.
2 if (m_enabled) emit connectionLost();
169 }
170 89 }
171
172 // ═══════════════════════════════════════════════════════
173 // Phase: Check / Create sync folder
174 // ═══════════════════════════════════════════════════════
175
176 13 void SettingsSyncService::checkOrCreateFolder() {
177 13 m_phase = Phase::CheckingFolder;
178
6/6
✓ Branch 2 → 3 taken 13 times.
✓ Branch 6 → 7 taken 13 times.
✓ Branch 7 → 8 taken 13 times.
✓ Branch 8 → 9 taken 13 times.
✓ Branch 12 → 4 taken 13 times.
✓ Branch 12 → 13 taken 13 times.
26 qCInfo(lcSync) << "Checking if sync folder exists:" << m_syncFolder;
179 13 m_imap->listFolders(); // Will trigger onFolderListReceived
180 13 }
181
182 15 void SettingsSyncService::onFolderListReceived(
183 const QList<FolderInfo> &folders) {
184
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 14 times.
15 if (m_phase != Phase::CheckingFolder)
185 1 return;
186
187 14 bool found = false;
188
2/2
✓ Branch 12 → 6 taken 15 times.
✓ Branch 12 → 13 taken 1 time.
16 for (const auto &f : folders) {
189
2/2
✓ Branch 8 → 9 taken 13 times.
✓ Branch 8 → 10 taken 2 times.
15 if (f.path == m_syncFolder) {
190 13 found = true;
191 13 break;
192 }
193 }
194
195
2/2
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 25 taken 1 time.
14 if (found) {
196
5/5
✓ Branch 14 → 15 taken 13 times.
✓ Branch 18 → 19 taken 13 times.
✓ Branch 19 → 20 taken 13 times.
✓ Branch 23 → 16 taken 13 times.
✓ Branch 23 → 24 taken 13 times.
26 qCInfo(lcSync) << "Sync folder exists, selecting...";
197 13 m_phase = Phase::SelectingFolder;
198 13 m_imap->selectFolder(m_syncFolder);
199 } else {
200
6/6
✓ Branch 25 → 26 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 35 → 27 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
2 qCInfo(lcSync) << "Sync folder not found, creating:" << m_syncFolder;
201 1 m_phase = Phase::CreatingFolder;
202 1 m_imap->createFolder(m_syncFolder);
203 }
204 }
205
206 2 void SettingsSyncService::onFolderCreated(const QString &folderPath) {
207
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (m_phase != Phase::CreatingFolder)
208 ✗ return;
209
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 17 taken 1 time.
2 if (folderPath == m_syncFolder) {
210
5/5
✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 8 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
2 qCInfo(lcSync) << "Sync folder created, selecting...";
211 1 m_phase = Phase::SelectingFolder;
212 1 m_imap->selectFolder(m_syncFolder);
213 }
214 }
215
216 19 void SettingsSyncService::onFolderSelected(const QString &path,
217 int messageCount,
218 quint32 uidValidity,
219 quint64 /*highestModseq*/) {
220
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 18 times.
19 if (path != m_syncFolder)
221 1 return;
222
223 18 m_messageCount = messageCount;
224
6/6
✓ Branch 5 → 6 taken 18 times.
✓ Branch 9 → 10 taken 18 times.
✓ Branch 10 → 11 taken 18 times.
✓ Branch 11 → 12 taken 18 times.
✓ Branch 15 → 7 taken 18 times.
✓ Branch 15 → 16 taken 18 times.
36 qCInfo(lcSync) << "Sync folder selected, messages:" << messageCount;
225
226 18 emit syncFolderReady();
227
228
2/2
✓ Branch 17 → 18 taken 14 times.
✓ Branch 17 → 19 taken 4 times.
18 if (m_phase == Phase::SelectingFolder) {
229 // Initial connect: fetch latest settings, then start watching
230 14 doFetch();
231
2/2
✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 49 taken 1 time.
4 } else if (m_phase == Phase::CleaningUp) {
232 // APPEND may race another client or a deleted/recreated mailbox. Only
233 // prune the previously validated payload in the same mailbox epoch.
234
4/4
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 23 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 1 time.
3 const bool sameEpoch = uidValidity != 0 && uidValidity == m_uploadUidValidity;
235
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 2 times.
3 m_validatedSettingsUid = sameEpoch ? m_newAppendedUid : -1;
236
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 2 times.
3 m_validatedUidValidity = sameEpoch ? uidValidity : 0;
237
1/2
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 36 not taken.
1 if (sameEpoch && m_previousSettingsUid > 0 &&
238
6/8
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 36 taken 2 times.
✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 36 not taken.
✓ Branch 34 → 35 taken 1 time.
✗ Branch 34 → 36 not taken.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 48 taken 2 times.
4 m_previousSettingsUid < m_newAppendedUid && m_imap->hasUidExpungeCapability()) {
239
1/1
✓ Branch 41 → 42 taken 1 time.
2 m_imap->storeFlag(m_previousSettingsUid, QStringLiteral("\\Deleted"), true);
240
2/2
✓ Branch 44 → 45 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
1 m_imap->expungeUids({m_previousSettingsUid}, m_syncFolder, uidValidity);
241 } else {
242 2 finishUpload();
243 }
244 }
245 }
246
247 // ═══════════════════════════════════════════════════════
248 // Phase: Fetch latest settings
249 // ═══════════════════════════════════════════════════════
250
251 31 void SettingsSyncService::fetchLatestSettings() {
252
3/4
✓ Branch 2 → 3 taken 30 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 30 times.
31 if (!m_imap || !m_enabled)
253 1 return;
254
4/4
✓ Branch 5 → 6 taken 29 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 28 times.
30 if (m_phase != Phase::Idle && m_phase != Phase::WatchingIdle) return;
255 // Reserve admission before DONE completes. Another foreground request or
256 // IDLE notification must not replace this operation while it is queued.
257 29 m_phase = Phase::Fetching;
258
259
1/1
✓ Branch 9 → 10 taken 29 times.
29 afterIdle([this]() {
260 // Make sure we're in the right folder
261
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 28 times.
29 if (m_imap->selectedFolder() != m_syncFolder) {
262 1 m_phase = Phase::SelectingFolder;
263 1 m_imap->selectFolder(m_syncFolder);
264 } else {
265 28 doFetch();
266 }
267 29 });
268 }
269
270 44 void SettingsSyncService::doFetch() {
271 44 m_phase = Phase::Fetching;
272 44 m_currentSettingsUid = -1;
273
274
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 16 taken 30 times.
44 if (m_messageCount == 0) {
275
5/5
✓ Branch 3 → 4 taken 14 times.
✓ Branch 7 → 8 taken 14 times.
✓ Branch 8 → 9 taken 14 times.
✓ Branch 12 → 5 taken 14 times.
✓ Branch 12 → 13 taken 14 times.
28 qCInfo(lcSync) << "Sync folder is empty, no settings to fetch";
276 14 startIdleWatch();
277 14 emit fetchComplete(false);
278 14 return;
279 }
280
281 // Search for settings messages
282
5/5
✓ Branch 16 → 17 taken 30 times.
✓ Branch 20 → 21 taken 30 times.
✓ Branch 21 → 22 taken 30 times.
✓ Branch 25 → 18 taken 30 times.
✓ Branch 25 → 26 taken 30 times.
60 qCInfo(lcSync) << "Searching for settings messages...";
283
2/2
✓ Branch 30 → 31 taken 30 times.
✓ Branch 31 → 32 taken 30 times.
60 m_imap->searchText(QString::fromLatin1(SYNC_SUBJECT),
284 60 QStringLiteral("SUBJECT"));
285 }
286
287 35 void SettingsSyncService::onSearchResultReceived(const QList<qint64> &uids) {
288
2/2
✓ Branch 2 → 3 taken 31 times.
✓ Branch 2 → 23 taken 4 times.
35 if (m_phase == Phase::Fetching) {
289
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 18 taken 30 times.
31 if (uids.isEmpty()) {
290
5/5
✓ Branch 5 → 6 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 7 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
2 qCInfo(lcSync) << "No settings messages found";
291 1 startIdleWatch();
292 1 emit fetchComplete(false);
293 1 return;
294 }
295
296 // Newest (highest UID) first; older ones are fallbacks for a newest
297 // message that was delivered by mail instead of uploaded by a client.
298 30 m_candidateUids = uids;
299 30 std::sort(m_candidateUids.begin(), m_candidateUids.end(), std::greater<qint64>());
300 30 fetchNextCandidate();
301 }
302 }
303
304 32 void SettingsSyncService::fetchNextCandidate() {
305
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 17 taken 31 times.
32 if (m_candidateUids.isEmpty()) {
306
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCInfo(lcSync) << "No uploaded settings message found";
307 1 startIdleWatch();
308 1 emit fetchComplete(false);
309 1 return;
310 }
311 31 m_currentSettingsUid = m_candidateUids.takeFirst();
312
6/6
✓ Branch 18 → 19 taken 31 times.
✓ Branch 22 → 23 taken 31 times.
✓ Branch 23 → 24 taken 31 times.
✓ Branch 24 → 25 taken 31 times.
✓ Branch 28 → 20 taken 31 times.
✓ Branch 28 → 29 taken 31 times.
62 qCInfo(lcSync) << "Fetching settings message UID:" << m_currentSettingsUid;
313 31 m_imap->fetchBody(m_currentSettingsUid, SyncPayload::MaxRfcMessageBytes + 1);
314 }
315
316 37 void SettingsSyncService::onRawBodyReceived(qint64 uid,
317 const QByteArray &rawBody) {
318
5/6
✓ Branch 2 → 3 taken 36 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 3 → 4 taken 36 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 35 times.
37 if (m_phase != Phase::Fetching || uid <= 0 || uid != m_currentSettingsUid)
319 7 return;
320
321
6/6
✓ Branch 6 → 7 taken 35 times.
✓ Branch 10 → 11 taken 35 times.
✓ Branch 11 → 12 taken 35 times.
✓ Branch 12 → 13 taken 35 times.
✓ Branch 19 → 8 taken 35 times.
✓ Branch 19 → 20 taken 35 times.
70 qCInfo(lcSync) << "Received settings message body, UID:" << uid
322
2/2
✓ Branch 13 → 14 taken 35 times.
✓ Branch 15 → 16 taken 35 times.
35 << "size:" << rawBody.size();
323
324
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 43 taken 34 times.
35 if (rawBody.size() > SyncPayload::MaxRfcMessageBytes) {
325
6/6
✓ Branch 22 → 23 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 34 → 24 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
2 qCWarning(lcSync) << "Settings message too large:" << rawBody.size()
326
1/1
✓ Branch 30 → 31 taken 1 time.
1 << "bytes";
327 1 m_phase = Phase::Idle;
328
1/1
✓ Branch 35 → 36 taken 1 time.
1 startIdleWatch();
329
1/1
✓ Branch 39 → 40 taken 1 time.
1 emit syncError(QStringLiteral("Settings sync message too large"));
330 1 return;
331 }
332
333
3/3
✓ Branch 43 → 44 taken 34 times.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 58 taken 32 times.
34 if (SyncPayload::deliveredByMailTransport(rawBody)) {
334
6/6
✓ Branch 45 → 46 taken 2 times.
✓ Branch 49 → 50 taken 2 times.
✓ Branch 50 → 51 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 55 → 47 taken 2 times.
✓ Branch 55 → 56 taken 2 times.
4 qCWarning(lcSync) << "Ignored a settings message delivered by mail (Received header), UID:" << uid;
335
1/1
✓ Branch 56 → 57 taken 2 times.
2 fetchNextCandidate();
336 2 return;
337 }
338
1/1
✓ Branch 58 → 59 taken 32 times.
32 SyncPayload payload = SyncPayload::fromRfcMessage(rawBody);
339
8/9
✓ Branch 60 → 61 taken 31 times.
✓ Branch 60 → 64 taken 1 time.
✓ Branch 61 → 62 taken 30 times.
✓ Branch 61 → 64 taken 1 time.
✓ Branch 62 → 63 taken 30 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 30 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 85 taken 30 times.
32 if (payload.clientId.isEmpty() || payload.version != 1 || !payload.lastModified.isValid()) {
340
5/5
✓ Branch 67 → 68 taken 2 times.
✓ Branch 71 → 72 taken 2 times.
✓ Branch 72 → 73 taken 2 times.
✓ Branch 76 → 69 taken 2 times.
✓ Branch 76 → 77 taken 2 times.
4 qCWarning(lcSync) << "Failed to parse settings message";
341
1/1
✓ Branch 77 → 78 taken 2 times.
2 startIdleWatch();
342
1/1
✓ Branch 81 → 82 taken 2 times.
2 emit syncError(QStringLiteral("Invalid settings sync message"));
343 2 return;
344 }
345
346 30 m_currentSettingsUid = uid;
347 30 m_validatedSettingsUid = uid;
348
1/2
✓ Branch 85 → 86 taken 30 times.
✗ Branch 85 → 87 not taken.
30 m_validatedUidValidity = m_imap ? m_imap->selectedUidValidity() : 0;
349 // Reach the stable phase before notifying an owner that may immediately
350 // queue an upload, change account or retire this transport.
351
1/1
✓ Branch 88 → 89 taken 30 times.
30 startIdleWatch();
352 30 const auto generation = m_generation;
353
1/1
✓ Branch 89 → 90 taken 30 times.
30 const QPointer<SettingsSyncService> guard(this);
354
1/1
✓ Branch 90 → 91 taken 30 times.
30 emit settingsReceived(payload);
355
6/7
✓ Branch 92 → 93 taken 30 times.
✗ Branch 92 → 96 not taken.
✓ Branch 94 → 95 taken 29 times.
✓ Branch 94 → 96 taken 1 time.
✓ Branch 97 → 98 taken 29 times.
✓ Branch 97 → 99 taken 1 time.
✓ Branch 98 → 99 taken 29 times.
30 if (guard && guard->m_generation == generation) emit fetchComplete(true);
356 32 }
357
358 // ═══════════════════════════════════════════════════════
359 // Phase: Upload settings
360 // ═══════════════════════════════════════════════════════
361
362 22 void SettingsSyncService::uploadSettings(const SyncPayload &payload) {
363
3/4
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 15 taken 21 times.
22 if (!m_imap || !m_enabled) {
364
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCWarning(lcSync) << "Cannot upload: sync not enabled/connected";
365 1 return;
366 }
367
368
4/4
✓ Branch 15 → 16 taken 20 times.
✓ Branch 15 → 18 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 19 times.
21 if (m_phase != Phase::Idle && m_phase != Phase::WatchingIdle) return;
369
370
1/1
✓ Branch 18 → 19 taken 20 times.
20 m_pendingUpload = payload.toRfcMessage();
371
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 44 taken 19 times.
20 if (m_pendingUpload.size() > SyncPayload::MaxRfcMessageBytes) {
372
6/6
✓ Branch 23 → 24 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 35 → 25 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
2 qCWarning(lcSync) << "Settings upload too large:" << m_pendingUpload.size()
373
1/1
✓ Branch 31 → 32 taken 1 time.
1 << "bytes";
374 1 m_pendingUpload.clear();
375
1/1
✓ Branch 40 → 41 taken 1 time.
1 emit syncError(QStringLiteral("Settings sync upload too large"));
376 1 return;
377 }
378
379 19 m_phase = Phase::Uploading;
380
1/1
✓ Branch 45 → 46 taken 19 times.
19 afterIdle([this]() {
381 18 doUpload();
382 18 });
383 }
384
385 38 bool SettingsSyncService::readyForOperation() const {
386
5/6
✓ Branch 2 → 3 taken 37 times.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 3 → 4 taken 37 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 36 times.
✓ Branch 4 → 6 taken 1 time.
38 return m_enabled && m_imap && m_phase == Phase::WatchingIdle;
387 }
388
389 19 void SettingsSyncService::doUpload() {
390
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 18 times.
19 if (m_pendingUpload.isEmpty())
391 1 return;
392
393 18 m_phase = Phase::Uploading;
394 36 m_uploadUidValidity = m_imap->selectedFolder() == m_syncFolder
395
1/2
✓ Branch 7 → 8 taken 18 times.
✗ Branch 7 → 9 not taken.
18 ? m_imap->selectedUidValidity() : 0;
396
2/2
✓ Branch 12 → 13 taken 7 times.
✓ Branch 12 → 14 taken 10 times.
17 m_previousSettingsUid = m_uploadUidValidity != 0 && m_uploadUidValidity == m_validatedUidValidity
397
2/2
✓ Branch 11 → 12 taken 17 times.
✓ Branch 11 → 14 taken 1 time.
35 ? m_validatedSettingsUid : -1;
398
399 // Make sure we're in the right folder
400 18 if (m_imap->selectedFolder() != m_syncFolder) {
401 // After selecting, onFolderSelected will handle the rest
402 // but we need a different path – just append directly
403 }
404
405
6/6
✓ Branch 19 → 20 taken 18 times.
✓ Branch 23 → 24 taken 18 times.
✓ Branch 24 → 25 taken 18 times.
✓ Branch 26 → 27 taken 18 times.
✓ Branch 31 → 21 taken 18 times.
✓ Branch 31 → 32 taken 18 times.
36 qCInfo(lcSync) << "Uploading settings (" << m_pendingUpload.size()
406
1/1
✓ Branch 27 → 28 taken 18 times.
18 << "bytes)";
407
1/1
✓ Branch 35 → 36 taken 18 times.
36 m_imap->appendMessage(m_syncFolder, m_pendingUpload,
408 36 QStringLiteral("\\Seen"));
409 }
410
411 19 void SettingsSyncService::onMessageAppended(const QString &folder,
412 qint64 uid) {
413
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 19 times.
19 if (m_phase != Phase::Uploading)
414 ✗ return;
415
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 18 times.
19 if (folder != m_syncFolder)
416 1 return;
417
418 // T-316 fix: the message count is otherwise only refreshed on SELECT, so
419 // after uploading into an initially empty folder a subsequent
420 // fetchLatestSettings() wrongly reported "sync folder is empty".
421 18 ++m_messageCount;
422
423 18 m_newAppendedUid = uid;
424 18 m_pendingUpload.clear();
425
426
2/2
✓ Branch 8 → 9 taken 17 times.
✓ Branch 8 → 32 taken 1 time.
18 if (uid <= 0) {
427
5/5
✓ Branch 9 → 10 taken 17 times.
✓ Branch 13 → 14 taken 17 times.
✓ Branch 14 → 15 taken 17 times.
✓ Branch 18 → 11 taken 17 times.
✓ Branch 18 → 19 taken 17 times.
34 qCInfo(lcSync) << "Settings uploaded without APPENDUID";
428
4/4
✓ Branch 19 → 20 taken 17 times.
✓ Branch 23 → 24 taken 17 times.
✓ Branch 29 → 21 taken 17 times.
✓ Branch 29 → 30 taken 17 times.
34 qCWarning(lcSync)
429
1/1
✓ Branch 24 → 25 taken 17 times.
17 << "Settings upload returned no APPENDUID; skipping cleanup to avoid"
430
1/1
✓ Branch 25 → 26 taken 17 times.
17 << "deleting the newly appended settings message";
431 17 m_validatedSettingsUid = -1;
432 17 m_validatedUidValidity = 0;
433 17 finishUpload();
434 17 return;
435 }
436
437
6/6
✓ Branch 32 → 33 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 43 → 34 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
2 qCInfo(lcSync) << "Settings uploaded, UID:" << uid
438
1/1
✓ Branch 39 → 40 taken 1 time.
1 << "- cleaning up old versions";
439
440 // Clean up old settings messages
441 1 cleanupOldSettings(uid);
442 }
443
444 2 void SettingsSyncService::cleanupOldSettings(qint64 keepUid) {
445 2 m_phase = Phase::CleaningUp;
446 2 m_newAppendedUid = keepUid;
447
448
3/8
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 6 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 11 not taken.
2 if (!m_imap || m_uploadUidValidity == 0 || !m_imap->hasUidExpungeCapability()) {
449 2 m_validatedSettingsUid = -1;
450 2 m_validatedUidValidity = 0;
451 2 finishUpload();
452 2 return;
453 }
454 // Always reselect, even if this connection already selected the folder:
455 // the server may have recreated it since we read the prior version.
456 ✗ m_imap->selectFolder(m_syncFolder);
457 }
458
459 3 void SettingsSyncService::onAppendError(const QString &error) {
460
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 2 times.
3 if (m_phase != Phase::Uploading)
461 1 return;
462
6/6
✓ Branch 4 → 5 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 14 → 6 taken 2 times.
✓ Branch 14 → 15 taken 2 times.
4 qCWarning(lcSync) << "Settings upload failed:" << error;
463 2 m_pendingUpload.clear();
464 2 m_phase = Phase::Idle;
465 2 startIdleWatch();
466
3/3
✓ Branch 17 → 18 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
4 emit syncError(tr("Settings upload failed: %1").arg(error));
467 }
468
469 2 void SettingsSyncService::onFolderOperationError(const QString &op,
470 const QString &error) {
471
7/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 13 → 4 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
4 qCWarning(lcSync) << "Folder operation failed:" << op << error;
472
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 22 taken 1 time.
2 if (m_phase == Phase::CreatingFolder) {
473 1 m_phase = Phase::Idle;
474
1/1
✓ Branch 18 → 19 taken 1 time.
1 emit syncError(
475
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
3 tr("Could not create sync folder: %1").arg(error));
476 }
477 2 }
478
479 // ═══════════════════════════════════════════════════════
480 // Phase: IDLE watching
481 // ═══════════════════════════════════════════════════════
482
483 4 void SettingsSyncService::startWatching() {
484
3/4
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
4 if (!m_imap || !m_enabled)
485 1 return;
486
487
1/1
✓ Branch 6 → 7 taken 3 times.
3 afterIdle([this]() {
488
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 1 time.
3 if (m_imap->selectedFolder() != m_syncFolder) {
489 2 m_phase = Phase::SelectingFolder;
490 2 m_imap->selectFolder(m_syncFolder);
491 } else {
492 1 startIdleWatch();
493 }
494 3 });
495 }
496
497 4 void SettingsSyncService::stopWatching() {
498
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 9 taken 1 time.
4 if (m_imap) {
499
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 2 times.
3 if (m_imap->isNotifying()) m_imap->stopNotify();
500
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
2 else if (m_imap->isIdling()) m_imap->stopIdle();
501 }
502 4 m_phase = Phase::Idle;
503 4 }
504
505 80 void SettingsSyncService::startIdleWatch() {
506
4/4
✓ Branch 2 → 3 taken 78 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 5 taken 68 times.
80 if (!m_imap || !m_enabled)
507 12 return;
508
509 68 m_phase = Phase::WatchingIdle;
510 // T-324: Prefer NOTIFY over IDLE for sync folder
511
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 25 taken 68 times.
68 if (m_imap->usesNotifyPush()) {
512 ✗ qCInfo(lcSync) << "Starting NOTIFY watch on sync folder";
513 ✗ m_imap->startNotify({m_syncFolder});
514 } else {
515
5/5
✓ Branch 25 → 26 taken 68 times.
✓ Branch 29 → 30 taken 68 times.
✓ Branch 30 → 31 taken 68 times.
✓ Branch 34 → 27 taken 68 times.
✓ Branch 34 → 35 taken 68 times.
136 qCInfo(lcSync) << "Starting IDLE watch on sync folder";
516 68 m_imap->startIdle();
517 }
518 ✗ }
519
520 6 void SettingsSyncService::onExpungeComplete() {
521
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 4 times.
6 if (m_phase != Phase::CleaningUp)
522 2 return;
523
524 4 finishUpload();
525 }
526
527 1 void SettingsSyncService::onExpungeFailed(const QString &error) {
528
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (m_phase != Phase::CleaningUp) return;
529 // APPEND was already confirmed. Failed history pruning must not report
530 // the latest settings as unsaved or leave the upload cycle stuck.
531
6/6
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 6 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
2 qCWarning(lcSync) << "Settings saved; previous version cleanup failed:" << error;
532 1 finishUpload();
533 }
534
535 28 void SettingsSyncService::finishUpload() {
536
2/2
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 4 taken 18 times.
28 m_currentSettingsUid = m_newAppendedUid > 0 ? m_newAppendedUid : -1;
537 28 m_phase = Phase::Idle;
538 28 startIdleWatch();
539 // A second client may have appended a newer version before our reselect.
540 // That message will not produce a later IDLE notification. Read back after
541 // completion; a reconfigured owner or an already started operation wins.
542 28 const auto generation = m_generation;
543
1/1
✓ Branch 6 → 7 taken 28 times.
28 QTimer::singleShot(0, this, [this, generation] {
544
3/4
✓ Branch 2 → 3 taken 17 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 3 → 4 taken 17 times.
✗ Branch 3 → 5 not taken.
18 if (m_generation == generation && m_phase == Phase::WatchingIdle)
545 17 fetchLatestSettings();
546 18 });
547 28 emit uploadComplete();
548 28 }
549
550 3 void SettingsSyncService::onIdleNewMessages(int newCount) {
551
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 1 time.
3 if (m_phase != Phase::WatchingIdle)
552 2 return;
553
554
6/6
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 6 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
2 qCInfo(lcSync) << "IDLE: new messages in sync folder:" << newCount;
555
556 // T-79.F2/M14: keep the message count in sync with the push. When the
557 // folder was empty at SELECT time, m_messageCount stayed 0 and
558 // doFetch() dropped every remote push ("folder is empty") until the
559 // next reconnect — remote settings uploads never arrived.
560 1 m_messageCount += newCount;
561
562 // Must use executeAfterIdle — stopIdle() sends DONE but state is still
563 // Idling until the OK response arrives. Direct calls would fail because
564 // searchText() etc. require state == Selected.
565 1 fetchLatestSettings();
566 }
567
568 // ═══════════════════════════════════════════════════════
569 // Reconnect logic
570 // ═══════════════════════════════════════════════════════
571 //
572 // T-720: handleDisconnect() + onReconnectTimer() have been removed.
573 // The dedicated IMAP connection is now wrapped in a
574 // ConnectionHealthMonitor (see configure()), which owns the periodic
575 // liveness probe, the exponential-backoff reconnect, and the
576 // suspend/network reactivity. The service keeps the Phase::Idle reset
577 // on Error/Disconnected so a stale WatchingIdle phase cannot receive
578 // push notifications on the next (reconnected) session.
579