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 : : }
|