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: 89.9% 745 / 0 / 829
Functions: 91.1% 51 / 0 / 56
Branches: 76.6% 1238 / 14 / 1630

data/AccountConfig.cpp
Line Branch Exec Source
1 #include "AccountConfig.h"
2 #include "CredentialStore.h"
3 #include "util/SafePathComponent.h"
4
5 #include <QCoreApplication>
6 #include <QCryptographicHash>
7 #include <QDir>
8 #include <QFile>
9 #include <QFileInfo>
10 #include <QJsonDocument>
11 #include <QJsonObject>
12 #include <QEventLoop>
13 #include <QLoggingCategory>
14 #include <QPointer>
15 #include <QSaveFile>
16 #include <QSet>
17 #include <QStandardPaths>
18 #include <QTimer>
19 #include <QUuid>
20 #include <utility>
21
22
4/7
✓ Branch 2 → 3 taken 28 times.
✓ Branch 2 → 8 taken 1864 times.
✓ Branch 4 → 5 taken 28 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 28 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
1892 Q_LOGGING_CATEGORY(lcAccountConfig, "mailjd.accountconfig")
23
24 struct AccountCredentialMigration {
25 QString path;
26 QByteArray digest;
27 bool imapStored = false;
28 bool smtpStored = false;
29 };
30
31 namespace {
32
33 18 bool writeAccountBytes(const QString &path, const QByteArray &bytes) {
34
1/1
✓ Branch 2 → 3 taken 18 times.
18 QSaveFile file(path);
35
1/1
✓ Branch 4 → 5 taken 18 times.
18 return file.open(QIODevice::WriteOnly) &&
36
3/5
✓ Branch 7 → 8 taken 18 times.
✓ Branch 8 → 9 taken 18 times.
✗ Branch 8 → 15 not taken.
✓ Branch 11 → 12 taken 18 times.
✗ Branch 11 → 15 not taken.
36 file.setPermissions(QFile::ReadOwner | QFile::WriteOwner) &&
37
4/6
✓ Branch 5 → 6 taken 18 times.
✗ Branch 5 → 15 not taken.
✓ Branch 9 → 10 taken 18 times.
✓ Branch 12 → 13 taken 18 times.
✓ Branch 13 → 14 taken 18 times.
✗ Branch 13 → 15 not taken.
72 file.write(bytes) == bytes.size() && file.commit();
38 18 }
39
40 194 QString legacyCredentialName(const AccountConfig &config) {
41
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 194 times.
194 return config.id.isEmpty() ? config.name : config.id;
42 }
43
44 54 QByteArray accountMetadata(const AccountConfig &config) {
45
1/1
✓ Branch 2 → 3 taken 54 times.
54 QJsonObject imapObj;
46
4/4
✓ Branch 3 → 4 taken 54 times.
✓ Branch 4 → 5 taken 54 times.
✓ Branch 5 → 6 taken 54 times.
✓ Branch 6 → 7 taken 54 times.
54 imapObj["host"] = config.imap.host;
47
4/4
✓ Branch 9 → 10 taken 54 times.
✓ Branch 10 → 11 taken 54 times.
✓ Branch 11 → 12 taken 54 times.
✓ Branch 12 → 13 taken 54 times.
54 imapObj["port"] = config.imap.port;
48
4/4
✓ Branch 15 → 16 taken 54 times.
✓ Branch 16 → 17 taken 54 times.
✓ Branch 17 → 18 taken 54 times.
✓ Branch 18 → 19 taken 54 times.
54 imapObj["security"] = config.imap.security;
49
4/4
✓ Branch 21 → 22 taken 54 times.
✓ Branch 22 → 23 taken 54 times.
✓ Branch 23 → 24 taken 54 times.
✓ Branch 24 → 25 taken 54 times.
54 imapObj["username"] = config.imap.username;
50
4/4
✓ Branch 28 → 29 taken 54 times.
✓ Branch 29 → 30 taken 54 times.
✓ Branch 30 → 31 taken 54 times.
✓ Branch 31 → 32 taken 54 times.
54 imapObj["password"] = QString();
51
2/2
✓ Branch 36 → 37 taken 5 times.
✓ Branch 36 → 44 taken 49 times.
54 if (!config.imap.credentialId.isEmpty())
52
4/4
✓ Branch 37 → 38 taken 5 times.
✓ Branch 38 → 39 taken 5 times.
✓ Branch 39 → 40 taken 5 times.
✓ Branch 40 → 41 taken 5 times.
5 imapObj["credentialId"] = config.imap.credentialId;
53
54
1/1
✓ Branch 44 → 45 taken 54 times.
54 QJsonObject smtpObj;
55
4/4
✓ Branch 45 → 46 taken 54 times.
✓ Branch 46 → 47 taken 54 times.
✓ Branch 47 → 48 taken 54 times.
✓ Branch 48 → 49 taken 54 times.
54 smtpObj["host"] = config.smtp.host;
56
4/4
✓ Branch 51 → 52 taken 54 times.
✓ Branch 52 → 53 taken 54 times.
✓ Branch 53 → 54 taken 54 times.
✓ Branch 54 → 55 taken 54 times.
54 smtpObj["port"] = config.smtp.port;
57
4/4
✓ Branch 57 → 58 taken 54 times.
✓ Branch 58 → 59 taken 54 times.
✓ Branch 59 → 60 taken 54 times.
✓ Branch 60 → 61 taken 54 times.
54 smtpObj["security"] = config.smtp.security;
58
4/4
✓ Branch 63 → 64 taken 54 times.
✓ Branch 64 → 65 taken 54 times.
✓ Branch 65 → 66 taken 54 times.
✓ Branch 66 → 67 taken 54 times.
54 smtpObj["username"] = config.smtp.username;
59
4/4
✓ Branch 70 → 71 taken 54 times.
✓ Branch 71 → 72 taken 54 times.
✓ Branch 72 → 73 taken 54 times.
✓ Branch 73 → 74 taken 54 times.
54 smtpObj["password"] = QString();
60
2/2
✓ Branch 78 → 79 taken 5 times.
✓ Branch 78 → 86 taken 49 times.
54 if (!config.smtp.credentialId.isEmpty())
61
4/4
✓ Branch 79 → 80 taken 5 times.
✓ Branch 80 → 81 taken 5 times.
✓ Branch 81 → 82 taken 5 times.
✓ Branch 82 → 83 taken 5 times.
5 smtpObj["credentialId"] = config.smtp.credentialId;
62
1/6
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 95 taken 54 times.
✗ Branch 88 → 89 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 90 → 91 not taken.
✗ Branch 91 → 92 not taken.
54 if (config.smtp.sharesIncomingLogin()) smtpObj["login"] = config.smtp.login;
63
64
1/1
✓ Branch 95 → 96 taken 54 times.
54 QJsonObject root;
65
2/2
✓ Branch 97 → 98 taken 4 times.
✓ Branch 97 → 158 taken 50 times.
54 if (!config.sieve.isDefault()) {
66
1/1
✓ Branch 98 → 99 taken 4 times.
4 QJsonObject sieveObj;
67
4/4
✓ Branch 99 → 100 taken 4 times.
✓ Branch 100 → 101 taken 4 times.
✓ Branch 101 → 102 taken 4 times.
✓ Branch 102 → 103 taken 4 times.
4 sieveObj["mode"] = config.sieve.mode;
68
2/2
✓ Branch 107 → 108 taken 1 time.
✓ Branch 107 → 127 taken 3 times.
4 if (config.sieve.mode == QLatin1String("manual")) {
69
4/4
✓ Branch 108 → 109 taken 1 time.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 110 → 111 taken 1 time.
✓ Branch 111 → 112 taken 1 time.
1 sieveObj["host"] = config.sieve.host;
70
4/4
✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
✓ Branch 116 → 117 taken 1 time.
✓ Branch 117 → 118 taken 1 time.
1 sieveObj["port"] = config.sieve.port;
71
4/4
✓ Branch 120 → 121 taken 1 time.
✓ Branch 121 → 122 taken 1 time.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 123 → 124 taken 1 time.
1 sieveObj["security"] = config.sieve.security;
72 }
73
4/4
✓ Branch 127 → 128 taken 4 times.
✓ Branch 128 → 129 taken 4 times.
✓ Branch 129 → 130 taken 4 times.
✓ Branch 130 → 131 taken 4 times.
4 sieveObj["login"] = config.sieve.login;
74
2/2
✓ Branch 134 → 135 taken 2 times.
✓ Branch 134 → 150 taken 2 times.
4 if (config.sieve.usesSeparateLogin()) {
75
4/4
✓ Branch 135 → 136 taken 2 times.
✓ Branch 136 → 137 taken 2 times.
✓ Branch 137 → 138 taken 2 times.
✓ Branch 138 → 139 taken 2 times.
2 sieveObj["username"] = config.sieve.username;
76
1/2
✓ Branch 142 → 143 taken 2 times.
✗ Branch 142 → 150 not taken.
2 if (!config.sieve.credentialId.isEmpty())
77
4/4
✓ Branch 143 → 144 taken 2 times.
✓ Branch 144 → 145 taken 2 times.
✓ Branch 145 → 146 taken 2 times.
✓ Branch 146 → 147 taken 2 times.
2 sieveObj["credentialId"] = config.sieve.credentialId;
78 }
79
4/4
✓ Branch 150 → 151 taken 4 times.
✓ Branch 151 → 152 taken 4 times.
✓ Branch 152 → 153 taken 4 times.
✓ Branch 153 → 154 taken 4 times.
4 root["sieve"] = sieveObj;
80 4 }
81
4/4
✓ Branch 158 → 159 taken 54 times.
✓ Branch 159 → 160 taken 54 times.
✓ Branch 160 → 161 taken 54 times.
✓ Branch 161 → 162 taken 54 times.
54 root["id"] = config.id;
82
4/4
✓ Branch 164 → 165 taken 54 times.
✓ Branch 165 → 166 taken 54 times.
✓ Branch 166 → 167 taken 54 times.
✓ Branch 167 → 168 taken 54 times.
54 root["name"] = config.name;
83
4/4
✓ Branch 170 → 171 taken 54 times.
✓ Branch 171 → 172 taken 54 times.
✓ Branch 172 → 173 taken 54 times.
✓ Branch 173 → 174 taken 54 times.
54 root["email"] = config.email;
84
2/2
✓ Branch 177 → 178 taken 5 times.
✓ Branch 177 → 185 taken 49 times.
54 if (!config.legacyKeyringName.isEmpty())
85
4/4
✓ Branch 178 → 179 taken 5 times.
✓ Branch 179 → 180 taken 5 times.
✓ Branch 180 → 181 taken 5 times.
✓ Branch 181 → 182 taken 5 times.
5 root["legacyKeyringName"] = config.legacyKeyringName;
86
4/4
✓ Branch 185 → 186 taken 54 times.
✓ Branch 186 → 187 taken 54 times.
✓ Branch 187 → 188 taken 54 times.
✓ Branch 188 → 189 taken 54 times.
54 root["imap"] = imapObj;
87
4/4
✓ Branch 191 → 192 taken 54 times.
✓ Branch 192 → 193 taken 54 times.
✓ Branch 193 → 194 taken 54 times.
✓ Branch 194 → 195 taken 54 times.
54 root["smtp"] = smtpObj;
88
2/2
✓ Branch 197 → 198 taken 54 times.
✓ Branch 198 → 199 taken 54 times.
108 return QJsonDocument(root).toJson(QJsonDocument::Indented);
89 54 }
90
91 // S106-203: a port outside 1..65535 reads as 0 (refused by validation)
92 // instead of wrapping into another valid port (65600 became 64). A missing
93 // or non-numeric value keeps the protocol default, as before.
94 2159 quint16 portValue(const QJsonValue &value, int fallback) {
95
2/2
✓ Branch 3 → 4 taken 44 times.
✓ Branch 3 → 5 taken 2115 times.
2159 if (!value.isDouble()) return quint16(fallback);
96 2115 const double port = value.toDouble();
97
6/6
✓ Branch 6 → 7 taken 2114 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 7 → 8 taken 2113 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 2112 times.
2115 if (port < 1 || port > 65535 || port != double(qint64(port))) return 0;
98 2112 return quint16(port);
99 }
100
101 16 bool readSecretBlocking(const QString &service, const QString &accountName,
102 QByteArray *password) {
103
1/1
✓ Branch 2 → 3 taken 16 times.
16 CredentialStore store;
104
1/1
✓ Branch 3 → 4 taken 16 times.
16 QEventLoop loop;
105
1/1
✓ Branch 4 → 5 taken 16 times.
16 QTimer timeout;
106
1/1
✓ Branch 5 → 6 taken 16 times.
16 timeout.setSingleShot(true);
107
108 16 bool completed = false;
109 16 bool success = false;
110
1/1
✓ Branch 6 → 7 taken 16 times.
16 QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
111 ✗ if (completed)
112 ✗ return;
113 ✗ completed = true;
114 ✗ loop.quit();
115 });
116
1/1
✓ Branch 9 → 10 taken 16 times.
16 store.readPassword(service, accountName,
117
1/1
✓ Branch 8 → 9 taken 16 times.
16 [&](bool ok, const QByteArray &value) {
118
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 16 times.
16 if (completed)
119 ✗ return;
120 16 completed = true;
121 16 success = ok;
122
3/4
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 7 taken 14 times.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
16 if (ok && password)
123 2 *password = value;
124 16 loop.quit();
125 });
126 // S106-201: a refusal (or any answer) can arrive synchronously; quit()
127 // before exec() is lost, and the loop would then never end.
128
1/2
✓ Branch 11 → 12 taken 16 times.
✗ Branch 11 → 16 not taken.
16 if (!completed) {
129
1/1
✓ Branch 12 → 13 taken 16 times.
16 timeout.start(30000);
130
1/1
✓ Branch 14 → 15 taken 16 times.
16 loop.exec();
131 }
132 16 return success;
133 16 }
134
135 97 bool writeSecretBlocking(const QString &service, const QString &accountName,
136 const QByteArray &password, QString *error) {
137
2/2
✓ Branch 3 → 4 taken 60 times.
✓ Branch 3 → 5 taken 37 times.
97 if (password.isEmpty())
138 60 return true;
139
140
1/1
✓ Branch 5 → 6 taken 37 times.
37 CredentialStore store;
141
1/1
✓ Branch 6 → 7 taken 37 times.
37 QEventLoop loop;
142
1/1
✓ Branch 7 → 8 taken 37 times.
37 QTimer timeout;
143
1/1
✓ Branch 8 → 9 taken 37 times.
37 timeout.setSingleShot(true);
144
145 37 bool completed = false;
146 37 bool success = false;
147 37 QString writeError;
148
1/1
✓ Branch 9 → 10 taken 37 times.
37 QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
149 ✗ if (completed)
150 ✗ return;
151 ✗ completed = true;
152 ✗ writeError = QStringLiteral("Timed out writing password to keyring");
153 ✗ loop.quit();
154 });
155
1/1
✓ Branch 12 → 13 taken 37 times.
37 store.writePassword(service, accountName, password,
156
1/1
✓ Branch 11 → 12 taken 37 times.
37 [&](bool ok, const QString &message) {
157
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 37 times.
37 if (completed)
158 ✗ return;
159 37 completed = true;
160 37 success = ok;
161 37 writeError = message;
162 37 loop.quit();
163 });
164 // S106-201: a refusal (or any answer) can arrive synchronously; quit()
165 // before exec() is lost, and the loop would then never end.
166
1/2
✓ Branch 14 → 15 taken 37 times.
✗ Branch 14 → 19 not taken.
37 if (!completed) {
167
1/1
✓ Branch 15 → 16 taken 37 times.
37 timeout.start(30000);
168
1/1
✓ Branch 17 → 18 taken 37 times.
37 loop.exec();
169 }
170
171
3/4
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 32 taken 36 times.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 32 not taken.
37 if (!success && error) {
172 1 *error = writeError.isEmpty()
173
2/4
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 1 time.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 1 time.
2 ? QStringLiteral("Could not write password to keyring")
174 1 : writeError;
175 }
176 37 return success;
177 37 }
178
179
180 12 bool deleteSecretBlocking(const QString &service, const QString &accountName,
181 QString *error) {
182
1/1
✓ Branch 2 → 3 taken 12 times.
12 CredentialStore store;
183
1/1
✓ Branch 3 → 4 taken 12 times.
12 QEventLoop loop;
184
1/1
✓ Branch 4 → 5 taken 12 times.
12 QTimer timeout;
185
1/1
✓ Branch 5 → 6 taken 12 times.
12 timeout.setSingleShot(true);
186
187 12 bool completed = false;
188 12 bool success = false;
189
190
1/1
✓ Branch 6 → 7 taken 12 times.
12 QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
191 ✗ if (completed)
192 ✗ return;
193 ✗ completed = true;
194 ✗ if (error)
195 ✗ *error = QStringLiteral("Timed out deleting password from keyring");
196 ✗ loop.quit();
197 });
198
199
2/2
✓ Branch 8 → 9 taken 12 times.
✓ Branch 9 → 10 taken 12 times.
12 store.deletePassword(service, accountName, [&](bool ok) {
200
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 12 times.
12 if (completed)
201 ✗ return;
202 12 completed = true;
203 12 success = ok;
204 12 loop.quit();
205 });
206
207 // S106-201: a refusal (or any answer) can arrive synchronously; quit()
208 // before exec() is lost, and the loop would then never end.
209
1/2
✓ Branch 11 → 12 taken 12 times.
✗ Branch 11 → 16 not taken.
12 if (!completed) {
210
1/1
✓ Branch 12 → 13 taken 12 times.
12 timeout.start(30000);
211
1/1
✓ Branch 14 → 15 taken 12 times.
12 loop.exec();
212 }
213
214
6/8
✓ Branch 16 → 17 taken 10 times.
✓ Branch 16 → 21 taken 2 times.
✓ Branch 17 → 18 taken 10 times.
✗ Branch 17 → 21 not taken.
✓ Branch 19 → 20 taken 10 times.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 10 times.
✓ Branch 22 → 30 taken 2 times.
12 if (!success && error && error->isEmpty())
215 10 *error = QStringLiteral("Could not delete password from keyring");
216 12 return success;
217 12 }
218
219 } // namespace
220
221 class AccountAsyncSave final : public QObject {
222 2 Q_DECLARE_TR_FUNCTIONS(AccountAsyncSave)
223 public:
224 10 AccountAsyncSave(QObject *owner, AccountConfig config, QString directory,
225 AccountConfigLoader::AsyncSaveCallback callback)
226 20 : QObject(owner), m_config(std::move(config)), m_directory(std::move(directory)),
227
5/8
✓ Branch 5 → 6 taken 10 times.
✓ Branch 13 → 14 taken 10 times.
✓ Branch 14 → 15 taken 10 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 10 times.
✓ Branch 17 → 18 taken 10 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
30 m_callback(std::move(callback)), m_store(new CredentialStore(this)) {
228
1/1
✓ Branch 19 → 20 taken 10 times.
10 m_deadline.setSingleShot(true);
229
1/1
✓ Branch 20 → 21 taken 10 times.
10 m_deadline.setInterval(30000);
230 10 connect(&m_deadline, &QTimer::timeout, this,
231
3/3
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 21 → 22 taken 10 times.
11 [this] { fail(tr("Timed out while saving credentials.")); });
232
1/1
✓ Branch 23 → 24 taken 10 times.
18 QTimer::singleShot(0, this, [this] { begin(); });
233 10 }
234
235 20 ~AccountAsyncSave() override {
236
4/6
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 6 taken 8 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 13 taken 10 times.
10 if (!m_finished && !m_staged.isEmpty())
237 ✗ CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), m_staged, {});
238 20 }
239
240 private:
241 8 void begin() {
242
1/1
✓ Branch 2 → 3 taken 8 times.
8 QDir directory(m_directory);
243
5/16
✓ Branch 3 → 4 taken 8 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 11 taken 8 times.
✗ Branch 8 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 8 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 8 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 21 taken 8 times.
✗ Branch 213 → 214 not taken.
✗ Branch 213 → 215 not taken.
✗ Branch 217 → 218 not taken.
✗ Branch 217 → 219 not taken.
8 if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) {
244 ✗ fail(tr("The account directory could not be created."));
245 ✗ return;
246 }
247
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 28 taken 7 times.
8 if (m_config.id.isEmpty())
248
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
1 m_config.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
249
1/1
✓ Branch 28 → 29 taken 8 times.
8 const auto errors = AccountConfigLoader::validate(m_config);
250
1/4
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 40 taken 8 times.
✗ Branch 34 → 35 not taken.
✗ Branch 35 → 36 not taken.
8 if (!errors.isEmpty()) { fail(errors.join(QStringLiteral("; "))); return; }
251
1/1
✓ Branch 40 → 41 taken 8 times.
8 const auto uniqueness = AccountConfigLoader::checkNameUniqueness(m_config, m_directory);
252
1/3
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 47 taken 8 times.
✗ Branch 44 → 45 not taken.
8 if (!uniqueness.isEmpty()) { fail(uniqueness); return; }
253
254
3/5
✓ Branch 53 → 54 taken 8 times.
✓ Branch 56 → 57 taken 8 times.
✓ Branch 56 → 58 taken 8 times.
✗ Branch 241 → 242 not taken.
✗ Branch 241 → 243 not taken.
24 const auto files = directory.entryList({QStringLiteral("*.json")}, QDir::Files, QDir::Name);
255
2/2
✓ Branch 111 → 61 taken 8 times.
✓ Branch 111 → 112 taken 8 times.
16 for (const auto &fileName : files) {
256
1/1
✓ Branch 62 → 63 taken 8 times.
8 const QString path = directory.absoluteFilePath(fileName);
257 auto existing = AccountConfigLoader::loadFromFile(
258
1/1
✓ Branch 63 → 64 taken 8 times.
8 path, AccountConfigLoader::LoadMode::MetadataOnly);
259
5/6
✓ Branch 65 → 66 taken 8 times.
✗ Branch 65 → 69 not taken.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 70 taken 7 times.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 7 times.
8 if (!existing || existing->id != m_config.id) continue;
260
1/2
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 79 taken 7 times.
7 if (!m_sourcePath.isEmpty()) {
261 ✗ fail(tr("The account identity is stored more than once."));
262 ✗ return;
263 }
264
1/1
✓ Branch 79 → 80 taken 7 times.
7 QFile source(path);
265
2/5
✓ Branch 81 → 82 taken 7 times.
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 87 taken 7 times.
✗ Branch 83 → 84 not taken.
✗ Branch 84 → 85 not taken.
7 if (!source.open(QIODevice::ReadOnly)) { fail(source.errorString()); return; }
266
1/1
✓ Branch 87 → 88 taken 7 times.
7 m_sourceBytes = source.readAll();
267 7 m_sourcePath = path;
268 7 m_previous = *existing;
269 9 }
270
271
1/1
✓ Branch 117 → 118 taken 8 times.
16 m_targetPath = directory.absoluteFilePath(
272
2/2
✓ Branch 115 → 116 taken 8 times.
✓ Branch 116 → 117 taken 8 times.
24 AccountConfigLoader::fileStem(m_config, m_directory) + QStringLiteral(".json"));
273
5/7
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 129 taken 7 times.
✓ Branch 126 → 127 taken 1 time.
✗ Branch 127 → 128 not taken.
✓ Branch 127 → 129 taken 1 time.
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 135 taken 8 times.
8 if (m_sourcePath.isEmpty() && QFileInfo::exists(m_targetPath)) {
274 ✗ fail(tr("The account name is already in use."));
275 ✗ return;
276 }
277
2/2
✓ Branch 136 → 137 taken 7 times.
✓ Branch 136 → 154 taken 1 time.
8 if (!m_sourcePath.isEmpty()) {
278
2/2
✓ Branch 138 → 139 taken 4 times.
✓ Branch 138 → 140 taken 3 times.
7 if (m_config.imap.credentialId.isEmpty())
279 4 m_config.imap.credentialId = m_previous.imap.credentialId;
280
2/2
✓ Branch 141 → 142 taken 4 times.
✓ Branch 141 → 143 taken 3 times.
7 if (m_config.smtp.credentialId.isEmpty())
281 4 m_config.smtp.credentialId = m_previous.smtp.credentialId;
282
6/6
✓ Branch 144 → 145 taken 5 times.
✓ Branch 144 → 148 taken 2 times.
✓ Branch 146 → 147 taken 1 time.
✓ Branch 146 → 148 taken 4 times.
✓ Branch 149 → 150 taken 1 time.
✓ Branch 149 → 151 taken 6 times.
7 if (m_config.sieve.credentialId.isEmpty() && m_config.sieve.usesSeparateLogin())
283 1 m_config.sieve.credentialId = m_previous.sieve.credentialId;
284
1/2
✓ Branch 152 → 153 taken 7 times.
✗ Branch 152 → 154 not taken.
7 if (m_config.legacyKeyringName.isEmpty())
285 7 m_config.legacyKeyringName = m_previous.legacyKeyringName;
286 }
287
288 8 const QByteArray imapPassword = !m_config.imap.password.isEmpty()
289
2/2
✓ Branch 155 → 156 taken 4 times.
✓ Branch 155 → 157 taken 4 times.
8 ? m_config.imap.password : m_previous.imap.password;
290 8 const QByteArray smtpPassword = !m_config.smtp.password.isEmpty()
291
2/2
✓ Branch 160 → 161 taken 4 times.
✓ Branch 160 → 162 taken 4 times.
8 ? m_config.smtp.password : m_previous.smtp.password;
292
1/1
✓ Branch 167 → 168 taken 8 times.
8 stage(QStringLiteral("imap"), imapPassword,
293 8 m_config.imap.credentialId);
294
1/1
✓ Branch 173 → 174 taken 8 times.
8 stage(QStringLiteral("smtp"), smtpPassword,
295 8 m_config.smtp.credentialId);
296
2/2
✓ Branch 177 → 178 taken 2 times.
✓ Branch 177 → 185 taken 6 times.
8 if (m_config.sieve.usesSeparateLogin())
297
1/1
✓ Branch 181 → 182 taken 2 times.
4 stage(QStringLiteral("sieve"), m_config.sieve.password, m_config.sieve.credentialId);
298 else
299 6 m_config.sieve.credentialId.clear();
300
1/1
✓ Branch 186 → 187 taken 8 times.
8 m_config.imap.password.clear();
301
1/1
✓ Branch 187 → 188 taken 8 times.
8 m_config.smtp.password.clear();
302
1/1
✓ Branch 188 → 189 taken 8 times.
8 m_config.sieve.password.clear();
303
3/3
✓ Branch 189 → 190 taken 2 times.
✓ Branch 189 → 191 taken 6 times.
✓ Branch 190 → 192 taken 2 times.
8 if (m_pending == 0) commit();
304
1/1
✓ Branch 191 → 192 taken 6 times.
6 else m_deadline.start();
305 24 }
306
307 18 void stage(const QString &service, const QByteArray &password,
308 QString &credentialId) {
309
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 12 times.
18 if (password.isEmpty()) return;
310
2/2
✓ Branch 5 → 6 taken 12 times.
✓ Branch 6 → 7 taken 12 times.
12 credentialId = QUuid::createUuid().toString(QUuid::WithoutBraces);
311 12 const CredentialStore::SecretKey key{service, credentialId};
312
1/1
✓ Branch 11 → 12 taken 12 times.
12 m_staged.append(key);
313 12 ++m_pending;
314
1/1
✓ Branch 13 → 14 taken 12 times.
12 m_store->writePassword(service, credentialId, password,
315 12 [this](bool success, const QString &error) {
316
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 10 times.
10 if (m_finished) return;
317
1/6
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 10 times.
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 9 not taken.
✗ Branch 9 → 10 not taken.
10 if (!success) { fail(error.isEmpty() ? tr("The password could not be stored.") : error); return; }
318
2/2
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 14 taken 5 times.
10 if (--m_pending == 0) commit();
319 });
320 12 }
321
322 7 void commit() {
323
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 7 times.
8 if (m_finished) return;
324
1/1
✓ Branch 4 → 5 taken 7 times.
7 m_deadline.stop();
325
1/2
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 29 not taken.
7 if (!m_sourcePath.isEmpty()) {
326
1/1
✓ Branch 7 → 8 taken 7 times.
7 QFile source(m_sourcePath);
327
8/12
✓ Branch 9 → 10 taken 7 times.
✓ Branch 10 → 11 taken 7 times.
✗ Branch 10 → 14 not taken.
✓ Branch 11 → 12 taken 7 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 6 times.
✓ Branch 16 → 17 taken 7 times.
✗ Branch 16 → 18 not taken.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 23 taken 6 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
7 if (!source.open(QIODevice::ReadOnly) || source.readAll() != m_sourceBytes) {
328
2/2
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
1 fail(tr("The account changed while it was being saved. Review the current values and try again."));
329 1 return;
330 }
331
2/5
✓ Branch 25 → 26 taken 6 times.
✓ Branch 25 → 28 taken 1 time.
✗ Branch 29 → 30 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 35 not taken.
7 } else if (QFileInfo::exists(m_targetPath)) {
332 ✗ fail(tr("The account changed while it was being saved. Review the current values and try again."));
333 ✗ return;
334 }
335
1/1
✓ Branch 35 → 36 taken 6 times.
6 const auto uniqueness = AccountConfigLoader::checkNameUniqueness(m_config, m_directory);
336
1/3
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 42 taken 6 times.
✗ Branch 39 → 40 not taken.
6 if (!uniqueness.isEmpty()) { fail(uniqueness); return; }
337
5/7
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 47 taken 4 times.
✓ Branch 44 → 45 taken 2 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 2 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 53 taken 6 times.
6 if (m_targetPath != m_sourcePath && QFileInfo::exists(m_targetPath)) {
338 ✗ fail(tr("The account name is already in use."));
339 ✗ return;
340 }
341
3/4
✓ Branch 53 → 54 taken 6 times.
✓ Branch 54 → 55 taken 6 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 61 taken 6 times.
6 if (!writeAccountBytes(m_targetPath, accountMetadata(m_config))) {
342 ✗ fail(tr("The account metadata could not be saved."));
343 ✗ return;
344 }
345
4/6
✓ Branch 62 → 63 taken 6 times.
✗ Branch 62 → 68 not taken.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 68 taken 4 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 75 taken 6 times.
8 if (!m_sourcePath.isEmpty() && m_sourcePath != m_targetPath &&
346
2/3
✓ Branch 65 → 66 taken 2 times.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 68 taken 2 times.
2 !QFile::remove(m_sourcePath)) {
347 ✗ QFile::remove(m_targetPath);
348 ✗ fail(tr("The previous account file could not be replaced."));
349 ✗ return;
350 }
351
352 6 QList<CredentialStore::SecretKey> obsolete;
353
1/2
✓ Branch 76 → 77 taken 6 times.
✗ Branch 76 → 138 not taken.
6 if (!m_sourcePath.isEmpty()) {
354 6 const QString oldImap = AccountConfigLoader::imapCredentialName(m_previous);
355 6 const QString oldSmtp = AccountConfigLoader::smtpCredentialName(m_previous);
356
6/8
✓ Branch 80 → 81 taken 6 times.
✗ Branch 80 → 85 not taken.
✓ Branch 83 → 84 taken 3 times.
✓ Branch 83 → 85 taken 3 times.
✓ Branch 86 → 87 taken 6 times.
✗ Branch 86 → 88 not taken.
✓ Branch 88 → 89 taken 3 times.
✓ Branch 88 → 97 taken 3 times.
6 if (!oldImap.isEmpty() && oldImap != AccountConfigLoader::imapCredentialName(m_config))
357 3 obsolete.append({QStringLiteral("imap"), oldImap});
358
6/8
✓ Branch 98 → 99 taken 6 times.
✗ Branch 98 → 103 not taken.
✓ Branch 101 → 102 taken 3 times.
✓ Branch 101 → 103 taken 3 times.
✓ Branch 104 → 105 taken 6 times.
✗ Branch 104 → 106 not taken.
✓ Branch 106 → 107 taken 3 times.
✓ Branch 106 → 115 taken 3 times.
6 if (!oldSmtp.isEmpty() && oldSmtp != AccountConfigLoader::smtpCredentialName(m_config))
359 3 obsolete.append({QStringLiteral("smtp"), oldSmtp});
360 6 const QString oldSieve = AccountConfigLoader::sieveCredentialName(m_previous);
361
7/8
✓ Branch 117 → 118 taken 1 time.
✓ Branch 117 → 122 taken 5 times.
✓ Branch 120 → 121 taken 1 time.
✗ Branch 120 → 122 not taken.
✓ Branch 123 → 124 taken 1 time.
✓ Branch 123 → 125 taken 5 times.
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 134 taken 5 times.
6 if (!oldSieve.isEmpty() && oldSieve != AccountConfigLoader::sieveCredentialName(m_config))
362 1 obsolete.append({QStringLiteral("sieve"), oldSieve});
363 6 }
364 6 m_finished = true;
365
1/1
✓ Branch 138 → 139 taken 6 times.
6 m_staged.clear();
366 6 const auto callback = std::move(m_callback);
367
2/2
✓ Branch 142 → 143 taken 4 times.
✓ Branch 142 → 148 taken 2 times.
6 if (!obsolete.isEmpty())
368
1/1
✓ Branch 145 → 146 taken 4 times.
4 CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), obsolete, {});
369
2/3
✓ Branch 149 → 150 taken 6 times.
✗ Branch 149 → 154 not taken.
✓ Branch 151 → 152 taken 6 times.
6 if (callback) callback(true, m_config, {});
370
1/1
✓ Branch 154 → 155 taken 6 times.
6 deleteLater();
371 13 }
372
373 2 void fail(QString error) {
374
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (m_finished) return;
375 2 m_finished = true;
376
1/1
✓ Branch 4 → 5 taken 2 times.
2 m_deadline.stop();
377 // A failed keyring write reports from inside the finished signal of a
378 // job owned by m_store; deleting the store synchronously freed that job
379 // while it was still emitting (use-after-free, crash on a later timer;
380 // Sprint 100 SB-14). Remaining jobs are dropped with the store after
381 // the current event; their callbacks see m_finished and return.
382
2/3
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 2 times.
2 if (m_store) m_store->deleteLater();
383 2 m_store = nullptr;
384 2 const auto staged = std::exchange(m_staged, {});
385 2 const auto callback = std::move(m_callback);
386
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 19 taken 2 times.
2 if (staged.isEmpty()) {
387 ✗ if (callback) callback(false, m_config, error);
388 ✗ deleteLater();
389 ✗ return;
390 }
391
1/1
✓ Branch 23 → 24 taken 2 times.
2 CredentialStore::deletePasswordsAsync(this, staged,
392
2/4
✓ Branch 19 → 20 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
4 [this, callback, error = std::move(error)](bool) {
393
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
2 if (callback) callback(false, m_config, error);
394 2 deleteLater();
395 2 });
396 2 }
397
398 AccountConfig m_config;
399 AccountConfig m_previous;
400 QString m_directory;
401 QString m_sourcePath;
402 QString m_targetPath;
403 QByteArray m_sourceBytes;
404 AccountConfigLoader::AsyncSaveCallback m_callback;
405 CredentialStore *m_store;
406 QTimer m_deadline;
407 QList<CredentialStore::SecretKey> m_staged;
408 int m_pending = 0;
409 bool m_finished = false;
410 };
411
412 10 QObject *AccountConfigLoader::saveAsync(QObject *owner, AccountConfig config,
413 const QString &configDir,
414 AsyncSaveCallback callback) {
415
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 10 times.
10 if (!owner) return nullptr;
416 10 return new AccountAsyncSave(owner, std::move(config), configDir,
417
2/5
✓ Branch 10 → 11 taken 10 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 10 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
20 std::move(callback));
418 }
419
420 114 QString AccountConfigLoader::imapCredentialName(const AccountConfig &account) {
421
2/2
✓ Branch 3 → 4 taken 107 times.
✓ Branch 3 → 5 taken 7 times.
114 return account.imap.credentialId.isEmpty() ? legacyCredentialName(account)
422 114 : account.imap.credentialId;
423 }
424
425 94 QString AccountConfigLoader::smtpCredentialName(const AccountConfig &account) {
426
2/2
✓ Branch 3 → 4 taken 87 times.
✓ Branch 3 → 5 taken 7 times.
94 return account.smtp.credentialId.isEmpty() ? legacyCredentialName(account)
427 94 : account.smtp.credentialId;
428 }
429
430 8 QString AccountConfigLoader::sieveCredentialName(const AccountConfig &account) {
431
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 6 times.
8 return account.sieve.usesSeparateLogin() ? account.sieve.credentialId : QString();
432 }
433
434 1 QString AccountConfigLoader::defaultConfigDir() {
435
1/1
✓ Branch 2 → 3 taken 1 time.
2 return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
436
1/1
✓ Branch 3 → 4 taken 1 time.
2 "/mailjd/accounts";
437 }
438
439 ✗ std::vector<AccountConfig> AccountConfigLoader::loadAll() {
440 ✗ return loadAll(defaultConfigDir());
441 }
442
443 std::vector<AccountConfig>
444 884 AccountConfigLoader::loadAll(const QString &configDir, LoadMode mode) {
445 884 std::vector<AccountConfig> accounts;
446
1/1
✓ Branch 2 → 3 taken 884 times.
884 QDir dir(configDir);
447
448
3/3
✓ Branch 3 → 4 taken 884 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 17 taken 881 times.
884 if (!dir.exists()) {
449
4/4
✓ Branch 5 → 6 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 15 → 7 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
6 qCWarning(lcAccountConfig)
450
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
3 << "Config directory does not exist:" << configDir;
451 3 return accounts;
452 }
453
454
3/5
✓ Branch 21 → 22 taken 881 times.
✓ Branch 24 → 25 taken 881 times.
✓ Branch 24 → 26 taken 881 times.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
2643 const auto files = dir.entryList({"*.json"}, QDir::Files, QDir::Name);
455
2/2
✓ Branch 78 → 28 taken 925 times.
✓ Branch 78 → 79 taken 881 times.
1806 for (const auto &fileName : files) {
456
1/1
✓ Branch 29 → 30 taken 925 times.
925 const auto path = dir.absoluteFilePath(fileName);
457
1/1
✓ Branch 30 → 31 taken 925 times.
925 auto account = loadFromFile(path, mode);
458
2/2
✓ Branch 32 → 33 taken 924 times.
✓ Branch 32 → 74 taken 1 time.
925 if (account.has_value()) {
459
2/2
✓ Branch 33 → 34 taken 924 times.
✓ Branch 34 → 35 taken 924 times.
924 auto errors = validate(account.value());
460
2/2
✓ Branch 36 → 37 taken 921 times.
✓ Branch 36 → 54 taken 3 times.
924 if (errors.isEmpty()) {
461 // The id, not the name: names are usually the mail address
462 // (S102.4.6, no address in the log at info level).
463
4/4
✓ Branch 37 → 38 taken 921 times.
✓ Branch 41 → 42 taken 921 times.
✓ Branch 50 → 39 taken 921 times.
✓ Branch 50 → 51 taken 921 times.
1842 qCInfo(lcAccountConfig)
464
4/4
✓ Branch 42 → 43 taken 921 times.
✓ Branch 44 → 45 taken 921 times.
✓ Branch 45 → 46 taken 921 times.
✓ Branch 46 → 47 taken 921 times.
921 << "Loaded account:" << account->id << "from" << fileName;
465
2/2
✓ Branch 51 → 52 taken 921 times.
✓ Branch 53 → 72 taken 921 times.
921 accounts.push_back(std::move(account.value()));
466 } else {
467
4/4
✓ Branch 54 → 55 taken 3 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 70 → 56 taken 3 times.
✓ Branch 70 → 71 taken 3 times.
6 qCWarning(lcAccountConfig)
468
6/6
✓ Branch 59 → 60 taken 3 times.
✓ Branch 60 → 61 taken 3 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 63 → 64 taken 3 times.
✓ Branch 64 → 65 taken 3 times.
3 << "Invalid account config" << fileName << ":" << errors.join(", ");
469 }
470 924 }
471 925 }
472
473
7/7
✓ Branch 79 → 80 taken 881 times.
✓ Branch 83 → 84 taken 881 times.
✓ Branch 84 → 85 taken 881 times.
✓ Branch 86 → 87 taken 881 times.
✓ Branch 87 → 88 taken 881 times.
✓ Branch 92 → 81 taken 881 times.
✓ Branch 92 → 93 taken 881 times.
1762 qCInfo(lcAccountConfig) << "Loaded" << accounts.size() << "accounts from"
474
1/1
✓ Branch 88 → 89 taken 881 times.
881 << configDir;
475 881 return accounts;
476 1765 }
477
478 std::optional<AccountConfig>
479 1072 AccountConfigLoader::loadFromFile(const QString &path, LoadMode mode) {
480
1/1
✓ Branch 2 → 3 taken 1072 times.
1072 QFile file(path);
481
3/3
✓ Branch 4 → 5 taken 1072 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 21 taken 1071 times.
1072 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
482
4/4
✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 19 → 8 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
2 qCWarning(lcAccountConfig)
483
4/4
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
1 << "Cannot open config file:" << path << file.errorString();
484 1 return std::nullopt;
485 }
486
487 1071 QJsonParseError parseError;
488
1/1
✓ Branch 21 → 22 taken 1071 times.
1071 QByteArray source = file.readAll();
489
1/1
✓ Branch 22 → 23 taken 1071 times.
1071 auto doc = QJsonDocument::fromJson(source, &parseError);
490
3/3
✓ Branch 23 → 24 taken 1071 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 41 taken 1067 times.
1071 if (doc.isNull()) {
491
4/4
✓ Branch 25 → 26 taken 4 times.
✓ Branch 29 → 30 taken 4 times.
✓ Branch 39 → 27 taken 4 times.
✓ Branch 39 → 40 taken 4 times.
8 qCWarning(lcAccountConfig)
492
5/5
✓ Branch 30 → 31 taken 4 times.
✓ Branch 31 → 32 taken 4 times.
✓ Branch 32 → 33 taken 4 times.
✓ Branch 33 → 34 taken 4 times.
✓ Branch 34 → 35 taken 4 times.
4 << "JSON parse error in" << path << ":" << parseError.errorString();
493 4 return std::nullopt;
494 }
495
496
3/3
✓ Branch 41 → 42 taken 1067 times.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 55 taken 1066 times.
1067 if (!doc.isObject()) {
497
6/6
✓ Branch 43 → 44 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 53 → 45 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
2 qCWarning(lcAccountConfig) << "JSON root is not an object in" << path;
498 1 return std::nullopt;
499 }
500
501
1/1
✓ Branch 55 → 56 taken 1066 times.
1066 const auto root = doc.object();
502
1/1
✓ Branch 56 → 57 taken 1066 times.
1066 AccountConfig config;
503
504
3/3
✓ Branch 57 → 58 taken 1066 times.
✓ Branch 58 → 59 taken 1066 times.
✓ Branch 59 → 60 taken 1066 times.
1066 config.id = root.value("id").toString();
505
6/6
✓ Branch 65 → 66 taken 1048 times.
✓ Branch 65 → 69 taken 18 times.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 69 taken 1047 times.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 70 → 83 taken 1065 times.
1066 if (!config.id.isEmpty() && !isSafeId(config.id)) {
506
6/6
✓ Branch 71 → 72 taken 1 time.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 81 → 73 taken 1 time.
✓ Branch 81 → 82 taken 1 time.
2 qCWarning(lcAccountConfig) << "Account file refused: its id is not a plain name" << path;
507 1 return std::nullopt;
508 }
509
3/3
✓ Branch 83 → 84 taken 1065 times.
✓ Branch 84 → 85 taken 1065 times.
✓ Branch 85 → 86 taken 1065 times.
1065 config.name = root.value("name").toString();
510
3/3
✓ Branch 90 → 91 taken 1065 times.
✓ Branch 91 → 92 taken 1065 times.
✓ Branch 92 → 93 taken 1065 times.
1065 config.email = root.value("email").toString();
511
3/3
✓ Branch 97 → 98 taken 1065 times.
✓ Branch 98 → 99 taken 1065 times.
✓ Branch 99 → 100 taken 1065 times.
1065 config.legacyKeyringName = root.value("legacyKeyringName").toString();
512 1065 bool metadataNeedsRewrite = false;
513
2/2
✓ Branch 105 → 106 taken 18 times.
✓ Branch 105 → 112 taken 1047 times.
1065 if (config.id.isEmpty()) {
514
2/2
✓ Branch 106 → 107 taken 18 times.
✓ Branch 107 → 108 taken 18 times.
18 config.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
515 18 config.legacyKeyringName = config.name;
516 18 metadataNeedsRewrite = true;
517 }
518
519 // IMAP
520
3/3
✓ Branch 112 → 113 taken 1065 times.
✓ Branch 113 → 114 taken 1065 times.
✓ Branch 114 → 115 taken 1065 times.
1065 const auto imapObj = root.value("imap").toObject();
521
3/3
✓ Branch 117 → 118 taken 1065 times.
✓ Branch 118 → 119 taken 1065 times.
✓ Branch 119 → 120 taken 1065 times.
1065 config.imap.host = imapObj.value("host").toString();
522
3/3
✓ Branch 124 → 125 taken 1065 times.
✓ Branch 125 → 126 taken 1065 times.
✓ Branch 126 → 127 taken 1065 times.
1065 config.imap.port = portValue(imapObj.value("port"), 993);
523
4/4
✓ Branch 129 → 130 taken 1065 times.
✓ Branch 130 → 131 taken 1065 times.
✓ Branch 131 → 132 taken 1065 times.
✓ Branch 132 → 133 taken 1065 times.
1065 config.imap.security = imapObj.value("security").toString("ssl");
524
3/3
✓ Branch 138 → 139 taken 1065 times.
✓ Branch 139 → 140 taken 1065 times.
✓ Branch 140 → 141 taken 1065 times.
1065 config.imap.username = imapObj.value("username").toString();
525
4/4
✓ Branch 145 → 146 taken 1065 times.
✓ Branch 146 → 147 taken 1065 times.
✓ Branch 147 → 148 taken 1065 times.
✓ Branch 148 → 149 taken 1065 times.
1065 config.imap.password = imapObj.value("password").toString().toUtf8();
526
3/3
✓ Branch 154 → 155 taken 1065 times.
✓ Branch 155 → 156 taken 1065 times.
✓ Branch 156 → 157 taken 1065 times.
1065 config.imap.credentialId = imapObj.value("credentialId").toString();
527
528 // SMTP
529
3/3
✓ Branch 161 → 162 taken 1065 times.
✓ Branch 162 → 163 taken 1065 times.
✓ Branch 163 → 164 taken 1065 times.
1065 const auto smtpObj = root.value("smtp").toObject();
530
3/3
✓ Branch 166 → 167 taken 1065 times.
✓ Branch 167 → 168 taken 1065 times.
✓ Branch 168 → 169 taken 1065 times.
1065 config.smtp.host = smtpObj.value("host").toString();
531
3/3
✓ Branch 173 → 174 taken 1065 times.
✓ Branch 174 → 175 taken 1065 times.
✓ Branch 175 → 176 taken 1065 times.
1065 config.smtp.port = portValue(smtpObj.value("port"), 587);
532
4/4
✓ Branch 178 → 179 taken 1065 times.
✓ Branch 179 → 180 taken 1065 times.
✓ Branch 180 → 181 taken 1065 times.
✓ Branch 181 → 182 taken 1065 times.
1065 config.smtp.security = smtpObj.value("security").toString("starttls");
533
3/3
✓ Branch 187 → 188 taken 1065 times.
✓ Branch 188 → 189 taken 1065 times.
✓ Branch 189 → 190 taken 1065 times.
1065 config.smtp.username = smtpObj.value("username").toString();
534
4/4
✓ Branch 194 → 195 taken 1065 times.
✓ Branch 195 → 196 taken 1065 times.
✓ Branch 196 → 197 taken 1065 times.
✓ Branch 197 → 198 taken 1065 times.
1065 config.smtp.password = smtpObj.value("password").toString().toUtf8();
535
3/3
✓ Branch 203 → 204 taken 1065 times.
✓ Branch 204 → 205 taken 1065 times.
✓ Branch 205 → 206 taken 1065 times.
1065 config.smtp.credentialId = smtpObj.value("credentialId").toString();
536
4/5
✓ Branch 211 → 212 taken 1065 times.
✓ Branch 212 → 213 taken 1065 times.
✓ Branch 213 → 214 taken 1065 times.
✗ Branch 215 → 216 not taken.
✓ Branch 215 → 219 taken 1065 times.
4260 config.smtp.login = smtpObj.value("login").toString() == QLatin1String("imap") ? QStringLiteral("imap")
537
2/4
✓ Branch 224 → 225 taken 1065 times.
✗ Branch 224 → 226 not taken.
✗ Branch 226 → 227 not taken.
✓ Branch 226 → 228 taken 1065 times.
2130 : QStringLiteral("separate");
538
539 // Sieve (absent = auto with the IMAP login)
540
3/3
✓ Branch 231 → 232 taken 1065 times.
✓ Branch 232 → 233 taken 1065 times.
✓ Branch 233 → 234 taken 1065 times.
1065 const auto sieveObj = root.value("sieve").toObject();
541
3/3
✓ Branch 236 → 237 taken 1065 times.
✓ Branch 237 → 238 taken 29 times.
✓ Branch 237 → 301 taken 1036 times.
1065 if (!sieveObj.isEmpty()) {
542
3/3
✓ Branch 238 → 239 taken 29 times.
✓ Branch 239 → 240 taken 29 times.
✓ Branch 243 → 244 taken 29 times.
58 config.sieve.mode = sieveObj.value("mode").toString(QStringLiteral("auto"));
543
3/3
✓ Branch 250 → 251 taken 29 times.
✓ Branch 251 → 252 taken 29 times.
✓ Branch 252 → 253 taken 29 times.
29 config.sieve.host = sieveObj.value("host").toString();
544
3/3
✓ Branch 257 → 258 taken 29 times.
✓ Branch 258 → 259 taken 29 times.
✓ Branch 259 → 260 taken 29 times.
29 config.sieve.port = portValue(sieveObj.value("port"), 4190);
545
3/3
✓ Branch 262 → 263 taken 29 times.
✓ Branch 263 → 264 taken 29 times.
✓ Branch 267 → 268 taken 29 times.
58 config.sieve.security = sieveObj.value("security").toString(QStringLiteral("starttls"));
546
3/3
✓ Branch 274 → 275 taken 29 times.
✓ Branch 275 → 276 taken 29 times.
✓ Branch 279 → 280 taken 29 times.
58 config.sieve.login = sieveObj.value("login").toString(QStringLiteral("imap"));
547
3/3
✓ Branch 286 → 287 taken 29 times.
✓ Branch 287 → 288 taken 29 times.
✓ Branch 288 → 289 taken 29 times.
29 config.sieve.username = sieveObj.value("username").toString();
548
3/3
✓ Branch 293 → 294 taken 29 times.
✓ Branch 294 → 295 taken 29 times.
✓ Branch 295 → 296 taken 29 times.
29 config.sieve.credentialId = sieveObj.value("credentialId").toString();
549 }
550
551 const bool hasPlaintextPassword =
552
4/4
✓ Branch 302 → 303 taken 1030 times.
✓ Branch 302 → 305 taken 35 times.
✓ Branch 304 → 305 taken 4 times.
✓ Branch 304 → 306 taken 1026 times.
1065 !config.imap.password.isEmpty() || !config.smtp.password.isEmpty();
553
2/2
✓ Branch 307 → 308 taken 1042 times.
✓ Branch 307 → 353 taken 23 times.
1065 if (mode == LoadMode::MetadataOnly) {
554 // Establish the stable destination identity without touching credentials.
555 // Preserve existing plaintext until both keyring writes are acknowledged.
556
2/2
✓ Branch 308 → 309 taken 9 times.
✓ Branch 308 → 335 taken 1033 times.
1042 if (metadataNeedsRewrite) {
557 9 auto identified = root;
558
3/3
✓ Branch 310 → 311 taken 9 times.
✓ Branch 311 → 312 taken 9 times.
✓ Branch 312 → 313 taken 9 times.
9 identified.insert("id", config.id);
559
3/3
✓ Branch 315 → 316 taken 9 times.
✓ Branch 316 → 317 taken 9 times.
✓ Branch 317 → 318 taken 9 times.
9 identified.insert("legacyKeyringName", config.legacyKeyringName);
560
2/2
✓ Branch 320 → 321 taken 9 times.
✓ Branch 321 → 322 taken 9 times.
9 source = QJsonDocument(identified).toJson(QJsonDocument::Indented);
561
2/3
✓ Branch 325 → 326 taken 9 times.
✗ Branch 326 → 327 not taken.
✓ Branch 326 → 329 taken 9 times.
9 if (!writeAccountBytes(path, source)) return std::nullopt;
562 9 }
563
6/6
✓ Branch 335 → 336 taken 1008 times.
✓ Branch 335 → 338 taken 34 times.
✓ Branch 337 → 338 taken 5 times.
✓ Branch 337 → 339 taken 1003 times.
✓ Branch 340 → 341 taken 39 times.
✓ Branch 340 → 352 taken 1003 times.
1042 if (hasPlaintextPassword || !config.legacyKeyringName.isEmpty()) {
564
1/1
✓ Branch 341 → 342 taken 39 times.
39 config.deferredCredentialMigration = std::make_shared<AccountCredentialMigration>();
565 39 config.deferredCredentialMigration->path = path;
566
1/1
✓ Branch 347 → 348 taken 39 times.
39 config.deferredCredentialMigration->digest = QCryptographicHash::hash(source, QCryptographicHash::Sha256);
567 }
568 1042 return config;
569 }
570 23 bool imapLegacyReady = false;
571 23 bool smtpLegacyReady = false;
572
2/2
✓ Branch 354 → 355 taken 9 times.
✓ Branch 354 → 437 taken 14 times.
23 if (!config.legacyKeyringName.isEmpty()) {
573 9 imapLegacyReady = !config.imap.password.isEmpty();
574 9 smtpLegacyReady = !config.smtp.password.isEmpty();
575 9 QByteArray password;
576
2/2
✓ Branch 357 → 358 taken 4 times.
✓ Branch 357 → 394 taken 5 times.
9 if (!imapLegacyReady) {
577 const bool stableFound =
578
4/13
✓ Branch 362 → 363 taken 4 times.
✗ Branch 363 → 364 not taken.
✓ Branch 363 → 367 taken 4 times.
✓ Branch 370 → 371 taken 4 times.
✗ Branch 370 → 372 not taken.
✓ Branch 372 → 373 taken 4 times.
✗ Branch 372 → 374 not taken.
✗ Branch 779 → 780 not taken.
✗ Branch 779 → 781 not taken.
✗ Branch 783 → 784 not taken.
✗ Branch 783 → 785 not taken.
✗ Branch 787 → 788 not taken.
✗ Branch 787 → 789 not taken.
12 readSecretBlocking(QStringLiteral("imap"), imapCredentialName(config), &password) &&
579
1/4
✗ Branch 365 → 366 not taken.
✗ Branch 365 → 367 not taken.
✓ Branch 368 → 369 taken 4 times.
✗ Branch 368 → 370 not taken.
4 !password.isEmpty();
580
1/2
✗ Branch 374 → 375 not taken.
✓ Branch 374 → 377 taken 4 times.
4 if (stableFound) {
581 ✗ config.imap.password = password;
582 ✗ imapLegacyReady = true;
583 } else {
584
1/1
✓ Branch 377 → 378 taken 4 times.
4 password.clear();
585 const bool legacyFound =
586
1/6
✓ Branch 389 → 390 taken 4 times.
✗ Branch 389 → 391 not taken.
✗ Branch 791 → 792 not taken.
✗ Branch 791 → 793 not taken.
✗ Branch 795 → 796 not taken.
✗ Branch 795 → 797 not taken.
8 readSecretBlocking(QStringLiteral("imap"),
587
3/3
✓ Branch 381 → 382 taken 4 times.
✓ Branch 382 → 383 taken 1 time.
✓ Branch 382 → 386 taken 3 times.
9 config.legacyKeyringName, &password) &&
588
2/4
✓ Branch 384 → 385 taken 1 time.
✗ Branch 384 → 386 not taken.
✓ Branch 387 → 388 taken 4 times.
✗ Branch 387 → 389 not taken.
5 !password.isEmpty();
589
2/2
✓ Branch 391 → 392 taken 1 time.
✓ Branch 391 → 394 taken 3 times.
4 if (legacyFound) {
590 1 config.imap.password = password;
591 1 imapLegacyReady = true;
592 }
593 }
594 }
595
596
1/1
✓ Branch 394 → 395 taken 9 times.
9 password.clear();
597
2/2
✓ Branch 395 → 396 taken 4 times.
✓ Branch 395 → 432 taken 5 times.
9 if (!smtpLegacyReady) {
598 const bool stableFound =
599
4/13
✓ Branch 400 → 401 taken 4 times.
✗ Branch 401 → 402 not taken.
✓ Branch 401 → 405 taken 4 times.
✓ Branch 408 → 409 taken 4 times.
✗ Branch 408 → 410 not taken.
✓ Branch 410 → 411 taken 4 times.
✗ Branch 410 → 412 not taken.
✗ Branch 799 → 800 not taken.
✗ Branch 799 → 801 not taken.
✗ Branch 803 → 804 not taken.
✗ Branch 803 → 805 not taken.
✗ Branch 807 → 808 not taken.
✗ Branch 807 → 809 not taken.
12 readSecretBlocking(QStringLiteral("smtp"), smtpCredentialName(config), &password) &&
600
1/4
✗ Branch 403 → 404 not taken.
✗ Branch 403 → 405 not taken.
✓ Branch 406 → 407 taken 4 times.
✗ Branch 406 → 408 not taken.
4 !password.isEmpty();
601
1/2
✗ Branch 412 → 413 not taken.
✓ Branch 412 → 415 taken 4 times.
4 if (stableFound) {
602 ✗ config.smtp.password = password;
603 ✗ smtpLegacyReady = true;
604 } else {
605
1/1
✓ Branch 415 → 416 taken 4 times.
4 password.clear();
606 const bool legacyFound =
607
1/6
✓ Branch 427 → 428 taken 4 times.
✗ Branch 427 → 429 not taken.
✗ Branch 811 → 812 not taken.
✗ Branch 811 → 813 not taken.
✗ Branch 815 → 816 not taken.
✗ Branch 815 → 817 not taken.
8 readSecretBlocking(QStringLiteral("smtp"),
608
3/3
✓ Branch 419 → 420 taken 4 times.
✓ Branch 420 → 421 taken 1 time.
✓ Branch 420 → 424 taken 3 times.
9 config.legacyKeyringName, &password) &&
609
2/4
✓ Branch 422 → 423 taken 1 time.
✗ Branch 422 → 424 not taken.
✓ Branch 425 → 426 taken 4 times.
✗ Branch 425 → 427 not taken.
5 !password.isEmpty();
610
2/2
✓ Branch 429 → 430 taken 1 time.
✓ Branch 429 → 432 taken 3 times.
4 if (legacyFound) {
611 1 config.smtp.password = password;
612 1 smtpLegacyReady = true;
613 }
614 }
615 }
616
617
3/4
✓ Branch 432 → 433 taken 6 times.
✓ Branch 432 → 435 taken 3 times.
✓ Branch 433 → 434 taken 6 times.
✗ Branch 433 → 435 not taken.
9 if (imapLegacyReady && smtpLegacyReady)
618 6 config.legacyKeyringName.clear();
619 9 metadataNeedsRewrite = true;
620 9 }
621
622
3/4
✓ Branch 437 → 438 taken 14 times.
✓ Branch 437 → 439 taken 9 times.
✗ Branch 438 → 439 not taken.
✓ Branch 438 → 503 taken 14 times.
23 if (metadataNeedsRewrite || hasPlaintextPassword) {
623
2/3
✓ Branch 439 → 440 taken 9 times.
✓ Branch 440 → 441 taken 9 times.
✗ Branch 440 → 491 not taken.
9 if (saveToFile(config, path)) {
624
4/8
✓ Branch 442 → 443 taken 6 times.
✓ Branch 442 → 445 taken 3 times.
✗ Branch 443 → 444 not taken.
✓ Branch 443 → 445 taken 6 times.
✗ Branch 444 → 445 not taken.
✗ Branch 444 → 446 not taken.
✓ Branch 447 → 448 taken 9 times.
✗ Branch 447 → 479 not taken.
9 if (!config.legacyKeyringName.isEmpty() || imapLegacyReady ||
625 smtpLegacyReady) {
626 const QString storedLegacyName =
627
3/3
✓ Branch 448 → 449 taken 9 times.
✓ Branch 449 → 450 taken 9 times.
✓ Branch 450 → 451 taken 9 times.
9 root.value("legacyKeyringName").toString();
628 const QString legacyName =
629
1/2
✓ Branch 454 → 455 taken 9 times.
✗ Branch 454 → 456 not taken.
9 storedLegacyName.isEmpty() ? config.name : storedLegacyName;
630 9 QString deleteError;
631
2/2
✓ Branch 458 → 459 taken 6 times.
✓ Branch 458 → 466 taken 3 times.
9 if (imapLegacyReady)
632
1/1
✓ Branch 462 → 463 taken 6 times.
6 deleteSecretBlocking(QStringLiteral("imap"), legacyName,
633 &deleteError);
634 9 deleteError.clear();
635
2/2
✓ Branch 467 → 468 taken 6 times.
✓ Branch 467 → 475 taken 3 times.
9 if (smtpLegacyReady)
636
1/1
✓ Branch 471 → 472 taken 6 times.
6 deleteSecretBlocking(QStringLiteral("smtp"), legacyName,
637 &deleteError);
638 9 }
639
5/5
✓ Branch 479 → 480 taken 9 times.
✓ Branch 483 → 484 taken 9 times.
✓ Branch 484 → 485 taken 9 times.
✓ Branch 489 → 481 taken 9 times.
✓ Branch 489 → 490 taken 9 times.
18 qCInfo(lcAccountConfig) << "Migrated account credential metadata:"
640
1/1
✓ Branch 485 → 486 taken 9 times.
9 << path;
641 } else {
642 ✗ qCWarning(lcAccountConfig) // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; migration failure remains measured.
643 << "Could not migrate account credentials; retaining"
644 ✗ " the original config file:"
645 ✗ << path;
646 }
647 }
648
649 23 return config;
650 1072 }
651
652 namespace {
653 // A host name goes to the socket and the TLS name check as is: no spaces,
654 // no control characters (Sprint 102 fuzz target account_file).
655 1921 bool hasInvalidHostCharacters(const QString &host) {
656
2/2
✓ Branch 13 → 4 taken 18013 times.
✓ Branch 13 → 14 taken 1919 times.
19932 for (const QChar character : host)
657
5/6
✓ Branch 5 → 6 taken 18011 times.
✓ Branch 5 → 8 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 18011 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 18011 times.
18013 if (character.unicode() <= 0x20 || character.unicode() == 0x7f)
658 2 return true;
659 1919 return false;
660 }
661 } // namespace
662
663 957 QStringList AccountConfigLoader::validate(const AccountConfig &config) {
664 957 QStringList errors;
665
666
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 8 taken 953 times.
957 if (config.name.isEmpty())
667
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 5 → 6 taken 4 times.
4 errors << "Missing 'name'";
668
2/2
✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 14 taken 952 times.
957 if (config.email.isEmpty())
669
2/2
✓ Branch 10 → 11 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
5 errors << "Missing 'email'";
670
671 // IMAP validation
672
2/2
✓ Branch 15 → 16 taken 5 times.
✓ Branch 15 → 20 taken 952 times.
957 if (config.imap.host.isEmpty())
673
2/2
✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
5 errors << "Missing 'imap.host'";
674
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 26 taken 951 times.
952 else if (hasInvalidHostCharacters(config.imap.host))
675
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
1 errors << "Invalid characters in 'imap.host'";
676
2/2
✓ Branch 27 → 28 taken 5 times.
✓ Branch 27 → 32 taken 952 times.
957 if (config.imap.username.isEmpty())
677
2/2
✓ Branch 28 → 29 taken 5 times.
✓ Branch 29 → 30 taken 5 times.
5 errors << "Missing 'imap.username'";
678
2/2
✓ Branch 32 → 33 taken 4 times.
✓ Branch 32 → 37 taken 953 times.
957 if (config.imap.port == 0)
679
2/2
✓ Branch 33 → 34 taken 4 times.
✓ Branch 34 → 35 taken 4 times.
4 errors << "Invalid 'imap.port'";
680
5/6
✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 42 taken 955 times.
✓ Branch 40 → 41 taken 2 times.
✗ Branch 40 → 42 not taken.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 48 taken 955 times.
957 if (config.imap.security != "ssl" && config.imap.security != "starttls")
681
2/2
✓ Branch 44 → 45 taken 2 times.
✓ Branch 45 → 46 taken 2 times.
2 errors << "Invalid 'imap.security' (must be 'ssl' or 'starttls')";
682
683 // SMTP validation
684
2/2
✓ Branch 49 → 50 taken 5 times.
✓ Branch 49 → 54 taken 952 times.
957 if (config.smtp.host.isEmpty())
685
2/2
✓ Branch 50 → 51 taken 5 times.
✓ Branch 51 → 52 taken 5 times.
5 errors << "Missing 'smtp.host'";
686
2/2
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 60 taken 951 times.
952 else if (hasInvalidHostCharacters(config.smtp.host))
687
2/2
✓ Branch 56 → 57 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
1 errors << "Invalid characters in 'smtp.host'";
688
2/2
✓ Branch 61 → 62 taken 5 times.
✓ Branch 61 → 66 taken 952 times.
957 if (config.smtp.username.isEmpty())
689
2/2
✓ Branch 62 → 63 taken 5 times.
✓ Branch 63 → 64 taken 5 times.
5 errors << "Missing 'smtp.username'";
690
2/2
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 71 taken 956 times.
957 if (config.smtp.port == 0)
691
2/2
✓ Branch 67 → 68 taken 1 time.
✓ Branch 68 → 69 taken 1 time.
1 errors << "Invalid 'smtp.port'";
692
6/6
✓ Branch 72 → 73 taken 937 times.
✓ Branch 72 → 76 taken 20 times.
✓ Branch 74 → 75 taken 1 time.
✓ Branch 74 → 76 taken 936 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 82 taken 956 times.
957 if (config.smtp.security != "ssl" && config.smtp.security != "starttls")
693
2/2
✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
1 errors << "Invalid 'smtp.security' (must be 'ssl' or 'starttls')";
694
695 // Sieve validation (S95.1.3)
696 957 const SieveConfig &sieve = config.sieve;
697
7/8
✓ Branch 83 → 84 taken 19 times.
✓ Branch 83 → 89 taken 938 times.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 85 → 89 taken 18 times.
✓ Branch 87 → 88 taken 1 time.
✗ Branch 87 → 89 not taken.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 95 taken 956 times.
957 if (sieve.mode != "auto" && sieve.mode != "manual" && sieve.mode != "off")
698
2/2
✓ Branch 91 → 92 taken 1 time.
✓ Branch 92 → 93 taken 1 time.
1 errors << "Invalid 'sieve.mode' (must be 'auto', 'manual' or 'off')";
699
2/2
✓ Branch 96 → 97 taken 18 times.
✓ Branch 96 → 127 taken 939 times.
957 if (sieve.mode == "manual") {
700
3/3
✓ Branch 97 → 98 taken 18 times.
✓ Branch 100 → 101 taken 1 time.
✓ Branch 100 → 105 taken 17 times.
18 if (sieve.host.trimmed().isEmpty())
701
2/2
✓ Branch 101 → 102 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
1 errors << "Missing 'sieve.host'";
702
1/2
✗ Branch 106 → 107 not taken.
✓ Branch 106 → 111 taken 17 times.
17 else if (hasInvalidHostCharacters(sieve.host))
703 ✗ errors << "Invalid characters in 'sieve.host'";
704
1/2
✗ Branch 111 → 112 not taken.
✓ Branch 111 → 116 taken 18 times.
18 if (sieve.port == 0)
705 ✗ errors << "Invalid 'sieve.port'";
706
5/6
✓ Branch 117 → 118 taken 18 times.
✗ Branch 117 → 121 not taken.
✓ Branch 119 → 120 taken 1 time.
✓ Branch 119 → 121 taken 17 times.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 122 → 127 taken 17 times.
18 if (sieve.security != "ssl" && sieve.security != "starttls")
707
2/2
✓ Branch 123 → 124 taken 1 time.
✓ Branch 124 → 125 taken 1 time.
1 errors << "Invalid 'sieve.security' (must be 'ssl' or 'starttls')";
708 }
709
4/6
✓ Branch 128 → 129 taken 4 times.
✓ Branch 128 → 132 taken 953 times.
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 132 taken 4 times.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 138 taken 957 times.
957 if (sieve.login != "imap" && sieve.login != "separate")
710 ✗ errors << "Invalid 'sieve.login' (must be 'imap' or 'separate')";
711
6/6
✓ Branch 139 → 140 taken 4 times.
✓ Branch 139 → 143 taken 953 times.
✓ Branch 141 → 142 taken 2 times.
✓ Branch 141 → 143 taken 2 times.
✓ Branch 144 → 145 taken 2 times.
✓ Branch 144 → 149 taken 955 times.
957 if (sieve.usesSeparateLogin() && sieve.username.isEmpty())
712
2/2
✓ Branch 145 → 146 taken 2 times.
✓ Branch 146 → 147 taken 2 times.
2 errors << "Missing 'sieve.username'";
713
714 957 return errors;
715 ✗ }
716
717 53 QString AccountConfigLoader::checkNameUniqueness(
718 const AccountConfig &config, const QString &configDir) {
719
1/1
✓ Branch 2 → 3 taken 53 times.
53 QDir dir(configDir);
720
2/3
✓ Branch 3 → 4 taken 53 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 53 times.
53 if (!dir.exists())
721 ✗ return {};
722 // An unreadable file holding the chosen file name cannot be verified:
723 // refuse rather than overwrite it.
724
2/2
✓ Branch 9 → 10 taken 53 times.
✓ Branch 10 → 11 taken 53 times.
53 const QString target = fileStem(config, configDir) + QStringLiteral(".json");
725
3/5
✓ Branch 18 → 19 taken 53 times.
✓ Branch 21 → 22 taken 53 times.
✓ Branch 21 → 23 taken 53 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
159 const auto files = dir.entryList({"*.json"}, QDir::Files, QDir::Name);
726
2/2
✓ Branch 71 → 25 taken 49 times.
✓ Branch 71 → 72 taken 51 times.
100 for (const auto &fileName : files) {
727
2/2
✓ Branch 26 → 27 taken 49 times.
✓ Branch 27 → 28 taken 49 times.
49 auto existing = loadFromFile(dir.absoluteFilePath(fileName), LoadMode::MetadataOnly);
728
2/2
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 42 taken 48 times.
49 if (!existing) {
729
1/2
✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 41 not taken.
1 if (fileName == target)
730
1/1
✓ Branch 37 → 38 taken 1 time.
2 return QStringLiteral("Account file '%1' is already in use").arg(fileName);
731 ✗ continue;
732 }
733
5/6
✓ Branch 43 → 44 taken 48 times.
✗ Branch 43 → 48 not taken.
✓ Branch 46 → 47 taken 25 times.
✓ Branch 46 → 48 taken 23 times.
✓ Branch 49 → 50 taken 25 times.
✓ Branch 49 → 51 taken 23 times.
48 if (!config.id.isEmpty() && existing->id == config.id)
734 25 continue;
735
2/2
✓ Branch 53 → 54 taken 1 time.
✓ Branch 53 → 62 taken 22 times.
23 if (existing->name.compare(config.name, Qt::CaseInsensitive) == 0)
736 2 return QStringLiteral("Account name '%1' is already in use")
737
1/1
✓ Branch 58 → 59 taken 1 time.
1 .arg(config.name);
738 49 }
739 51 return {};
740 106 }
741
742 98 QString AccountConfigLoader::fileStem(const AccountConfig &config, const QString &configDir) {
743
1/1
✓ Branch 2 → 3 taken 98 times.
98 const QString base = slugify(config.name);
744
3/3
✓ Branch 3 → 4 taken 98 times.
✓ Branch 7 → 8 taken 98 times.
✓ Branch 8 → 9 taken 98 times.
196 const QString path = QDir(configDir).absoluteFilePath(base + QStringLiteral(".json"));
745
3/3
✓ Branch 13 → 14 taken 98 times.
✓ Branch 14 → 15 taken 64 times.
✓ Branch 14 → 17 taken 34 times.
98 if (!QFileInfo::exists(path))
746 64 return base;
747
1/1
✓ Branch 17 → 18 taken 34 times.
34 const auto owner = loadFromFile(path, LoadMode::MetadataOnly);
748 // An unreadable file may be this very account, damaged: keep its name,
749 // checkNameUniqueness() then refuses instead of saving a second copy.
750
7/8
✓ Branch 19 → 20 taken 33 times.
✓ Branch 19 → 25 taken 1 time.
✓ Branch 21 → 22 taken 33 times.
✗ Branch 21 → 26 not taken.
✓ Branch 24 → 25 taken 24 times.
✓ Branch 24 → 26 taken 9 times.
✓ Branch 27 → 28 taken 25 times.
✓ Branch 27 → 30 taken 9 times.
34 if (!owner || (!config.id.isEmpty() && owner->id == config.id))
751 25 return base;
752 const QByteArray digest =
753
4/4
✓ Branch 30 → 31 taken 9 times.
✓ Branch 32 → 33 taken 9 times.
✓ Branch 33 → 34 taken 9 times.
✓ Branch 34 → 35 taken 9 times.
9 QCryptographicHash::hash(config.name.toUtf8(), QCryptographicHash::Sha256).toHex().left(8);
754
3/3
✓ Branch 38 → 39 taken 9 times.
✓ Branch 41 → 42 taken 9 times.
✓ Branch 42 → 43 taken 9 times.
9 return base + QLatin1Char('_') + QString::fromLatin1(digest);
755 98 }
756
757 38 bool AccountConfigLoader::save(AccountConfig &config, const QString &configDir) {
758
1/1
✓ Branch 2 → 3 taken 38 times.
38 QDir dir(configDir);
759
3/3
✓ Branch 3 → 4 taken 38 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 21 taken 35 times.
38 if (!dir.exists()) {
760
3/4
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 21 taken 3 times.
3 if (!dir.mkpath(".")) {
761 ✗ qCWarning(lcAccountConfig)
762 ✗ << "Failed to create config directory:" << configDir;
763 ✗ return false;
764 }
765 }
766
767 38 AccountConfig storedConfig = config;
768
2/2
✓ Branch 23 → 24 taken 26 times.
✓ Branch 23 → 29 taken 12 times.
38 if (storedConfig.id.isEmpty())
769
2/2
✓ Branch 24 → 25 taken 26 times.
✓ Branch 25 → 26 taken 26 times.
26 storedConfig.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
770
771 const QString uniquenessError =
772
1/1
✓ Branch 29 → 30 taken 38 times.
38 checkNameUniqueness(storedConfig, configDir);
773
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 44 taken 37 times.
38 if (!uniquenessError.isEmpty()) {
774
5/5
✓ Branch 32 → 33 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 42 → 34 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
2 qCWarning(lcAccountConfig) << "Refusing to save account:"
775
1/1
✓ Branch 38 → 39 taken 1 time.
1 << uniquenessError;
776 1 return false;
777 }
778
779
1/1
✓ Branch 44 → 45 taken 37 times.
37 auto newSlug = fileStem(storedConfig, configDir);
780
1/1
✓ Branch 45 → 46 taken 37 times.
37 auto filename = newSlug + ".json";
781
1/1
✓ Branch 46 → 47 taken 37 times.
37 auto path = dir.absoluteFilePath(filename);
782
783 37 QStringList oldPaths;
784 37 bool targetHasSameId = false;
785 12 const auto retainLegacyCredentials = [&storedConfig](const AccountConfig &existing) {
786 // Blank form fields mean keep. In an offline legacy profile the secrets
787 // may still be in the original JSON or under a display-name key.
788
2/2
✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 5 taken 3 times.
12 if (storedConfig.imap.password.isEmpty()) storedConfig.imap.password = existing.imap.password;
789
2/2
✓ Branch 6 → 7 taken 9 times.
✓ Branch 6 → 8 taken 3 times.
12 if (storedConfig.smtp.password.isEmpty()) storedConfig.smtp.password = existing.smtp.password;
790
1/2
✓ Branch 9 → 10 taken 12 times.
✗ Branch 9 → 11 not taken.
12 if (storedConfig.legacyKeyringName.isEmpty()) storedConfig.legacyKeyringName = existing.legacyKeyringName;
791
1/2
✓ Branch 12 → 13 taken 12 times.
✗ Branch 12 → 14 not taken.
12 if (storedConfig.imap.credentialId.isEmpty()) storedConfig.imap.credentialId = existing.imap.credentialId;
792
1/2
✓ Branch 15 → 16 taken 12 times.
✗ Branch 15 → 17 not taken.
12 if (storedConfig.smtp.credentialId.isEmpty()) storedConfig.smtp.credentialId = existing.smtp.credentialId;
793
3/6
✓ Branch 18 → 19 taken 12 times.
✗ Branch 18 → 22 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 12 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 12 times.
12 if (storedConfig.sieve.credentialId.isEmpty() && storedConfig.sieve.usesSeparateLogin())
794 ✗ storedConfig.sieve.credentialId = existing.sieve.credentialId;
795 49 };
796
3/3
✓ Branch 47 → 48 taken 37 times.
✓ Branch 48 → 49 taken 6 times.
✓ Branch 48 → 62 taken 31 times.
37 if (QFileInfo::exists(path)) {
797
1/1
✓ Branch 49 → 50 taken 6 times.
6 const auto targetAccount = loadFromFile(path, LoadMode::MetadataOnly);
798
2/4
✓ Branch 51 → 52 taken 6 times.
✗ Branch 51 → 56 not taken.
✓ Branch 54 → 55 taken 6 times.
✗ Branch 54 → 56 not taken.
6 targetHasSameId = targetAccount && targetAccount->id == storedConfig.id;
799
1/2
✓ Branch 57 → 58 taken 6 times.
✗ Branch 57 → 60 not taken.
6 if (targetHasSameId) retainLegacyCredentials(*targetAccount);
800 6 }
801
3/5
✓ Branch 66 → 67 taken 37 times.
✓ Branch 69 → 70 taken 37 times.
✓ Branch 69 → 71 taken 37 times.
✗ Branch 180 → 181 not taken.
✗ Branch 180 → 182 not taken.
111 const auto files = dir.entryList({"*.json"}, QDir::Files, QDir::Name);
802
2/2
✓ Branch 99 → 73 taken 33 times.
✓ Branch 99 → 100 taken 37 times.
70 for (const auto &fileName : files) {
803
1/1
✓ Branch 74 → 75 taken 33 times.
33 const auto oldPath = dir.absoluteFilePath(fileName);
804
2/2
✓ Branch 76 → 77 taken 6 times.
✓ Branch 76 → 78 taken 27 times.
33 if (oldPath == path)
805 6 continue;
806
1/1
✓ Branch 78 → 79 taken 27 times.
27 auto oldAccount = loadFromFile(oldPath, LoadMode::MetadataOnly);
807
5/6
✓ Branch 80 → 81 taken 27 times.
✗ Branch 80 → 85 not taken.
✓ Branch 83 → 84 taken 6 times.
✓ Branch 83 → 85 taken 21 times.
✓ Branch 86 → 87 taken 6 times.
✓ Branch 86 → 90 taken 21 times.
27 if (oldAccount && oldAccount->id == storedConfig.id) {
808
1/1
✓ Branch 87 → 88 taken 6 times.
6 oldPaths.append(oldPath);
809 6 retainLegacyCredentials(*oldAccount);
810 }
811 33 }
812
813
7/8
✓ Branch 101 → 102 taken 37 times.
✗ Branch 101 → 105 not taken.
✓ Branch 102 → 103 taken 6 times.
✓ Branch 102 → 106 taken 31 times.
✓ Branch 104 → 105 taken 1 time.
✓ Branch 104 → 106 taken 5 times.
✓ Branch 107 → 108 taken 1 time.
✓ Branch 107 → 125 taken 36 times.
37 if (oldPaths.size() > 1 || (targetHasSameId && !oldPaths.isEmpty())) {
814 3 qCWarning(lcAccountConfig) // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; credential failure remains measured.
815
1/1
✓ Branch 113 → 114 taken 1 time.
1 << "Refusing ambiguous account save: duplicate account ID"
816
3/3
✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
✓ Branch 117 → 118 taken 1 time.
2 << storedConfig.id << "appears in" << oldPaths;
817 1 return false;
818 }
819
820
3/3
✓ Branch 125 → 126 taken 36 times.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 126 → 128 taken 35 times.
36 if (!saveToFile(storedConfig, path))
821 1 return false;
822
823
4/4
✓ Branch 128 → 129 taken 35 times.
✓ Branch 129 → 130 taken 35 times.
✓ Branch 148 → 131 taken 4 times.
✓ Branch 148 → 149 taken 35 times.
39 for (const auto &oldPath : oldPaths) {
824
2/3
✓ Branch 132 → 133 taken 4 times.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 146 taken 4 times.
4 if (!QFile::remove(oldPath)) {
825 ✗ qCWarning(lcAccountConfig)
826 ✗ << "Saved renamed account but could not remove old file:" << oldPath;
827 }
828 }
829 35 config.id = storedConfig.id;
830 35 return true;
831 75 }
832
833 1 bool AccountConfigLoader::save(const AccountConfig &config,
834 const QString &configDir) {
835 1 AccountConfig storedConfig = config;
836
1/1
✓ Branch 3 → 4 taken 1 time.
2 return save(storedConfig, configDir);
837 1 }
838
839 49 bool AccountConfigLoader::saveToFile(AccountConfig &config,
840 const QString &path) {
841 49 AccountConfig storedConfig = config;
842
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 10 taken 45 times.
49 if (storedConfig.id.isEmpty())
843
2/2
✓ Branch 5 → 6 taken 4 times.
✓ Branch 6 → 7 taken 4 times.
4 storedConfig.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
844
845 49 QString keyringError;
846
1/1
✓ Branch 14 → 15 taken 49 times.
49 if (!writeSecretBlocking(QStringLiteral("imap"),
847
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 31 taken 48 times.
98 imapCredentialName(storedConfig),
848 storedConfig.imap.password, &keyringError)) {
849
4/4
✓ Branch 19 → 20 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 29 → 21 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
2 qCWarning(lcAccountConfig)
850
1/1
✓ Branch 24 → 25 taken 1 time.
1 << "Refusing to save account after IMAP keyring write failed:"
851
1/1
✓ Branch 25 → 26 taken 1 time.
1 << keyringError;
852 1 return false;
853 }
854
1/1
✓ Branch 35 → 36 taken 48 times.
48 if (!writeSecretBlocking(QStringLiteral("smtp"),
855
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 52 taken 48 times.
96 smtpCredentialName(storedConfig),
856 storedConfig.smtp.password, &keyringError)) {
857 ✗ qCWarning(lcAccountConfig)
858 ✗ << "Refusing to save account after SMTP keyring write failed:"
859 ✗ << keyringError;
860 ✗ return false;
861 }
862
2/6
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 57 taken 48 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 85 taken 48 times.
48 if (storedConfig.sieve.usesSeparateLogin() && !storedConfig.sieve.password.isEmpty()) {
863 ✗ if (storedConfig.sieve.credentialId.isEmpty())
864 ✗ storedConfig.sieve.credentialId = QUuid::createUuid().toString(QUuid::WithoutBraces);
865 ✗ if (!writeSecretBlocking(QStringLiteral("sieve"), storedConfig.sieve.credentialId,
866 storedConfig.sieve.password, &keyringError)) {
867 ✗ qCWarning(lcAccountConfig) << "Refusing to save account after Sieve keyring write failed:"
868 ✗ << keyringError;
869 ✗ return false;
870 }
871 }
872
873 // T-501: Use QSaveFile for atomic write — prevents TOCTOU permission race
874
1/1
✓ Branch 85 → 86 taken 48 times.
48 QSaveFile file(path);
875
2/3
✓ Branch 87 → 88 taken 48 times.
✗ Branch 88 → 89 not taken.
✓ Branch 88 → 104 taken 48 times.
48 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
876 ✗ qCWarning(lcAccountConfig)
877 ✗ << "Cannot write config file:" << path << file.errorString();
878 ✗ return false;
879 }
880
881 // Set restrictive permissions BEFORE writing (prevents TOCTOU race)
882
1/1
✓ Branch 105 → 106 taken 48 times.
48 file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
883
884
2/2
✓ Branch 106 → 107 taken 48 times.
✓ Branch 107 → 108 taken 48 times.
48 file.write(accountMetadata(storedConfig));
885
886
2/3
✓ Branch 109 → 110 taken 48 times.
✗ Branch 110 → 111 not taken.
✓ Branch 110 → 123 taken 48 times.
48 if (!file.commit()) {
887 ✗ qCWarning(lcAccountConfig)
888 ✗ << "Failed to commit config file:" << path;
889 ✗ return false;
890 }
891
892
7/7
✓ Branch 123 → 124 taken 48 times.
✓ Branch 127 → 128 taken 48 times.
✓ Branch 128 → 129 taken 48 times.
✓ Branch 129 → 130 taken 48 times.
✓ Branch 130 → 131 taken 48 times.
✓ Branch 135 → 125 taken 48 times.
✓ Branch 135 → 136 taken 48 times.
96 qCInfo(lcAccountConfig) << "Saved account:" << storedConfig.id << "to"
893
1/1
✓ Branch 131 → 132 taken 48 times.
48 << path;
894 48 config.id = storedConfig.id;
895 48 return true;
896 49 }
897
898 1 bool AccountConfigLoader::saveToFile(const AccountConfig &config,
899 const QString &path) {
900 1 AccountConfig storedConfig = config;
901
1/1
✓ Branch 3 → 4 taken 1 time.
2 return saveToFile(storedConfig, path);
902 1 }
903
904 1062 bool AccountConfigLoader::isSafeId(const QString &id) {
905 1062 return SafePathComponent::isSafe(id);
906 }
907
908 // Every keyring name the account files in `dir` refer to (ids, credential
909 // ids and legacy names), read as plain metadata.
910 10 static QSet<QString> credentialNamesInUse(const QDir &dir) {
911 10 QSet<QString> names;
912
3/5
✓ Branch 7 → 8 taken 10 times.
✓ Branch 10 → 11 taken 10 times.
✓ Branch 10 → 12 taken 10 times.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
30 const auto files = dir.entryList({"*.json"}, QDir::Files);
913
2/2
✓ Branch 132 → 14 taken 3 times.
✓ Branch 132 → 133 taken 10 times.
13 for (const auto &fileName : files) {
914
2/2
✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
3 QFile file(dir.absoluteFilePath(fileName));
915
2/3
✓ Branch 19 → 20 taken 3 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 3 times.
3 if (!file.open(QIODevice::ReadOnly)) continue;
916
2/2
✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
3 const auto document = QJsonDocument::fromJson(file.readAll());
917
2/3
✓ Branch 25 → 26 taken 3 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 3 times.
3 if (!document.isObject()) continue;
918
1/1
✓ Branch 28 → 29 taken 3 times.
3 const auto row = document.object();
919 const QStringList candidates{
920
1/1
✓ Branch 32 → 33 taken 3 times.
6 row.value(QStringLiteral("id")).toString(),
921
1/1
✓ Branch 37 → 38 taken 3 times.
6 row.value(QStringLiteral("name")).toString(),
922
1/1
✓ Branch 42 → 43 taken 3 times.
6 row.value(QStringLiteral("legacyKeyringName")).toString(),
923
3/3
✓ Branch 47 → 48 taken 3 times.
✓ Branch 48 → 49 taken 3 times.
✓ Branch 52 → 53 taken 3 times.
9 row.value(QStringLiteral("imap")).toObject().value(QStringLiteral("credentialId")).toString(),
924
3/3
✓ Branch 57 → 58 taken 3 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
9 row.value(QStringLiteral("smtp")).toObject().value(QStringLiteral("credentialId")).toString(),
925
5/7
✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 72 → 73 taken 3 times.
✓ Branch 76 → 77 taken 18 times.
✓ Branch 76 → 78 taken 3 times.
✗ Branch 159 → 160 not taken.
✗ Branch 159 → 161 not taken.
27 row.value(QStringLiteral("sieve")).toObject().value(QStringLiteral("credentialId")).toString()};
926
2/2
✓ Branch 117 → 110 taken 18 times.
✓ Branch 117 → 118 taken 3 times.
21 for (const QString &name : candidates)
927
3/3
✓ Branch 112 → 113 taken 6 times.
✓ Branch 112 → 115 taken 12 times.
✓ Branch 113 → 114 taken 6 times.
18 if (!name.isEmpty()) names.insert(name);
928 3 }
929 10 return names;
930 41 }
931
932 12 bool AccountConfigLoader::remove(const QString &accountName,
933 const QString &configDir,
934 const QString &expectedId) {
935
1/1
✓ Branch 2 → 3 taken 12 times.
12 QDir dir(configDir);
936
2/3
✓ Branch 3 → 4 taken 12 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 17 taken 12 times.
12 if (!dir.exists()) {
937 ✗ qCWarning(lcAccountConfig)
938 ✗ << "Config directory does not exist:" << configDir;
939 ✗ return false;
940 }
941
942 // Read only identity metadata. loadFromFile() may migrate old credentials
943 // through a nested event loop; an account being removed needs no migration.
944
3/5
✓ Branch 21 → 22 taken 12 times.
✓ Branch 24 → 25 taken 12 times.
✓ Branch 24 → 26 taken 12 times.
✗ Branch 338 → 339 not taken.
✗ Branch 338 → 340 not taken.
36 const auto files = dir.entryList({"*.json"}, QDir::Files);
945
2/2
✓ Branch 313 → 28 taken 12 times.
✓ Branch 313 → 314 taken 2 times.
14 for (const auto &fileName : files) {
946
1/1
✓ Branch 29 → 30 taken 12 times.
12 auto path = dir.absoluteFilePath(fileName);
947
1/1
✓ Branch 30 → 31 taken 12 times.
12 QFile file(path);
948
2/3
✓ Branch 32 → 33 taken 12 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 12 times.
12 if (!file.open(QIODevice::ReadOnly)) continue;
949
2/2
✓ Branch 35 → 36 taken 12 times.
✓ Branch 36 → 37 taken 12 times.
12 const auto document = QJsonDocument::fromJson(file.readAll());
950
1/1
✓ Branch 38 → 39 taken 12 times.
12 file.close();
951
2/3
✓ Branch 39 → 40 taken 12 times.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 12 times.
12 if (!document.isObject()) continue;
952
1/1
✓ Branch 42 → 43 taken 12 times.
12 const auto row = document.object();
953
2/2
✓ Branch 46 → 47 taken 12 times.
✓ Branch 47 → 48 taken 12 times.
12 const QString id = row.value(QStringLiteral("id")).toString();
954
12/24
✓ Branch 54 → 55 taken 12 times.
✓ Branch 55 → 56 taken 12 times.
✓ Branch 57 → 58 taken 11 times.
✓ Branch 57 → 63 taken 1 time.
✓ Branch 59 → 60 taken 6 times.
✓ Branch 59 → 62 taken 5 times.
✓ Branch 64 → 65 taken 12 times.
✗ Branch 64 → 66 not taken.
✓ Branch 66 → 67 taken 12 times.
✗ Branch 66 → 68 not taken.
✓ Branch 68 → 69 taken 12 times.
✗ Branch 68 → 70 not taken.
✓ Branch 70 → 71 taken 12 times.
✗ Branch 70 → 72 not taken.
✓ Branch 72 → 73 taken 10 times.
✓ Branch 72 → 286 taken 2 times.
✗ Branch 363 → 364 not taken.
✗ Branch 363 → 365 not taken.
✗ Branch 367 → 368 not taken.
✗ Branch 367 → 369 not taken.
✗ Branch 371 → 372 not taken.
✗ Branch 371 → 373 not taken.
✗ Branch 375 → 376 not taken.
✗ Branch 375 → 377 not taken.
41 if (row.value(QStringLiteral("name")).toString() == accountName &&
955
2/2
✓ Branch 61 → 62 taken 5 times.
✓ Branch 61 → 63 taken 1 time.
17 (expectedId.isEmpty() || id == expectedId)) {
956
2/3
✓ Branch 73 → 74 taken 10 times.
✓ Branch 74 → 75 taken 10 times.
✗ Branch 74 → 274 not taken.
10 if (QFile::remove(path)) {
957
2/2
✓ Branch 78 → 79 taken 10 times.
✓ Branch 79 → 80 taken 10 times.
10 const auto imap = row.value(QStringLiteral("imap")).toObject();
958
2/2
✓ Branch 86 → 87 taken 10 times.
✓ Branch 87 → 88 taken 10 times.
10 const auto smtp = row.value(QStringLiteral("smtp")).toObject();
959
2/4
✓ Branch 98 → 99 taken 10 times.
✓ Branch 98 → 100 taken 10 times.
✗ Branch 398 → 399 not taken.
✗ Branch 398 → 400 not taken.
20 QStringList imapNames{id.isEmpty() ? accountName : id};
960
2/4
✓ Branch 107 → 108 taken 10 times.
✓ Branch 107 → 109 taken 10 times.
✗ Branch 408 → 409 not taken.
✗ Branch 408 → 410 not taken.
20 QStringList smtpNames{id.isEmpty() ? accountName : id};
961
2/2
✓ Branch 112 → 113 taken 10 times.
✓ Branch 113 → 114 taken 10 times.
10 const QString imapCredential = imap.value(QStringLiteral("credentialId")).toString();
962
2/2
✓ Branch 120 → 121 taken 10 times.
✓ Branch 121 → 122 taken 10 times.
10 const QString smtpCredential = smtp.value(QStringLiteral("credentialId")).toString();
963
3/3
✓ Branch 126 → 127 taken 2 times.
✓ Branch 126 → 128 taken 8 times.
✓ Branch 127 → 128 taken 2 times.
10 if (!imapCredential.isEmpty()) imapNames.append(imapCredential);
964
3/3
✓ Branch 129 → 130 taken 2 times.
✓ Branch 129 → 131 taken 8 times.
✓ Branch 130 → 131 taken 2 times.
10 if (!smtpCredential.isEmpty()) smtpNames.append(smtpCredential);
965
2/2
✓ Branch 134 → 135 taken 10 times.
✓ Branch 135 → 136 taken 10 times.
10 const QString legacyName = row.value(QStringLiteral("legacyKeyringName")).toString();
966
1/4
✗ Branch 140 → 141 not taken.
✓ Branch 140 → 143 taken 10 times.
✗ Branch 141 → 142 not taken.
✗ Branch 142 → 143 not taken.
10 if (!legacyName.isEmpty()) { imapNames.append(legacyName); smtpNames.append(legacyName); }
967
1/1
✓ Branch 143 → 144 taken 10 times.
10 imapNames.removeDuplicates();
968
1/1
✓ Branch 144 → 145 taken 10 times.
10 smtpNames.removeDuplicates();
969 // S106-198: the names come from the file being deleted. A name that a
970 // remaining account file still uses is another account's secret and
971 // stays in the keyring.
972
1/1
✓ Branch 145 → 146 taken 10 times.
10 const QSet<QString> stillUsed = credentialNamesInUse(dir);
973 10 QList<CredentialStore::SecretKey> keys;
974
4/4
✓ Branch 146 → 147 taken 10 times.
✓ Branch 147 → 148 taken 10 times.
✓ Branch 162 → 149 taken 12 times.
✓ Branch 162 → 163 taken 10 times.
22 for (const auto &name : imapNames)
975
2/2
✓ Branch 151 → 152 taken 11 times.
✓ Branch 151 → 160 taken 1 time.
23 if (!stillUsed.contains(name)) keys.append({QStringLiteral("imap"), name});
976
4/4
✓ Branch 163 → 164 taken 10 times.
✓ Branch 164 → 165 taken 10 times.
✓ Branch 179 → 166 taken 12 times.
✓ Branch 179 → 180 taken 10 times.
22 for (const auto &name : smtpNames)
977
1/2
✓ Branch 168 → 169 taken 12 times.
✗ Branch 168 → 177 not taken.
24 if (!stillUsed.contains(name)) keys.append({QStringLiteral("smtp"), name});
978
2/2
✓ Branch 183 → 184 taken 10 times.
✓ Branch 184 → 185 taken 10 times.
20 const QString sieveCredential = row.value(QStringLiteral("sieve")).toObject()
979
2/2
✓ Branch 188 → 189 taken 10 times.
✓ Branch 189 → 190 taken 10 times.
10 .value(QStringLiteral("credentialId")).toString();
980
5/6
✓ Branch 198 → 199 taken 1 time.
✓ Branch 198 → 202 taken 9 times.
✓ Branch 200 → 201 taken 1 time.
✗ Branch 200 → 202 not taken.
✓ Branch 203 → 204 taken 1 time.
✓ Branch 203 → 212 taken 9 times.
10 if (!sieveCredential.isEmpty() && !stillUsed.contains(sieveCredential))
981 1 keys.append({QStringLiteral("sieve"), sieveCredential});
982 // D-8: the local rule version history leaves with the account. The
983 // id is a directory name only when it is one plain path component.
984
2/2
✓ Branch 213 → 214 taken 8 times.
✓ Branch 213 → 231 taken 2 times.
10 if (isSafeId(id))
985
5/5
✓ Branch 214 → 215 taken 8 times.
✓ Branch 218 → 219 taken 8 times.
✓ Branch 219 → 220 taken 8 times.
✓ Branch 220 → 221 taken 8 times.
✓ Branch 221 → 222 taken 8 times.
24 QDir(QDir::cleanPath(QDir(configDir).absoluteFilePath(QStringLiteral("../sieve/") + id)))
986
1/1
✓ Branch 222 → 223 taken 8 times.
8 .removeRecursively();
987
2/2
✓ Branch 232 → 233 taken 1 time.
✓ Branch 232 → 244 taken 1 time.
2 else if (!id.isEmpty())
988
5/5
✓ Branch 233 → 234 taken 1 time.
✓ Branch 237 → 238 taken 1 time.
✓ Branch 238 → 239 taken 1 time.
✓ Branch 242 → 235 taken 1 time.
✓ Branch 242 → 243 taken 1 time.
2 qCWarning(lcAccountConfig) << "Removed account had an unsafe id; its rule history was left alone";
989
2/2
✓ Branch 245 → 246 taken 10 times.
✓ Branch 247 → 248 taken 10 times.
10 CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), keys,
990 20 [accountName](bool success) {
991
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 15 taken 4 times.
8 if (!success)
992
4/4
✓ Branch 3 → 4 taken 4 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 13 → 5 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
8 qCWarning(lcAccountConfig)
993
2/2
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
4 << "Account removed, but keyring cleanup failed for" << accountName;
994 8 });
995
4/4
✓ Branch 250 → 251 taken 10 times.
✓ Branch 254 → 255 taken 10 times.
✓ Branch 262 → 252 taken 10 times.
✓ Branch 262 → 263 taken 10 times.
20 qCInfo(lcAccountConfig)
996
4/4
✓ Branch 255 → 256 taken 10 times.
✓ Branch 256 → 257 taken 10 times.
✓ Branch 257 → 258 taken 10 times.
✓ Branch 258 → 259 taken 10 times.
10 << "Deleted account:" << accountName << "file:" << fileName;
997 10 return true;
998 10 } else {
999 ✗ qCWarning(lcAccountConfig) << "Failed to delete file:" << path; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; deletion result remains measured.
1000 ✗ return false;
1001 }
1002 }
1003 52 }
1004
1005
4/4
✓ Branch 314 → 315 taken 2 times.
✓ Branch 318 → 319 taken 2 times.
✓ Branch 324 → 316 taken 2 times.
✓ Branch 324 → 325 taken 2 times.
4 qCWarning(lcAccountConfig)
1006
2/2
✓ Branch 319 → 320 taken 2 times.
✓ Branch 320 → 321 taken 2 times.
2 << "No config file found for account:" << accountName;
1007 2 return false;
1008 68 }
1009
1010 119 QString AccountConfigLoader::slugify(const QString &name) {
1011 // F-74 (SPRINT-104): every stripped non-ASCII code point is percent-
1012 // hex-encoded into the slug instead of collapsing onto one shared
1013 // fallback — two accounts named 北京 and 上海 (or any other purely
1014 // non-Latin names) previously both became "account". The slug is still
1015 // not injective (collapsed separators, folded accents, the 50-character
1016 // cut); fileStem() disambiguates the file name (S106-204).
1017 119 QString result;
1018
1/1
✓ Branch 3 → 4 taken 119 times.
119 result.reserve(name.size());
1019
1020 // Normalize: lowercase, replace non-ASCII and special chars
1021
2/2
✓ Branch 4 → 5 taken 119 times.
✓ Branch 5 → 6 taken 119 times.
119 auto normalized = name.toLower().normalized(QString::NormalizationForm_KD);
1022
1023
4/4
✓ Branch 7 → 8 taken 119 times.
✓ Branch 8 → 9 taken 119 times.
✓ Branch 61 → 10 taken 1354 times.
✓ Branch 61 → 62 taken 119 times.
1473 for (const auto &ch : normalized) {
1024
6/6
✓ Branch 11 → 12 taken 1276 times.
✓ Branch 11 → 15 taken 78 times.
✓ Branch 13 → 14 taken 1252 times.
✓ Branch 13 → 15 taken 24 times.
✓ Branch 16 → 17 taken 1252 times.
✓ Branch 16 → 18 taken 102 times.
1354 if (ch.isLetterOrNumber() && ch.unicode() < 128) {
1025
1/1
✓ Branch 17 → 60 taken 1252 times.
1252 result.append(ch);
1026
8/8
✓ Branch 20 → 21 taken 56 times.
✓ Branch 20 → 27 taken 46 times.
✓ Branch 23 → 24 taken 49 times.
✓ Branch 23 → 27 taken 7 times.
✓ Branch 26 → 27 taken 5 times.
✓ Branch 26 → 28 taken 44 times.
✓ Branch 29 → 30 taken 58 times.
✓ Branch 29 → 42 taken 44 times.
102 } else if (ch == ' ' || ch == '-' || ch == '_') {
1027 // Collapse consecutive separators
1028
7/7
✓ Branch 31 → 32 taken 53 times.
✓ Branch 31 → 37 taken 5 times.
✓ Branch 33 → 34 taken 53 times.
✓ Branch 35 → 36 taken 45 times.
✓ Branch 35 → 37 taken 8 times.
✓ Branch 38 → 39 taken 45 times.
✓ Branch 38 → 60 taken 13 times.
58 if (!result.isEmpty() && result.back() != '_') {
1029
1/1
✓ Branch 40 → 41 taken 45 times.
45 result.append('_');
1030 }
1031
6/6
✓ Branch 43 → 44 taken 30 times.
✓ Branch 43 → 47 taken 14 times.
✓ Branch 45 → 46 taken 24 times.
✓ Branch 45 → 47 taken 6 times.
✓ Branch 48 → 49 taken 24 times.
✓ Branch 48 → 60 taken 20 times.
74 } else if (ch.unicode() > 127 &&
1032 30 ch.category() != QChar::Mark_NonSpacing) {
1033 // Accent marks stripped by the decomposition above stay stripped
1034 // (Büro keeps its "buro" slug); real letters of other scripts are
1035 // percent-hex encoded so non-Latin names stay distinct.
1036
1/1
✓ Branch 50 → 51 taken 24 times.
24 const QString hex = QString::number(ch.unicode(), 16);
1037
1/1
✓ Branch 52 → 53 taken 24 times.
24 result.append('%');
1038
3/3
✓ Branch 55 → 56 taken 84 times.
✓ Branch 57 → 55 taken 84 times.
✓ Branch 57 → 58 taken 24 times.
108 for (const auto &nibble : hex) result.append(nibble);
1039 24 }
1040 // Skip remaining ASCII special characters and combining marks
1041 }
1042
1043 // Remove trailing underscore
1044
2/3
✓ Branch 65 → 66 taken 119 times.
✗ Branch 66 → 63 not taken.
✓ Branch 66 → 67 taken 119 times.
119 while (result.endsWith('_')) {
1045 ✗ result.chop(1);
1046 }
1047
1048 // Limit length
1049
2/2
✓ Branch 68 → 69 taken 7 times.
✓ Branch 68 → 75 taken 112 times.
119 if (result.size() > 50) {
1050
1/1
✓ Branch 69 → 70 taken 7 times.
7 result.truncate(50);
1051 // Don't end on underscore after truncation
1052
3/3
✓ Branch 73 → 74 taken 8 times.
✓ Branch 74 → 71 taken 1 time.
✓ Branch 74 → 75 taken 7 times.
8 while (result.endsWith('_')) {
1053
1/1
✓ Branch 71 → 72 taken 1 time.
1 result.chop(1);
1054 }
1055 }
1056
1057 // Fallback for an empty result (an ASCII-only special-character name)
1058
2/2
✓ Branch 76 → 77 taken 5 times.
✓ Branch 76 → 78 taken 114 times.
119 if (result.isEmpty()) {
1059
1/1
✓ Branch 77 → 78 taken 5 times.
5 result = "account";
1060 }
1061
1062 119 return result;
1063 119 }
1064
1065 // SEC-01/02: Async keyring password resolution
1066 22 bool AccountConfigLoader::finishCredentialMigration(AccountConfig &account, QObject *owner) {
1067 22 const auto migration = account.deferredCredentialMigration;
1068
2/2
✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 6 taken 9 times.
22 if (!migration) return true;
1069
6/6
✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 10 taken 3 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 5 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 5 times.
9 if (!migration->imapStored || !migration->smtpStored) return false;
1070
2/8
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 5 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 20 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 5 times.
5 if (!owner && !account.legacyKeyringName.isEmpty() && account.legacyKeyringName != account.id)
1071 ✗ return false;
1072
1/1
✓ Branch 24 → 25 taken 5 times.
5 QFile source(migration->path);
1073
3/3
✓ Branch 26 → 27 taken 5 times.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 4 times.
5 if (!source.open(QIODevice::ReadOnly)) return false;
1074
1/1
✓ Branch 29 → 30 taken 4 times.
4 const auto bytes = source.readAll();
1075
3/3
✓ Branch 32 → 33 taken 4 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 3 times.
4 if (QCryptographicHash::hash(bytes, QCryptographicHash::Sha256) != migration->digest)
1076 1 return false;
1077
2/2
✓ Branch 37 → 38 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
3 auto root = QJsonDocument::fromJson(bytes).object();
1078
4/5
✓ Branch 40 → 41 taken 3 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 42 → 43 taken 3 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 3 times.
3 if (root.value("id").toString() != account.id) return false;
1079
2/2
✓ Branch 66 → 51 taken 6 times.
✓ Branch 66 → 67 taken 3 times.
9 for (const auto *protocol : {"imap", "smtp"}) {
1080
3/3
✓ Branch 51 → 52 taken 6 times.
✓ Branch 52 → 53 taken 6 times.
✓ Branch 53 → 54 taken 6 times.
6 auto values = root.value(protocol).toObject();
1081
2/2
✓ Branch 56 → 57 taken 6 times.
✓ Branch 57 → 58 taken 6 times.
6 values.remove("password");
1082
3/3
✓ Branch 59 → 60 taken 6 times.
✓ Branch 60 → 61 taken 6 times.
✓ Branch 61 → 62 taken 6 times.
6 root.insert(protocol, values);
1083 6 }
1084
2/2
✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
3 root.remove("legacyKeyringName");
1085
4/5
✓ Branch 70 → 71 taken 3 times.
✓ Branch 71 → 72 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 3 times.
3 if (!writeAccountBytes(migration->path, QJsonDocument(root).toJson(QJsonDocument::Indented)))
1086 ✗ return false;
1087 3 const auto legacy = account.legacyKeyringName;
1088 3 account.legacyKeyringName.clear();
1089 3 account.deferredCredentialMigration.reset();
1090
4/8
✓ Branch 81 → 82 taken 3 times.
✗ Branch 81 → 87 not taken.
✓ Branch 83 → 84 taken 3 times.
✗ Branch 83 → 87 not taken.
✓ Branch 85 → 86 taken 3 times.
✗ Branch 85 → 87 not taken.
✓ Branch 88 → 89 taken 3 times.
✗ Branch 88 → 102 not taken.
3 if (owner && !legacy.isEmpty() && legacy != account.id)
1091
3/5
✓ Branch 95 → 96 taken 3 times.
✓ Branch 98 → 99 taken 6 times.
✓ Branch 98 → 100 taken 3 times.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
12 CredentialStore::deletePasswordsAsync(owner, {{"imap", legacy}, {"smtp", legacy}}, {});
1092 3 return true;
1093 25 }
1094
1095 31 void AccountConfigLoader::resolvePasswords(
1096 std::vector<AccountConfig> &accounts, CredentialStore *store,
1097 std::function<void()> callback) {
1098
3/6
✓ Branch 2 → 3 taken 31 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 31 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 12 taken 31 times.
31 if (!store || accounts.empty()) {
1099 ✗ if (callback) callback();
1100 1 return;
1101 }
1102 31 int pending = 0;
1103
2/2
✓ Branch 33 → 14 taken 31 times.
✓ Branch 33 → 34 taken 31 times.
62 for (const auto &account : accounts) {
1104
6/6
✓ Branch 16 → 17 taken 7 times.
✓ Branch 16 → 19 taken 24 times.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 18 → 20 taken 1 time.
✓ Branch 21 → 22 taken 30 times.
✓ Branch 21 → 23 taken 1 time.
31 if (account.imap.password.isEmpty() || account.deferredCredentialMigration) ++pending;
1105
6/6
✓ Branch 24 → 25 taken 7 times.
✓ Branch 24 → 27 taken 24 times.
✓ Branch 26 → 27 taken 6 times.
✓ Branch 26 → 28 taken 1 time.
✓ Branch 29 → 30 taken 30 times.
✓ Branch 29 → 31 taken 1 time.
31 if (account.smtp.password.isEmpty() || account.deferredCredentialMigration) ++pending;
1106 }
1107
4/5
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 39 taken 30 times.
✓ Branch 36 → 37 taken 1 time.
✗ Branch 36 → 38 not taken.
✓ Branch 37 → 38 taken 1 time.
31 if (!pending) { if (callback) callback(); return; }
1108
1/1
✓ Branch 39 → 40 taken 30 times.
30 auto remaining = std::make_shared<int>(pending);
1109
6/9
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 7 taken 22 times.
✓ Branch 5 → 6 taken 22 times.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 22 times.
✓ Branch 41 → 42 taken 30 times.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
74 const auto done = [remaining, callback] { if (--*remaining == 0 && callback) callback(); };
1110
2/2
✓ Branch 99 → 44 taken 30 times.
✓ Branch 99 → 100 taken 30 times.
60 for (auto &account : accounts) {
1111
2/2
✓ Branch 96 → 47 taken 60 times.
✓ Branch 96 → 97 taken 30 times.
90 for (const auto *protocol : {"imap", "smtp"}) {
1112
1/1
✓ Branch 48 → 49 taken 60 times.
60 const QString service = QString::fromLatin1(protocol);
1113
2/2
✓ Branch 50 → 51 taken 30 times.
✓ Branch 50 → 52 taken 30 times.
60 auto *password = service == "imap" ? &account.imap.password : &account.smtp.password;
1114 60 const auto migration = account.deferredCredentialMigration;
1115
4/6
✓ Branch 55 → 56 taken 12 times.
✓ Branch 55 → 59 taken 48 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 59 taken 12 times.
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 60 times.
60 if (!password->isEmpty() && !migration) continue;
1116
2/2
✓ Branch 63 → 64 taken 30 times.
✓ Branch 63 → 65 taken 30 times.
60 const QString identity = service == "imap" ? imapCredentialName(account)
1117 60 : smtpCredentialName(account);
1118 37 const auto stored = [migration, service] {
1119
2/2
✓ Branch 3 → 4 taken 26 times.
✓ Branch 3 → 5 taken 11 times.
37 if (!migration) return;
1120
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 9 taken 5 times.
11 if (service == "imap") migration->imapStored = true;
1121 5 else migration->smtpStored = true;
1122 60 };
1123 11 const auto write = [store, identity, service, stored, done](
1124 const QByteArray &value, const QString &legacyCleanup = {}) {
1125
1/1
✓ Branch 8 → 9 taken 11 times.
11 store->writePassword(service, identity, value,
1126
2/10
✓ Branch 5 → 6 taken 11 times.
✓ Branch 7 → 8 taken 11 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
22 [store, identity, service, stored, done, legacyCleanup](bool success, const QString &) {
1127
2/2
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 3 times.
11 if (success) stored();
1128
4/8
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 10 taken 3 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 10 taken 8 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 18 taken 11 times.
11 if (success && !legacyCleanup.isEmpty() && legacyCleanup != identity)
1129 ✗ store->deletePassword(service, legacyCleanup, [done](bool) { done(); });
1130 11 else done();
1131 11 });
1132
1/7
✓ Branch 71 → 72 taken 60 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
71 };
1133
1/1
✓ Branch 79 → 80 taken 60 times.
60 store->readPassword(service, identity,
1134
3/13
✓ Branch 76 → 77 taken 60 times.
✓ Branch 77 → 78 taken 60 times.
✓ Branch 78 → 79 taken 60 times.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 130 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
✗ Branch 134 → 135 not taken.
✗ Branch 134 → 136 not taken.
120 [store, password, migration, legacy = account.legacyKeyringName,
1135 service, stored, write, done](bool success, const QByteArray &value) {
1136
5/6
✓ Branch 2 → 3 taken 29 times.
✓ Branch 2 → 6 taken 15 times.
✓ Branch 4 → 5 taken 29 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 29 times.
✓ Branch 7 → 12 taken 15 times.
44 if (success && !value.isEmpty()) {
1137 29 *password = value;
1138 29 stored();
1139 29 done();
1140 29 return;
1141 }
1142
3/3
✓ Branch 13 → 14 taken 9 times.
✓ Branch 13 → 18 taken 6 times.
✓ Branch 15 → 16 taken 9 times.
15 if (!password->isEmpty()) { write(*password); return; }
1143
2/2
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 22 taken 2 times.
6 if (legacy.isEmpty()) { done(); return; }
1144
1/1
✓ Branch 27 → 28 taken 2 times.
2 store->readPassword(service, legacy,
1145
3/9
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
4 [password, migration, legacy, write, done](bool found, const QByteArray &value) {
1146
3/6
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 2 times.
2 if (!found || value.isEmpty()) { done(); return; }
1147 2 *password = value;
1148 // The deferred path retains fallback keys until its file commit.
1149
2/3
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 2 times.
2 write(value, migration ? QString{} : legacy);
1150 });
1151 });
1152 60 }
1153 }
1154 30 }
1155