Branch data Line data Source code
1 : : #pragma once
2 : :
3 : : #include <QByteArray>
4 : : #include <QDateTime>
5 : : #include <QJsonArray>
6 : : #include <QJsonDocument>
7 : : #include <QJsonObject>
8 : : #include <QList>
9 : : #include <QMap>
10 : : #include <QString>
11 : : #include <QStringList>
12 : :
13 : : // T-311: Settings synchronisation data model.
14 : : // Represents the complete set of syncable settings as a JSON payload
15 : : // stored inside a virtual IMAP mail message.
16 : : //
17 : : // The mail has:
18 : : // Subject: X-MailJD-Settings-Sync
19 : : // Content-Type: application/json; charset=utf-8
20 : : // Body: JSON serialisation of this struct
21 : :
22 : : struct SyncPayload {
23 : : static constexpr qsizetype MaxRfcMessageBytes = 1024 * 1024;
24 : :
25 : : int version = 1;
26 : : QDateTime lastModified;
27 : : QString clientId; // UUID identifying this installation
28 : :
29 : : // --- Syncable categories ---
30 : :
31 : : QMap<QString, QString> folderIcons; // folderPath → MDI icon name
32 : : QMap<QString, QString> folderColors; // folderPath → #hex color
33 : : QMap<QString, QString> calendarColors; // calendarPath → #hex color
34 : :
35 : : QStringList hiddenFolders;
36 : :
37 : : struct WhitelistItem {
38 : : QString type; // "sender" or "domain"
39 : : QString value; // e.g. "user@example.com" or "example.com"
40 : : };
41 : : QList<WhitelistItem> externalContentWhitelist;
42 : :
43 : : struct CardDavAccount {
44 : : QString id; // UUID
45 : : QString serverUrl; // e.g. "https://cloud.example.com"
46 : : QString username;
47 : : // NOTE: password is intentionally NOT synced (security)
48 : : QStringList selectedBooks; // DAV paths
49 : : };
50 : : QList<CardDavAccount> carddavAccounts;
51 : :
52 : : // T-334: CalDAV calendar sync config (Sprint 32)
53 : : // Credentials are shared with CardDAV — referenced by account ID.
54 : : struct CalDavSyncConfig {
55 : : QString carddavAccountId; // References CardDavAccount.id
56 : : QStringList selectedCalendars; // DAV paths of selected calendars
57 : : QStringList readOnlyCalendars; // Subset marked read-only
58 : : // Sprint 73: per-config interval is LEGACY/deferred metadata only. The
59 : : // active CalDAV scheduler is the single global 'caldav/syncIntervalMin'
60 : : // key (one timer for all DAV accounts). This field is still round-
61 : : // tripped by settings sync for backward compatibility, but the current
62 : : // scheduler does NOT read it. Do not wire UI controls to it unless a
63 : : // per-account scheduler is implemented.
64 : : int syncIntervalMinutes = 15;
65 : : };
66 : : QList<CalDavSyncConfig> caldavConfigs;
67 : :
68 : : struct GeneralSettings {
69 : : QString defaultView = QStringLiteral("text");
70 : : QString externalContent = QStringLiteral("block");
71 : : QString language = QStringLiteral("auto");
72 : : int carddavSyncInterval = 0; // minutes, 0 = off
73 : : int caldavSyncInterval = 15; // T-334: default minutes, 0 = off
74 : : };
75 : : GeneralSettings general;
76 : :
77 : : // Which categories are enabled for sync
78 : : QStringList enabledCategories;
79 : :
80 : : // --- Serialisation ---
81 : :
82 : 39 : QJsonObject toJson() const {
83 [ + - ]: 39 : QJsonObject root;
84 [ + - + - : 78 : root[QStringLiteral("version")] = version;
+ - ]
85 [ + - ]: 78 : root[QStringLiteral("lastModified")] =
86 [ + - + - : 117 : lastModified.toUTC().toString(Qt::ISODate);
+ - + - ]
87 [ + - + - : 78 : root[QStringLiteral("clientId")] = clientId;
+ - ]
88 : :
89 [ + - ]: 39 : QJsonObject settings;
90 : :
91 : : // folderIcons
92 [ + - ]: 39 : QJsonObject icons;
93 [ + - + - : 63 : for (auto it = folderIcons.constBegin(); it != folderIcons.constEnd(); ++it)
+ + ]
94 [ + - + - : 24 : icons[it.key()] = it.value();
+ - ]
95 [ + - + - : 78 : settings[QStringLiteral("folderIcons")] = icons;
+ - ]
96 : :
97 : : // folderColors
98 [ + - ]: 39 : QJsonObject colors;
99 [ + - + - : 57 : for (auto it = folderColors.constBegin(); it != folderColors.constEnd();
+ + ]
100 : 18 : ++it)
101 [ + - + - : 18 : colors[it.key()] = it.value();
+ - ]
102 [ + - + - : 78 : settings[QStringLiteral("folderColors")] = colors;
+ - ]
103 : :
104 : : // calendarColors
105 [ + - ]: 39 : QJsonObject calColors;
106 [ + - + - : 49 : for (auto it = calendarColors.constBegin(); it != calendarColors.constEnd();
+ + ]
107 : 10 : ++it)
108 [ + - + - : 10 : calColors[it.key()] = it.value();
+ - ]
109 [ + - + - : 78 : settings[QStringLiteral("calendarColors")] = calColors;
+ - ]
110 : :
111 : : // hiddenFolders
112 [ + - ]: 39 : QJsonArray hidden;
113 [ + + ]: 62 : for (const auto &f : hiddenFolders)
114 [ + - + - ]: 23 : hidden.append(f);
115 [ + - + - : 78 : settings[QStringLiteral("hiddenFolders")] = hidden;
+ - ]
116 : :
117 : : // externalContentWhitelist
118 [ + - ]: 39 : QJsonArray wl;
119 [ + + ]: 53 : for (const auto &item : externalContentWhitelist) {
120 [ + - ]: 14 : QJsonObject obj;
121 [ + - + - : 28 : obj[QStringLiteral("type")] = item.type;
+ - ]
122 [ + - + - : 28 : obj[QStringLiteral("value")] = item.value;
+ - ]
123 [ + - + - ]: 14 : wl.append(obj);
124 : 14 : }
125 [ + - + - : 78 : settings[QStringLiteral("externalContentWhitelist")] = wl;
+ - ]
126 : :
127 : : // carddavAccounts (NO passwords!)
128 [ + - ]: 39 : QJsonArray cdArr;
129 [ + + ]: 50 : for (const auto &acc : carddavAccounts) {
130 [ + - ]: 11 : QJsonObject obj;
131 [ + - + - : 22 : obj[QStringLiteral("id")] = acc.id;
+ - ]
132 [ + - + - : 22 : obj[QStringLiteral("serverUrl")] = acc.serverUrl;
+ - ]
133 [ + - + - : 22 : obj[QStringLiteral("username")] = acc.username;
+ - ]
134 [ + - ]: 11 : QJsonArray books;
135 [ + + ]: 25 : for (const auto &b : acc.selectedBooks)
136 [ + - + - ]: 14 : books.append(b);
137 [ + - + - : 22 : obj[QStringLiteral("selectedBooks")] = books;
+ - ]
138 [ + - + - ]: 11 : cdArr.append(obj);
139 : 11 : }
140 [ + - + - : 78 : settings[QStringLiteral("carddavAccounts")] = cdArr;
+ - ]
141 : :
142 : : // T-334: caldavConfigs (calendar sync tied to CardDAV accounts)
143 [ + - ]: 39 : QJsonArray caArr;
144 [ + + ]: 49 : for (const auto &cfg : caldavConfigs) {
145 [ + - ]: 10 : QJsonObject obj;
146 [ + - + - : 20 : obj[QStringLiteral("carddavAccountId")] = cfg.carddavAccountId;
+ - ]
147 [ + - ]: 10 : QJsonArray cals;
148 [ + + ]: 23 : for (const auto &c : cfg.selectedCalendars)
149 [ + - + - ]: 13 : cals.append(c);
150 [ + - + - : 20 : obj[QStringLiteral("selectedCalendars")] = cals;
+ - ]
151 [ + - ]: 10 : QJsonArray roCals;
152 [ + + ]: 16 : for (const auto &r : cfg.readOnlyCalendars)
153 [ + - + - ]: 6 : roCals.append(r);
154 [ + - + - : 20 : obj[QStringLiteral("readOnlyCalendars")] = roCals;
+ - ]
155 [ + - + - : 20 : obj[QStringLiteral("syncIntervalMinutes")] = cfg.syncIntervalMinutes;
+ - ]
156 [ + - + - ]: 10 : caArr.append(obj);
157 : 10 : }
158 [ + - + - : 78 : settings[QStringLiteral("caldavConfigs")] = caArr;
+ - ]
159 : :
160 : : // general
161 [ + - ]: 39 : QJsonObject gen;
162 [ + - + - : 78 : gen[QStringLiteral("defaultView")] = general.defaultView;
+ - ]
163 [ + - + - : 78 : gen[QStringLiteral("externalContent")] = general.externalContent;
+ - ]
164 [ + - + - : 78 : gen[QStringLiteral("language")] = general.language;
+ - ]
165 [ + - + - : 78 : gen[QStringLiteral("carddavSyncInterval")] = general.carddavSyncInterval;
+ - ]
166 [ + - + - : 78 : gen[QStringLiteral("caldavSyncInterval")] = general.caldavSyncInterval;
+ - ]
167 [ + - + - : 78 : settings[QStringLiteral("general")] = gen;
+ - ]
168 : :
169 [ + - + - : 78 : root[QStringLiteral("settings")] = settings;
+ - ]
170 : :
171 : : // enabledCategories
172 [ + - ]: 39 : QJsonArray cats;
173 [ + + ]: 98 : for (const auto &c : enabledCategories)
174 [ + - + - ]: 59 : cats.append(c);
175 [ + - + - : 78 : root[QStringLiteral("enabledCategories")] = cats;
+ - ]
176 : :
177 : 39 : return root;
178 : 39 : }
179 : :
180 : 47 : static SyncPayload fromJson(const QJsonObject &root) {
181 [ + - ]: 47 : SyncPayload p;
182 [ + - + - ]: 47 : p.version = root.value(QStringLiteral("version")).toInt(1);
183 [ + - ]: 94 : p.lastModified = QDateTime::fromString(
184 [ + - + - ]: 141 : root.value(QStringLiteral("lastModified")).toString(), Qt::ISODate);
185 [ + - + - ]: 47 : p.clientId = root.value(QStringLiteral("clientId")).toString();
186 : :
187 [ + - + - ]: 47 : auto settings = root.value(QStringLiteral("settings")).toObject();
188 : :
189 : : // folderIcons
190 [ + - + - ]: 47 : auto icons = settings.value(QStringLiteral("folderIcons")).toObject();
191 [ + - + + ]: 66 : for (auto it = icons.constBegin(); it != icons.constEnd(); ++it)
192 [ + - + - : 19 : p.folderIcons[it.key()] = it.value().toString();
+ - ]
193 : :
194 : : // folderColors
195 [ + - + - ]: 47 : auto colors = settings.value(QStringLiteral("folderColors")).toObject();
196 [ + - + + ]: 63 : for (auto it = colors.constBegin(); it != colors.constEnd(); ++it)
197 [ + - + - : 16 : p.folderColors[it.key()] = it.value().toString();
+ - ]
198 : :
199 : : // calendarColors (backward-compatible: absent = empty)
200 : : auto calColors =
201 [ + - + - ]: 47 : settings.value(QStringLiteral("calendarColors")).toObject();
202 [ + - + + ]: 55 : for (auto it = calColors.constBegin(); it != calColors.constEnd(); ++it)
203 [ + - + - : 8 : p.calendarColors[it.key()] = it.value().toString();
+ - ]
204 : :
205 : : // hiddenFolders
206 [ + - + - ]: 47 : auto hidden = settings.value(QStringLiteral("hiddenFolders")).toArray();
207 [ + - + - : 67 : for (const auto &v : hidden)
+ + ]
208 [ + - + - ]: 20 : p.hiddenFolders.append(v.toString());
209 : :
210 : : // externalContentWhitelist
211 : : auto wl =
212 [ + - + - ]: 47 : settings.value(QStringLiteral("externalContentWhitelist")).toArray();
213 [ + - + - : 59 : for (const auto &v : wl) {
+ + ]
214 [ + - ]: 12 : auto obj = v.toObject();
215 : 12 : WhitelistItem item;
216 [ + - + - ]: 12 : item.type = obj.value(QStringLiteral("type")).toString();
217 [ + - + - ]: 12 : item.value = obj.value(QStringLiteral("value")).toString();
218 [ + - ]: 12 : p.externalContentWhitelist.append(item);
219 : 12 : }
220 : :
221 : : // carddavAccounts
222 : : auto cdArr =
223 [ + - + - ]: 47 : settings.value(QStringLiteral("carddavAccounts")).toArray();
224 [ + - + - : 55 : for (const auto &v : cdArr) {
+ + ]
225 [ + - ]: 8 : auto obj = v.toObject();
226 : 8 : CardDavAccount acc;
227 [ + - + - ]: 8 : acc.id = obj.value(QStringLiteral("id")).toString();
228 [ + - + - ]: 8 : acc.serverUrl = obj.value(QStringLiteral("serverUrl")).toString();
229 [ + - + - ]: 8 : acc.username = obj.value(QStringLiteral("username")).toString();
230 [ + - + - ]: 8 : auto books = obj.value(QStringLiteral("selectedBooks")).toArray();
231 [ + - + - : 19 : for (const auto &b : books)
+ + ]
232 [ + - + - ]: 11 : acc.selectedBooks.append(b.toString());
233 [ + - ]: 8 : p.carddavAccounts.append(acc);
234 : 8 : }
235 : :
236 : : // T-334: caldavConfigs (backward-compatible: absent = empty list)
237 : : auto caArr =
238 [ + - + - ]: 47 : settings.value(QStringLiteral("caldavConfigs")).toArray();
239 [ + - + - : 57 : for (const auto &v : caArr) {
+ + ]
240 [ + - ]: 10 : auto obj = v.toObject();
241 : 10 : CalDavSyncConfig cfg;
242 : : cfg.carddavAccountId =
243 [ + - + - ]: 10 : obj.value(QStringLiteral("carddavAccountId")).toString();
244 [ + - + - ]: 10 : auto cals = obj.value(QStringLiteral("selectedCalendars")).toArray();
245 [ + - + - : 21 : for (const auto &c : cals)
+ + ]
246 [ + - + - ]: 11 : cfg.selectedCalendars.append(c.toString());
247 [ + - + - ]: 10 : auto roCals = obj.value(QStringLiteral("readOnlyCalendars")).toArray();
248 [ + - + - : 15 : for (const auto &r : roCals)
+ + ]
249 [ + - + - ]: 5 : cfg.readOnlyCalendars.append(r.toString());
250 : 10 : cfg.syncIntervalMinutes =
251 [ + - + - ]: 10 : obj.value(QStringLiteral("syncIntervalMinutes")).toInt(15);
252 [ + - ]: 10 : p.caldavConfigs.append(cfg);
253 : 10 : }
254 : :
255 : : // general
256 [ + - + - ]: 47 : auto gen = settings.value(QStringLiteral("general")).toObject();
257 [ + - + + ]: 47 : if (!gen.isEmpty()) {
258 : : p.general.defaultView =
259 [ + - + - ]: 62 : gen.value(QStringLiteral("defaultView")).toString(QStringLiteral("text"));
260 : : p.general.externalContent =
261 [ + - + - ]: 62 : gen.value(QStringLiteral("externalContent")).toString(QStringLiteral("block"));
262 : : p.general.language =
263 [ + - + - ]: 62 : gen.value(QStringLiteral("language")).toString(QStringLiteral("auto"));
264 : 31 : p.general.carddavSyncInterval =
265 [ + - + - ]: 31 : gen.value(QStringLiteral("carddavSyncInterval")).toInt(0);
266 : 31 : p.general.caldavSyncInterval =
267 [ + - + - ]: 31 : gen.value(QStringLiteral("caldavSyncInterval")).toInt(15);
268 : : }
269 : :
270 : : // enabledCategories
271 [ + - + - ]: 47 : auto cats = root.value(QStringLiteral("enabledCategories")).toArray();
272 [ + - + - : 91 : for (const auto &v : cats)
+ + ]
273 [ + - + - ]: 44 : p.enabledCategories.append(v.toString());
274 : :
275 : 47 : return p;
276 : 47 : }
277 : :
278 : : // --- RFC-2822 message serialisation ---
279 : :
280 : : // Build a minimal RFC-2822 message wrapping the JSON payload.
281 : : // This message is stored via IMAP APPEND in the sync folder.
282 : 15 : QByteArray toRfcMessage() const {
283 : 15 : QByteArray msg;
284 [ + - ]: 15 : msg += "From: mailjd-settings-sync@localhost\r\n";
285 [ + - ]: 15 : msg += "Subject: X-MailJD-Settings-Sync\r\n";
286 : 15 : msg += "Date: " +
287 [ + - + - : 30 : lastModified.toUTC().toString(Qt::RFC2822Date).toUtf8() + "\r\n";
+ - + - +
- + - ]
288 [ + - ]: 15 : msg += "MIME-Version: 1.0\r\n";
289 [ + - ]: 15 : msg += "Content-Type: application/json; charset=utf-8\r\n";
290 [ + - ]: 15 : msg += "Content-Transfer-Encoding: 8bit\r\n";
291 [ + - + - : 15 : msg += "X-MailJD-ClientId: " + clientId.toUtf8() + "\r\n";
+ - + - ]
292 [ + - ]: 15 : msg += "\r\n"; // Header/body separator
293 : :
294 [ + - + - ]: 15 : QJsonDocument doc(toJson());
295 [ + - + - ]: 15 : msg += doc.toJson(QJsonDocument::Indented);
296 : :
297 : 15 : return msg;
298 : 15 : }
299 : :
300 : : // Parse a SyncPayload from a raw RFC-2822 message (as fetched via IMAP).
301 : 18 : static SyncPayload fromRfcMessage(const QByteArray &raw) {
302 [ + + ]: 18 : if (raw.size() > MaxRfcMessageBytes)
303 : 1 : return {};
304 : :
305 : : // Find header/body separator (blank line = \r\n\r\n or \n\n)
306 : 17 : int bodyStart = raw.indexOf("\r\n\r\n");
307 [ + + ]: 17 : if (bodyStart >= 0) {
308 : 13 : bodyStart += 4;
309 : : } else {
310 : 4 : bodyStart = raw.indexOf("\n\n");
311 [ + + ]: 4 : if (bodyStart >= 0)
312 : 2 : bodyStart += 2;
313 : : else
314 : 2 : bodyStart = 0; // Fallback: treat entire content as body
315 : : }
316 : :
317 [ + - ]: 17 : QByteArray body = raw.mid(bodyStart);
318 : :
319 [ + - ]: 17 : auto doc = QJsonDocument::fromJson(body);
320 [ + - + + : 17 : if (doc.isNull() || !doc.isObject())
+ - + + +
+ ]
321 : 2 : return {};
322 : :
323 [ + - + - ]: 15 : SyncPayload p = fromJson(doc.object());
324 : :
325 : : // Also extract clientId from header if missing from JSON
326 [ + + ]: 15 : if (p.clientId.isEmpty()) {
327 : : // Parse X-MailJD-ClientId header
328 [ + - ]: 5 : QByteArray header = raw.left(bodyStart);
329 : 5 : int idx = header.indexOf("X-MailJD-ClientId:");
330 [ + + ]: 5 : if (idx >= 0) {
331 : 3 : int lineEnd = header.indexOf('\n', idx);
332 [ - + ]: 3 : if (lineEnd < 0)
333 : 0 : lineEnd = header.size();
334 : : QByteArray val =
335 [ + - + - ]: 3 : header.mid(idx + 18, lineEnd - idx - 18).trimmed();
336 : : // Remove trailing \r if present
337 [ + - - + ]: 3 : if (val.endsWith('\r'))
338 [ # # ]: 0 : val.chop(1);
339 [ + - ]: 3 : p.clientId = QString::fromUtf8(val);
340 : 3 : }
341 : 5 : }
342 : :
343 : 15 : return p;
344 : 29 : }
345 : :
346 : : // --- Default categories ---
347 : :
348 : 105 : static QStringList allCategories() {
349 : : return {
350 : 0 : QStringLiteral("folderIcons"),
351 : 105 : QStringLiteral("folderColors"),
352 : 105 : QStringLiteral("calendarColors"),
353 : 105 : QStringLiteral("hiddenFolders"),
354 : 105 : QStringLiteral("externalContentWhitelist"),
355 : 105 : QStringLiteral("davAccounts"),
356 : 105 : QStringLiteral("general"),
357 [ + + - - ]: 945 : };
358 [ + - - - : 840 : }
- - ]
359 : : };
|