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: 88.5% 361 / 0 / 408
Functions: 87.2% 41 / 0 / 47
Branches: 63.4% 388 / 0 / 612

app/core/ContactSyncCoordinator.cpp
Line Branch Exec Source
1 #include "ContactSyncCoordinator.h"
2 #include "data/DavSelectionList.h"
3
4 #include "ContactBookSync.h"
5 #include "ContactOutbox.h"
6 #include "data/ContactQueries.h"
7 #include "data/ContactStore.h"
8 #include "data/MailAddressList.h"
9 #include "data/VCardCodec.h"
10 #include "service/CardDavClient.h"
11 #include "util/SecureUtil.h"
12
13 #include <QDateTime>
14 #include <QLoggingCategory>
15 #include <QSettings>
16 #include <QSet>
17 #include <QTimeZone>
18
19
4/7
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 3 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
4 Q_LOGGING_CATEGORY(lcContactSync, "mailjd.contactsync")
20
21 904 ContactSyncCoordinator::ContactSyncCoordinator(const QString &contactsDbPath, QObject *parent)
22
2/2
✓ Branch 4 → 5 taken 904 times.
✓ Branch 5 → 6 taken 904 times.
904 : QObject(parent), m_databasePath(contactsDbPath) {
23
3/6
✓ Branch 18 → 19 taken 904 times.
✓ Branch 19 → 20 taken 904 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 904 times.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
904 m_store = new ContactStore(this);
24
2/3
✓ Branch 22 → 23 taken 904 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 36 taken 904 times.
904 if (!m_store->open(contactsDbPath))
25 ✗ qCWarning(lcContactSync) << "ContactSyncCoordinator: store open failed at" << contactsDbPath;
26
1/1
✓ Branch 36 → 37 taken 904 times.
904 m_syncTimer.setSingleShot(false);
27
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 37 → 38 taken 904 times.
904 connect(&m_syncTimer, &QTimer::timeout, this, [this] { startRun({}, DavSyncReasonPoll); });
28
1/1
✓ Branch 39 → 40 taken 904 times.
904 m_initialSyncTimer.setSingleShot(true);
29
2/2
✓ Branch 3 → 4 taken 97 times.
✓ Branch 40 → 41 taken 904 times.
1001 connect(&m_initialSyncTimer, &QTimer::timeout, this, [this] { startRun({}, DavSyncReasonInitial); });
30 // Defaults: the production keychain resolver and the real client factory.
31 904 m_asyncCredentialResolver = [](QObject *owner, const QString &id, const QString &url, const QString &user,
32 DavCredentials::PasswordCallback callback) {
33 ✗ DavCredentials::readPasswordAsync(owner, id, url, user, std::move(callback));
34 904 };
35 904 m_clientFactory = [](const QString &serverUrl, const QString &username, const QString &password) {
36 ✗ return new CardDavClient(serverUrl, username, password);
37 904 };
38 904 ContactOutbox::Deps deps;
39 904 deps.store = m_store;
40 2003 deps.endpoint = [this](QObject *owner, const QString &accountId, ContactOutbox::EndpointCallback callback) {
41
3/3
✓ Branch 2 → 3 taken 195 times.
✓ Branch 3 → 4 taken 195 times.
✓ Branch 4 → 5 taken 195 times.
195 resolveEndpoint(owner, accountId, [callback](QString url, QString user, QByteArray password) {
42
5/6
✓ Branch 3 → 4 taken 13 times.
✓ Branch 3 → 6 taken 182 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 13 times.
✓ Branch 8 → 9 taken 182 times.
✓ Branch 8 → 13 taken 13 times.
195 if (url.isEmpty() || password.isEmpty()) {
43
1/1
✓ Branch 10 → 11 taken 182 times.
182 callback(std::nullopt);
44 182 return;
45 }
46
1/5
✓ Branch 17 → 18 taken 13 times.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
13 callback(ContactOutbox::Endpoint{url, user, password});
47 13 SecureUtil::zeroMemory(password);
48 });
49 1099 };
50 1821 deps.makeClient = [this](const QString &url, const QString &user, const QString &password) {
51 13 return m_clientFactory(url, user, password);
52 904 };
53
4/4
✓ Branch 3 → 4 taken 76 times.
✓ Branch 3 → 6 taken 252 times.
✓ Branch 5 → 6 taken 29 times.
✓ Branch 5 → 7 taken 47 times.
1232 deps.networkAllowed = [this] { return !m_networkGate || m_networkGate(); };
54
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 565 times.
1469 deps.now = [this] { return m_clock ? m_clock() : QDateTime::currentSecsSinceEpoch(); };
55 1821 deps.pushDontNotify = [this](const QString &accountId, const QString &path) {
56
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 12 times.
13 return m_pushDontNotify ? m_pushDontNotify(accountId, path) : QString();
57 904 };
58
3/6
✓ Branch 50 → 51 taken 904 times.
✓ Branch 53 → 54 taken 904 times.
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 57 taken 904 times.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 80 not taken.
904 m_outbox = new ContactOutbox(std::move(deps), this);
59
1/1
✓ Branch 57 → 58 taken 904 times.
904 connect(m_outbox, &ContactOutbox::cardsChanged, this, &ContactSyncCoordinator::cardsChanged);
60
1/1
✓ Branch 59 → 60 taken 904 times.
904 connect(m_outbox, &ContactOutbox::statusMessage, this, &ContactSyncCoordinator::statusMessage);
61 904 }
62
63 1790 ContactSyncCoordinator::~ContactSyncCoordinator() {
64 904 ++m_generation;
65
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 904 times.
904 delete m_run.data();
66 904 wipeResolvedPasswords();
67 1790 }
68
69 821 bool ContactSyncCoordinator::open() { return m_store->isOpen(); }
70
71 996 int ContactSyncCoordinator::configuredIntervalMinutes() {
72
1/1
✓ Branch 2 → 3 taken 996 times.
996 QSettings s;
73 // Preserve the predecessor's independent CardDAV interval. Profiles created
74 // during the Quick shared-interval period keep that value until explicitly
75 // choosing a contact interval; never silently turn an old disabled sync on.
76
1/1
✓ Branch 13 → 14 taken 996 times.
1992 return qBound(0, s.value(QStringLiteral("carddav/syncIntervalMin"),
77
3/3
✓ Branch 8 → 9 taken 996 times.
✓ Branch 14 → 15 taken 996 times.
✓ Branch 15 → 16 taken 996 times.
4980 s.value(QStringLiteral("caldav/syncIntervalMin"), 15)).toInt(), 1440);
78 996 }
79
80 851 void ContactSyncCoordinator::startBackgroundSync() {
81 851 const int interval = configuredIntervalMinutes();
82 851 m_initialSyncTimer.stop();
83 851 m_syncTimer.stop();
84
2/2
✓ Branch 5 → 6 taken 54 times.
✓ Branch 5 → 7 taken 797 times.
851 if (interval <= 0)
85 54 return;
86 797 m_syncTimer.setInterval(interval * 60 * 1000);
87 797 m_syncTimer.start();
88 // Same startup freshness as the calendar coordinator.
89 797 m_initialSyncTimer.start(3000);
90 }
91
92 33 void ContactSyncCoordinator::stopBackgroundSync() {
93 33 m_syncTimer.stop();
94 33 m_initialSyncTimer.stop();
95 33 ++m_generation;
96 33 const bool wasRunning = m_run;
97
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 9 taken 32 times.
33 if (m_run)
98 1 m_run->deleteLater();
99 33 m_run = nullptr;
100 33 wipeResolvedPasswords();
101 33 m_queuedBooks = 0;
102 33 m_queuedAll = false;
103 33 m_queuedFilter.clear();
104 33 m_queuedReasons = 0;
105 33 m_bookResults.clear();
106
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 32 times.
33 if (wasRunning)
107 1 emit syncStateChanged(false);
108 33 }
109
110 3 void ContactSyncCoordinator::retryPendingWrites() {
111
1/2
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
3 if (m_store->isOpen())
112 3 m_outbox->flush(true);
113 3 }
114
115
3/4
✓ Branch 3 → 4 taken 295 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 4 → 5 taken 295 times.
✗ Branch 4 → 6 not taken.
296 bool ContactSyncCoordinator::runCurrent(quint64 generation) const { return m_run && m_generation == generation; }
116
117 // Selected address books live in the carddav/accounts array (the rows
118 // DavAccountSettings persists); reading them here keeps the coordinator in
119 // the application core.
120 643 QList<ContactSyncCoordinator::SyncAccount> ContactSyncCoordinator::readSyncAccounts() {
121 643 QList<SyncAccount> accounts;
122
1/1
✓ Branch 2 → 3 taken 643 times.
643 QSettings settings;
123
1/1
✓ Branch 7 → 8 taken 643 times.
643 const int count = settings.beginReadArray(QStringLiteral("carddav/accounts"));
124
2/2
✓ Branch 72 → 11 taken 639 times.
✓ Branch 72 → 73 taken 643 times.
1282 for (int i = 0; i < count; ++i) {
125
1/1
✓ Branch 11 → 12 taken 639 times.
639 settings.setArrayIndex(i);
126 639 SyncAccount account;
127
2/2
✓ Branch 16 → 17 taken 639 times.
✓ Branch 17 → 18 taken 639 times.
639 account.accountId = settings.value(QStringLiteral("id")).toString();
128
2/2
✓ Branch 27 → 28 taken 639 times.
✓ Branch 28 → 29 taken 639 times.
639 account.serverUrl = settings.value(QStringLiteral("serverUrl")).toString();
129
2/2
✓ Branch 38 → 39 taken 639 times.
✓ Branch 39 → 40 taken 639 times.
639 account.username = settings.value(QStringLiteral("username")).toString();
130
2/2
✓ Branch 49 → 50 taken 639 times.
✓ Branch 50 → 51 taken 639 times.
639 account.selectedBooks = davPathList(settings.value(QStringLiteral("selectedBooks")));
131
2/2
✓ Branch 61 → 62 taken 639 times.
✓ Branch 62 → 63 taken 639 times.
1278 account.requiresLocalAuthorization = settings.value(QStringLiteral("requiresLocalAuthorization"), false).toBool();
132
1/2
✓ Branch 68 → 69 taken 639 times.
✗ Branch 68 → 70 not taken.
639 if (!account.accountId.isEmpty())
133
1/1
✓ Branch 69 → 70 taken 639 times.
639 accounts.append(account);
134 639 }
135
1/1
✓ Branch 73 → 74 taken 643 times.
643 settings.endArray();
136 643 return accounts;
137 643 }
138
139 195 void ContactSyncCoordinator::resolveEndpoint(QObject *owner, const QString &accountId,
140 std::function<void(QString, QString, QByteArray)> done) {
141
5/5
✓ Branch 2 → 3 taken 195 times.
✓ Branch 3 → 4 taken 195 times.
✓ Branch 4 → 5 taken 195 times.
✓ Branch 33 → 6 taken 13 times.
✓ Branch 33 → 34 taken 182 times.
195 for (const SyncAccount &account : readSyncAccounts()) {
142
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 13 times.
13 if (account.accountId != accountId)
143 ✗ continue;
144
4/8
✓ Branch 11 → 12 taken 13 times.
✗ Branch 11 → 15 not taken.
✓ Branch 13 → 14 taken 13 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 13 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 13 times.
13 if (account.serverUrl.isEmpty() || account.username.isEmpty() || account.requiresLocalAuthorization)
145 ✗ break;
146 13 const QString url = account.serverUrl;
147 13 const QString user = account.username;
148
3/7
✓ Branch 21 → 22 taken 13 times.
✓ Branch 24 → 25 taken 13 times.
✓ Branch 25 → 26 taken 13 times.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
13 m_asyncCredentialResolver(owner, accountId, url, user, [done, url, user](QByteArray password, const QString &) {
149
1/1
✓ Branch 5 → 6 taken 13 times.
13 done(url, user, password);
150 13 SecureUtil::zeroMemory(password);
151 13 });
152 13 return;
153 208 }
154
1/1
✓ Branch 41 → 43 taken 182 times.
182 done({}, {}, {});
155 }
156
157 // Books follow the selection: new ones are created, deselected ones leave
158 // the store with their cards (the local and the legacy book stay).
159 71 void ContactSyncCoordinator::reconcileBooks(const QList<SyncAccount> &accounts) {
160 71 QSet<QPair<QString, QString>> selected;
161
2/2
✓ Branch 18 → 5 taken 69 times.
✓ Branch 18 → 19 taken 71 times.
140 for (const SyncAccount &account : accounts)
162
2/2
✓ Branch 15 → 8 taken 85 times.
✓ Branch 15 → 16 taken 69 times.
154 for (const QString &path : account.selectedBooks) {
163
1/1
✓ Branch 10 → 11 taken 85 times.
85 selected.insert({account.accountId, path});
164
1/1
✓ Branch 12 → 13 taken 85 times.
85 m_store->ensureBook(account.accountId, path);
165 }
166 71 bool removed = false;
167
5/5
✓ Branch 19 → 20 taken 71 times.
✓ Branch 20 → 21 taken 71 times.
✓ Branch 21 → 22 taken 71 times.
✓ Branch 59 → 23 taken 92 times.
✓ Branch 59 → 60 taken 71 times.
163 for (const AddressBookInfo &book : m_store->books()) {
168
5/8
✓ Branch 25 → 26 taken 89 times.
✓ Branch 25 → 31 taken 3 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 32 taken 89 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 35 taken 89 times.
92 if (book.isLocal() || (book.accountId.isEmpty() && book.path == QLatin1String("legacy")))
169 3 continue;
170
2/2
✓ Branch 38 → 39 taken 4 times.
✓ Branch 38 → 57 taken 85 times.
89 if (!selected.contains({book.accountId, book.path})) {
171
7/7
✓ Branch 39 → 40 taken 4 times.
✓ Branch 43 → 44 taken 4 times.
✓ Branch 44 → 45 taken 4 times.
✓ Branch 45 → 46 taken 4 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 50 → 41 taken 4 times.
✓ Branch 50 → 51 taken 4 times.
8 qCInfo(lcContactSync) << "Address book" << book.id << "is no longer selected; its cards are removed";
172
2/5
✓ Branch 51 → 52 taken 4 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 4 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
4 removed = m_store->removeBook(book.id) || removed;
173 }
174 71 }
175
2/2
✓ Branch 61 → 62 taken 4 times.
✓ Branch 61 → 63 taken 67 times.
71 if (removed)
176
1/1
✓ Branch 62 → 63 taken 4 times.
4 emit contactsChanged();
177
1/1
✓ Branch 63 → 64 taken 71 times.
71 emit booksChanged();
178 71 }
179
180
1/1
✓ Branch 3 → 4 taken 58 times.
58 void ContactSyncCoordinator::triggerSync() { startRun({}, DavSyncReasonManual); }
181
182 377 QStringList ContactSyncCoordinator::selectedBooks(const QString &accountId) {
183
5/5
✓ Branch 2 → 3 taken 377 times.
✓ Branch 3 → 4 taken 377 times.
✓ Branch 4 → 5 taken 377 times.
✓ Branch 13 → 6 taken 467 times.
✓ Branch 13 → 14 taken 3 times.
470 for (const SyncAccount &account : readSyncAccounts())
184
2/2
✓ Branch 8 → 9 taken 374 times.
✓ Branch 8 → 11 taken 93 times.
467 if (account.accountId == accountId)
185
2/2
✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 19 taken 374 times.
377 return account.selectedBooks;
186 3 return {};
187 }
188
189 2 QString ContactSyncCoordinator::bookSyncToken(const QString &accountId, const QString &path) const {
190
3/4
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 const auto book = m_store->isOpen() ? m_store->book(accountId, path) : std::nullopt;
191
1/2
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 10 not taken.
4 return book ? book->syncToken : QString();
192 2 }
193
194 13 void ContactSyncCoordinator::syncBooks(const QString &accountId, const QStringList &paths, unsigned reasons) {
195
1/1
✓ Branch 4 → 5 taken 13 times.
13 QSet<QString> books(paths.cbegin(), paths.cend());
196
2/2
✓ Branch 6 → 7 taken 11 times.
✓ Branch 6 → 15 taken 2 times.
13 if (books.isEmpty()) {
197
1/1
✓ Branch 7 → 8 taken 11 times.
11 const QStringList selected = selectedBooks(accountId);
198
1/1
✓ Branch 10 → 11 taken 11 times.
11 books = QSet<QString>(selected.cbegin(), selected.cend());
199 11 }
200
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 12 times.
13 if (books.isEmpty())
201 1 return;
202
3/5
✓ Branch 20 → 21 taken 12 times.
✓ Branch 23 → 24 taken 12 times.
✓ Branch 23 → 25 taken 12 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
24 startRun({{accountId, books}}, reasons);
203 25 }
204
205 170 void ContactSyncCoordinator::startRun(const QHash<QString, QSet<QString>> &filter, unsigned reasons) {
206 // A request during a run (timer, manual, push) becomes one follow-up run.
207
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 17 taken 168 times.
170 if (m_run) {
208
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 1 time.
2 if (filter.isEmpty())
209 1 m_queuedAll = true;
210
2/2
✓ Branch 15 → 8 taken 1 time.
✓ Branch 15 → 16 taken 2 times.
3 for (auto it = filter.cbegin(); it != filter.cend(); ++it)
211
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
1 m_queuedFilter[it.key()].unite(it.value());
212 2 m_queuedReasons |= reasons;
213 104 return;
214 }
215
7/7
✓ Branch 18 → 19 taken 108 times.
✓ Branch 18 → 22 taken 60 times.
✓ Branch 19 → 20 taken 108 times.
✓ Branch 20 → 21 taken 97 times.
✓ Branch 20 → 22 taken 11 times.
✓ Branch 23 → 24 taken 97 times.
✓ Branch 23 → 25 taken 71 times.
168 if (m_networkGate && !m_networkGate())
216 97 return; // offline: no keyring, no DAV
217
2/3
✓ Branch 25 → 26 taken 71 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 31 taken 71 times.
71 if (!m_store->isOpen()) {
218 ✗ emit statusMessage(tr("Contacts cannot be synchronized right now."));
219 ✗ return;
220 }
221 71 QList<SyncAccount> accounts;
222 71 int unauthorizedAccounts = 0;
223
1/1
✓ Branch 31 → 32 taken 71 times.
71 const QList<SyncAccount> configured = readSyncAccounts();
224
1/1
✓ Branch 32 → 33 taken 71 times.
71 reconcileBooks(configured);
225
2/2
✓ Branch 58 → 35 taken 69 times.
✓ Branch 58 → 59 taken 71 times.
140 for (const SyncAccount &account : configured) {
226
7/8
✓ Branch 37 → 38 taken 67 times.
✓ Branch 37 → 42 taken 2 times.
✓ Branch 39 → 40 taken 12 times.
✓ Branch 39 → 43 taken 55 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 12 times.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 46 taken 67 times.
69 if (account.selectedBooks.isEmpty() || (!filter.isEmpty() && !filter.contains(account.accountId)))
227 2 continue;
228
6/8
✓ Branch 47 → 48 taken 67 times.
✗ Branch 47 → 51 not taken.
✓ Branch 49 → 50 taken 67 times.
✗ Branch 49 → 51 not taken.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 66 times.
✓ Branch 53 → 54 taken 1 time.
✓ Branch 53 → 55 taken 66 times.
67 if (account.serverUrl.isEmpty() || account.username.isEmpty() || account.requiresLocalAuthorization) {
229 1 ++unauthorizedAccounts;
230 1 continue;
231 }
232
1/1
✓ Branch 55 → 56 taken 66 times.
66 accounts.append(account);
233 }
234
2/2
✓ Branch 60 → 61 taken 5 times.
✓ Branch 60 → 72 taken 66 times.
71 if (accounts.isEmpty()) {
235
2/2
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 66 taken 4 times.
5 if (unauthorizedAccounts > 0)
236
2/2
✓ Branch 62 → 63 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
1 emit statusMessage(tr("Authorize the DAV account locally to sync contacts."));
237 else
238
2/2
✓ Branch 66 → 67 taken 4 times.
✓ Branch 67 → 68 taken 4 times.
4 emit statusMessage(tr("No address books selected for contact sync."));
239
1/1
✓ Branch 70 → 71 taken 5 times.
5 m_outbox->flush(false);
240 5 return;
241 }
242
4/7
✓ Branch 72 → 73 taken 66 times.
✓ Branch 73 → 74 taken 66 times.
✓ Branch 74 → 75 taken 66 times.
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 77 taken 66 times.
✗ Branch 156 → 157 not taken.
✗ Branch 156 → 158 not taken.
66 m_run = new QObject(this);
243 66 const quint64 generation = ++m_generation;
244 66 m_runAccounts = accounts;
245 66 m_runFilter = filter;
246 66 m_runReasons = reasons;
247
1/1
✓ Branch 79 → 80 taken 66 times.
66 m_bookResults.clear();
248 66 m_pendingResolutions = int(accounts.size());
249 66 m_queuedBooks = 0;
250 66 m_failedBooks = 0;
251 66 m_runRequests = 0;
252 66 m_runCards = 0;
253 66 m_runBulk = false;
254
1/1
✓ Branch 81 → 82 taken 66 times.
66 m_runKeys.clear();
255
1/1
✓ Branch 82 → 83 taken 66 times.
66 wipeResolvedPasswords();
256
1/1
✓ Branch 83 → 84 taken 66 times.
66 emit syncStateChanged(true);
257
1/1
✓ Branch 84 → 85 taken 66 times.
66 QPointer<ContactSyncCoordinator> guard(this);
258 // Phase 1: resolve each account's secret once, owner-bound and fenced.
259
4/4
✓ Branch 85 → 86 taken 66 times.
✓ Branch 86 → 87 taken 66 times.
✓ Branch 129 → 88 taken 66 times.
✓ Branch 129 → 130 taken 66 times.
132 for (const SyncAccount &account : accounts) {
260
1/2
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 66 times.
66 if (!runCurrent(generation))
261 ✗ return;
262
3/6
✓ Branch 92 → 93 taken 66 times.
✓ Branch 94 → 95 taken 66 times.
✗ Branch 95 → 96 not taken.
✓ Branch 95 → 97 taken 66 times.
✗ Branch 159 → 160 not taken.
✗ Branch 159 → 161 not taken.
66 auto *operation = new QObject(m_run);
263
1/1
✓ Branch 97 → 98 taken 66 times.
66 QPointer<QObject> pending(operation);
264
1/5
✓ Branch 100 → 101 taken 66 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
66 QTimer::singleShot(60000, operation, [guard, pending, generation] {
265 ✗ if (!guard || !pending || generation != guard->m_generation || pending->property("finished").toBool())
266 ✗ return;
267 ✗ emit guard->statusMessage(tr("Contact sync timed out. Try again."));
268 ✗ guard->finishResolution(pending, generation);
269 });
270 66 const QString accountId = account.accountId;
271
1/1
✓ Branch 107 → 108 taken 66 times.
66 m_asyncCredentialResolver(operation, accountId, account.serverUrl, account.username,
272
1/5
✓ Branch 106 → 107 taken 66 times.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 177 not taken.
✗ Branch 178 → 179 not taken.
✗ Branch 178 → 180 not taken.
132 [guard, pending, generation, accountId](QByteArray password, const QString &) {
273
2/4
✓ Branch 5 → 6 taken 66 times.
✗ Branch 5 → 16 not taken.
✓ Branch 7 → 8 taken 66 times.
✗ Branch 7 → 16 not taken.
66 if (!guard || !pending || generation != guard->m_generation ||
274
10/20
✓ Branch 3 → 4 taken 66 times.
✗ Branch 3 → 16 not taken.
✓ Branch 9 → 10 taken 66 times.
✓ Branch 10 → 11 taken 66 times.
✓ Branch 11 → 12 taken 66 times.
✗ Branch 11 → 16 not taken.
✓ Branch 13 → 14 taken 66 times.
✓ Branch 14 → 15 taken 66 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 66 times.
✓ Branch 18 → 19 taken 66 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 66 times.
✗ Branch 20 → 22 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 66 times.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
132 pending->property("finished").toBool() || pending->property("resolved").toBool())
275 ✗ return;
276
1/1
✓ Branch 26 → 27 taken 66 times.
66 pending->setProperty("resolved", true);
277
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 41 taken 66 times.
66 if (password.isEmpty())
278 ✗ qCWarning(lcContactSync) << "Skipping CardDAV account without local credentials";
279 else
280 66 guard->m_resolvedPasswords.insert(accountId, password);
281 66 guard->finishResolution(pending, generation);
282 });
283
3/6
✓ Branch 111 → 112 taken 66 times.
✗ Branch 111 → 114 not taken.
✗ Branch 113 → 114 not taken.
✓ Branch 113 → 115 taken 66 times.
✗ Branch 116 → 117 not taken.
✓ Branch 116 → 118 taken 66 times.
66 if (!guard || !runCurrent(generation))
284 ✗ return;
285 66 }
286 76 }
287
288 66 void ContactSyncCoordinator::finishResolution(QObject *operation, quint64 generation) {
289
7/14
✓ Branch 3 → 4 taken 66 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 66 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 66 times.
✓ Branch 6 → 7 taken 66 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 66 times.
✓ Branch 10 → 11 taken 66 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 66 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
66 if (!m_run || generation != m_generation || operation->property("finished").toBool())
290 ✗ return;
291
1/1
✓ Branch 15 → 16 taken 66 times.
66 operation->setProperty("finished", true);
292 66 operation->deleteLater();
293
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 66 times.
66 if (--m_pendingResolutions > 0)
294 ✗ return;
295 66 startBookSyncs(generation);
296 }
297
298 66 void ContactSyncCoordinator::startBookSyncs(quint64 generation) {
299 struct Target {
300 SyncAccount account;
301 qint64 bookId;
302 };
303 66 QList<Target> targets;
304 66 int skipped = 0;
305
4/4
✓ Branch 2 → 3 taken 66 times.
✓ Branch 3 → 4 taken 66 times.
✓ Branch 42 → 5 taken 66 times.
✓ Branch 42 → 43 taken 66 times.
132 for (const SyncAccount &account : m_runAccounts) {
306
2/2
✓ Branch 39 → 8 taken 84 times.
✓ Branch 39 → 40 taken 66 times.
150 for (const QString &path : account.selectedBooks) {
307
8/8
✓ Branch 10 → 11 taken 18 times.
✓ Branch 10 → 15 taken 66 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 16 times.
✓ Branch 16 → 17 taken 18 times.
✓ Branch 16 → 18 taken 66 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 82 times.
84 if (!m_runFilter.isEmpty() && !m_runFilter.value(account.accountId).contains(path))
308 2 continue;
309
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 27 taken 82 times.
82 if (!m_resolvedPasswords.contains(account.accountId)) {
310 ✗ m_bookResults.append({account.accountId, path, false, {}});
311 ✗ ++skipped;
312 ✗ continue;
313 }
314
2/3
✓ Branch 27 → 28 taken 82 times.
✓ Branch 29 → 30 taken 82 times.
✗ Branch 29 → 35 not taken.
82 if (const auto book = m_store->book(account.accountId, path))
315 82 targets.append({account, book->id});
316 }
317 }
318
1/2
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 48 taken 66 times.
66 if (skipped > 0) {
319 ✗ emit statusMessage(tr("Authorize the DAV account locally to sync contacts."));
320 ✗ m_failedBooks += skipped;
321 }
322 66 m_queuedBooks = int(targets.size());
323
1/2
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 52 taken 66 times.
66 if (m_queuedBooks == 0) {
324 ✗ runCompleted();
325 ✗ return;
326 }
327 66 QHash<QString, QStringList> started;
328
4/4
✓ Branch 52 → 53 taken 66 times.
✓ Branch 53 → 54 taken 66 times.
✓ Branch 65 → 55 taken 82 times.
✓ Branch 65 → 66 taken 66 times.
148 for (const Target &target : targets)
329
2/3
✓ Branch 56 → 57 taken 82 times.
✓ Branch 58 → 59 taken 82 times.
✗ Branch 58 → 62 not taken.
82 if (const auto book = m_store->book(target.bookId))
330
2/2
✓ Branch 59 → 60 taken 82 times.
✓ Branch 61 → 62 taken 82 times.
82 started[target.account.accountId].append(book->path);
331
2/2
✓ Branch 73 → 67 taken 66 times.
✓ Branch 73 → 74 taken 66 times.
132 for (auto it = started.cbegin(); it != started.cend(); ++it)
332
1/1
✓ Branch 69 → 70 taken 66 times.
66 emit booksRunStarted(it.key(), it.value());
333
1/1
✓ Branch 74 → 75 taken 66 times.
66 QPointer<ContactSyncCoordinator> guard(this);
334
4/4
✓ Branch 75 → 76 taken 66 times.
✓ Branch 76 → 77 taken 66 times.
✓ Branch 148 → 78 taken 82 times.
✓ Branch 148 → 149 taken 66 times.
148 for (const Target &target : targets) {
335
1/2
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 82 taken 82 times.
82 if (!runCurrent(generation))
336 ✗ return;
337 82 const QString accountId = target.account.accountId;
338
4/8
✓ Branch 83 → 84 taken 82 times.
✓ Branch 85 → 86 taken 82 times.
✗ Branch 85 → 89 not taken.
✓ Branch 86 → 87 taken 82 times.
✓ Branch 90 → 91 taken 82 times.
✗ Branch 90 → 92 not taken.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 194 not taken.
82 const QString bookPath = m_store->book(target.bookId) ? m_store->book(target.bookId)->path : QString();
339
1/1
✓ Branch 95 → 96 taken 82 times.
82 CardDavClient *client = m_clientFactory(target.account.serverUrl, target.account.username,
340
1/1
✓ Branch 94 → 95 taken 82 times.
164 QString::fromUtf8(m_resolvedPasswords.value(target.account.accountId)));
341
1/2
✗ Branch 98 → 99 not taken.
✓ Branch 98 → 103 taken 82 times.
82 if (!client) {
342 ✗ bookFinished(generation, false, {}, false);
343 ✗ continue;
344 }
345
3/6
✓ Branch 103 → 104 taken 82 times.
✓ Branch 105 → 106 taken 82 times.
✗ Branch 106 → 107 not taken.
✓ Branch 106 → 108 taken 82 times.
✗ Branch 208 → 209 not taken.
✗ Branch 208 → 210 not taken.
82 auto *sync = new ContactBookSync(m_store, client, target.bookId, m_run);
346
1/1
✓ Branch 108 → 109 taken 82 times.
82 client->setParent(sync);
347 // Inactivity timeout: every answer restarts it (large books take many requests).
348
3/6
✓ Branch 109 → 110 taken 82 times.
✓ Branch 110 → 111 taken 82 times.
✗ Branch 111 → 112 not taken.
✓ Branch 111 → 113 taken 82 times.
✗ Branch 211 → 212 not taken.
✗ Branch 211 → 213 not taken.
82 auto *watchdog = new QTimer(sync);
349
1/1
✓ Branch 113 → 114 taken 82 times.
82 watchdog->setSingleShot(true);
350
1/1
✓ Branch 114 → 115 taken 82 times.
82 watchdog->setInterval(120000);
351
1/1
✓ Branch 116 → 117 taken 82 times.
82 connect(sync, &ContactBookSync::progressed, watchdog, qOverload<>(&QTimer::start));
352
1/1
✓ Branch 118 → 119 taken 82 times.
82 QPointer<ContactBookSync> pending(sync);
353
1/7
✓ Branch 123 → 124 taken 82 times.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
✗ Branch 221 → 222 not taken.
✗ Branch 221 → 223 not taken.
✗ Branch 224 → 225 not taken.
✗ Branch 224 → 226 not taken.
82 connect(watchdog, &QTimer::timeout, this, [guard, pending, generation, accountId, bookPath] {
354 ✗ if (!guard || !pending || pending->property("finished").toBool())
355 ✗ return;
356 ✗ pending->setProperty("finished", true);
357 ✗ guard->m_bookResults.append({accountId, bookPath, false, pending->pushOffer()});
358 ✗ emit guard->statusMessage(tr("Contact sync timed out. Try again."));
359 ✗ guard->bookFinished(generation, false, {}, false);
360 ✗ });
361 82 connect(sync, &ContactBookSync::finished, this,
362
1/7
✓ Branch 130 → 131 taken 82 times.
✗ Branch 231 → 232 not taken.
✗ Branch 231 → 233 not taken.
✗ Branch 234 → 235 not taken.
✗ Branch 234 → 236 not taken.
✗ Branch 237 → 238 not taken.
✗ Branch 237 → 239 not taken.
164 [guard, pending, generation, accountId, bookPath](bool ok, const QStringList &keys, bool bulk) {
363
7/14
✓ Branch 3 → 4 taken 82 times.
✗ Branch 3 → 10 not taken.
✓ Branch 5 → 6 taken 82 times.
✗ Branch 5 → 10 not taken.
✓ Branch 7 → 8 taken 82 times.
✓ Branch 8 → 9 taken 82 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 82 times.
✓ Branch 12 → 13 taken 82 times.
✗ Branch 12 → 14 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 82 times.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
82 if (!guard || !pending || pending->property("finished").toBool())
364 ✗ return;
365
1/1
✓ Branch 18 → 19 taken 82 times.
82 pending->setProperty("finished", true);
366 164 guard->m_bookResults.append({accountId, bookPath, ok, pending->pushOffer()});
367 82 guard->m_runRequests += pending->requests();
368 82 guard->m_runCards += pending->cardsDownloaded();
369 82 guard->bookFinished(generation, ok, keys, bulk);
370
1/5
✓ Branch 25 → 26 taken 82 times.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
82 });
371
1/1
✓ Branch 133 → 134 taken 82 times.
82 watchdog->start();
372
1/1
✓ Branch 134 → 135 taken 82 times.
82 sync->start();
373 82 }
374 148 }
375
376 82 void ContactSyncCoordinator::bookFinished(quint64 generation, bool ok, const QStringList &keys, bool bulk) {
377
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 81 times.
82 if (!runCurrent(generation))
378 1 return;
379
2/2
✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 7 taken 72 times.
81 if (!ok)
380 9 ++m_failedBooks;
381 81 m_runKeys += keys;
382
3/4
✓ Branch 8 → 9 taken 81 times.
✗ Branch 8 → 10 not taken.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 79 times.
81 m_runBulk = m_runBulk || bulk;
383
2/2
✓ Branch 12 → 13 taken 16 times.
✓ Branch 12 → 14 taken 65 times.
81 if (--m_queuedBooks > 0)
384 16 return;
385 65 runCompleted();
386 }
387
388 // S106-063: every path that drops the resolved keyring passwords wipes them
389 // first (only the success path did; start, stop and destruction just freed
390 // the buffers).
391 1068 void ContactSyncCoordinator::wipeResolvedPasswords() {
392
3/3
✓ Branch 2 → 3 taken 1068 times.
✓ Branch 8 → 4 taken 66 times.
✓ Branch 8 → 9 taken 1068 times.
1134 for (auto &secret : m_resolvedPasswords)
393
1/1
✓ Branch 5 → 6 taken 66 times.
66 SecureUtil::zeroMemory(secret);
394 1068 m_resolvedPasswords.clear();
395 1068 }
396
397 65 void ContactSyncCoordinator::runCompleted() {
398 65 wipeResolvedPasswords();
399 65 m_runAccounts.clear();
400 // F103-27: placeholders no sync owns leave once every server book was fetched.
401
2/3
✓ Branch 4 → 5 taken 65 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 65 times.
65 if (const ContactStore::LegacyRetirement retired = m_store->retireLegacyBook(); retired.removed + retired.moved > 0) {
402 ✗ emit contactsChanged();
403 ✗ emit booksChanged();
404 }
405 65 m_store->dropLegacyTableWhenDone();
406 65 m_lastRequests = m_runRequests;
407 65 m_lastCards = m_runCards;
408
2/2
✓ Branch 9 → 10 taken 9 times.
✓ Branch 9 → 14 taken 56 times.
65 if (m_failedBooks > 0)
409
2/2
✓ Branch 10 → 11 taken 9 times.
✓ Branch 11 → 12 taken 9 times.
9 emit statusMessage(tr("Some address books could not be synchronized. MailJD tries again later."));
410
1/2
✓ Branch 15 → 16 taken 65 times.
✗ Branch 15 → 18 not taken.
65 if (m_run)
411 65 m_run->deleteLater();
412 65 m_run = nullptr;
413
2/2
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 63 times.
65 if (m_runBulk)
414 2 emit contactsChanged();
415
2/2
✓ Branch 22 → 23 taken 35 times.
✓ Branch 22 → 24 taken 28 times.
63 else if (!m_runKeys.isEmpty())
416 35 emit cardsChanged(m_runKeys);
417 65 emit booksChanged();
418 65 reportBooks();
419 65 emit syncStateChanged(false);
420 65 m_outbox->flush(false);
421
6/6
✓ Branch 28 → 29 taken 64 times.
✓ Branch 28 → 31 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 32 taken 63 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 47 taken 63 times.
65 if (m_queuedAll || !m_queuedFilter.isEmpty()) {
422
4/4
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 40 taken 1 time.
2 const auto filter = m_queuedAll ? QHash<QString, QSet<QString>>() : std::exchange(m_queuedFilter, {});
423 2 const unsigned reasons = std::exchange(m_queuedReasons, 0u);
424 2 m_queuedAll = false;
425 2 m_queuedFilter.clear();
426
1/3
✓ Branch 43 → 44 taken 2 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
4 QTimer::singleShot(0, this, [this, filter, reasons] { startRun(filter, reasons); });
427 2 }
428 65 }
429
430 // Sprint 96: what the push source needs from a finished run, per account.
431 65 void ContactSyncCoordinator::reportBooks() {
432 65 QHash<QString, DavPushSyncReport> reports;
433 65 QHash<QString, QList<DavPushDiscovery>> offers;
434
2/2
✓ Branch 21 → 5 taken 81 times.
✓ Branch 21 → 22 taken 65 times.
146 for (const BookResult &result : std::as_const(m_bookResults)) {
435
1/1
✓ Branch 6 → 7 taken 81 times.
81 DavPushSyncReport &report = reports[result.accountId];
436 81 report.accountId = result.accountId;
437 81 report.reasons = m_runReasons;
438
3/3
✓ Branch 8 → 9 taken 72 times.
✓ Branch 8 → 10 taken 9 times.
✓ Branch 11 → 12 taken 81 times.
81 (result.ok ? report.syncedPaths : report.failedPaths).append(result.path);
439
2/2
✓ Branch 12 → 13 taken 73 times.
✓ Branch 12 → 19 taken 8 times.
81 if (result.offer.capability != DavPushCapability::Unknown)
440
1/1
✓ Branch 13 → 14 taken 73 times.
146 offers[result.accountId].append({result.path, result.offer});
441 }
442
1/1
✓ Branch 22 → 23 taken 65 times.
65 m_bookResults.clear();
443
2/2
✓ Branch 30 → 24 taken 64 times.
✓ Branch 30 → 31 taken 65 times.
129 for (auto it = offers.cbegin(); it != offers.cend(); ++it)
444
1/1
✓ Branch 26 → 27 taken 64 times.
64 emit pushOffersRead(it.key(), it.value());
445
2/2
✓ Branch 38 → 34 taken 65 times.
✓ Branch 38 → 39 taken 65 times.
130 for (const DavPushSyncReport &report : std::as_const(reports))
446
1/1
✓ Branch 35 → 36 taken 65 times.
65 emit booksSynced(report);
447 138 }
448
449 // ─── collected addresses ───────────────────────────────────
450
451 40 bool ContactSyncCoordinator::collectAddressesEnabled() {
452
3/3
✓ Branch 2 → 3 taken 40 times.
✓ Branch 8 → 9 taken 40 times.
✓ Branch 9 → 10 taken 40 times.
80 return QSettings().value(QStringLiteral("contacts/collectAddresses"), true).toBool();
453 }
454
455 6 void ContactSyncCoordinator::setCollectAddressesEnabled(bool enabled) {
456
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 8 → 9 taken 6 times.
12 QSettings().setValue(QStringLiteral("contacts/collectAddresses"), enabled);
457 6 }
458
459 2 void ContactSyncCoordinator::recordSentRecipients(const QList<CollectedAddressStore::Recipient> &recipients) {
460
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2 times.
2 if (!m_store->isOpen() || recipients.isEmpty())
461 ✗ return;
462
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
2 CollectedAddressStore collected(m_store->database());
463
2/2
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
4 if (collected.recordSent(recipients, collectAddressesEnabled(), m_clock
464
3/10
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 2 times.
✗ Branch 16 → 17 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 26 not taken.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
4 ? QDateTime::fromSecsSinceEpoch(m_clock(), QTimeZone::UTC)
465 : QDateTime()))
466
1/1
✓ Branch 25 → 26 taken 2 times.
2 emit contactsChanged();
467 2 }
468
469 ✗ void ContactSyncCoordinator::recordSentHeaders(const QStringList &headers) {
470 ✗ QList<CollectedAddressStore::Recipient> recipients;
471 ✗ for (const QString &header : headers)
472 ✗ for (const MailAddressList::Mailbox &mailbox : MailAddressList::parse(header))
473 ✗ recipients.append({mailbox.name, mailbox.address});
474 ✗ recordSentRecipients(recipients);
475 ✗ }
476
477 2 bool ContactSyncCoordinator::forgetCollectedAddresses() {
478
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 if (!m_store->isOpen())
479 ✗ return false;
480
3/3
✓ Branch 5 → 6 taken 2 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
2 const bool ok = CollectedAddressStore(m_store->database()).forgetAll();
481 2 emit contactsChanged();
482 2 return ok;
483 }
484
485 // ─── D-5 local-only contacts ───────────────────────────────
486
487 270 int ContactSyncCoordinator::localOnlyCount() const {
488
3/7
✓ Branch 3 → 4 taken 270 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 270 times.
✓ Branch 9 → 10 taken 270 times.
✗ Branch 9 → 11 not taken.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 15 not taken.
270 return m_store->isOpen() ? int(m_store->pendingLocalOnlyCards().size()) : 0;
489 }
490
491 2 bool ContactSyncCoordinator::uploadLocalOnly(qint64 bookId) {
492
1/1
✓ Branch 2 → 3 taken 2 times.
2 const QList<qint64> ids = m_store->pendingLocalOnlyCards();
493 2 bool ok = true;
494 2 m_outbox->beginBatch();
495
2/2
✓ Branch 30 → 6 taken 1 time.
✓ Branch 30 → 31 taken 2 times.
3 for (qint64 id : ids) {
496
1/1
✓ Branch 7 → 8 taken 1 time.
1 const auto card = m_store->cardById(id);
497
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
1 if (!card)
498 ✗ continue;
499 1 QString error;
500
4/9
✓ Branch 13 → 14 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
1 ok = !m_outbox->moveCard(card->bookId, card->uid, bookId, &error).isEmpty() && ok;
501 1 }
502
1/1
✓ Branch 31 → 32 taken 2 times.
2 m_outbox->endBatch();
503
1/2
✓ Branch 32 → 33 taken 2 times.
✗ Branch 32 → 34 not taken.
2 if (ok)
504
1/1
✓ Branch 33 → 34 taken 2 times.
2 m_store->clearPendingLocalOnlyCards();
505
1/1
✓ Branch 34 → 35 taken 2 times.
2 emit contactsChanged();
506 2 return ok;
507 2 }
508
509 2 void ContactSyncCoordinator::keepLocalOnly() {
510 2 m_store->clearPendingLocalOnlyCards();
511 2 emit booksChanged();
512 2 }
513