MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 98.4% 247 / 0 / 251
Functions: 100.0% 9 / 0 / 9
Branches: 95.9% 419 / 0 / 437

data/SettingsSyncModels.h
Line Branch Exec Source
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 #include <utility>
13
14 // T-311: Settings synchronisation data model.
15 // Represents the complete set of syncable settings as a JSON payload
16 // stored inside a virtual IMAP mail message.
17 //
18 // The mail has:
19 // Subject: X-MailJD-Settings-Sync
20 // Content-Type: application/json; charset=utf-8
21 // Body: JSON serialisation of this struct
22
23 struct SyncPayload {
24 static constexpr qsizetype MaxRfcMessageBytes = 1024 * 1024;
25
26 int version = 1;
27 QDateTime lastModified;
28 QString clientId; // UUID identifying this installation
29
30 // --- Syncable categories ---
31
32 QMap<QString, QString> folderIcons; // folderPath → MDI icon name
33 QMap<QString, QString> folderColors; // folderPath → #hex color
34 // SettingsCollector v2 key (accountId + path) → #hex color. Plain path keys
35 // remain readable for payloads created before account-aware synchronization.
36 QMap<QString, QString> calendarColors;
37
38 QStringList hiddenFolders;
39
40 struct WhitelistItem {
41 QString type; // "sender" or "domain"
42 QString value; // e.g. "user@example.com" or "example.com"
43 };
44 QList<WhitelistItem> externalContentWhitelist;
45
46 struct CardDavAccount {
47 QString id; // UUID
48 QString serverUrl; // e.g. "https://cloud.example.com"
49 QString username;
50 // NOTE: password is intentionally NOT synced (security)
51 QStringList selectedBooks; // DAV paths
52 };
53 QList<CardDavAccount> carddavAccounts;
54
55 // T-334: CalDAV calendar sync config (Sprint 32)
56 // Credentials are shared with CardDAV — referenced by account ID.
57 struct CalDavSyncConfig {
58 QString carddavAccountId; // References CardDavAccount.id
59 QStringList selectedCalendars; // DAV paths of selected calendars
60 QStringList readOnlyCalendars; // Subset marked read-only
61 // Sprint 73: per-config interval is LEGACY/deferred metadata only. The
62 // active CalDAV scheduler is the single global 'caldav/syncIntervalMin'
63 // key (one timer for all DAV accounts). This field is still round-
64 // tripped by settings sync for backward compatibility, but the current
65 // scheduler does NOT read it. Do not wire UI controls to it unless a
66 // per-account scheduler is implemented.
67 int syncIntervalMinutes = 15;
68 };
69 QList<CalDavSyncConfig> caldavConfigs;
70
71 struct GeneralSettings {
72 QString defaultView = QStringLiteral("text");
73 QString externalContent = QStringLiteral("block");
74 QString language = QStringLiteral("auto");
75 int carddavSyncInterval = 0; // minutes, 0 = off
76 int caldavSyncInterval = 15; // T-334: default minutes, 0 = off
77 };
78 GeneralSettings general;
79
80 // Sprint 108 S108.5.4 (ComposeSettingsSync): the composer's identities
81 // (key "<account address>|<IMAP host>|<address>") and templates (key: the
82 // lower-case name); values are the synchronized fields as JSON objects.
83 // Parsing keeps at most MaxComposeEntries per map and only object values.
84 static constexpr int MaxComposeEntries = 200;
85 QMap<QString, QJsonObject> identities;
86 QMap<QString, QJsonObject> templates;
87
88 // Which categories are enabled for sync
89 QStringList enabledCategories;
90
91 // --- Serialisation ---
92
93 404 QJsonObject toJson() const {
94
1/1
✓ Branch 2 → 3 taken 404 times.
404 QJsonObject root;
95
3/3
✓ Branch 3 → 4 taken 404 times.
✓ Branch 7 → 8 taken 404 times.
✓ Branch 8 → 9 taken 404 times.
808 root[QStringLiteral("version")] = version;
96
1/1
✓ Branch 18 → 19 taken 404 times.
808 root[QStringLiteral("lastModified")] =
97
4/4
✓ Branch 12 → 13 taken 404 times.
✓ Branch 13 → 14 taken 404 times.
✓ Branch 14 → 15 taken 404 times.
✓ Branch 19 → 20 taken 404 times.
1212 lastModified.toUTC().toString(Qt::ISODate);
98
3/3
✓ Branch 25 → 26 taken 404 times.
✓ Branch 29 → 30 taken 404 times.
✓ Branch 30 → 31 taken 404 times.
808 root[QStringLiteral("clientId")] = clientId;
99
100
1/1
✓ Branch 34 → 35 taken 404 times.
404 QJsonObject settings;
101
102 // folderIcons
103
1/1
✓ Branch 35 → 36 taken 404 times.
404 QJsonObject icons;
104
4/4
✓ Branch 36 → 37 taken 404 times.
✓ Branch 45 → 46 taken 30680 times.
✓ Branch 47 → 38 taken 30276 times.
✓ Branch 47 → 48 taken 404 times.
30680 for (auto it = folderIcons.constBegin(); it != folderIcons.constEnd(); ++it)
105
3/3
✓ Branch 39 → 40 taken 30276 times.
✓ Branch 41 → 42 taken 30276 times.
✓ Branch 42 → 43 taken 30276 times.
30276 icons[it.key()] = it.value();
106
3/3
✓ Branch 48 → 49 taken 404 times.
✓ Branch 52 → 53 taken 404 times.
✓ Branch 53 → 54 taken 404 times.
808 settings[QStringLiteral("folderIcons")] = icons;
107
108 // folderColors
109
1/1
✓ Branch 57 → 58 taken 404 times.
404 QJsonObject colors;
110
4/4
✓ Branch 58 → 59 taken 404 times.
✓ Branch 67 → 68 taken 454 times.
✓ Branch 69 → 60 taken 50 times.
✓ Branch 69 → 70 taken 404 times.
454 for (auto it = folderColors.constBegin(); it != folderColors.constEnd();
111 50 ++it)
112
3/3
✓ Branch 61 → 62 taken 50 times.
✓ Branch 63 → 64 taken 50 times.
✓ Branch 64 → 65 taken 50 times.
50 colors[it.key()] = it.value();
113
3/3
✓ Branch 70 → 71 taken 404 times.
✓ Branch 74 → 75 taken 404 times.
✓ Branch 75 → 76 taken 404 times.
808 settings[QStringLiteral("folderColors")] = colors;
114
115 // calendarColors
116
1/1
✓ Branch 79 → 80 taken 404 times.
404 QJsonObject calColors;
117
4/4
✓ Branch 80 → 81 taken 404 times.
✓ Branch 89 → 90 taken 418 times.
✓ Branch 91 → 82 taken 14 times.
✓ Branch 91 → 92 taken 404 times.
418 for (auto it = calendarColors.constBegin(); it != calendarColors.constEnd();
118 14 ++it)
119
3/3
✓ Branch 83 → 84 taken 14 times.
✓ Branch 85 → 86 taken 14 times.
✓ Branch 86 → 87 taken 14 times.
14 calColors[it.key()] = it.value();
120
3/3
✓ Branch 92 → 93 taken 404 times.
✓ Branch 96 → 97 taken 404 times.
✓ Branch 97 → 98 taken 404 times.
808 settings[QStringLiteral("calendarColors")] = calColors;
121
122 // hiddenFolders
123
1/1
✓ Branch 101 → 102 taken 404 times.
404 QJsonArray hidden;
124
2/2
✓ Branch 110 → 104 taken 52 times.
✓ Branch 110 → 111 taken 404 times.
456 for (const auto &f : hiddenFolders)
125
2/2
✓ Branch 105 → 106 taken 52 times.
✓ Branch 106 → 107 taken 52 times.
52 hidden.append(f);
126
3/3
✓ Branch 111 → 112 taken 404 times.
✓ Branch 115 → 116 taken 404 times.
✓ Branch 116 → 117 taken 404 times.
808 settings[QStringLiteral("hiddenFolders")] = hidden;
127
128 // externalContentWhitelist
129
1/1
✓ Branch 120 → 121 taken 404 times.
404 QJsonArray wl;
130
2/2
✓ Branch 149 → 123 taken 10 times.
✓ Branch 149 → 150 taken 404 times.
414 for (const auto &item : externalContentWhitelist) {
131
1/1
✓ Branch 124 → 125 taken 10 times.
10 QJsonObject obj;
132
3/3
✓ Branch 125 → 126 taken 10 times.
✓ Branch 129 → 130 taken 10 times.
✓ Branch 130 → 131 taken 10 times.
20 obj[QStringLiteral("type")] = item.type;
133
3/3
✓ Branch 134 → 135 taken 10 times.
✓ Branch 138 → 139 taken 10 times.
✓ Branch 139 → 140 taken 10 times.
20 obj[QStringLiteral("value")] = item.value;
134
2/2
✓ Branch 143 → 144 taken 10 times.
✓ Branch 144 → 145 taken 10 times.
10 wl.append(obj);
135 10 }
136
3/3
✓ Branch 150 → 151 taken 404 times.
✓ Branch 154 → 155 taken 404 times.
✓ Branch 155 → 156 taken 404 times.
808 settings[QStringLiteral("externalContentWhitelist")] = wl;
137
138 // carddavAccounts (NO passwords!)
139
1/1
✓ Branch 159 → 160 taken 404 times.
404 QJsonArray cdArr;
140
2/2
✓ Branch 217 → 162 taken 59 times.
✓ Branch 217 → 218 taken 404 times.
463 for (const auto &acc : carddavAccounts) {
141
1/1
✓ Branch 163 → 164 taken 59 times.
59 QJsonObject obj;
142
3/3
✓ Branch 164 → 165 taken 59 times.
✓ Branch 168 → 169 taken 59 times.
✓ Branch 169 → 170 taken 59 times.
118 obj[QStringLiteral("id")] = acc.id;
143
3/3
✓ Branch 173 → 174 taken 59 times.
✓ Branch 177 → 178 taken 59 times.
✓ Branch 178 → 179 taken 59 times.
118 obj[QStringLiteral("serverUrl")] = acc.serverUrl;
144
3/3
✓ Branch 182 → 183 taken 59 times.
✓ Branch 186 → 187 taken 59 times.
✓ Branch 187 → 188 taken 59 times.
118 obj[QStringLiteral("username")] = acc.username;
145
1/1
✓ Branch 191 → 192 taken 59 times.
59 QJsonArray books;
146
2/2
✓ Branch 200 → 194 taken 52 times.
✓ Branch 200 → 201 taken 59 times.
111 for (const auto &b : acc.selectedBooks)
147
2/2
✓ Branch 195 → 196 taken 52 times.
✓ Branch 196 → 197 taken 52 times.
52 books.append(b);
148
3/3
✓ Branch 201 → 202 taken 59 times.
✓ Branch 205 → 206 taken 59 times.
✓ Branch 206 → 207 taken 59 times.
118 obj[QStringLiteral("selectedBooks")] = books;
149
2/2
✓ Branch 210 → 211 taken 59 times.
✓ Branch 211 → 212 taken 59 times.
59 cdArr.append(obj);
150 59 }
151
3/3
✓ Branch 218 → 219 taken 404 times.
✓ Branch 222 → 223 taken 404 times.
✓ Branch 223 → 224 taken 404 times.
808 settings[QStringLiteral("carddavAccounts")] = cdArr;
152
153 // T-334: caldavConfigs (calendar sync tied to CardDAV accounts)
154
1/1
✓ Branch 227 → 228 taken 404 times.
404 QJsonArray caArr;
155
2/2
✓ Branch 296 → 230 taken 52 times.
✓ Branch 296 → 297 taken 404 times.
456 for (const auto &cfg : caldavConfigs) {
156
1/1
✓ Branch 231 → 232 taken 52 times.
52 QJsonObject obj;
157
3/3
✓ Branch 232 → 233 taken 52 times.
✓ Branch 236 → 237 taken 52 times.
✓ Branch 237 → 238 taken 52 times.
104 obj[QStringLiteral("carddavAccountId")] = cfg.carddavAccountId;
158
1/1
✓ Branch 241 → 242 taken 52 times.
52 QJsonArray cals;
159
2/2
✓ Branch 250 → 244 taken 52 times.
✓ Branch 250 → 251 taken 52 times.
104 for (const auto &c : cfg.selectedCalendars)
160
2/2
✓ Branch 245 → 246 taken 52 times.
✓ Branch 246 → 247 taken 52 times.
52 cals.append(c);
161
3/3
✓ Branch 251 → 252 taken 52 times.
✓ Branch 255 → 256 taken 52 times.
✓ Branch 256 → 257 taken 52 times.
104 obj[QStringLiteral("selectedCalendars")] = cals;
162
1/1
✓ Branch 260 → 261 taken 52 times.
52 QJsonArray roCals;
163
2/2
✓ Branch 269 → 263 taken 6 times.
✓ Branch 269 → 270 taken 52 times.
58 for (const auto &r : cfg.readOnlyCalendars)
164
2/2
✓ Branch 264 → 265 taken 6 times.
✓ Branch 265 → 266 taken 6 times.
6 roCals.append(r);
165
3/3
✓ Branch 270 → 271 taken 52 times.
✓ Branch 274 → 275 taken 52 times.
✓ Branch 275 → 276 taken 52 times.
104 obj[QStringLiteral("readOnlyCalendars")] = roCals;
166
3/3
✓ Branch 279 → 280 taken 52 times.
✓ Branch 283 → 284 taken 52 times.
✓ Branch 284 → 285 taken 52 times.
104 obj[QStringLiteral("syncIntervalMinutes")] = cfg.syncIntervalMinutes;
167
2/2
✓ Branch 288 → 289 taken 52 times.
✓ Branch 289 → 290 taken 52 times.
52 caArr.append(obj);
168 52 }
169
3/3
✓ Branch 297 → 298 taken 404 times.
✓ Branch 301 → 302 taken 404 times.
✓ Branch 302 → 303 taken 404 times.
808 settings[QStringLiteral("caldavConfigs")] = caArr;
170
171 // general
172
1/1
✓ Branch 306 → 307 taken 404 times.
404 QJsonObject gen;
173
3/3
✓ Branch 307 → 308 taken 404 times.
✓ Branch 311 → 312 taken 404 times.
✓ Branch 312 → 313 taken 404 times.
808 gen[QStringLiteral("defaultView")] = general.defaultView;
174
3/3
✓ Branch 316 → 317 taken 404 times.
✓ Branch 320 → 321 taken 404 times.
✓ Branch 321 → 322 taken 404 times.
808 gen[QStringLiteral("externalContent")] = general.externalContent;
175
3/3
✓ Branch 325 → 326 taken 404 times.
✓ Branch 329 → 330 taken 404 times.
✓ Branch 330 → 331 taken 404 times.
808 gen[QStringLiteral("language")] = general.language;
176
3/3
✓ Branch 334 → 335 taken 404 times.
✓ Branch 338 → 339 taken 404 times.
✓ Branch 339 → 340 taken 404 times.
808 gen[QStringLiteral("carddavSyncInterval")] = general.carddavSyncInterval;
177
3/3
✓ Branch 343 → 344 taken 404 times.
✓ Branch 347 → 348 taken 404 times.
✓ Branch 348 → 349 taken 404 times.
808 gen[QStringLiteral("caldavSyncInterval")] = general.caldavSyncInterval;
178
3/3
✓ Branch 352 → 353 taken 404 times.
✓ Branch 356 → 357 taken 404 times.
✓ Branch 357 → 358 taken 404 times.
808 settings[QStringLiteral("general")] = gen;
179
180
1/1
✓ Branch 361 → 362 taken 404 times.
404 QJsonObject identityObjects;
181
4/4
✓ Branch 362 → 363 taken 404 times.
✓ Branch 371 → 372 taken 416 times.
✓ Branch 373 → 364 taken 12 times.
✓ Branch 373 → 374 taken 404 times.
416 for (auto it = identities.constBegin(); it != identities.constEnd(); ++it)
182
3/3
✓ Branch 365 → 366 taken 12 times.
✓ Branch 367 → 368 taken 12 times.
✓ Branch 368 → 369 taken 12 times.
12 identityObjects[it.key()] = it.value();
183
3/3
✓ Branch 374 → 375 taken 404 times.
✓ Branch 378 → 379 taken 404 times.
✓ Branch 379 → 380 taken 404 times.
808 settings[QStringLiteral("identities")] = identityObjects;
184
1/1
✓ Branch 383 → 384 taken 404 times.
404 QJsonObject templateObjects;
185
4/4
✓ Branch 384 → 385 taken 404 times.
✓ Branch 393 → 394 taken 423 times.
✓ Branch 395 → 386 taken 19 times.
✓ Branch 395 → 396 taken 404 times.
423 for (auto it = templates.constBegin(); it != templates.constEnd(); ++it)
186
3/3
✓ Branch 387 → 388 taken 19 times.
✓ Branch 389 → 390 taken 19 times.
✓ Branch 390 → 391 taken 19 times.
19 templateObjects[it.key()] = it.value();
187
3/3
✓ Branch 396 → 397 taken 404 times.
✓ Branch 400 → 401 taken 404 times.
✓ Branch 401 → 402 taken 404 times.
808 settings[QStringLiteral("templates")] = templateObjects;
188
189
3/3
✓ Branch 405 → 406 taken 404 times.
✓ Branch 409 → 410 taken 404 times.
✓ Branch 410 → 411 taken 404 times.
808 root[QStringLiteral("settings")] = settings;
190
191 // enabledCategories
192
1/1
✓ Branch 414 → 415 taken 404 times.
404 QJsonArray cats;
193
2/2
✓ Branch 423 → 417 taken 843 times.
✓ Branch 423 → 424 taken 404 times.
1247 for (const auto &c : enabledCategories)
194
2/2
✓ Branch 418 → 419 taken 843 times.
✓ Branch 419 → 420 taken 843 times.
843 cats.append(c);
195
3/3
✓ Branch 424 → 425 taken 404 times.
✓ Branch 428 → 429 taken 404 times.
✓ Branch 429 → 430 taken 404 times.
808 root[QStringLiteral("enabledCategories")] = cats;
196
197 404 return root;
198 404 }
199
200 169 static SyncPayload fromJson(const QJsonObject &root) {
201
1/1
✓ Branch 2 → 3 taken 169 times.
169 SyncPayload p;
202
2/2
✓ Branch 6 → 7 taken 169 times.
✓ Branch 7 → 8 taken 169 times.
169 p.version = root.value(QStringLiteral("version")).toInt(1);
203
1/1
✓ Branch 16 → 17 taken 169 times.
338 p.lastModified = QDateTime::fromString(
204
2/2
✓ Branch 14 → 15 taken 169 times.
✓ Branch 15 → 16 taken 169 times.
507 root.value(QStringLiteral("lastModified")).toString(), Qt::ISODate);
205
2/2
✓ Branch 26 → 27 taken 169 times.
✓ Branch 27 → 28 taken 169 times.
169 p.clientId = root.value(QStringLiteral("clientId")).toString();
206
207
2/2
✓ Branch 36 → 37 taken 169 times.
✓ Branch 37 → 38 taken 169 times.
169 auto settings = root.value(QStringLiteral("settings")).toObject();
208
209 // folderIcons
210
2/2
✓ Branch 44 → 45 taken 169 times.
✓ Branch 45 → 46 taken 169 times.
169 auto icons = settings.value(QStringLiteral("folderIcons")).toObject();
211
3/3
✓ Branch 60 → 61 taken 310 times.
✓ Branch 62 → 50 taken 141 times.
✓ Branch 62 → 63 taken 169 times.
310 for (auto it = icons.constBegin(); it != icons.constEnd(); ++it)
212
3/3
✓ Branch 52 → 53 taken 141 times.
✓ Branch 53 → 54 taken 141 times.
✓ Branch 54 → 55 taken 141 times.
141 p.folderIcons[it.key()] = it.value().toString();
213
214 // folderColors
215
2/2
✓ Branch 66 → 67 taken 169 times.
✓ Branch 67 → 68 taken 169 times.
169 auto colors = settings.value(QStringLiteral("folderColors")).toObject();
216
3/3
✓ Branch 82 → 83 taken 204 times.
✓ Branch 84 → 72 taken 35 times.
✓ Branch 84 → 85 taken 169 times.
204 for (auto it = colors.constBegin(); it != colors.constEnd(); ++it)
217
3/3
✓ Branch 74 → 75 taken 35 times.
✓ Branch 75 → 76 taken 35 times.
✓ Branch 76 → 77 taken 35 times.
35 p.folderColors[it.key()] = it.value().toString();
218
219 // calendarColors (backward-compatible: absent = empty)
220 auto calColors =
221
2/2
✓ Branch 88 → 89 taken 169 times.
✓ Branch 89 → 90 taken 169 times.
169 settings.value(QStringLiteral("calendarColors")).toObject();
222
3/3
✓ Branch 104 → 105 taken 175 times.
✓ Branch 106 → 94 taken 6 times.
✓ Branch 106 → 107 taken 169 times.
175 for (auto it = calColors.constBegin(); it != calColors.constEnd(); ++it)
223
3/3
✓ Branch 96 → 97 taken 6 times.
✓ Branch 97 → 98 taken 6 times.
✓ Branch 98 → 99 taken 6 times.
6 p.calendarColors[it.key()] = it.value().toString();
224
225 // hiddenFolders
226
2/2
✓ Branch 110 → 111 taken 169 times.
✓ Branch 111 → 112 taken 169 times.
169 auto hidden = settings.value(QStringLiteral("hiddenFolders")).toArray();
227
4/4
✓ Branch 115 → 116 taken 169 times.
✓ Branch 116 → 117 taken 169 times.
✓ Branch 126 → 118 taken 36 times.
✓ Branch 126 → 127 taken 169 times.
205 for (const auto &v : hidden)
228
2/2
✓ Branch 120 → 121 taken 36 times.
✓ Branch 121 → 122 taken 36 times.
36 p.hiddenFolders.append(v.toString());
229
230 // externalContentWhitelist
231 auto wl =
232
2/2
✓ Branch 130 → 131 taken 169 times.
✓ Branch 131 → 132 taken 169 times.
169 settings.value(QStringLiteral("externalContentWhitelist")).toArray();
233
4/4
✓ Branch 135 → 136 taken 169 times.
✓ Branch 136 → 137 taken 169 times.
✓ Branch 165 → 138 taken 8 times.
✓ Branch 165 → 166 taken 169 times.
177 for (const auto &v : wl) {
234
1/1
✓ Branch 139 → 140 taken 8 times.
8 auto obj = v.toObject();
235 8 WhitelistItem item;
236
2/2
✓ Branch 143 → 144 taken 8 times.
✓ Branch 144 → 145 taken 8 times.
8 item.type = obj.value(QStringLiteral("type")).toString();
237
2/2
✓ Branch 153 → 154 taken 8 times.
✓ Branch 154 → 155 taken 8 times.
8 item.value = obj.value(QStringLiteral("value")).toString();
238
1/1
✓ Branch 160 → 161 taken 8 times.
8 p.externalContentWhitelist.append(item);
239 8 }
240
241 // carddavAccounts
242 auto cdArr =
243
2/2
✓ Branch 169 → 170 taken 169 times.
✓ Branch 170 → 171 taken 169 times.
169 settings.value(QStringLiteral("carddavAccounts")).toArray();
244
4/4
✓ Branch 174 → 175 taken 169 times.
✓ Branch 175 → 176 taken 169 times.
✓ Branch 235 → 177 taken 40 times.
✓ Branch 235 → 236 taken 169 times.
209 for (const auto &v : cdArr) {
245
1/1
✓ Branch 178 → 179 taken 40 times.
40 auto obj = v.toObject();
246 40 CardDavAccount acc;
247
2/2
✓ Branch 182 → 183 taken 40 times.
✓ Branch 183 → 184 taken 40 times.
40 acc.id = obj.value(QStringLiteral("id")).toString();
248
2/2
✓ Branch 192 → 193 taken 40 times.
✓ Branch 193 → 194 taken 40 times.
40 acc.serverUrl = obj.value(QStringLiteral("serverUrl")).toString();
249
2/2
✓ Branch 202 → 203 taken 40 times.
✓ Branch 203 → 204 taken 40 times.
40 acc.username = obj.value(QStringLiteral("username")).toString();
250
2/2
✓ Branch 212 → 213 taken 40 times.
✓ Branch 213 → 214 taken 40 times.
40 auto books = obj.value(QStringLiteral("selectedBooks")).toArray();
251
4/4
✓ Branch 217 → 218 taken 40 times.
✓ Branch 218 → 219 taken 40 times.
✓ Branch 228 → 220 taken 38 times.
✓ Branch 228 → 229 taken 40 times.
78 for (const auto &b : books)
252
2/2
✓ Branch 222 → 223 taken 38 times.
✓ Branch 223 → 224 taken 38 times.
38 acc.selectedBooks.append(b.toString());
253
1/1
✓ Branch 229 → 230 taken 40 times.
40 p.carddavAccounts.append(acc);
254 40 }
255
256 // T-334: caldavConfigs (backward-compatible: absent = empty list)
257 auto caArr =
258
2/2
✓ Branch 239 → 240 taken 169 times.
✓ Branch 240 → 241 taken 169 times.
169 settings.value(QStringLiteral("caldavConfigs")).toArray();
259
4/4
✓ Branch 244 → 245 taken 169 times.
✓ Branch 245 → 246 taken 169 times.
✓ Branch 314 → 247 taken 39 times.
✓ Branch 314 → 315 taken 169 times.
208 for (const auto &v : caArr) {
260
1/1
✓ Branch 248 → 249 taken 39 times.
39 auto obj = v.toObject();
261 39 CalDavSyncConfig cfg;
262 cfg.carddavAccountId =
263
2/2
✓ Branch 252 → 253 taken 39 times.
✓ Branch 253 → 254 taken 39 times.
39 obj.value(QStringLiteral("carddavAccountId")).toString();
264
2/2
✓ Branch 262 → 263 taken 39 times.
✓ Branch 263 → 264 taken 39 times.
39 auto cals = obj.value(QStringLiteral("selectedCalendars")).toArray();
265
4/4
✓ Branch 267 → 268 taken 39 times.
✓ Branch 268 → 269 taken 39 times.
✓ Branch 278 → 270 taken 33 times.
✓ Branch 278 → 279 taken 39 times.
72 for (const auto &c : cals)
266
2/2
✓ Branch 272 → 273 taken 33 times.
✓ Branch 273 → 274 taken 33 times.
33 cfg.selectedCalendars.append(c.toString());
267
2/2
✓ Branch 282 → 283 taken 39 times.
✓ Branch 283 → 284 taken 39 times.
39 auto roCals = obj.value(QStringLiteral("readOnlyCalendars")).toArray();
268
4/4
✓ Branch 287 → 288 taken 39 times.
✓ Branch 288 → 289 taken 39 times.
✓ Branch 298 → 290 taken 4 times.
✓ Branch 298 → 299 taken 39 times.
43 for (const auto &r : roCals)
269
2/2
✓ Branch 292 → 293 taken 4 times.
✓ Branch 293 → 294 taken 4 times.
4 cfg.readOnlyCalendars.append(r.toString());
270 39 cfg.syncIntervalMinutes =
271
2/2
✓ Branch 302 → 303 taken 39 times.
✓ Branch 303 → 304 taken 39 times.
39 obj.value(QStringLiteral("syncIntervalMinutes")).toInt(15);
272
1/1
✓ Branch 307 → 308 taken 39 times.
39 p.caldavConfigs.append(cfg);
273 39 }
274
275 // general
276
2/2
✓ Branch 318 → 319 taken 169 times.
✓ Branch 319 → 320 taken 169 times.
169 auto gen = settings.value(QStringLiteral("general")).toObject();
277
3/3
✓ Branch 323 → 324 taken 169 times.
✓ Branch 324 → 325 taken 156 times.
✓ Branch 324 → 387 taken 13 times.
169 if (!gen.isEmpty()) {
278 p.general.defaultView =
279
2/2
✓ Branch 328 → 329 taken 156 times.
✓ Branch 332 → 333 taken 156 times.
312 gen.value(QStringLiteral("defaultView")).toString(QStringLiteral("text"));
280 p.general.externalContent =
281
2/2
✓ Branch 343 → 344 taken 156 times.
✓ Branch 347 → 348 taken 156 times.
312 gen.value(QStringLiteral("externalContent")).toString(QStringLiteral("block"));
282 p.general.language =
283
2/2
✓ Branch 358 → 359 taken 156 times.
✓ Branch 362 → 363 taken 156 times.
312 gen.value(QStringLiteral("language")).toString(QStringLiteral("auto"));
284 156 p.general.carddavSyncInterval =
285
2/2
✓ Branch 373 → 374 taken 156 times.
✓ Branch 374 → 375 taken 156 times.
156 gen.value(QStringLiteral("carddavSyncInterval")).toInt(0);
286 156 p.general.caldavSyncInterval =
287
2/2
✓ Branch 381 → 382 taken 156 times.
✓ Branch 382 → 383 taken 156 times.
156 gen.value(QStringLiteral("caldavSyncInterval")).toInt(15);
288 }
289
290 // identities, templates (Sprint 108; absent = none)
291 1183 for (const auto &[name, target] : {std::pair{QStringLiteral("identities"), &p.identities},
292
2/2
✓ Branch 435 → 402 taken 338 times.
✓ Branch 435 → 436 taken 169 times.
1521 std::pair{QStringLiteral("templates"), &p.templates}}) {
293
2/2
✓ Branch 404 → 405 taken 338 times.
✓ Branch 405 → 406 taken 338 times.
338 const auto objects = settings.value(name).toObject();
294
3/3
✓ Branch 430 → 431 taken 353 times.
✓ Branch 432 → 408 taken 15 times.
✓ Branch 432 → 433 taken 338 times.
353 for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) {
295
2/3
✓ Branch 408 → 409 taken 15 times.
✗ Branch 409 → 410 not taken.
✓ Branch 409 → 411 taken 15 times.
15 if (target->size() >= MaxComposeEntries) break;
296
5/11
✓ Branch 413 → 414 taken 15 times.
✗ Branch 413 → 418 not taken.
✓ Branch 414 → 415 taken 15 times.
✓ Branch 416 → 417 taken 15 times.
✗ Branch 416 → 418 not taken.
✓ Branch 419 → 420 taken 15 times.
✗ Branch 419 → 421 not taken.
✓ Branch 421 → 422 taken 15 times.
✗ Branch 421 → 429 not taken.
✗ Branch 844 → 845 not taken.
✗ Branch 844 → 846 not taken.
15 if (it.value().isObject() && it.key().size() <= 1024)
297
3/3
✓ Branch 423 → 424 taken 15 times.
✓ Branch 424 → 425 taken 15 times.
✓ Branch 425 → 426 taken 15 times.
15 target->insert(it.key(), it.value().toObject());
298 }
299 845 }
300
301 // enabledCategories
302
2/2
✓ Branch 445 → 446 taken 169 times.
✓ Branch 446 → 447 taken 169 times.
169 auto cats = root.value(QStringLiteral("enabledCategories")).toArray();
303
4/4
✓ Branch 450 → 451 taken 169 times.
✓ Branch 451 → 452 taken 169 times.
✓ Branch 461 → 453 taken 413 times.
✓ Branch 461 → 462 taken 169 times.
582 for (const auto &v : cats)
304
2/2
✓ Branch 455 → 456 taken 413 times.
✓ Branch 456 → 457 taken 413 times.
413 p.enabledCategories.append(v.toString());
305
306 169 return p;
307 169 }
308
309 // --- RFC-2822 message serialisation ---
310
311 // A header value without control characters. The client id comes from
312 // JSON, which may carry a raw CR/LF inside a string; written into the
313 // header it would end the header block early (Sprint 102 fuzz target
314 // settings_payload).
315 126 static QByteArray headerValue(const QString &value) {
316 126 QString clean;
317
1/1
✓ Branch 3 → 4 taken 126 times.
126 clean.reserve(value.size());
318
2/2
✓ Branch 15 → 6 taken 3197 times.
✓ Branch 15 → 16 taken 126 times.
3323 for (const QChar character : value)
319
5/6
✓ Branch 7 → 8 taken 3193 times.
✓ Branch 7 → 11 taken 4 times.
✓ Branch 9 → 10 taken 3193 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 3193 times.
✓ Branch 12 → 14 taken 4 times.
3197 if (character.unicode() >= 0x20 && character.unicode() != 0x7f)
320
1/1
✓ Branch 13 → 14 taken 3193 times.
3193 clean.append(character);
321
1/1
✓ Branch 16 → 17 taken 126 times.
252 return clean.toUtf8();
322 126 }
323
324 // Build a minimal RFC-2822 message wrapping the JSON payload.
325 // This message is stored via IMAP APPEND in the sync folder.
326 126 QByteArray toRfcMessage() const {
327 126 QByteArray msg;
328
1/1
✓ Branch 2 → 3 taken 126 times.
126 msg += "From: mailjd-settings-sync@localhost\r\n";
329
1/1
✓ Branch 3 → 4 taken 126 times.
126 msg += "Subject: X-MailJD-Settings-Sync\r\n";
330 126 msg += "Date: " +
331
6/6
✓ Branch 4 → 5 taken 126 times.
✓ Branch 5 → 6 taken 126 times.
✓ Branch 6 → 7 taken 126 times.
✓ Branch 7 → 8 taken 126 times.
✓ Branch 8 → 9 taken 126 times.
✓ Branch 9 → 10 taken 126 times.
252 lastModified.toUTC().toString(Qt::RFC2822Date).toUtf8() + "\r\n";
332
1/1
✓ Branch 15 → 16 taken 126 times.
126 msg += "MIME-Version: 1.0\r\n";
333
1/1
✓ Branch 16 → 17 taken 126 times.
126 msg += "Content-Type: application/json; charset=utf-8\r\n";
334
1/1
✓ Branch 17 → 18 taken 126 times.
126 msg += "Content-Transfer-Encoding: 8bit\r\n";
335
4/4
✓ Branch 18 → 19 taken 126 times.
✓ Branch 19 → 20 taken 126 times.
✓ Branch 20 → 21 taken 126 times.
✓ Branch 21 → 22 taken 126 times.
126 msg += "X-MailJD-ClientId: " + headerValue(clientId) + "\r\n";
336
1/1
✓ Branch 25 → 26 taken 126 times.
126 msg += "\r\n"; // Header/body separator
337
338
2/2
✓ Branch 26 → 27 taken 126 times.
✓ Branch 27 → 28 taken 126 times.
126 QJsonDocument doc(toJson());
339
2/2
✓ Branch 29 → 30 taken 126 times.
✓ Branch 30 → 31 taken 126 times.
126 msg += doc.toJson(QJsonDocument::Indented);
340
341 126 return msg;
342 126 }
343
344 // Sprint 102 S102.5.4: true when the message was delivered by mail
345 // transport (an MTA added a Received: header). MailJD uploads its payload
346 // with IMAP APPEND, which adds none; a sender cannot remove the MTA's
347 // header. Subaddressing (user+MailJD-Settings@…) can otherwise deliver a
348 // stranger's message into the sync folder.
349 38 static bool deliveredByMailTransport(const QByteArray &raw) {
350 38 qsizetype end = raw.indexOf("\r\n\r\n");
351
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 37 times.
38 if (end < 0) end = raw.indexOf("\n\n");
352
3/3
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 37 times.
✓ Branch 10 → 11 taken 37 times.
38 const QByteArray header = end < 0 ? raw : raw.left(end);
353 38 qsizetype lineStart = 0;
354
2/2
✓ Branch 23 → 12 taken 231 times.
✓ Branch 23 → 24 taken 34 times.
265 while (lineStart < header.size()) {
355 231 qsizetype lineEnd = header.indexOf('\n', lineStart);
356
2/2
✓ Branch 13 → 14 taken 35 times.
✓ Branch 13 → 15 taken 196 times.
231 if (lineEnd < 0) lineEnd = header.size();
357
3/3
✓ Branch 15 → 16 taken 231 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 227 times.
231 if (header.mid(lineStart, 9).compare("Received:", Qt::CaseInsensitive) == 0)
358 4 return true;
359 227 lineStart = lineEnd + 1;
360 }
361 34 return false;
362 38 }
363
364 // Parse a SyncPayload from a raw RFC-2822 message (as fetched via IMAP).
365 59 static SyncPayload fromRfcMessage(const QByteArray &raw) {
366
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 18 taken 58 times.
59 if (raw.size() > MaxRfcMessageBytes)
367 1 return {};
368
369 // Find header/body separator (blank line = \r\n\r\n or \n\n)
370 58 int bodyStart = raw.indexOf("\r\n\r\n");
371
2/2
✓ Branch 20 → 21 taken 53 times.
✓ Branch 20 → 22 taken 5 times.
58 if (bodyStart >= 0) {
372 53 bodyStart += 4;
373 } else {
374 5 bodyStart = raw.indexOf("\n\n");
375
2/2
✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 26 taken 3 times.
5 if (bodyStart >= 0)
376 2 bodyStart += 2;
377 else
378 3 bodyStart = 0; // Fallback: treat entire content as body
379 }
380
381
1/1
✓ Branch 27 → 28 taken 58 times.
58 QByteArray body = raw.mid(bodyStart);
382
383
1/1
✓ Branch 28 → 29 taken 58 times.
58 auto doc = QJsonDocument::fromJson(body);
384
8/8
✓ Branch 29 → 30 taken 58 times.
✓ Branch 30 → 31 taken 56 times.
✓ Branch 30 → 33 taken 2 times.
✓ Branch 31 → 32 taken 56 times.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 55 times.
✓ Branch 35 → 36 taken 3 times.
✓ Branch 35 → 50 taken 55 times.
58 if (doc.isNull() || !doc.isObject())
385 3 return {};
386
387
2/2
✓ Branch 50 → 51 taken 55 times.
✓ Branch 51 → 52 taken 55 times.
55 SyncPayload p = fromJson(doc.object());
388
389 // Also extract clientId from header if missing from JSON
390
2/2
✓ Branch 54 → 55 taken 5 times.
✓ Branch 54 → 76 taken 50 times.
55 if (p.clientId.isEmpty()) {
391 // Parse X-MailJD-ClientId header
392
1/1
✓ Branch 55 → 56 taken 5 times.
5 QByteArray header = raw.left(bodyStart);
393 5 int idx = header.indexOf("X-MailJD-ClientId:");
394
2/2
✓ Branch 58 → 59 taken 3 times.
✓ Branch 58 → 74 taken 2 times.
5 if (idx >= 0) {
395 3 int lineEnd = header.indexOf('\n', idx);
396
1/2
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 63 taken 3 times.
3 if (lineEnd < 0)
397 ✗ lineEnd = header.size();
398 QByteArray val =
399
2/2
✓ Branch 63 → 64 taken 3 times.
✓ Branch 64 → 65 taken 3 times.
3 header.mid(idx + 18, lineEnd - idx - 18).trimmed();
400 // Remove trailing \r if present
401
2/3
✓ Branch 66 → 67 taken 3 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 3 times.
3 if (val.endsWith('\r'))
402 ✗ val.chop(1);
403
1/1
✓ Branch 69 → 70 taken 3 times.
3 p.clientId = QString::fromUtf8(val);
404 3 }
405 5 }
406
407 55 return p;
408 74 }
409
410 // --- Default categories ---
411
412 7054 static QStringList allCategories() {
413 return {
414 ✗ QStringLiteral("folderIcons"),
415 7054 QStringLiteral("folderColors"),
416 7054 QStringLiteral("calendarColors"),
417 7054 QStringLiteral("hiddenFolders"),
418 7054 QStringLiteral("davAccounts"),
419 7054 QStringLiteral("general"),
420 7054 QStringLiteral("identities"),
421 7054 QStringLiteral("templates"),
422
2/4
✓ Branch 28 → 29 taken 56432 times.
✓ Branch 28 → 30 taken 7054 times.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
70540 };
423 63486 }
424
425 // The Widgets client stored CardDAV and CalDAV accounts as separate
426 // categories; both are one category ("davAccounts") since Sprint 30.
427 6233 static QString normalizedCategory(const QString &category) {
428
7/8
✓ Branch 6 → 7 taken 6231 times.
✓ Branch 6 → 12 taken 2 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 6228 times.
✓ Branch 20 → 21 taken 6233 times.
✗ Branch 20 → 22 not taken.
✓ Branch 22 → 23 taken 5 times.
✓ Branch 22 → 28 taken 6228 times.
24930 if (category == QStringLiteral("carddavAccounts") ||
429
5/6
✓ Branch 14 → 15 taken 6231 times.
✓ Branch 14 → 16 taken 2 times.
✓ Branch 16 → 17 taken 6231 times.
✓ Branch 16 → 18 taken 2 times.
✓ Branch 18 → 19 taken 6233 times.
✗ Branch 18 → 20 not taken.
12464 category == QStringLiteral("caldavAccounts"))
430 5 return QStringLiteral("davAccounts");
431 6228 return category;
432 }
433
434 // Stored category policy as known categories: legacy names mapped,
435 // duplicates and unknown names (older or newer clients) dropped.
436 832 static QStringList normalizedCategories(const QStringList &categories) {
437 832 QStringList result;
438
2/2
✓ Branch 20 → 4 taken 5973 times.
✓ Branch 20 → 21 taken 832 times.
6805 for (const auto &raw : categories) {
439
1/1
✓ Branch 5 → 6 taken 5973 times.
5973 const auto category = normalizedCategory(raw);
440
8/11
✓ Branch 6 → 7 taken 5973 times.
✓ Branch 8 → 9 taken 5972 times.
✓ Branch 8 → 12 taken 1 time.
✓ Branch 10 → 11 taken 5971 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 5973 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 5971 times.
✓ Branch 15 → 17 taken 2 times.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
5973 if (allCategories().contains(category) && !result.contains(category))
441
1/1
✓ Branch 16 → 17 taken 5971 times.
5971 result.append(category);
442 5973 }
443 832 return result;
444 ✗ }
445 };
446