MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - data - SettingsCollector.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 100.0 % 211 211
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 4 4
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 65.9 % 510 336

             Branch data     Line data    Source code
       1                 :             : #include "data/SettingsCollector.h"
       2                 :             : 
       3                 :             : #include <QDir>
       4                 :             : #include <QLoggingCategory>
       5                 :             : #include <QSettings>
       6                 :             : 
       7                 :             : #include "data/CalendarStore.h"
       8                 :             : #include "data/MailCache.h"
       9                 :             : #include "ui/FolderSubscriptionDialog.h"
      10                 :             : 
      11   [ +  +  +  -  :          57 : Q_LOGGING_CATEGORY(lcSettingsSync, "mailjd.settingssync")
             +  -  -  - ]
      12                 :             : 
      13                 :             : // ═══════════════════════════════════════════════════════
      14                 :             : // collectLocal – gather all local settings into SyncPayload
      15                 :             : // ═══════════════════════════════════════════════════════
      16                 :             : 
      17                 :          12 : SyncPayload SettingsCollector::collectLocal(MailCache *cache,
      18                 :             :                                            const QString &configDir,
      19                 :             :                                            CalendarStore *calStore) {
      20         [ +  - ]:          12 :   SyncPayload p;
      21                 :          12 :   p.version = 1;
      22         [ +  - ]:          12 :   p.lastModified = QDateTime::currentDateTimeUtc();
      23                 :             : 
      24         [ +  - ]:          12 :   QSettings s;
      25                 :             : 
      26                 :             :   // clientId
      27   [ +  -  +  - ]:          12 :   p.clientId = s.value(QStringLiteral("sync/clientId")).toString();
      28                 :             : 
      29                 :             :   // --- folderIcons ---
      30                 :             :   // Use allKeys() instead of childKeys() because QSettings treats '/' as
      31                 :             :   // a group separator. Folder paths with '/' delimiter (e.g. "Work/Projects")
      32                 :             :   // become nested subgroups that childKeys() would miss.
      33         [ +  - ]:          12 :   s.beginGroup(QStringLiteral("folder/icon"));
      34         [ +  - ]:          12 :   const auto iconKeys = s.allKeys();
      35         [ +  + ]:          17 :   for (const auto &key : iconKeys) {
      36   [ +  -  +  -  :           5 :     p.folderIcons[key] = s.value(key).toString();
                   +  - ]
      37                 :             :   }
      38         [ +  - ]:          12 :   s.endGroup();
      39                 :             : 
      40                 :             :   // --- folderColors ---
      41         [ +  - ]:          12 :   s.beginGroup(QStringLiteral("folder/color"));
      42         [ +  - ]:          12 :   const auto colorKeys = s.allKeys();
      43         [ +  + ]:          16 :   for (const auto &key : colorKeys) {
      44   [ +  -  +  -  :           4 :     p.folderColors[key] = s.value(key).toString();
                   +  - ]
      45                 :             :   }
      46         [ +  - ]:          12 :   s.endGroup();
      47                 :             : 
      48                 :             :   // --- hiddenFolders ---
      49         [ +  - ]:          12 :   p.hiddenFolders = FolderSubscriptionDialog::loadHidden(configDir);
      50                 :             : 
      51                 :             :   // --- externalContentWhitelist ---
      52         [ +  + ]:          12 :   if (cache) {
      53         [ +  - ]:          10 :     auto entries = cache->whitelistEntries();
      54   [ +  -  +  -  :          15 :     for (const auto &e : entries) {
                   +  + ]
      55                 :           5 :       SyncPayload::WhitelistItem item;
      56                 :           5 :       item.type = e.type;
      57                 :           5 :       item.value = e.value;
      58         [ +  - ]:           5 :       p.externalContentWhitelist.append(item);
      59                 :           5 :     }
      60                 :          10 :   }
      61                 :             : 
      62                 :             :   // --- carddavAccounts ---
      63         [ +  - ]:          12 :   int count = s.beginReadArray(QStringLiteral("carddav/accounts"));
      64         [ +  + ]:          14 :   for (int i = 0; i < count; ++i) {
      65         [ +  - ]:           2 :     s.setArrayIndex(i);
      66                 :           2 :     SyncPayload::CardDavAccount acc;
      67   [ +  -  +  - ]:           2 :     acc.id = s.value(QStringLiteral("id")).toString();
      68   [ +  -  +  - ]:           2 :     acc.serverUrl = s.value(QStringLiteral("serverUrl")).toString();
      69   [ +  -  +  - ]:           2 :     acc.username = s.value(QStringLiteral("username")).toString();
      70                 :             :     // NOTE: password intentionally NOT collected (security)
      71   [ +  -  +  - ]:           2 :     acc.selectedBooks = s.value(QStringLiteral("selectedBooks")).toStringList();
      72         [ +  - ]:           2 :     p.carddavAccounts.append(acc);
      73                 :           2 :   }
      74         [ +  - ]:          12 :   s.endArray();
      75                 :             : 
      76                 :             :   // --- caldavConfigs (T-334: references CardDAV accounts) ---
      77         [ +  - ]:          12 :   int caCount = s.beginReadArray(QStringLiteral("caldav/configs"));
      78         [ +  + ]:          15 :   for (int i = 0; i < caCount; ++i) {
      79         [ +  - ]:           3 :     s.setArrayIndex(i);
      80                 :           3 :     SyncPayload::CalDavSyncConfig cfg;
      81   [ +  -  +  - ]:           3 :     cfg.carddavAccountId = s.value(QStringLiteral("carddavAccountId")).toString();
      82   [ +  -  +  - ]:           3 :     cfg.selectedCalendars = s.value(QStringLiteral("selectedCalendars")).toStringList();
      83   [ +  -  +  - ]:           3 :     cfg.readOnlyCalendars = s.value(QStringLiteral("readOnlyCalendars")).toStringList();
      84   [ +  -  +  - ]:           6 :     cfg.syncIntervalMinutes = s.value(QStringLiteral("syncIntervalMin"), 15).toInt();
      85         [ +  - ]:           3 :     p.caldavConfigs.append(cfg);
      86                 :           3 :   }
      87         [ +  - ]:          12 :   s.endArray();
      88                 :             : 
      89                 :             :   // --- calendarColors (from CalendarStore SQLite) ---
      90         [ +  + ]:          12 :   if (calStore) {
      91         [ +  - ]:           8 :     const auto calendars = calStore->allCalendars();
      92         [ +  + ]:          15 :     for (const auto &cal : calendars) {
      93         [ +  - ]:           7 :       QString color = calStore->calendarColor(cal.path);
      94         [ +  + ]:           7 :       if (!color.isEmpty())
      95         [ +  - ]:           6 :         p.calendarColors[cal.path] = color;
      96                 :           7 :     }
      97                 :           8 :   }
      98                 :             : 
      99                 :             :   // --- general ---
     100                 :             :   p.general.defaultView =
     101         [ +  - ]:          36 :       s.value(QStringLiteral("view/defaultMode"), QStringLiteral("text"))
     102         [ +  - ]:          24 :           .toString();
     103                 :             :   p.general.externalContent =
     104         [ +  - ]:          36 :       s.value(QStringLiteral("view/externalContent"), QStringLiteral("block"))
     105         [ +  - ]:          24 :           .toString();
     106                 :             :   p.general.language =
     107         [ +  - ]:          36 :       s.value(QStringLiteral("i18n/language"), QStringLiteral("auto"))
     108         [ +  - ]:          12 :           .toString();
     109                 :          12 :   p.general.carddavSyncInterval =
     110   [ +  -  +  - ]:          24 :       s.value(QStringLiteral("carddav/syncIntervalMin"), 0).toInt();
     111                 :          12 :   p.general.caldavSyncInterval =
     112   [ +  -  +  - ]:          24 :       s.value(QStringLiteral("caldav/syncIntervalMin"), 15).toInt();
     113                 :             : 
     114                 :             :   // --- enabledCategories ---
     115                 :             :   p.enabledCategories =
     116         [ +  - ]:          36 :       s.value(QStringLiteral("sync/categories"),
     117         [ +  - ]:          24 :               SyncPayload::allCategories())
     118         [ +  - ]:          12 :           .toStringList();
     119                 :             : 
     120                 :          12 :   return p;
     121                 :          12 : }
     122                 :             : 
     123                 :             : // ═══════════════════════════════════════════════════════
     124                 :             : // applyRemote – apply a remote SyncPayload to local settings
     125                 :             : // ═══════════════════════════════════════════════════════
     126                 :             : 
     127                 :          17 : void SettingsCollector::applyRemote(const SyncPayload &payload,
     128                 :             :                                    MailCache *cache,
     129                 :             :                                    const QString &configDir,
     130                 :             :                                    CalendarStore *calStore) {
     131         [ +  - ]:          17 :   QSettings s;
     132                 :          17 :   const auto &cats = payload.enabledCategories;
     133                 :             : 
     134   [ +  -  +  -  :          34 :   qCInfo(lcSettingsSync) << "Applying remote settings, categories:"
             +  -  +  + ]
     135   [ +  -  +  - ]:          17 :                          << cats.join(QStringLiteral(", "));
     136                 :             : 
     137                 :             :   // --- folderIcons ---
     138         [ +  + ]:          17 :   if (cats.contains(QStringLiteral("folderIcons"))) {
     139                 :             :     // Clear existing folder icons
     140         [ +  - ]:           5 :     s.beginGroup(QStringLiteral("folder/icon"));
     141         [ +  - ]:           5 :     s.remove(QString()); // removes all keys in the group
     142         [ +  - ]:           5 :     s.endGroup();
     143                 :             :     // Write new ones
     144         [ +  - ]:           5 :     for (auto it = payload.folderIcons.constBegin();
     145   [ +  -  +  + ]:           9 :          it != payload.folderIcons.constEnd(); ++it) {
     146   [ +  -  +  - ]:           8 :       s.setValue(QStringLiteral("folder/icon/") + it.key(), it.value());
     147                 :             :     }
     148   [ +  -  +  -  :          10 :     qCInfo(lcSettingsSync) << "Applied" << payload.folderIcons.size()
          +  -  +  -  +  
                -  +  + ]
     149         [ +  - ]:           5 :                            << "folder icons";
     150                 :             :   }
     151                 :             : 
     152                 :             :   // --- folderColors ---
     153         [ +  + ]:          17 :   if (cats.contains(QStringLiteral("folderColors"))) {
     154         [ +  - ]:           5 :     s.beginGroup(QStringLiteral("folder/color"));
     155         [ +  - ]:           5 :     s.remove(QString());
     156         [ +  - ]:           5 :     s.endGroup();
     157         [ +  - ]:           5 :     for (auto it = payload.folderColors.constBegin();
     158   [ +  -  +  + ]:           9 :          it != payload.folderColors.constEnd(); ++it) {
     159   [ +  -  +  - ]:           8 :       s.setValue(QStringLiteral("folder/color/") + it.key(), it.value());
     160                 :             :     }
     161   [ +  -  +  -  :          10 :     qCInfo(lcSettingsSync) << "Applied" << payload.folderColors.size()
          +  -  +  -  +  
                -  +  + ]
     162         [ +  - ]:           5 :                            << "folder colors";
     163                 :             :   }
     164                 :             : 
     165                 :             :   // --- hiddenFolders ---
     166         [ +  + ]:          17 :   if (cats.contains(QStringLiteral("hiddenFolders"))) {
     167         [ +  - ]:           4 :     FolderSubscriptionDialog::saveHidden(configDir, payload.hiddenFolders);
     168   [ +  -  +  -  :           8 :     qCInfo(lcSettingsSync) << "Applied" << payload.hiddenFolders.size()
          +  -  +  -  +  
                      + ]
     169         [ +  - ]:           4 :                            << "hidden folders";
     170                 :             :   }
     171                 :             : 
     172                 :             :   // --- externalContentWhitelist ---
     173   [ +  +  +  +  :          34 :   if (cats.contains(QStringLiteral("externalContentWhitelist")) && cache) {
          +  -  +  -  +  
                      + ]
     174                 :           5 :     QList<QPair<QString, QString>> entries;
     175         [ +  - ]:           5 :     entries.reserve(payload.externalContentWhitelist.size());
     176         [ +  + ]:          10 :     for (const auto &item : payload.externalContentWhitelist) {
     177   [ +  -  +  - ]:           5 :       entries.append(qMakePair(item.type, item.value));
     178                 :             :     }
     179   [ +  -  +  + ]:           5 :     if (!cache->replaceWhitelistEntries(entries)) {
     180   [ +  -  +  -  :           4 :       qCWarning(lcSettingsSync) << "Failed to apply whitelist entries";
             +  -  +  + ]
     181                 :             :     } else {
     182   [ +  -  +  -  :           6 :       qCInfo(lcSettingsSync) << "Applied"
             +  -  +  + ]
     183         [ +  - ]:           3 :                              << payload.externalContentWhitelist.size()
     184         [ +  - ]:           3 :                              << "whitelist entries";
     185                 :             :     }
     186                 :           5 :   }
     187                 :             : 
     188                 :             :   // --- carddavAccounts ---
     189   [ +  +  +  +  :          67 :   if (cats.contains(QStringLiteral("carddavAccounts")) ||
             +  -  +  + ]
     190   [ +  +  +  +  :          33 :       cats.contains(QStringLiteral("davAccounts"))) {
                   +  - ]
     191                 :             :     // Write synced accounts without secrets. DAV passwords live in the OS
     192                 :             :     // keyring and are keyed by the local endpoint, not by the sync payload.
     193         [ +  - ]:           7 :     s.remove(QStringLiteral("carddav/accounts"));
     194         [ +  - ]:          14 :     s.beginWriteArray(QStringLiteral("carddav/accounts"),
     195                 :           7 :                       payload.carddavAccounts.size());
     196         [ +  + ]:          12 :     for (int i = 0; i < payload.carddavAccounts.size(); ++i) {
     197         [ +  - ]:           5 :       s.setArrayIndex(i);
     198                 :           5 :       const auto &acc = payload.carddavAccounts[i];
     199         [ +  - ]:          10 :       s.setValue(QStringLiteral("id"), acc.id);
     200         [ +  - ]:          10 :       s.setValue(QStringLiteral("serverUrl"), acc.serverUrl);
     201         [ +  - ]:          10 :       s.setValue(QStringLiteral("username"), acc.username);
     202         [ +  - ]:           5 :       s.remove(QStringLiteral("password"));
     203         [ +  - ]:          10 :       s.setValue(QStringLiteral("selectedBooks"), acc.selectedBooks);
     204                 :             :     }
     205         [ +  - ]:           7 :     s.endArray();
     206   [ +  -  +  -  :          14 :     qCInfo(lcSettingsSync) << "Applied" << payload.carddavAccounts.size()
          +  -  +  -  +  
                      + ]
     207         [ +  - ]:           7 :                            << "CardDAV accounts";
     208                 :             :   }
     209                 :             : 
     210                 :             :   // --- caldavConfigs (T-334: calendar selection per CardDAV account) ---
     211   [ +  +  +  +  :          67 :   if (cats.contains(QStringLiteral("caldavAccounts")) ||
             +  -  +  + ]
     212   [ +  +  +  +  :          33 :       cats.contains(QStringLiteral("davAccounts"))) {
                   +  - ]
     213         [ +  - ]:           7 :     s.remove(QStringLiteral("caldav/configs"));
     214         [ +  - ]:          14 :     s.beginWriteArray(QStringLiteral("caldav/configs"),
     215                 :           7 :                       payload.caldavConfigs.size());
     216         [ +  + ]:           9 :     for (int i = 0; i < payload.caldavConfigs.size(); ++i) {
     217         [ +  - ]:           2 :       s.setArrayIndex(i);
     218                 :           2 :       const auto &cfg = payload.caldavConfigs[i];
     219         [ +  - ]:           4 :       s.setValue(QStringLiteral("carddavAccountId"), cfg.carddavAccountId);
     220         [ +  - ]:           4 :       s.setValue(QStringLiteral("selectedCalendars"), cfg.selectedCalendars);
     221         [ +  - ]:           4 :       s.setValue(QStringLiteral("readOnlyCalendars"), cfg.readOnlyCalendars);
     222         [ +  - ]:           4 :       s.setValue(QStringLiteral("syncIntervalMin"), cfg.syncIntervalMinutes);
     223                 :             :     }
     224         [ +  - ]:           7 :     s.endArray();
     225   [ +  -  +  -  :          14 :     qCInfo(lcSettingsSync) << "Applied" << payload.caldavConfigs.size()
          +  -  +  -  +  
                      + ]
     226         [ +  - ]:           7 :                            << "CalDAV configs";
     227                 :             :   }
     228                 :             : 
     229                 :             :   // --- calendarColors (to CalendarStore SQLite) ---
     230   [ +  +  +  +  :          34 :   if (cats.contains(QStringLiteral("calendarColors")) && calStore) {
          +  -  +  -  +  
                      + ]
     231         [ +  - ]:           4 :     for (auto it = payload.calendarColors.constBegin();
     232   [ +  -  +  + ]:           7 :          it != payload.calendarColors.constEnd(); ++it) {
     233         [ +  - ]:           3 :       calStore->setCalendarColor(it.key(), it.value());
     234                 :             :     }
     235   [ +  -  +  -  :           8 :     qCInfo(lcSettingsSync) << "Applied" << payload.calendarColors.size()
          +  -  +  -  +  
                -  +  + ]
     236         [ +  - ]:           4 :                            << "calendar colors";
     237                 :             :   }
     238                 :             : 
     239                 :             :   // --- general ---
     240         [ +  + ]:          17 :   if (cats.contains(QStringLiteral("general"))) {
     241         [ +  - ]:           6 :     s.setValue(QStringLiteral("view/defaultMode"), payload.general.defaultView);
     242         [ +  - ]:           3 :     s.setValue(QStringLiteral("view/externalContent"),
     243                 :           3 :               payload.general.externalContent);
     244         [ +  - ]:           6 :     s.setValue(QStringLiteral("i18n/language"), payload.general.language);
     245         [ +  - ]:           3 :     s.setValue(QStringLiteral("carddav/syncIntervalMin"),
     246                 :           3 :               payload.general.carddavSyncInterval);
     247         [ +  - ]:           3 :     s.setValue(QStringLiteral("caldav/syncIntervalMin"),
     248                 :           3 :               payload.general.caldavSyncInterval);
     249   [ +  -  +  -  :           6 :     qCInfo(lcSettingsSync) << "Applied general settings";
             +  -  +  + ]
     250                 :             :   }
     251                 :             : 
     252                 :             :   // Update last sync timestamp
     253         [ +  - ]:          34 :   s.setValue(QStringLiteral("sync/lastSync"),
     254   [ +  -  +  - ]:          34 :              QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
     255                 :             : 
     256         [ +  - ]:          17 :   s.sync();
     257                 :          17 : }
     258                 :             : 
     259                 :             : // ═══════════════════════════════════════════════════════
     260                 :             : // categoryChanged – diff helper
     261                 :             : // ═══════════════════════════════════════════════════════
     262                 :             : 
     263                 :          49 : bool SettingsCollector::categoryChanged(const QString &category,
     264                 :             :                                         const SyncPayload &local,
     265                 :             :                                         const SyncPayload &remote) {
     266         [ +  + ]:          49 :   if (category == QStringLiteral("folderIcons"))
     267                 :           9 :     return local.folderIcons != remote.folderIcons;
     268         [ +  + ]:          40 :   if (category == QStringLiteral("folderColors"))
     269                 :           5 :     return local.folderColors != remote.folderColors;
     270         [ +  + ]:          35 :   if (category == QStringLiteral("hiddenFolders"))
     271                 :           4 :     return local.hiddenFolders != remote.hiddenFolders;
     272         [ +  + ]:          31 :   if (category == QStringLiteral("externalContentWhitelist")) {
     273         [ +  + ]:           8 :     if (local.externalContentWhitelist.size() !=
     274                 :           4 :         remote.externalContentWhitelist.size())
     275                 :           1 :       return true;
     276         [ +  + ]:           4 :     for (int i = 0; i < local.externalContentWhitelist.size(); ++i) {
     277                 :          12 :       if (local.externalContentWhitelist[i].type !=
     278   [ +  +  +  + ]:           5 :               remote.externalContentWhitelist[i].type ||
     279         [ +  + ]:           2 :           local.externalContentWhitelist[i].value !=
     280                 :           2 :               remote.externalContentWhitelist[i].value)
     281                 :           2 :         return true;
     282                 :             :     }
     283                 :           1 :     return false;
     284                 :             :   }
     285         [ +  + ]:          27 :   if (category == QStringLiteral("calendarColors"))
     286                 :           4 :     return local.calendarColors != remote.calendarColors;
     287   [ +  +  +  +  :          81 :   if (category == QStringLiteral("davAccounts") ||
             +  -  +  + ]
     288   [ +  +  +  +  :          35 :       category == QStringLiteral("carddavAccounts")) {
                   +  - ]
     289         [ +  + ]:          12 :     if (local.carddavAccounts.size() != remote.carddavAccounts.size())
     290                 :           1 :       return true;
     291         [ +  + ]:          15 :     for (int i = 0; i < local.carddavAccounts.size(); ++i) {
     292                 :           8 :       const auto &a = local.carddavAccounts[i];
     293                 :           8 :       const auto &b = remote.carddavAccounts[i];
     294         [ +  + ]:          15 :       if (a.id != b.id || a.serverUrl != b.serverUrl ||
     295   [ +  +  +  +  :          15 :           a.username != b.username || a.selectedBooks != b.selectedBooks)
             +  +  +  + ]
     296                 :           4 :         return true;
     297                 :             :     }
     298                 :             :     // Also check caldav configs
     299         [ +  + ]:           7 :     if (local.caldavConfigs.size() != remote.caldavConfigs.size())
     300                 :           1 :       return true;
     301         [ +  + ]:           7 :     for (int i = 0; i < local.caldavConfigs.size(); ++i) {
     302                 :           5 :       const auto &a = local.caldavConfigs[i];
     303                 :           5 :       const auto &b = remote.caldavConfigs[i];
     304                 :           5 :       if (a.carddavAccountId != b.carddavAccountId ||
     305         [ +  + ]:           4 :           a.selectedCalendars != b.selectedCalendars ||
     306   [ +  +  +  +  :          11 :           a.readOnlyCalendars != b.readOnlyCalendars ||
                   +  + ]
     307         [ +  + ]:           2 :           a.syncIntervalMinutes != b.syncIntervalMinutes)
     308                 :           4 :         return true;
     309                 :             :     }
     310                 :           2 :     return false;
     311                 :             :   }
     312         [ +  + ]:          11 :   if (category == QStringLiteral("general")) {
     313                 :           9 :     return local.general.defaultView != remote.general.defaultView ||
     314         [ +  + ]:           8 :            local.general.externalContent != remote.general.externalContent ||
     315         [ +  + ]:           7 :            local.general.language != remote.general.language ||
     316                 :           4 :            local.general.carddavSyncInterval !=
     317   [ +  +  +  + ]:          20 :                remote.general.carddavSyncInterval ||
     318                 :           3 :            local.general.caldavSyncInterval !=
     319         [ +  + ]:          12 :                remote.general.caldavSyncInterval;
     320                 :             :   }
     321                 :           2 :   return false;
     322                 :             : }
        

Generated by: LCOV version 2.0-1