Branch data Line data Source code
1 : : #include "service/SettingsSyncService.h"
2 : :
3 : : #include <QLoggingCategory>
4 : :
5 : : #include "service/ConnectionHealthMonitor.h"
6 : : #include "service/ImapService.h"
7 : :
8 [ + + + - : 47 : Q_LOGGING_CATEGORY(lcSync, "mailjd.sync")
+ - - - ]
9 : :
10 : : static constexpr const char *SYNC_SUBJECT = "X-MailJD-Settings-Sync";
11 : :
12 : : // ═══════════════════════════════════════════════════════
13 : : // Construction / Configuration
14 : : // ═══════════════════════════════════════════════════════
15 : :
16 [ + - ]: 18 : SettingsSyncService::SettingsSyncService(QObject *parent) : QObject(parent) {
17 : : // T-720: The reconnect timer/handleDisconnect() plumbing is gone —
18 : : // ConnectionHealthMonitor owns both the liveness probe + the
19 : : // exponential-backoff reconnect. The service owns the monitor.
20 : 18 : }
21 : :
22 : 19 : SettingsSyncService::~SettingsSyncService() { shutdown(); }
23 : :
24 : 7 : void SettingsSyncService::configure(const ImapConfig &config,
25 : : const QString &syncFolder) {
26 : 7 : m_config = config;
27 : 7 : m_syncFolder = syncFolder;
28 : :
29 : : // T-720: detach the monitor + tear down the OLD ImapService BEFORE
30 : : // assigning the new pointer. deleteLater() is asynchronous — the old
31 : : // socket survives until the event loop drains it, so its signals could
32 : : // otherwise reach a monitor that is already attached to the new
33 : : // service. detach() disconnects stateChanged/errorOccurred first.
34 [ + + ]: 7 : if (m_health) {
35 : 1 : m_health->setActive(false);
36 : 1 : m_health->detach();
37 : : }
38 [ + + ]: 7 : if (m_imap) {
39 : 1 : m_imap->disconnect();
40 : 1 : m_imap->deleteLater();
41 : : }
42 [ + - - + : 7 : m_imap = new ImapService(this);
- - ]
43 : 7 : m_imap->setAutoIdle(false); // We manage IDLE ourselves
44 : :
45 : : // Wire up signals
46 : 7 : connect(m_imap, &ImapService::stateChanged, this,
47 [ + - ]: 7 : &SettingsSyncService::onStateChanged);
48 : 7 : connect(m_imap, &ImapService::folderListReceived, this,
49 [ + - ]: 7 : &SettingsSyncService::onFolderListReceived);
50 : 7 : connect(m_imap, &ImapService::folderSelected, this,
51 [ + - ]: 7 : &SettingsSyncService::onFolderSelected);
52 : 7 : connect(m_imap, &ImapService::folderCreated, this,
53 [ + - ]: 7 : &SettingsSyncService::onFolderCreated);
54 : 7 : connect(m_imap, &ImapService::searchResultReceived, this,
55 [ + - ]: 7 : &SettingsSyncService::onSearchResultReceived);
56 : 7 : connect(m_imap, &ImapService::rawBodyReceived, this,
57 [ + - ]: 7 : &SettingsSyncService::onRawBodyReceived);
58 : 7 : connect(m_imap, &ImapService::messageAppended, this,
59 [ + - ]: 7 : &SettingsSyncService::onMessageAppended);
60 : 7 : connect(m_imap, &ImapService::appendError, this,
61 [ + - ]: 7 : &SettingsSyncService::onAppendError);
62 : 7 : connect(m_imap, &ImapService::folderOperationError, this,
63 [ + - ]: 7 : &SettingsSyncService::onFolderOperationError);
64 : 7 : connect(m_imap, &ImapService::idleNewMessages, this,
65 [ + - ]: 7 : &SettingsSyncService::onIdleNewMessages);
66 : 7 : connect(m_imap, &ImapService::expungeComplete, this,
67 [ + - ]: 7 : &SettingsSyncService::onExpungeComplete);
68 : : // T-720: errorOccurred still resets the phase, but reconnect scheduling
69 : : // is delegated to the monitor.
70 : 7 : connect(m_imap, &ImapService::errorOccurred, this,
71 [ + - ]: 7 : [this](const QString &err) {
72 [ + - + - : 2 : qCWarning(lcSync) << "IMAP error:" << err;
+ - + - +
+ ]
73 : 1 : m_phase = Phase::Idle;
74 : 1 : });
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 [ + + ]: 7 : if (!m_health)
81 [ + - - + : 6 : m_health = new ConnectionHealthMonitor(false, this);
- - ]
82 : 7 : m_health->attach(m_imap);
83 : 7 : m_health->setReconnectConfig(m_config);
84 : 7 : m_health->setActive(m_enabled);
85 : 7 : }
86 : :
87 : 9 : void SettingsSyncService::setEnabled(bool enabled) {
88 [ - + ]: 9 : if (m_enabled == enabled)
89 : 0 : return;
90 : 9 : m_enabled = enabled;
91 : :
92 [ + + + + ]: 9 : if (enabled && m_imap) {
93 [ + - + - : 8 : qCInfo(lcSync) << "Settings sync enabled, connecting...";
+ - + + ]
94 : 4 : m_imap->connectToServer(m_config);
95 [ + + ]: 9 : } else if (!enabled) {
96 [ + - + - : 6 : qCInfo(lcSync) << "Settings sync disabled";
+ - + + ]
97 : 3 : stopWatching();
98 [ + + ]: 3 : if (m_imap)
99 : 1 : m_imap->disconnect();
100 : : }
101 : : // T-720: Reflect the new enabled state on the monitor so reconnect is
102 : : // only active while the user has sync turned on.
103 [ + + ]: 9 : if (m_health)
104 : 5 : m_health->setActive(m_enabled);
105 : : }
106 : :
107 : 23 : void SettingsSyncService::shutdown() {
108 : 23 : m_enabled = false;
109 : 23 : m_phase = Phase::Idle;
110 : : // T-720: Stop the monitor BEFORE tearing down the IMAP connection.
111 [ + + ]: 23 : if (m_health) {
112 : 9 : m_health->setActive(false);
113 : 9 : m_health->detach();
114 : : }
115 [ + + ]: 23 : if (m_imap) {
116 : 8 : m_imap->disconnect();
117 : 8 : m_imap->deleteLater();
118 : 8 : m_imap = nullptr;
119 : : }
120 : 23 : }
121 : :
122 : : // ═══════════════════════════════════════════════════════
123 : : // IMAP State Machine
124 : : // ═══════════════════════════════════════════════════════
125 : :
126 : 33 : void SettingsSyncService::onStateChanged(ImapService::State state) {
127 [ + + ]: 33 : if (state == ImapService::State::Authenticated) {
128 [ + - + - : 4 : qCInfo(lcSync) << "Sync IMAP authenticated, checking folder...";
+ - + + ]
129 : 2 : emit connected();
130 : 2 : checkOrCreateFolder();
131 [ + + + + ]: 31 : } else if (state == ImapService::State::Disconnected ||
132 : : state == ImapService::State::Error) {
133 : : // T-720: Keep the phase reset so a stale WatchingIdle phase cannot
134 : : // receive push notifications on the next (reconnected) session.
135 : : // Reconnect scheduling itself moved to ConnectionHealthMonitor.
136 : 7 : m_phase = Phase::Idle;
137 : : }
138 : 33 : }
139 : :
140 : : // ═══════════════════════════════════════════════════════
141 : : // Phase: Check / Create sync folder
142 : : // ═══════════════════════════════════════════════════════
143 : :
144 : 2 : void SettingsSyncService::checkOrCreateFolder() {
145 : 2 : m_phase = Phase::CheckingFolder;
146 [ + - + - : 4 : qCInfo(lcSync) << "Checking if sync folder exists:" << m_syncFolder;
+ - + - +
+ ]
147 : 2 : m_imap->listFolders(); // Will trigger onFolderListReceived
148 : 2 : }
149 : :
150 : 2 : void SettingsSyncService::onFolderListReceived(
151 : : const QList<FolderInfo> &folders) {
152 [ - + ]: 2 : if (m_phase != Phase::CheckingFolder)
153 : 0 : return;
154 : :
155 : 2 : bool found = false;
156 [ + + ]: 17 : for (const auto &f : folders) {
157 [ - + ]: 15 : if (f.path == m_syncFolder) {
158 : 0 : found = true;
159 : 0 : break;
160 : : }
161 : : }
162 : :
163 [ - + ]: 2 : if (found) {
164 [ # # # # : 0 : qCInfo(lcSync) << "Sync folder exists, selecting...";
# # # # ]
165 : 0 : m_phase = Phase::SelectingFolder;
166 : 0 : m_imap->selectFolder(m_syncFolder);
167 : : } else {
168 [ + - + - : 4 : qCInfo(lcSync) << "Sync folder not found, creating:" << m_syncFolder;
+ - + - +
+ ]
169 : 2 : m_phase = Phase::CreatingFolder;
170 : 2 : m_imap->createFolder(m_syncFolder);
171 : : }
172 : : }
173 : :
174 : 2 : void SettingsSyncService::onFolderCreated(const QString &folderPath) {
175 [ - + ]: 2 : if (m_phase != Phase::CreatingFolder)
176 : 0 : return;
177 [ + - ]: 2 : if (folderPath == m_syncFolder) {
178 [ + - + - : 4 : qCInfo(lcSync) << "Sync folder created, selecting...";
+ - + + ]
179 : 2 : m_phase = Phase::SelectingFolder;
180 : 2 : m_imap->selectFolder(m_syncFolder);
181 : : }
182 : : }
183 : :
184 : 2 : void SettingsSyncService::onFolderSelected(const QString &path,
185 : : int messageCount,
186 : : quint32 /*uidValidity*/,
187 : : quint64 /*highestModseq*/) {
188 [ - + ]: 2 : if (path != m_syncFolder)
189 : 0 : return;
190 : :
191 : 2 : m_messageCount = messageCount;
192 [ + - + - : 4 : qCInfo(lcSync) << "Sync folder selected, messages:" << messageCount;
+ - + - +
+ ]
193 : :
194 : 2 : emit syncFolderReady();
195 : :
196 [ + - ]: 2 : if (m_phase == Phase::SelectingFolder) {
197 : : // Initial connect: fetch latest settings, then start watching
198 : 2 : doFetch();
199 [ # # ]: 0 : } else if (m_phase == Phase::CleaningUp) {
200 : : // Re-selected after upload — resume cleanup SEARCH
201 [ # # # # ]: 0 : m_imap->searchText(QString::fromLatin1(SYNC_SUBJECT),
202 : 0 : QStringLiteral("SUBJECT"));
203 : : }
204 : : }
205 : :
206 : : // ═══════════════════════════════════════════════════════
207 : : // Phase: Fetch latest settings
208 : : // ═══════════════════════════════════════════════════════
209 : :
210 : 2 : void SettingsSyncService::fetchLatestSettings() {
211 [ + - - + ]: 2 : if (!m_imap || !m_enabled)
212 : 0 : return;
213 : :
214 [ + - ]: 2 : m_imap->executeAfterIdle([this]() {
215 : : // Make sure we're in the right folder
216 [ - + ]: 2 : if (m_imap->selectedFolder() != m_syncFolder) {
217 : 0 : m_phase = Phase::SelectingFolder;
218 : 0 : m_imap->selectFolder(m_syncFolder);
219 : : } else {
220 : 2 : doFetch();
221 : : }
222 : 2 : });
223 : : }
224 : :
225 : 4 : void SettingsSyncService::doFetch() {
226 : 4 : m_phase = Phase::Fetching;
227 : :
228 [ + + ]: 4 : if (m_messageCount == 0) {
229 [ + - + - : 4 : qCInfo(lcSync) << "Sync folder is empty, no settings to fetch";
+ - + + ]
230 : 2 : startIdleWatch();
231 : 2 : return;
232 : : }
233 : :
234 : : // Search for settings messages
235 [ + - + - : 4 : qCInfo(lcSync) << "Searching for settings messages...";
+ - + + ]
236 [ + - + - ]: 4 : m_imap->searchText(QString::fromLatin1(SYNC_SUBJECT),
237 : 4 : QStringLiteral("SUBJECT"));
238 : : }
239 : :
240 : 4 : void SettingsSyncService::onSearchResultReceived(const QList<qint64> &uids) {
241 [ + + ]: 4 : if (m_phase == Phase::Fetching) {
242 [ - + ]: 2 : if (uids.isEmpty()) {
243 [ # # # # : 0 : qCInfo(lcSync) << "No settings messages found";
# # # # ]
244 : 0 : startIdleWatch();
245 : 0 : return;
246 : : }
247 : :
248 : : // Fetch the newest (highest UID)
249 [ + - ]: 2 : qint64 newestUid = *std::max_element(uids.begin(), uids.end());
250 : 2 : m_currentSettingsUid = newestUid;
251 [ + - + - : 4 : qCInfo(lcSync) << "Fetching settings message UID:" << newestUid;
+ - + - +
+ ]
252 : 2 : m_imap->fetchBody(newestUid, SyncPayload::MaxRfcMessageBytes + 1);
253 [ + - ]: 2 : } else if (m_phase == Phase::CleaningUp) {
254 : : // We're cleaning up old settings after upload
255 : 2 : QList<qint64> toDelete;
256 [ + + ]: 4 : for (qint64 uid : uids) {
257 [ - + ]: 2 : if (uid != m_newAppendedUid)
258 [ # # ]: 0 : toDelete.append(uid);
259 : : }
260 [ - + ]: 2 : if (!toDelete.isEmpty()) {
261 [ # # # # : 0 : qCInfo(lcSync) << "Deleting" << toDelete.size()
# # # # #
# ]
262 [ # # ]: 0 : << "old settings messages";
263 [ # # # # : 0 : for (qint64 uid : toDelete) {
# # ]
264 [ # # ]: 0 : m_imap->storeFlag(uid, QStringLiteral("\\Deleted"), true);
265 : : }
266 : : // EXPUNGE after marking — onExpungeComplete will finish the cycle
267 [ # # ]: 0 : m_imap->expunge();
268 : : } else {
269 : : // Nothing to delete — finish immediately
270 : 2 : m_currentSettingsUid = m_newAppendedUid;
271 : 2 : m_phase = Phase::Idle;
272 [ + - ]: 2 : emit uploadComplete();
273 [ + - ]: 2 : startIdleWatch();
274 : : }
275 : 2 : }
276 : : }
277 : :
278 : 3 : void SettingsSyncService::onRawBodyReceived(qint64 uid,
279 : : const QByteArray &rawBody) {
280 [ - + ]: 3 : if (m_phase != Phase::Fetching)
281 : 1 : return;
282 : :
283 [ + - + - : 6 : qCInfo(lcSync) << "Received settings message body, UID:" << uid
+ - + - +
+ ]
284 [ + - + - ]: 3 : << "size:" << rawBody.size();
285 : :
286 [ + + ]: 3 : if (rawBody.size() > SyncPayload::MaxRfcMessageBytes) {
287 [ + - + - : 2 : qCWarning(lcSync) << "Settings message too large:" << rawBody.size()
+ - + - +
+ ]
288 [ + - ]: 1 : << "bytes";
289 [ + - ]: 1 : emit syncError(QStringLiteral("Settings sync message too large"));
290 : 1 : m_phase = Phase::Idle;
291 [ + - ]: 1 : startIdleWatch();
292 : 1 : return;
293 : : }
294 : :
295 [ + - ]: 2 : SyncPayload payload = SyncPayload::fromRfcMessage(rawBody);
296 [ - - ]: 2 : if (payload.clientId.isEmpty() && payload.version == 1 &&
297 [ - + - - : 2 : payload.folderIcons.isEmpty() && payload.folderColors.isEmpty()) {
- - - - -
- - + ]
298 [ # # # # : 0 : qCWarning(lcSync) << "Failed to parse settings message";
# # # # ]
299 [ # # ]: 0 : startIdleWatch();
300 : 0 : return;
301 : : }
302 : :
303 : 2 : m_currentSettingsUid = uid;
304 [ + - ]: 2 : emit settingsReceived(payload);
305 : :
306 : : // Start watching for changes after initial fetch
307 [ + - ]: 2 : startIdleWatch();
308 [ + - ]: 2 : }
309 : :
310 : : // ═══════════════════════════════════════════════════════
311 : : // Phase: Upload settings
312 : : // ═══════════════════════════════════════════════════════
313 : :
314 : 4 : void SettingsSyncService::uploadSettings(const SyncPayload &payload) {
315 [ + - - + ]: 4 : if (!m_imap || !m_enabled) {
316 [ # # # # : 0 : qCWarning(lcSync) << "Cannot upload: sync not enabled/connected";
# # # # ]
317 : 0 : return;
318 : : }
319 : :
320 [ + - ]: 4 : m_pendingUpload = payload.toRfcMessage();
321 [ + + ]: 4 : if (m_pendingUpload.size() > SyncPayload::MaxRfcMessageBytes) {
322 [ + - + - : 2 : qCWarning(lcSync) << "Settings upload too large:" << m_pendingUpload.size()
+ - + - +
+ ]
323 [ + - ]: 1 : << "bytes";
324 : 1 : m_pendingUpload.clear();
325 [ + - ]: 1 : emit syncError(QStringLiteral("Settings sync upload too large"));
326 : 1 : return;
327 : : }
328 : :
329 [ + - ]: 3 : m_imap->executeAfterIdle([this]() {
330 : 3 : doUpload();
331 : 3 : });
332 : : }
333 : :
334 : 3 : void SettingsSyncService::doUpload() {
335 [ - + ]: 3 : if (m_pendingUpload.isEmpty())
336 : 0 : return;
337 : :
338 : 3 : m_phase = Phase::Uploading;
339 : :
340 : : // Make sure we're in the right folder
341 : 3 : if (m_imap->selectedFolder() != m_syncFolder) {
342 : : // After selecting, onFolderSelected will handle the rest
343 : : // but we need a different path – just append directly
344 : : }
345 : :
346 [ + - + - : 6 : qCInfo(lcSync) << "Uploading settings (" << m_pendingUpload.size()
+ - + - +
+ ]
347 [ + - ]: 3 : << "bytes)";
348 [ + - ]: 6 : m_imap->appendMessage(m_syncFolder, m_pendingUpload,
349 : 6 : QStringLiteral("\\Seen"));
350 : : }
351 : :
352 : 3 : void SettingsSyncService::onMessageAppended(const QString &folder,
353 : : qint64 uid) {
354 [ - + ]: 3 : if (m_phase != Phase::Uploading)
355 : 0 : return;
356 [ - + ]: 3 : if (folder != m_syncFolder)
357 : 0 : return;
358 : :
359 : : // T-316 fix: the message count is otherwise only refreshed on SELECT, so
360 : : // after uploading into an initially empty folder a subsequent
361 : : // fetchLatestSettings() wrongly reported "sync folder is empty".
362 : 3 : ++m_messageCount;
363 : :
364 : 3 : m_newAppendedUid = uid;
365 : 3 : m_pendingUpload.clear();
366 : :
367 [ + + ]: 3 : if (uid <= 0) {
368 [ + - + - : 2 : qCInfo(lcSync) << "Settings uploaded without APPENDUID";
+ - + + ]
369 [ + - + - : 2 : qCWarning(lcSync)
+ + ]
370 [ + - ]: 1 : << "Settings upload returned no APPENDUID; skipping cleanup to avoid"
371 [ + - ]: 1 : << "deleting the newly appended settings message";
372 : 1 : m_currentSettingsUid = -1;
373 : 1 : m_phase = Phase::Idle;
374 : 1 : emit uploadComplete();
375 : 1 : startIdleWatch();
376 : 1 : return;
377 : : }
378 : :
379 [ + - + - : 4 : qCInfo(lcSync) << "Settings uploaded, UID:" << uid
+ - + - +
+ ]
380 [ + - ]: 2 : << "- cleaning up old versions";
381 : :
382 : : // Clean up old settings messages
383 : 2 : cleanupOldSettings(uid);
384 : : }
385 : :
386 : 2 : void SettingsSyncService::cleanupOldSettings(qint64 keepUid) {
387 : 2 : m_phase = Phase::CleaningUp;
388 : 2 : m_newAppendedUid = keepUid;
389 : :
390 : : // Need to SELECT the sync folder to search/delete
391 [ - + ]: 2 : if (m_imap->selectedFolder() != m_syncFolder) {
392 : 0 : m_imap->selectFolder(m_syncFolder);
393 : : // onFolderSelected will resume – but we handle this in the phase
394 : 0 : return;
395 : : }
396 : :
397 : : // Search for all settings messages
398 [ + - + - ]: 4 : m_imap->searchText(QString::fromLatin1(SYNC_SUBJECT),
399 : 4 : QStringLiteral("SUBJECT"));
400 : : }
401 : :
402 : 1 : void SettingsSyncService::onAppendError(const QString &error) {
403 [ - + ]: 1 : if (m_phase != Phase::Uploading)
404 : 0 : return;
405 [ + - + - : 2 : qCWarning(lcSync) << "Settings upload failed:" << error;
+ - + - +
+ ]
406 : 1 : m_pendingUpload.clear();
407 : 1 : m_phase = Phase::Idle;
408 [ + - + - : 2 : emit syncError(tr("Settings upload failed: %1").arg(error));
+ - ]
409 : 1 : startIdleWatch();
410 : : }
411 : :
412 : 2 : void SettingsSyncService::onFolderOperationError(const QString &op,
413 : : const QString &error) {
414 [ + - + - : 4 : qCWarning(lcSync) << "Folder operation failed:" << op << error;
+ - + - +
- + + ]
415 [ + + ]: 2 : if (m_phase == Phase::CreatingFolder) {
416 [ + - ]: 1 : emit syncError(
417 [ + - + - ]: 3 : tr("Could not create sync folder: %1").arg(error));
418 : 1 : m_phase = Phase::Idle;
419 : : }
420 : 2 : }
421 : :
422 : : // ═══════════════════════════════════════════════════════
423 : : // Phase: IDLE watching
424 : : // ═══════════════════════════════════════════════════════
425 : :
426 : 1 : void SettingsSyncService::startWatching() {
427 [ + - - + ]: 1 : if (!m_imap || !m_enabled)
428 : 0 : return;
429 : :
430 [ + - ]: 1 : m_imap->executeAfterIdle([this]() {
431 [ + - ]: 1 : if (m_imap->selectedFolder() != m_syncFolder) {
432 : 1 : m_phase = Phase::SelectingFolder;
433 : 1 : m_imap->selectFolder(m_syncFolder);
434 : : } else {
435 : 0 : startIdleWatch();
436 : : }
437 : 1 : });
438 : : }
439 : :
440 : 4 : void SettingsSyncService::stopWatching() {
441 [ + + ]: 4 : if (m_imap) {
442 [ - + ]: 2 : if (m_imap->isNotifying()) m_imap->stopNotify();
443 [ - + ]: 2 : else if (m_imap->isIdling()) m_imap->stopIdle();
444 : : }
445 : 4 : m_phase = Phase::Idle;
446 : 4 : }
447 : :
448 : 10 : void SettingsSyncService::startIdleWatch() {
449 [ + + + + ]: 10 : if (!m_imap || !m_enabled)
450 : 3 : return;
451 : :
452 : 7 : m_phase = Phase::WatchingIdle;
453 : : // T-324: Prefer NOTIFY over IDLE for sync folder
454 [ - + ]: 7 : if (m_imap->hasNotifyCapability()) {
455 [ # # # # : 0 : qCInfo(lcSync) << "Starting NOTIFY watch on sync folder";
# # # # ]
456 [ # # # # : 0 : m_imap->startNotify({m_syncFolder});
# # ]
457 : : } else {
458 [ + - + - : 14 : qCInfo(lcSync) << "Starting IDLE watch on sync folder";
+ - + + ]
459 : 7 : m_imap->startIdle();
460 : : }
461 [ # # # # : 0 : }
# # ]
462 : :
463 : 2 : void SettingsSyncService::onExpungeComplete() {
464 [ + + ]: 2 : if (m_phase != Phase::CleaningUp)
465 : 1 : return;
466 : :
467 [ + - + - : 2 : qCInfo(lcSync) << "Cleanup EXPUNGE complete, finishing upload cycle";
+ - + + ]
468 : 1 : m_currentSettingsUid = m_newAppendedUid;
469 : 1 : m_phase = Phase::Idle;
470 : 1 : emit uploadComplete();
471 : 1 : startIdleWatch();
472 : : }
473 : :
474 : 2 : void SettingsSyncService::onIdleNewMessages(int newCount) {
475 [ + - ]: 2 : if (m_phase != Phase::WatchingIdle)
476 : 2 : return;
477 : :
478 [ # # # # : 0 : qCInfo(lcSync) << "IDLE: new messages in sync folder:" << newCount;
# # # # #
# ]
479 : :
480 : : // Must use executeAfterIdle — stopIdle() sends DONE but state is still
481 : : // Idling until the OK response arrives. Direct calls would fail because
482 : : // searchText() etc. require state == Selected.
483 [ # # ]: 0 : m_imap->executeAfterIdle([this]() {
484 : 0 : doFetch();
485 : 0 : });
486 : : }
487 : :
488 : : // ═══════════════════════════════════════════════════════
489 : : // Reconnect logic
490 : : // ═══════════════════════════════════════════════════════
491 : : //
492 : : // T-720: handleDisconnect() + onReconnectTimer() have been removed.
493 : : // The dedicated IMAP connection is now wrapped in a
494 : : // ConnectionHealthMonitor (see configure()), which owns the periodic
495 : : // liveness probe, the exponential-backoff reconnect, and the
496 : : // suspend/network reactivity. The service keeps the Phase::Idle reset
497 : : // on Error/Disconnected so a stale WatchingIdle phase cannot receive
498 : : // push notifications on the next (reconnected) session.
|