MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - service - SettingsSyncService.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 84.9 % 292 248
Test Date: 2026-06-21 21:10:19 Functions: 96.8 % 31 30
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 48.5 % 532 258

             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.
        

Generated by: LCOV version 2.0-1