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: 67.3% 331 / 0 / 492
Functions: 68.8% 33 / 0 / 48
Branches: 51.2% 458 / 0 / 895

presentation/QuickAccountSettings.cpp
Line Branch Exec Source
1 #include "QuickAccountSettings.h"
2 #include "QuickCalendarSync.h"
3
4 #include "ComposeDraftStore.h"
5 #include "DavAccountSettings.h"
6 #include "QuickPimBackend.h"
7 #include "QuickPreferences.h"
8 #include "WorkspaceState.h"
9 #include "app/core/AccountSession.h"
10 #include "app/core/CalendarSyncCoordinator.h"
11 #include "data/CalendarStore.h"
12 #include "app/core/ContactSyncCoordinator.h"
13 #include "app/core/MailSessionCore.h"
14 #include "controller/UndoManager.h"
15 #include "data/AccountConfig.h"
16 #include "data/CredentialStore.h"
17 #include "data/DavCredentials.h"
18 #include "data/MailCache.h"
19 #include "quick/ExternalWhitelistModel.h"
20 #include "ui/ExternalContentPolicy.h"
21 #include <QDir>
22 #include <QLoggingCategory>
23 #include <QPointer>
24 #include <QSettings>
25 #include <QTimer>
26 #include <QUuid>
27
28 ✗ Q_LOGGING_CATEGORY(lcAccountSettings, "mailjd.quick.accountsettings")
29
30 795 QuickAccountSettings::QuickAccountSettings(MailSessionCore &session, WorkspaceState &workspace,
31 QuickPreferences &preferences, QuickPimBackend &pim,
32 const QString &profileDirectory, Deps deps,
33 795 QObject *parent)
34 795 : QObject(parent), m_session(session), m_workspace(workspace), m_preferences(preferences),
35 795 m_pim(pim), m_profileDirectory(profileDirectory), m_deps(std::move(deps)),
36
4/7
✓ Branch 5 → 6 taken 795 times.
✓ Branch 7 → 8 taken 795 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 795 times.
✓ Branch 16 → 17 taken 795 times.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
1590 m_whitelist(new ExternalWhitelistModel(*session.cache(), this)), m_serviceCheck(session.accounts(), this),
37
5/5
✓ Branch 10 → 11 taken 795 times.
✓ Branch 11 → 12 taken 795 times.
✓ Branch 12 → 13 taken 795 times.
✓ Branch 13 → 14 taken 795 times.
✓ Branch 18 → 19 taken 795 times.
2385 m_setup(this, session.accounts(), this) {
38
1/1
✓ Branch 19 → 20 taken 795 times.
795 connect(this, &QuickAccountSettings::davSettingsChanged, this, &QuickAccountSettings::accountOverviewChanged);
39
1/1
✓ Branch 22 → 23 taken 795 times.
795 connect(session.accounts(), &AccountSession::availableAccountsChanged, this,
40 795 &QuickAccountSettings::accountOverviewChanged);
41
1/1
✓ Branch 25 → 26 taken 795 times.
795 connect(session.accounts(), &AccountSession::configurationChanged, this,
42 795 &QuickAccountSettings::accountOverviewChanged);
43
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 32 taken 795 times.
795 if (auto *sync = m_pim.sync())
44 ✗ connect(sync, &QuickCalendarSync::changed, this, &QuickAccountSettings::accountOverviewChanged);
45 // D-21: a calendar with appointments not uploaded yet is not removed.
46 795 m_davDiscovery.setPendingCounter([&session](const QString &account, const QString &path, bool calendar) {
47 ✗ if (!calendar || !session.calendarStore()) return 0;
48 ✗ return int(session.calendarStore()->outboxUids(account, path).size());
49 });
50 795 }
51
52 2474 QVariantList QuickAccountSettings::accountOverview() const {
53 2474 QList<AccountOverview::Mail> mails;
54
4/4
✓ Branch 3 → 4 taken 2474 times.
✓ Branch 4 → 5 taken 2474 times.
✓ Branch 73 → 6 taken 2503 times.
✓ Branch 73 → 74 taken 2474 times.
4977 for (const auto &value : configuredAccounts()) {
55
1/1
✓ Branch 7 → 8 taken 2503 times.
2503 const auto row = value.toMap();
56
2/2
✓ Branch 12 → 13 taken 2503 times.
✓ Branch 18 → 19 taken 2503 times.
12515 mails.append({row.value(QStringLiteral("id")).toString(), row.value(QStringLiteral("name")).toString(),
57
2/2
✓ Branch 24 → 25 taken 2503 times.
✓ Branch 30 → 31 taken 2503 times.
7509 row.value(QStringLiteral("email")).toString(), row.value(QStringLiteral("imapHost")).toString(),
58
2/2
✓ Branch 36 → 37 taken 2503 times.
✓ Branch 42 → 43 taken 2503 times.
7509 row.value(QStringLiteral("imapUsername")).toString(), row.value(QStringLiteral("sieveMode")).toString()});
59 4977 }
60 2474 QHash<QString, QVariantMap> syncStates;
61
1/2
✓ Branch 76 → 77 taken 2474 times.
✗ Branch 76 → 104 not taken.
2474 if (m_pim.sync())
62
5/5
✓ Branch 78 → 79 taken 2474 times.
✓ Branch 79 → 80 taken 2474 times.
✓ Branch 80 → 81 taken 2474 times.
✓ Branch 101 → 82 taken 624 times.
✓ Branch 101 → 102 taken 2474 times.
3098 for (const auto &value : m_pim.sync()->accounts())
63
5/5
✓ Branch 83 → 84 taken 624 times.
✓ Branch 84 → 85 taken 624 times.
✓ Branch 89 → 90 taken 624 times.
✓ Branch 90 → 91 taken 624 times.
✓ Branch 91 → 92 taken 624 times.
3722 syncStates.insert(value.toMap().value(QStringLiteral("accountId")).toString(), value.toMap());
64
1/1
✓ Branch 104 → 105 taken 2474 times.
2474 const auto configs = DavAccountSettings::calendarConfigs();
65 2474 QList<AccountOverview::Dav> davs;
66
5/5
✓ Branch 105 → 106 taken 2474 times.
✓ Branch 106 → 107 taken 2474 times.
✓ Branch 107 → 108 taken 2474 times.
✓ Branch 166 → 109 taken 624 times.
✓ Branch 166 → 167 taken 2474 times.
3098 for (const auto &account : DavAccountSettings::accounts()) {
67 624 AccountOverview::Dav dav;
68 624 dav.id = account.id;
69 624 dav.serverUrl = account.serverUrl;
70 624 dav.username = account.username;
71 624 dav.mailAccountId = account.mailAccountId;
72 624 dav.books = int(account.selectedBooks.size());
73
2/2
✓ Branch 124 → 117 taken 72 times.
✓ Branch 124 → 125 taken 624 times.
696 for (const auto &config : configs)
74
1/2
✓ Branch 119 → 120 taken 72 times.
✗ Branch 119 → 122 not taken.
72 if (config.accountId == account.id) dav.calendars = int(config.selectedCalendars.size());
75 624 const auto state = syncStates.value(account.id);
76
2/2
✓ Branch 130 → 131 taken 624 times.
✓ Branch 131 → 132 taken 624 times.
1248 dav.state = state.value(QStringLiteral("state")).toString();
77
2/2
✓ Branch 142 → 143 taken 624 times.
✓ Branch 143 → 144 taken 624 times.
1248 dav.error = state.value(QStringLiteral("error")).toString();
78
2/2
✓ Branch 154 → 155 taken 624 times.
✓ Branch 155 → 156 taken 624 times.
1248 dav.needsCredentials = state.value(QStringLiteral("needsCredentials")).toBool();
79 624 dav.requiresAuthorization = account.requiresLocalAuthorization;
80
1/1
✓ Branch 160 → 161 taken 624 times.
624 dav.fresh = m_serverHints.freshCount(account.id);
81
1/1
✓ Branch 161 → 162 taken 624 times.
624 davs.append(dav);
82 3098 }
83
3/4
✓ Branch 169 → 170 taken 2474 times.
✗ Branch 169 → 171 not taken.
✓ Branch 170 → 172 taken 2474 times.
✓ Branch 172 → 173 taken 2474 times.
4948 return AccountOverview::build(mails, davs, m_deps.activeState ? m_deps.activeState() : AccountOverview::Active{});
84 17492 }
85
86 ✗ bool QuickAccountSettings::linkDavAccount(const QString &davAccountId, const QString &mailAccountId) {
87 ✗ if (!DavAccountSettings::setMailAccountLink(davAccountId, mailAccountId)) return false;
88 ✗ emit davSettingsChanged();
89 ✗ return true;
90 }
91
92 // ─── D3 DAV group: carddav/accounts + caldav/syncIntervalMin ───
93 // The CalendarSyncCoordinator reads the same QSettings keys, so these are
94 // live settings, not sync-payload metadata.
95
96 namespace {
97 using DavAccountRow = DavAccountSettings::Account;
98 21 QList<DavAccountRow> readDavAccounts() { return DavAccountSettings::accounts(); }
99 }
100
101 // Account connection-test secrets, settings sync and reading-mode/DAV settings fan-out.
102 // Formerly QuickMailBackend::wireSettingsSync(); called by the backend at the
103 // same constructor position (Sprint 91 rule 4).
104 795 void QuickAccountSettings::connectSettingsSync() {
105 795 MailSessionCore &session = m_session;
106 795 WorkspaceState &workspace = m_workspace;
107
1/1
✓ Branch 7 → 8 taken 795 times.
795 m_connectionTest.setSecretReader(
108
2/2
✓ Branch 2 → 3 taken 795 times.
✓ Branch 6 → 7 taken 795 times.
2385 [directory = QDir(m_profileDirectory).filePath(QStringLiteral("accounts"))](
109 QObject *owner, const QString &accountId,
110 std::function<void(bool, const QByteArray &)> callback) {
111 ✗ QString identity;
112 ✗ for (const auto &account : AccountConfigLoader::loadAll(
113 ✗ directory, AccountConfigLoader::LoadMode::MetadataOnly)) {
114 ✗ if (account.id == accountId) {
115 ✗ identity = AccountConfigLoader::imapCredentialName(account);
116 ✗ break;
117 }
118 ✗ }
119 ✗ if (identity.isEmpty()) {
120 ✗ QTimer::singleShot(0, owner, [callback = std::move(callback)] {
121 ✗ callback(false, {});
122 ✗ });
123 ✗ return;
124 }
125 ✗ auto *store = new CredentialStore(owner);
126 ✗ store->readPassword(QStringLiteral("imap"), identity,
127 ✗ [store, callback = std::move(callback)](bool success, const QByteArray &password) {
128 ✗ store->deleteLater();
129 ✗ callback(success, password);
130 ✗ });
131 ✗ });
132
2/2
✓ Branch 15 → 16 taken 794 times.
✓ Branch 15 → 27 taken 1 time.
795 if (auto *sync = session.settingsSync()) {
133
1/1
✓ Branch 16 → 17 taken 794 times.
794 connect(this, &QuickAccountSettings::davSettingsChanged, sync, &SettingsSyncSession::localSettingsChanged);
134
1/1
✓ Branch 18 → 19 taken 794 times.
794 connect(this, &QuickAccountSettings::defaultReadingModeChanged, sync, &SettingsSyncSession::localSettingsChanged);
135
1/1
✓ Branch 20 → 21 taken 794 times.
794 connect(&m_pim, &QuickPimBackend::calendarChoicesChanged, sync, &SettingsSyncSession::localSettingsChanged);
136
1/1
✓ Branch 22 → 23 taken 794 times.
794 connect(&m_preferences, &QuickPreferences::changed, sync, &SettingsSyncSession::localSettingsChanged);
137
1/1
✓ Branch 24 → 25 taken 794 times.
794 connect(sync, &SettingsSyncSession::settingsApplied, this, [this](const QStringList &) {
138 ✗ emit m_preferences.changed();
139 ✗ emit defaultReadingModeChanged();
140 ✗ emit davSettingsChanged();
141 ✗ m_pim.calendarModel().reload();
142 ✗ m_pim.taskModel().reload();
143 ✗ m_deps.loadLocalFolders();
144 ✗ });
145 }
146
1/1
✓ Branch 27 → 28 taken 795 times.
795 connect(this, &QuickAccountSettings::defaultReadingModeChanged, &workspace, [&workspace] {
147
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
2 workspace.setConfiguredReadingMode(WorkspaceState::configuredReadingMode());
148 2 });
149
1/1
✓ Branch 29 → 30 taken 795 times.
795 connect(this, &QuickAccountSettings::davSettingsChanged, &m_davDiscovery, &DavCollectionDiscovery::invalidateConfiguration);
150
1/1
✓ Branch 31 → 32 taken 795 times.
795 connect(this, &QuickAccountSettings::davSettingsChanged, &m_preferences, &QuickPreferences::contactIntervalChanged);
151 795 }
152
153 // DAV discovery results re-arm the calendar sync.
154 // Formerly QuickMailBackend::wireDavDiscovery(); same constructor position.
155 795 void QuickAccountSettings::connectDavDiscovery() {
156 2 const auto applied = [this](const QString &id) {
157 2 emit davSettingsChanged();
158
4/5
✓ Branch 3 → 4 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 22 → 7 taken 2 times.
✗ Branch 22 → 23 not taken.
2 for (const auto &config : DavAccountSettings::calendarConfigs())
159
4/8
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 15 not taken.
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 15 not taken.
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 2 times.
✗ Branch 16 → 20 not taken.
2 if (config.accountId == id && !config.selectedCalendars.isEmpty() && m_session.calendarSync()) {
160
1/1
✓ Branch 18 → 19 taken 2 times.
2 QTimer::singleShot(0, m_session.calendarSync(), &CalendarSyncCoordinator::triggerSync);
161 2 break;
162 2 }
163 2 };
164
1/1
✓ Branch 2 → 3 taken 795 times.
795 connect(&m_davDiscovery, &DavCollectionDiscovery::applied, this, applied);
165 // S100.4.8: a collection added from a sidebar hint is an applied selection.
166
1/1
✓ Branch 4 → 5 taken 795 times.
795 connect(&m_serverHints, &DavServerHints::added, this, applied);
167
1/1
✓ Branch 6 → 7 taken 795 times.
795 connect(&m_davDiscovery, &DavCollectionDiscovery::stateChanged, &m_serverHints, &DavServerHints::refresh);
168
1/1
✓ Branch 8 → 9 taken 795 times.
795 connect(this, &QuickAccountSettings::davSettingsChanged, &m_serverHints, &DavServerHints::refresh);
169
1/1
✓ Branch 10 → 11 taken 795 times.
795 connect(&m_serverHints, &DavServerHints::changed, this, &QuickAccountSettings::accountOverviewChanged);
170 // D-22: every calendar sync already lists the account's calendars.
171
2/2
✓ Branch 13 → 14 taken 794 times.
✓ Branch 13 → 17 taken 1 time.
795 if (auto *calendars = m_session.calendarSync())
172 794 connect(calendars, &CalendarSyncCoordinator::collectionsDiscovered, this,
173
1/1
✓ Branch 14 → 15 taken 794 times.
1588 [this](const QString &accountId, const QList<CalendarInfo> &found) {
174 ✗ QList<DavCollectionDiscovery::Item> items;
175 ✗ for (const auto &calendar : found)
176 ✗ items.append({calendar.path, calendar.displayName, calendar.color, calendar.supportedComponents,
177 ✗ calendar.readOnly, calendar.push.capability == DavPushCapability::Supported});
178 ✗ m_serverHints.recordCalendars(accountId, items);
179 ✗ });
180 795 }
181
182 namespace {
183 // Sieve fields of the account form (S95.1.3). A form without them (older
184 // callers) keeps the stored rule settings instead of resetting them.
185 11 SieveConfig sieveConfigFrom(const QVariantMap &fields, const std::optional<AccountConfig> &stored) {
186
3/3
✓ Branch 5 → 6 taken 11 times.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 8 → 30 taken 2 times.
11 if (!fields.contains(QStringLiteral("sieveMode")))
187
8/8
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 13 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
✓ Branch 23 → 25 taken 2 times.
✓ Branch 25 → 26 taken 7 times.
✓ Branch 25 → 27 taken 2 times.
✓ Branch 27 → 28 taken 7 times.
✓ Branch 27 → 29 taken 2 times.
30 return stored ? stored->sieve : SieveConfig{};
188
1/1
✓ Branch 30 → 31 taken 2 times.
2 SieveConfig sieve;
189
2/2
✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
4 sieve.mode = fields.value(QStringLiteral("sieveMode")).toString();
190
3/3
✓ Branch 47 → 48 taken 2 times.
✓ Branch 48 → 49 taken 2 times.
✓ Branch 49 → 50 taken 2 times.
4 sieve.host = fields.value(QStringLiteral("sieveHost")).toString().trimmed();
191 // S106-250: the same whole-number range check as the mail ports; out of
192 // range reads as 0, which validate() refuses for a manual rule server
193 // (65600 used to wrap to 64).
194 2 bool portOk = false;
195
2/2
✓ Branch 61 → 62 taken 2 times.
✓ Branch 62 → 63 taken 2 times.
4 const double port = fields.value(QStringLiteral("sievePort"), 4190).toDouble(&portOk);
196
5/10
✓ Branch 67 → 68 taken 2 times.
✗ Branch 67 → 74 not taken.
✓ Branch 69 → 70 taken 2 times.
✗ Branch 69 → 74 not taken.
✓ Branch 70 → 71 taken 2 times.
✗ Branch 70 → 74 not taken.
✓ Branch 71 → 72 taken 2 times.
✗ Branch 71 → 74 not taken.
✓ Branch 72 → 73 taken 2 times.
✗ Branch 72 → 74 not taken.
2 sieve.port = portOk && std::isfinite(port) && port >= 1 && port <= 65535 && std::floor(port) == port
197 ? static_cast<quint16>(port) : 0;
198
2/2
✓ Branch 82 → 83 taken 2 times.
✓ Branch 83 → 84 taken 2 times.
4 sieve.security = fields.value(QStringLiteral("sieveSecurity"), QStringLiteral("starttls")).toString();
199
2/2
✓ Branch 99 → 100 taken 2 times.
✓ Branch 100 → 101 taken 2 times.
4 sieve.login = fields.value(QStringLiteral("sieveLogin"), QStringLiteral("imap")).toString();
200
3/3
✓ Branch 113 → 114 taken 2 times.
✓ Branch 114 → 115 taken 2 times.
✓ Branch 115 → 116 taken 2 times.
4 sieve.username = fields.value(QStringLiteral("sieveUsername")).toString().trimmed();
201
3/3
✓ Branch 127 → 128 taken 2 times.
✓ Branch 128 → 129 taken 2 times.
✓ Branch 129 → 130 taken 2 times.
4 sieve.password = fields.value(QStringLiteral("sievePassword")).toString().toUtf8();
202
3/6
✓ Branch 138 → 139 taken 2 times.
✗ Branch 138 → 142 not taken.
✗ Branch 140 → 141 not taken.
✓ Branch 140 → 142 taken 2 times.
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 146 taken 2 times.
2 if (stored && sieve.usesSeparateLogin())
203 ✗ sieve.credentialId = stored->sieve.credentialId;
204 2 return sieve;
205 2 }
206 } // namespace
207
208 2527 QVariantList QuickAccountSettings::configuredAccounts() const {
209 2527 return m_session.accounts()->configurationMetadata();
210 }
211
212 23 QVariantMap QuickAccountSettings::saveAccount(const QVariantMap &fields) {
213
2/2
✓ Branch 6 → 7 taken 23 times.
✓ Branch 7 → 8 taken 23 times.
46 const QString requestedId = fields.value(QStringLiteral("id")).toString();
214
2/2
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 44 taken 20 times.
23 if (!requestedId.isEmpty()) {
215 3 const auto accounts = configuredAccounts();
216
1/1
✓ Branch 17 → 18 taken 3 times.
3 const auto matches = std::count_if(accounts.cbegin(), accounts.cend(),
217 3 [&requestedId](const QVariant &row) {
218
3/3
✓ Branch 2 → 3 taken 3 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
6 return row.toMap().value(QStringLiteral("id")).toString() == requestedId;
219 });
220 // A stale editor must not recreate a removed credential identity while
221 // its asynchronous keyring cleanup is still in flight.
222
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 38 taken 2 times.
3 if (matches != 1)
223 2 return {{QStringLiteral("ok"), false},
224
3/5
✓ Branch 26 → 27 taken 1 time.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 32 taken 1 time.
✗ Branch 398 → 399 not taken.
✗ Branch 398 → 400 not taken.
5 {QStringLiteral("error"), tr("The account no longer exists.")}};
225 3 }
226
5/7
✓ Branch 44 → 45 taken 22 times.
✓ Branch 46 → 47 taken 5 times.
✓ Branch 46 → 50 taken 17 times.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 4 times.
✗ Branch 430 → 431 not taken.
✗ Branch 430 → 432 not taken.
49 const bool changesActive = m_deps.activeAccountId().isEmpty() ||
227
4/7
✓ Branch 47 → 48 taken 5 times.
✓ Branch 52 → 53 taken 5 times.
✓ Branch 52 → 54 taken 17 times.
✓ Branch 54 → 55 taken 22 times.
✗ Branch 54 → 56 not taken.
✗ Branch 426 → 427 not taken.
✗ Branch 426 → 428 not taken.
27 requestedId == m_deps.activeAccountId();
228
6/9
✓ Branch 56 → 57 taken 18 times.
✓ Branch 56 → 63 taken 4 times.
✓ Branch 57 → 58 taken 18 times.
✓ Branch 58 → 59 taken 18 times.
✗ Branch 58 → 62 not taken.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 18 times.
✗ Branch 64 → 65 not taken.
✓ Branch 64 → 84 taken 22 times.
22 if (changesActive && (m_deps.sendOrActionPending() || m_session.undo()->busy()))
229 ✗ return {{QStringLiteral("ok"), false}, {QStringLiteral("error"),
230 ✗ tr("Wait for the current send or action to finish before changing accounts.")}};
231
4/6
✓ Branch 84 → 85 taken 18 times.
✓ Branch 84 → 89 taken 4 times.
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 89 taken 18 times.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 110 taken 22 times.
22 if (changesActive && m_session.accounts()->resolvingCredentials())
232 ✗ return {{QStringLiteral("ok"), false}, {QStringLiteral("error"),
233 ✗ tr("Wait for credential loading to finish before saving this account.")}};
234 44 const auto port = [&fields](const QString &key, int fallback) -> std::optional<quint16> {
235 44 bool ok = false;
236
2/2
✓ Branch 3 → 4 taken 44 times.
✓ Branch 4 → 5 taken 44 times.
44 const double value = fields.value(key, fallback).toDouble(&ok);
237
12/12
✓ Branch 7 → 8 taken 42 times.
✓ Branch 7 → 13 taken 2 times.
✓ Branch 9 → 10 taken 40 times.
✓ Branch 9 → 13 taken 2 times.
✓ Branch 10 → 11 taken 36 times.
✓ Branch 10 → 13 taken 4 times.
✓ Branch 11 → 12 taken 32 times.
✓ Branch 11 → 13 taken 4 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 30 times.
✓ Branch 15 → 16 taken 14 times.
✓ Branch 15 → 19 taken 30 times.
44 if (!ok || !std::isfinite(value) || value < 1 || value > 65535 || std::floor(value) != value)
238 14 return std::nullopt;
239 30 return static_cast<quint16>(value);
240 22 };
241
1/1
✓ Branch 113 → 114 taken 22 times.
22 const auto imapPort = port(QStringLiteral("imapPort"), 993);
242
1/1
✓ Branch 119 → 120 taken 22 times.
22 const auto smtpPort = port(QStringLiteral("smtpPort"), 587);
243
6/6
✓ Branch 123 → 124 taken 15 times.
✓ Branch 123 → 126 taken 7 times.
✓ Branch 125 → 126 taken 7 times.
✓ Branch 125 → 127 taken 8 times.
✓ Branch 128 → 129 taken 14 times.
✓ Branch 128 → 148 taken 8 times.
22 if (!imapPort || !smtpPort)
244 56 return {{QStringLiteral("ok"), false}, {QStringLiteral("error"),
245
3/5
✓ Branch 136 → 137 taken 14 times.
✓ Branch 140 → 141 taken 28 times.
✓ Branch 140 → 142 taken 14 times.
✗ Branch 499 → 500 not taken.
✗ Branch 499 → 501 not taken.
70 tr("Enter whole port numbers between 1 and 65535.")}};
246
1/1
✓ Branch 148 → 149 taken 8 times.
8 AccountConfig config;
247 8 config.id = requestedId;
248
3/3
✓ Branch 154 → 155 taken 8 times.
✓ Branch 155 → 156 taken 8 times.
✓ Branch 156 → 157 taken 8 times.
16 config.name = fields.value(QStringLiteral("name")).toString().trimmed();
249
3/3
✓ Branch 168 → 169 taken 8 times.
✓ Branch 169 → 170 taken 8 times.
✓ Branch 170 → 171 taken 8 times.
16 config.email = fields.value(QStringLiteral("email")).toString().trimmed();
250
3/3
✓ Branch 182 → 183 taken 8 times.
✓ Branch 183 → 184 taken 8 times.
✓ Branch 184 → 185 taken 8 times.
16 config.imap.host = fields.value(QStringLiteral("imapHost")).toString().trimmed();
251 8 config.imap.port = *imapPort;
252
2/2
✓ Branch 200 → 201 taken 8 times.
✓ Branch 201 → 202 taken 8 times.
16 config.imap.security = fields.value(QStringLiteral("imapSecurity"), QStringLiteral("ssl")).toString();
253
3/3
✓ Branch 214 → 215 taken 8 times.
✓ Branch 215 → 216 taken 8 times.
✓ Branch 216 → 217 taken 8 times.
16 config.imap.username = fields.value(QStringLiteral("imapUsername")).toString().trimmed();
254
3/3
✓ Branch 228 → 229 taken 8 times.
✓ Branch 229 → 230 taken 8 times.
✓ Branch 230 → 231 taken 8 times.
16 config.imap.password = fields.value(QStringLiteral("imapPassword")).toString().toUtf8();
255
3/3
✓ Branch 242 → 243 taken 8 times.
✓ Branch 243 → 244 taken 8 times.
✓ Branch 244 → 245 taken 8 times.
16 config.smtp.host = fields.value(QStringLiteral("smtpHost")).toString().trimmed();
256 8 config.smtp.port = *smtpPort;
257
2/2
✓ Branch 260 → 261 taken 8 times.
✓ Branch 261 → 262 taken 8 times.
16 config.smtp.security = fields.value(QStringLiteral("smtpSecurity"), QStringLiteral("starttls")).toString();
258
3/3
✓ Branch 274 → 275 taken 8 times.
✓ Branch 275 → 276 taken 8 times.
✓ Branch 276 → 277 taken 8 times.
16 config.smtp.username = fields.value(QStringLiteral("smtpUsername")).toString().trimmed();
259
3/3
✓ Branch 288 → 289 taken 8 times.
✓ Branch 289 → 290 taken 8 times.
✓ Branch 290 → 291 taken 8 times.
16 config.smtp.password = fields.value(QStringLiteral("smtpPassword")).toString().toUtf8();
260
2/2
✓ Branch 299 → 300 taken 8 times.
✓ Branch 300 → 301 taken 8 times.
8 config.sieve = sieveConfigFrom(fields, m_session.accounts()->configuration(config.id));
261
1/1
✓ Branch 304 → 305 taken 8 times.
8 const QStringList errors = AccountConfigLoader::validate(config);
262
2/2
✓ Branch 306 → 307 taken 1 time.
✓ Branch 306 → 331 taken 7 times.
8 if (!errors.isEmpty())
263 2 return {{QStringLiteral("ok"), false},
264
3/5
✓ Branch 317 → 318 taken 1 time.
✓ Branch 321 → 322 taken 2 times.
✓ Branch 321 → 323 taken 1 time.
✗ Branch 695 → 696 not taken.
✗ Branch 695 → 697 not taken.
6 {QStringLiteral("error"), errors.join(QStringLiteral("; "))}};
265
2/2
✓ Branch 331 → 332 taken 7 times.
✓ Branch 335 → 336 taken 7 times.
14 const QString directory = QDir(m_profileDirectory).filePath(QStringLiteral("accounts"));
266
2/3
✓ Branch 339 → 340 taken 7 times.
✗ Branch 340 → 341 not taken.
✓ Branch 340 → 360 taken 7 times.
7 if (!AccountConfigLoader::save(config, directory))
267 ✗ return {{QStringLiteral("ok"), false},
268 ✗ {QStringLiteral("error"), tr("The account could not be saved.")}};
269
1/1
✓ Branch 361 → 362 taken 7 times.
7 m_session.accounts()->configurationSaved(config.id);
270
2/4
✓ Branch 372 → 373 taken 14 times.
✓ Branch 372 → 374 taken 7 times.
✗ Branch 762 → 763 not taken.
✗ Branch 762 → 764 not taken.
42 return {{QStringLiteral("ok"), true}, {QStringLiteral("id"), config.id}};
271 115 }
272
273 5 QVariantMap QuickAccountSettings::saveAccountAsync(const QVariantMap &fields) {
274 5 QVariantMap fieldErrors;
275 34 const auto required = [&fields, &fieldErrors](const QString &key,
276 const QString &message) {
277
5/5
✓ Branch 3 → 4 taken 34 times.
✓ Branch 4 → 5 taken 34 times.
✓ Branch 5 → 6 taken 34 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 16 taken 32 times.
34 if (fields.value(key).toString().trimmed().isEmpty())
278
1/1
✓ Branch 13 → 14 taken 2 times.
2 fieldErrors.insert(key, message);
279 34 };
280
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 6 → 7 taken 5 times.
10 required(QStringLiteral("name"), tr("Enter an account name."));
281
2/2
✓ Branch 10 → 11 taken 5 times.
✓ Branch 14 → 15 taken 5 times.
10 required(QStringLiteral("email"), tr("Enter an email address."));
282
2/2
✓ Branch 18 → 19 taken 5 times.
✓ Branch 22 → 23 taken 5 times.
10 required(QStringLiteral("imapHost"), tr("Enter the IMAP server."));
283
2/2
✓ Branch 26 → 27 taken 5 times.
✓ Branch 30 → 31 taken 5 times.
10 required(QStringLiteral("imapUsername"), tr("Enter the IMAP username."));
284
2/2
✓ Branch 34 → 35 taken 5 times.
✓ Branch 38 → 39 taken 5 times.
10 required(QStringLiteral("smtpHost"), tr("Enter the SMTP server."));
285
2/2
✓ Branch 42 → 43 taken 5 times.
✓ Branch 46 → 47 taken 5 times.
10 required(QStringLiteral("smtpUsername"), tr("Enter the SMTP username."));
286
2/2
✓ Branch 54 → 55 taken 5 times.
✓ Branch 55 → 56 taken 5 times.
10 const QString requestedId = fields.value(QStringLiteral("id")).toString();
287
2/2
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 79 taken 3 times.
5 if (requestedId.isEmpty()) {
288
2/2
✓ Branch 62 → 63 taken 2 times.
✓ Branch 66 → 67 taken 2 times.
4 required(QStringLiteral("imapPassword"), tr("Enter the IMAP password."));
289
2/2
✓ Branch 70 → 71 taken 2 times.
✓ Branch 74 → 75 taken 2 times.
4 required(QStringLiteral("smtpPassword"), tr("Enter the SMTP password."));
290 }
291 10 const auto port = [&fields, &fieldErrors, this](const QString &key, int fallback) -> std::optional<quint16> {
292 10 bool ok = false;
293
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 4 → 5 taken 10 times.
10 const double value = fields.value(key, fallback).toDouble(&ok);
294
6/12
✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 13 not taken.
✓ Branch 9 → 10 taken 10 times.
✗ Branch 9 → 13 not taken.
✓ Branch 10 → 11 taken 10 times.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 10 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 10 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 24 taken 10 times.
10 if (!ok || !std::isfinite(value) || value < 1 || value > 65535 || std::floor(value) != value) {
295 ✗ fieldErrors.insert(key, tr("Enter a whole port number between 1 and 65535."));
296 ✗ return std::nullopt;
297 }
298 10 return static_cast<quint16>(value);
299 5 };
300
1/1
✓ Branch 82 → 83 taken 5 times.
5 const auto imapPort = port(QStringLiteral("imapPort"), 993);
301
1/1
✓ Branch 88 → 89 taken 5 times.
5 const auto smtpPort = port(QStringLiteral("smtpPort"), 587);
302
3/3
✓ Branch 91 → 92 taken 5 times.
✓ Branch 92 → 93 taken 1 time.
✓ Branch 92 → 118 taken 4 times.
5 if (!fieldErrors.isEmpty())
303 4 return {{QStringLiteral("ok"), false}, {QStringLiteral("fieldErrors"), fieldErrors},
304
3/5
✓ Branch 104 → 105 taken 1 time.
✓ Branch 108 → 109 taken 3 times.
✓ Branch 108 → 110 taken 1 time.
✗ Branch 618 → 619 not taken.
✗ Branch 618 → 620 not taken.
6 {QStringLiteral("error"), tr("Correct the highlighted account fields.")}};
305
306
2/2
✓ Branch 119 → 120 taken 3 times.
✓ Branch 119 → 150 taken 1 time.
4 if (!requestedId.isEmpty()) {
307 3 const auto accounts = configuredAccounts();
308
1/1
✓ Branch 123 → 124 taken 3 times.
3 const auto matches = std::count_if(accounts.cbegin(), accounts.cend(),
309 3 [&requestedId](const QVariant &row) {
310
3/3
✓ Branch 2 → 3 taken 3 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
6 return row.toMap().value(QStringLiteral("id")).toString() == requestedId;
311 });
312
1/2
✗ Branch 124 → 125 not taken.
✓ Branch 124 → 144 taken 3 times.
3 if (matches != 1)
313 ✗ return {{QStringLiteral("ok"), false},
314 ✗ {QStringLiteral("error"), tr("The account no longer exists.")}};
315 3 }
316
7/14
✓ Branch 150 → 151 taken 4 times.
✓ Branch 152 → 153 taken 4 times.
✗ Branch 152 → 156 not taken.
✓ Branch 153 → 154 taken 4 times.
✓ Branch 155 → 156 taken 3 times.
✓ Branch 155 → 157 taken 1 time.
✓ Branch 158 → 159 taken 4 times.
✗ Branch 158 → 160 not taken.
✓ Branch 160 → 161 taken 4 times.
✗ Branch 160 → 162 not taken.
✗ Branch 678 → 679 not taken.
✗ Branch 678 → 680 not taken.
✗ Branch 682 → 683 not taken.
✗ Branch 682 → 684 not taken.
4 const bool changesActive = m_deps.activeAccountId().isEmpty() || requestedId == m_deps.activeAccountId();
317
6/9
✓ Branch 162 → 163 taken 3 times.
✓ Branch 162 → 169 taken 1 time.
✓ Branch 163 → 164 taken 3 times.
✓ Branch 164 → 165 taken 3 times.
✗ Branch 164 → 168 not taken.
✗ Branch 167 → 168 not taken.
✓ Branch 167 → 169 taken 3 times.
✗ Branch 170 → 171 not taken.
✓ Branch 170 → 190 taken 4 times.
4 if (changesActive && (m_deps.sendOrActionPending() || m_session.undo()->busy()))
318 ✗ return {{QStringLiteral("ok"), false}, {QStringLiteral("error"),
319 ✗ tr("Wait for the current send or action to finish before changing accounts.")}};
320
6/6
✓ Branch 190 → 191 taken 3 times.
✓ Branch 190 → 195 taken 1 time.
✓ Branch 193 → 194 taken 1 time.
✓ Branch 193 → 195 taken 2 times.
✓ Branch 196 → 197 taken 1 time.
✓ Branch 196 → 216 taken 3 times.
4 if (changesActive && m_session.accounts()->resolvingCredentials())
321 4 return {{QStringLiteral("ok"), false}, {QStringLiteral("error"),
322
3/5
✓ Branch 204 → 205 taken 1 time.
✓ Branch 208 → 209 taken 2 times.
✓ Branch 208 → 210 taken 1 time.
✗ Branch 713 → 714 not taken.
✗ Branch 713 → 715 not taken.
5 tr("Wait for credential loading to finish before saving this account.")}};
323
324 // F-48 (SPRINT-104): a rename re-keys the account's cached rows in the
325 // same step, so the cache, the queued actions and the session stay one
326 // identity instead of re-downloading the mailbox under the new name.
327 3 QString previousName;
328
2/2
✓ Branch 217 → 218 taken 2 times.
✓ Branch 217 → 260 taken 1 time.
3 if (!requestedId.isEmpty()) {
329
3/4
✓ Branch 219 → 220 taken 2 times.
✓ Branch 220 → 221 taken 2 times.
✓ Branch 257 → 222 taken 2 times.
✗ Branch 257 → 258 not taken.
2 for (const auto &entry : configuredAccounts()) {
330
1/1
✓ Branch 223 → 224 taken 2 times.
2 const auto account = entry.toMap();
331
3/4
✓ Branch 228 → 229 taken 2 times.
✓ Branch 229 → 230 taken 2 times.
✓ Branch 236 → 237 taken 2 times.
✗ Branch 236 → 250 not taken.
4 if (account.value(QStringLiteral("id")).toString() == requestedId) {
332
2/2
✓ Branch 241 → 242 taken 2 times.
✓ Branch 242 → 243 taken 2 times.
4 previousName = account.value(QStringLiteral("name")).toString();
333 2 break;
334 }
335 4 }
336 }
337
338
1/1
✓ Branch 260 → 261 taken 3 times.
3 AccountConfig config;
339 3 config.id = requestedId;
340
3/3
✓ Branch 266 → 267 taken 3 times.
✓ Branch 267 → 268 taken 3 times.
✓ Branch 268 → 269 taken 3 times.
6 config.name = fields.value(QStringLiteral("name")).toString().trimmed();
341
3/3
✓ Branch 280 → 281 taken 3 times.
✓ Branch 281 → 282 taken 3 times.
✓ Branch 282 → 283 taken 3 times.
6 config.email = fields.value(QStringLiteral("email")).toString().trimmed();
342
3/3
✓ Branch 294 → 295 taken 3 times.
✓ Branch 295 → 296 taken 3 times.
✓ Branch 296 → 297 taken 3 times.
6 config.imap.host = fields.value(QStringLiteral("imapHost")).toString().trimmed();
343 3 config.imap.port = *imapPort;
344
2/2
✓ Branch 312 → 313 taken 3 times.
✓ Branch 313 → 314 taken 3 times.
6 config.imap.security = fields.value(QStringLiteral("imapSecurity"), QStringLiteral("ssl")).toString();
345
3/3
✓ Branch 326 → 327 taken 3 times.
✓ Branch 327 → 328 taken 3 times.
✓ Branch 328 → 329 taken 3 times.
6 config.imap.username = fields.value(QStringLiteral("imapUsername")).toString().trimmed();
346
3/3
✓ Branch 340 → 341 taken 3 times.
✓ Branch 341 → 342 taken 3 times.
✓ Branch 342 → 343 taken 3 times.
6 config.imap.password = fields.value(QStringLiteral("imapPassword")).toString().toUtf8();
347
3/3
✓ Branch 354 → 355 taken 3 times.
✓ Branch 355 → 356 taken 3 times.
✓ Branch 356 → 357 taken 3 times.
6 config.smtp.host = fields.value(QStringLiteral("smtpHost")).toString().trimmed();
348 3 config.smtp.port = *smtpPort;
349
2/2
✓ Branch 372 → 373 taken 3 times.
✓ Branch 373 → 374 taken 3 times.
6 config.smtp.security = fields.value(QStringLiteral("smtpSecurity"), QStringLiteral("starttls")).toString();
350
3/3
✓ Branch 386 → 387 taken 3 times.
✓ Branch 387 → 388 taken 3 times.
✓ Branch 388 → 389 taken 3 times.
6 config.smtp.username = fields.value(QStringLiteral("smtpUsername")).toString().trimmed();
351
3/3
✓ Branch 400 → 401 taken 3 times.
✓ Branch 401 → 402 taken 3 times.
✓ Branch 402 → 403 taken 3 times.
6 config.smtp.password = fields.value(QStringLiteral("smtpPassword")).toString().toUtf8();
352
2/2
✓ Branch 411 → 412 taken 3 times.
✓ Branch 412 → 413 taken 3 times.
3 config.sieve = sieveConfigFrom(fields, m_session.accounts()->configuration(config.id));
353
1/1
✓ Branch 416 → 417 taken 3 times.
3 const auto errors = AccountConfigLoader::validate(config);
354
1/2
✗ Branch 418 → 419 not taken.
✓ Branch 418 → 443 taken 3 times.
3 if (!errors.isEmpty())
355 ✗ return {{QStringLiteral("ok"), false},
356 ✗ {QStringLiteral("error"), errors.join(QStringLiteral("; "))}};
357
358
2/2
✓ Branch 443 → 444 taken 3 times.
✓ Branch 444 → 445 taken 3 times.
3 const QString requestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
359
2/2
✓ Branch 445 → 446 taken 3 times.
✓ Branch 449 → 450 taken 3 times.
6 const QString directory = QDir(m_profileDirectory).filePath(QStringLiteral("accounts"));
360
1/1
✓ Branch 453 → 454 taken 3 times.
3 QPointer<QuickAccountSettings> guard(this);
361
1/1
✓ Branch 461 → 462 taken 3 times.
3 QObject *request = AccountConfigLoader::saveAsync(m_session.accounts(), std::move(config), directory,
362
1/5
✓ Branch 457 → 458 taken 3 times.
✗ Branch 989 → 990 not taken.
✗ Branch 989 → 991 not taken.
✗ Branch 992 → 993 not taken.
✗ Branch 992 → 994 not taken.
6 [guard, requestId, previousName](bool success, const AccountConfig &stored, const QString &error) {
363
3/6
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
2 if (!guard || !guard->m_settingsRequests.contains(requestId)) return;
364
1/1
✓ Branch 12 → 13 taken 2 times.
2 guard->m_settingsRequests.remove(requestId);
365
2/4
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 2 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
6 QVariantMap result{{QStringLiteral("ok"), success}};
366
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 48 taken 1 time.
2 if (success) {
367
1/1
✓ Branch 28 → 29 taken 1 time.
2 result.insert(QStringLiteral("id"), stored.id);
368
3/8
✓ Branch 33 → 34 taken 1 time.
✗ Branch 33 → 40 not taken.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 40 taken 1 time.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 45 taken 1 time.
1 if (!previousName.isEmpty() && previousName != stored.name &&
369 ✗ guard->m_session.cache())
370 ✗ guard->m_session.cache()->renameAccount(previousName, stored.name);
371
1/1
✓ Branch 47 → 62 taken 1 time.
1 guard->m_session.accounts()->configurationSaved(stored.id);
372 } else {
373
1/1
✓ Branch 56 → 57 taken 1 time.
3 result.insert(QStringLiteral("error"), error.isEmpty()
374
1/3
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 1 time.
✗ Branch 50 → 52 not taken.
2 ? QuickAccountSettings::tr("The account could not be saved.") : error);
375 }
376
1/1
✓ Branch 63 → 64 taken 2 times.
2 emit guard->accountSaveFinished(requestId, result);
377
1/5
✓ Branch 17 → 18 taken 2 times.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 82 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
6 });
378
1/2
✗ Branch 465 → 466 not taken.
✓ Branch 465 → 485 taken 3 times.
3 if (!request)
379 ✗ return {{QStringLiteral("ok"), false},
380 ✗ {QStringLiteral("error"), tr("The account save could not be started.")}};
381
2/2
✓ Branch 485 → 486 taken 3 times.
✓ Branch 486 → 487 taken 3 times.
3 m_settingsRequests.insert(requestId, request);
382 12 return {{QStringLiteral("ok"), true}, {QStringLiteral("pending"), true},
383
2/4
✓ Branch 502 → 503 taken 9 times.
✓ Branch 502 → 504 taken 3 times.
✗ Branch 1027 → 1028 not taken.
✗ Branch 1027 → 1029 not taken.
18 {QStringLiteral("requestId"), requestId}};
384 32 }
385
386 1 void QuickAccountSettings::cancelSettingsRequest(const QString &requestId) {
387
1/1
✓ Branch 2 → 3 taken 1 time.
1 const QPointer<QObject> request = m_settingsRequests.take(requestId);
388
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
1 delete request.data();
389 1 }
390
391 4 QVariantMap QuickAccountSettings::removeAccount(const QString &id) {
392
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 6 → 7 taken 4 times.
8 const QString directory = QDir(m_profileDirectory).filePath(QStringLiteral("accounts"));
393 4 QString victimName;
394 4 int matches = 0;
395
4/4
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 50 → 14 taken 6 times.
✓ Branch 50 → 51 taken 4 times.
10 for (const auto &entry : configuredAccounts()) {
396
1/1
✓ Branch 15 → 16 taken 6 times.
6 const auto account = entry.toMap();
397
4/4
✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 4 times.
12 if (account.value(QStringLiteral("id")).toString() != id) continue;
398
2/2
✓ Branch 34 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
8 victimName = account.value(QStringLiteral("name")).toString();
399 4 ++matches;
400 10 }
401
3/6
✓ Branch 52 → 53 taken 4 times.
✗ Branch 52 → 55 not taken.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 4 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 77 taken 4 times.
4 if (matches != 1 || victimName.isEmpty())
402 ✗ return {{QStringLiteral("ok"), false},
403 ✗ {QStringLiteral("error"), tr("The account no longer exists.")}};
404
5/9
✓ Branch 77 → 78 taken 4 times.
✓ Branch 79 → 80 taken 3 times.
✓ Branch 79 → 86 taken 1 time.
✓ Branch 87 → 88 taken 4 times.
✗ Branch 87 → 89 not taken.
✗ Branch 89 → 90 not taken.
✓ Branch 89 → 109 taken 4 times.
✗ Branch 238 → 239 not taken.
✗ Branch 238 → 240 not taken.
7 if (id == m_deps.activeAccountId() &&
405
3/5
✓ Branch 80 → 81 taken 3 times.
✓ Branch 81 → 82 taken 3 times.
✗ Branch 81 → 85 not taken.
✗ Branch 84 → 85 not taken.
✓ Branch 84 → 86 taken 3 times.
3 (m_deps.sendOrActionPending() || m_session.undo()->busy()))
406 ✗ return {{QStringLiteral("ok"), false}, {QStringLiteral("error"),
407 ✗ tr("Wait for the current send or action to finish before deleting the account.")}};
408
2/3
✓ Branch 109 → 110 taken 4 times.
✗ Branch 110 → 111 not taken.
✓ Branch 110 → 130 taken 4 times.
4 if (!AccountConfigLoader::remove(victimName, directory, id))
409 ✗ return {{QStringLiteral("ok"), false},
410 ✗ {QStringLiteral("error"), tr("The account could not be deleted.")}};
411 // F-43 (SPRINT-104): the downloaded mail leaves with the account —
412 // folders, headers, bodies, attachments, search documents and queued
413 // actions — instead of staying on disk under a name no account owns.
414
4/7
✓ Branch 131 → 132 taken 4 times.
✗ Branch 131 → 136 not taken.
✓ Branch 133 → 134 taken 4 times.
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 136 taken 4 times.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 149 taken 4 times.
4 if (m_session.cache() && !m_session.cache()->removeAccountData(victimName))
415 ✗ qCWarning(lcAccountSettings) << "Account removed, but its cached mail could not be deleted";
416 // The deleted account's recovery draft must not resurface later.
417
2/2
✓ Branch 150 → 151 taken 4 times.
✓ Branch 151 → 152 taken 4 times.
4 ComposeDraftStore(m_profileDirectory).clear(id);
418
1/1
✓ Branch 155 → 156 taken 4 times.
4 m_session.accounts()->configurationRemoved(id);
419
2/4
✓ Branch 162 → 163 taken 4 times.
✓ Branch 162 → 164 taken 4 times.
✗ Branch 306 → 307 not taken.
✗ Branch 306 → 308 not taken.
16 return {{QStringLiteral("ok"), true}};
420 16 }
421
422 49 int QuickAccountSettings::caldavSyncIntervalMinutes() const {
423
1/1
✓ Branch 2 → 3 taken 49 times.
49 QSettings settings;
424
2/2
✓ Branch 8 → 9 taken 49 times.
✓ Branch 9 → 10 taken 49 times.
147 return settings.value(QStringLiteral("caldav/syncIntervalMin"), 15).toInt();
425 49 }
426
427 1398 SettingsSyncSession *QuickAccountSettings::settingsSync() const { return m_session.settingsSync(); }
428
429 10 void QuickAccountSettings::setCaldavSyncIntervalMinutes(int minutes) {
430 10 minutes = qBound(0, minutes, 1440);
431
1/1
✓ Branch 3 → 4 taken 10 times.
10 QSettings settings;
432
2/2
✓ Branch 9 → 10 taken 10 times.
✓ Branch 10 → 11 taken 10 times.
20 const int current = settings.value(QStringLiteral("caldav/syncIntervalMin"), 15).toInt();
433
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 10 times.
10 if (current == minutes) return;
434
1/1
✓ Branch 22 → 23 taken 10 times.
20 settings.setValue(QStringLiteral("caldav/syncIntervalMin"), minutes);
435 // Re-arm the live scheduler: 0 stops it, anything else restarts the timer
436 // (the restart schedules one refresh after three seconds, like startup).
437 // The shared DAV interval drives the calendar and contact coordinators.
438
1/2
✓ Branch 27 → 28 taken 10 times.
✗ Branch 27 → 30 not taken.
10 if (const auto sync = m_session.calendarSync()) {
439
1/1
✓ Branch 28 → 29 taken 10 times.
10 sync->stopBackgroundSync();
440
1/1
✓ Branch 29 → 30 taken 10 times.
10 sync->startBackgroundSync();
441 }
442
1/2
✓ Branch 31 → 32 taken 10 times.
✗ Branch 31 → 34 not taken.
10 if (const auto contacts = m_session.contactSync()) {
443
1/1
✓ Branch 32 → 33 taken 10 times.
10 contacts->stopBackgroundSync();
444
1/1
✓ Branch 33 → 34 taken 10 times.
10 contacts->startBackgroundSync();
445 }
446
1/1
✓ Branch 34 → 35 taken 10 times.
10 emit davSettingsChanged();
447 10 }
448
449 21 QVariantList QuickAccountSettings::davAccounts() const {
450 21 QVariantList result;
451
5/5
✓ Branch 2 → 3 taken 21 times.
✓ Branch 3 → 4 taken 21 times.
✓ Branch 4 → 5 taken 21 times.
✓ Branch 35 → 6 taken 15 times.
✓ Branch 35 → 36 taken 21 times.
36 for (const auto &row : readDavAccounts())
452
1/1
✓ Branch 21 → 22 taken 15 times.
45 result.append(QVariantMap{{QStringLiteral("id"), row.id},
453 30 {QStringLiteral("serverUrl"), row.serverUrl},
454
2/4
✓ Branch 25 → 26 taken 45 times.
✓ Branch 25 → 27 taken 15 times.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
111 {QStringLiteral("username"), row.username}});
455 21 return result;
456 60 }
457
458 3 QVariantMap QuickAccountSettings::saveDavAccount(const QVariantMap &fields) {
459
1/1
✓ Branch 3 → 4 taken 3 times.
3 const auto result = DavAccountSettings::save(fields);
460
3/5
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 3 times.
✗ Branch 16 → 17 not taken.
6 if (result.value(QStringLiteral("ok")).toBool()) emit davSettingsChanged();
461 3 return result;
462 ✗ }
463
464 ✗ QVariantMap QuickAccountSettings::saveDavAccountAsync(const QVariantMap &fields) {
465 ✗ QVariantMap fieldErrors;
466 ✗ const QString requestedId = fields.value(QStringLiteral("id")).toString();
467 ✗ const QString username = fields.value(QStringLiteral("username")).toString().trimmed();
468 ✗ const QString serverUrl = fields.value(QStringLiteral("serverUrl")).toString().trimmed();
469 ✗ const QString password = fields.value(QStringLiteral("password")).toString();
470 ✗ if (username.isEmpty())
471 ✗ fieldErrors.insert(QStringLiteral("username"), tr("Enter a user name."));
472 ✗ const QString canonical = DavCredentials::canonicalOrigin(serverUrl);
473 ✗ if (canonical.isEmpty())
474 ✗ fieldErrors.insert(QStringLiteral("serverUrl"),
475 ✗ tr("Enter a server address starting with https://"));
476
477 ✗ std::optional<DavAccountSettings::Account> previous;
478 ✗ if (!requestedId.isEmpty()) {
479 ✗ int matches = 0;
480 ✗ for (const auto &account : DavAccountSettings::accounts()) {
481 ✗ if (account.id != requestedId) continue;
482 ✗ previous = account;
483 ✗ ++matches;
484 ✗ }
485 ✗ if (matches != 1)
486 ✗ return {{QStringLiteral("ok"), false},
487 ✗ {QStringLiteral("error"), tr("The DAV account no longer exists.")}};
488 }
489 ✗ if (password.isEmpty() && !previous)
490 ✗ fieldErrors.insert(QStringLiteral("password"), tr("Enter a password."));
491 ✗ if (password.isEmpty() && previous &&
492 ✗ (DavCredentials::canonicalOrigin(previous->serverUrl) != canonical ||
493 ✗ previous->username != username))
494 ✗ fieldErrors.insert(QStringLiteral("password"),
495 ✗ tr("Enter a password when changing the server or user name."));
496 ✗ if (!fieldErrors.isEmpty())
497 ✗ return {{QStringLiteral("ok"), false}, {QStringLiteral("fieldErrors"), fieldErrors},
498 ✗ {QStringLiteral("error"), tr("Correct the highlighted DAV fields.")}};
499
500 ✗ QVariantMap admitted = fields;
501 ✗ admitted.insert(QStringLiteral("serverUrl"), canonical);
502 ✗ const QString requestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
503 ✗ QPointer<QuickAccountSettings> guard(this);
504 ✗ QObject *request = DavAccountSettings::saveAsync(m_session.accounts(), admitted,
505 ✗ [guard, requestId](const QVariantMap &result) {
506 ✗ if (!guard || !guard->m_settingsRequests.contains(requestId)) return;
507 ✗ guard->m_settingsRequests.remove(requestId);
508 ✗ if (result.value(QStringLiteral("ok")).toBool()) emit guard->davSettingsChanged();
509 ✗ emit guard->davAccountSaveFinished(requestId, result);
510 });
511 ✗ if (!request)
512 ✗ return {{QStringLiteral("ok"), false},
513 ✗ {QStringLiteral("error"), tr("The DAV account save could not be started.")}};
514 ✗ m_settingsRequests.insert(requestId, request);
515 ✗ return {{QStringLiteral("ok"), true}, {QStringLiteral("pending"), true},
516 ✗ {QStringLiteral("requestId"), requestId}};
517 ✗ }
518
519 3 bool QuickAccountSettings::removeDavAccount(const QString &id) {
520
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
3 if (id.isEmpty()) return false;
521
1/1
✓ Branch 5 → 6 taken 3 times.
3 const auto removed = DavAccountSettings::removeMetadata(id);
522
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3 times.
3 if (!removed) return false;
523 // The removed UUID cannot be reused by a newly created DAV account. Keep
524 // cleanup alive across backend reloads, without processing nested UI events.
525
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
6 DavCredentials::deletePasswordAsync(QCoreApplication::instance(), removed->id,
526 6 removed->serverUrl, removed->username, [id](bool success) {
527 ✗ if (!success) qWarning() << "DAV account removed, but keyring cleanup failed for" << id;
528 3 }, removed->credentialId);
529 // S106-055: the account's calendars and tasks leave with it.
530
2/3
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 24 not taken.
✓ Branch 23 → 24 taken 3 times.
3 if (m_session.calendarSync()) m_session.calendarSync()->forgetAccount(removed->id);
531
1/1
✓ Branch 24 → 25 taken 3 times.
3 emit davSettingsChanged();
532 3 return true;
533 3 }
534
535 19 QAbstractItemModel *QuickAccountSettings::imageWhitelist() const { return m_whitelist; }
536
537 39 QString QuickAccountSettings::imagePolicyMode() const {
538 39 return ExternalContentPolicy::instance()->mode();
539 }
540
541 4 void QuickAccountSettings::setImagePolicyMode(const QString &mode) {
542
1/2
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 6 not taken.
4 if (ExternalContentPolicy::instance()->setMode(mode))
543 4 emit imagePolicyModeChanged();
544 4 }
545
546 36 QString QuickAccountSettings::defaultReadingMode() const {
547 36 return WorkspaceState::configuredReadingMode();
548 }
549
550 2 void QuickAccountSettings::setDefaultReadingMode(const QString &mode) {
551
2/4
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
2 const QString canonical = mode == QLatin1String("html-light") ? QStringLiteral("light") : mode;
552
3/4
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 21 taken 1 time.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 21 taken 1 time.
2 if (canonical != QLatin1String("text") && canonical != QLatin1String("light") &&
553
1/4
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2 times.
2 canonical != QLatin1String("html"))
554 ✗ return;
555
2/3
✓ Branch 24 → 25 taken 2 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 2 times.
2 if (canonical == WorkspaceState::configuredReadingMode()) return;
556
2/2
✓ Branch 29 → 30 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
4 QSettings().setValue(QStringLiteral("view/defaultMode"), canonical);
557 // The notification refreshes the settings surface and the WorkspaceState
558 // default used by later selections, without reading QSettings on each click.
559
1/1
✓ Branch 40 → 41 taken 2 times.
2 emit defaultReadingModeChanged();
560 2 }
561
562 7 bool QuickAccountSettings::addImageWhitelistDomain(const QString &domain) {
563
1/1
✓ Branch 2 → 3 taken 7 times.
7 const QString normalized = normalizeWhitelistDomain(domain);
564
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 4 times.
7 if (normalized.isEmpty()) return false;
565
1/1
✓ Branch 11 → 12 taken 4 times.
8 return m_session.cache()->allowlist().addWhitelistEntry(QStringLiteral("domain"), normalized);
566 7 }
567
568 3 bool QuickAccountSettings::removeImageWhitelistEntry(qint64 entryId) {
569 3 return m_session.cache()->allowlist().removeWhitelistEntry(entryId);
570 }
571
572 // Formerly inline in ~QuickMailBackend(); the backend destructor calls it at
573 // the same point (after the autosave writer stopped).
574 795 void QuickAccountSettings::destroyPendingRequests() {
575 795 const auto settingsRequests = m_settingsRequests;
576 795 m_settingsRequests.clear();
577
1/2
✗ Branch 12 → 6 not taken.
✓ Branch 12 → 13 taken 795 times.
795 for (const auto &request : settingsRequests)
578 ✗ delete request.data();
579 795 }
580
581 ✗ void QuickAccountSettings::notifyDavChanged(const QString &davAccountId) {
582 ✗ emit davSettingsChanged();
583 ✗ for (const auto &config : DavAccountSettings::calendarConfigs())
584 ✗ if (config.accountId == davAccountId && !config.selectedCalendars.isEmpty() && m_session.calendarSync()) {
585 ✗ QTimer::singleShot(0, m_session.calendarSync(), &CalendarSyncCoordinator::triggerSync);
586 ✗ break;
587 ✗ }
588 ✗ if (m_session.contactSync()) QTimer::singleShot(0, m_session.contactSync(), &ContactSyncCoordinator::triggerSync);
589 ✗ }
590
591 ✗ QVariantMap QuickAccountSettings::saveDavWithMailLogin(const QVariantMap &input) {
592 ✗ const QString mailId = input.value(QStringLiteral("mailAccountId")).toString();
593 ✗ const auto config = m_session.accounts()->configuration(mailId);
594 ✗ if (!config) return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), tr("The mail account no longer exists.")}};
595 // S106-252: the editor sends no id; the DAV account already linked to
596 // this mail account is updated instead of a second one being created.
597 ✗ QVariantMap fields = input;
598 ✗ if (fields.value(QStringLiteral("id")).toString().isEmpty())
599 ✗ if (const QString linked = DavAccountSettings::linkedTo(mailId); !linked.isEmpty())
600 ✗ fields.insert(QStringLiteral("id"), linked);
601 ✗ const QString requestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
602 ✗ auto *store = new CredentialStore(this);
603 ✗ m_settingsRequests.insert(requestId, store);
604 ✗ QPointer<QuickAccountSettings> guard(this);
605 ✗ store->readPassword(QStringLiteral("imap"), AccountConfigLoader::imapCredentialName(*config),
606 ✗ [guard, store, requestId, fields](bool success, const QByteArray &password) {
607 ✗ store->deleteLater();
608 ✗ if (!guard || !guard->m_settingsRequests.contains(requestId)) return;
609 ✗ if (!success || password.isEmpty()) {
610 ✗ guard->m_settingsRequests.remove(requestId);
611 ✗ emit guard->davAccountSaveFinished(requestId, {{QStringLiteral("ok"), false},
612 ✗ {QStringLiteral("error"), tr("The password of the mail account could not be read. Enter it once.")}});
613 ✗ return;
614 }
615 ✗ QVariantMap davFields = fields;
616 ✗ davFields.insert(QStringLiteral("password"), QString::fromUtf8(password));
617 ✗ QObject *request = DavAccountSettings::saveAsync(guard, davFields, [guard, requestId](const QVariantMap &result) {
618 ✗ if (!guard || !guard->m_settingsRequests.contains(requestId)) return;
619 ✗ guard->m_settingsRequests.remove(requestId);
620 ✗ if (result.value(QStringLiteral("ok")).toBool()) guard->notifyDavChanged(result.value(QStringLiteral("id")).toString());
621 ✗ emit guard->davAccountSaveFinished(requestId, result);
622 });
623 ✗ guard->m_settingsRequests.insert(requestId, request);
624 ✗ });
625 ✗ return {{QStringLiteral("ok"), true}, {QStringLiteral("requestId"), requestId}};
626 ✗ }
627