app/core/MailSessionCore.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "MailSessionCore.h" | ||
| 2 | #include "AccountSession.h" | ||
| 3 | #include "BackgroundAccountSession.h" | ||
| 4 | #include "SettingsSyncSession.h" | ||
| 5 | #include "CalendarSyncCoordinator.h" | ||
| 6 | #include "CalendarPushSource.h" | ||
| 7 | #include "ContactPushSource.h" | ||
| 8 | #include "DavPushCoordinator.h" | ||
| 9 | #include "DavPushSecrets.h" | ||
| 10 | #include "data/DavCredentials.h" | ||
| 11 | #include "service/NetworkLifecycleCoordinator.h" | ||
| 12 | #include "ContactSyncCoordinator.h" | ||
| 13 | #include "FolderPredictionSession.h" | ||
| 14 | #include "app/FtsIndexCoordinator.h" | ||
| 15 | #include "controller/MailController.h" | ||
| 16 | #include "controller/UndoManager.h" | ||
| 17 | #include "data/CalendarStore.h" | ||
| 18 | #include "data/MailCache.h" | ||
| 19 | #include "service/ConnectionHealthMonitor.h" | ||
| 20 | #include "service/ImapService.h" | ||
| 21 | #include "service/SmtpService.h" | ||
| 22 | #include "ui/MailListModel.h" | ||
| 23 | #include "ui/MailThreadModel.h" | ||
| 24 | #include "performance/PerformanceTrace.h" | ||
| 25 | #include "performance/StartupPerformanceTracker.h" | ||
| 26 | #include "performance/BackgroundPerformanceTracker.h" | ||
| 27 | #include <QDir> | ||
| 28 | #include <QFileInfo> | ||
| 29 | #include <QStandardPaths> | ||
| 30 | |||
| 31 | 854 | MailSessionCore::MailSessionCore(QObject *parent) | |
| 32 | : QObject(parent), | ||
| 33 | ✗ | m_imap(std::make_unique<ImapService>(this)), | |
| 34 |
1/1✓ Branch 4 → 5 taken 854 times.
|
854 | m_cache(std::make_unique<MailCache>(this)), |
| 35 |
1/1✓ Branch 5 → 6 taken 854 times.
|
854 | m_headers(std::make_unique<MailListModel>(this)), |
| 36 |
1/1✓ Branch 6 → 7 taken 854 times.
|
854 | m_threads(std::make_unique<MailThreadModel>(this)), |
| 37 |
1/1✓ Branch 7 → 8 taken 854 times.
|
854 | m_undo(std::make_unique<UndoManager>(this)), |
| 38 |
1/1✓ Branch 8 → 9 taken 854 times.
|
854 | m_health(std::make_unique<ConnectionHealthMonitor>(true, this)), |
| 39 | 854 | m_controller(std::make_unique<MailController>( | |
| 40 |
1/1✓ Branch 12 → 13 taken 854 times.
|
854 | m_imap.get(), m_cache.get(), m_headers.get(), nullptr, this)), |
| 41 |
1/1✓ Branch 13 → 14 taken 854 times.
|
854 | m_accounts(std::make_unique<AccountSession>(*this, this)), |
| 42 |
2/2✓ Branch 3 → 4 taken 854 times.
✓ Branch 15 → 16 taken 854 times.
|
1708 | m_predictions(std::make_unique<FolderPredictionSession>(*this, this)) { |
| 43 | 854 | m_controller->setThreadModel(m_threads.get()); | |
| 44 |
1/1✓ Branch 30 → 31 taken 854 times.
|
854 | m_controller->setUndoManager(m_undo.get()); |
| 45 |
1/1✓ Branch 33 → 34 taken 854 times.
|
854 | m_health->attach(m_imap.get()); |
| 46 | // F-50 (SPRINT-104, D-11): the non-selected accounts stay online with | ||
| 47 | // their own staggered connections — badges and arrival signals only; | ||
| 48 | // the selected account's own primary connection takes over on switch. | ||
| 49 | { | ||
| 50 | 854 | BackgroundAccountSession::Deps deps; | |
| 51 | 854 | deps.cache = m_cache.get(); | |
| 52 | 1751 | deps.configurations = [this] { return m_accounts->configurations(); }; | |
| 53 |
2/2✓ Branch 5 → 6 taken 890 times.
✓ Branch 5 → 10 taken 15 times.
|
905 | deps.selectedAccountId = [this] { return m_accounts->primary() ? m_accounts->primary()->id |
| 54 | 1759 | : QString(); }; | |
| 55 | 1751 | deps.networkEnabled = [this] { return m_accounts->networkEnabled(); }; | |
| 56 |
1/1✓ Branch 40 → 41 taken 854 times.
|
854 | m_backgroundAccounts = std::make_unique<BackgroundAccountSession>(std::move(deps), this); |
| 57 | // S106-185: the watches follow the session's network gate. Before, the | ||
| 58 | // background session's own gate was never opened in production (only | ||
| 59 | // tests called setNetworkEnabled), so no account was ever watched. | ||
| 60 |
1/1✓ Branch 45 → 46 taken 854 times.
|
854 | connect(m_accounts.get(), &AccountSession::accountsLoaded, m_backgroundAccounts.get(), [this] { |
| 61 | 866 | m_backgroundAccounts->onAccountsLoaded(); | |
| 62 | 866 | m_backgroundAccounts->setNetworkEnabled(m_accounts->networkEnabled()); | |
| 63 | 866 | }); | |
| 64 |
1/1✓ Branch 49 → 50 taken 854 times.
|
854 | connect(m_accounts.get(), &AccountSession::configurationChanged, |
| 65 | 854 | m_backgroundAccounts.get(), &BackgroundAccountSession::onConfigurationChanged); | |
| 66 | // configurationRemoved() is a method: connecting it failed at run time | ||
| 67 | // ("signal not found"), so a removed account kept its watch. | ||
| 68 |
1/1✓ Branch 53 → 54 taken 854 times.
|
854 | connect(m_accounts.get(), &AccountSession::accountRemoved, |
| 69 | 854 | m_backgroundAccounts.get(), &BackgroundAccountSession::onConfigurationRemoved); | |
| 70 | 854 | } | |
| 71 | 854 | } | |
| 72 | |||
| 73 | 1068 | MailSessionCore::~MailSessionCore() { | |
| 74 | // A real quit stops push reception; valid registrations stay for the | ||
| 75 | // next start (SPRINT-96.md §3.3). | ||
| 76 |
2/2✓ Branch 3 → 4 taken 820 times.
✓ Branch 3 → 6 taken 34 times.
|
854 | if (m_davPush) m_davPush->shutdown(); |
| 77 | 854 | m_davPush.reset(); | |
| 78 |
2/2✓ Branch 8 → 9 taken 820 times.
✓ Branch 8 → 11 taken 34 times.
|
854 | if (m_fts) m_fts->shutdown(); |
| 79 | 854 | m_health->setActive(false); | |
| 80 | 854 | m_health->detach(); | |
| 81 | 1068 | } | |
| 82 | |||
| 83 | 824 | bool MailSessionCore::openProfile(const QString &directory) { | |
| 84 | 824 | m_profileError.clear(); | |
| 85 |
9/12✓ Branch 3 → 4 taken 824 times.
✓ Branch 5 → 6 taken 823 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 823 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 822 times.
✓ Branch 10 → 11 taken 824 times.
✗ Branch 10 → 12 not taken.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 20 taken 822 times.
✗ Branch 274 → 275 not taken.
✗ Branch 274 → 276 not taken.
|
824 | if (directory.trimmed().isEmpty() || !QDir::isAbsolutePath(directory)) { |
| 86 | 2 | m_profileError = QStringLiteral("Cannot open the explicit mail profile directory."); | |
| 87 | 2 | return false; | |
| 88 | } | ||
| 89 |
2/2✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 44 taken 820 times.
|
822 | if (!m_profileDirectory.isEmpty()) { |
| 90 |
10/17✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 30 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 2 times.
✗ Branch 31 → 33 not taken.
✓ Branch 33 → 34 taken 2 times.
✗ Branch 33 → 35 not taken.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 1 time.
✗ Branch 278 → 279 not taken.
✗ Branch 278 → 280 not taken.
✗ Branch 282 → 283 not taken.
✗ Branch 282 → 284 not taken.
|
2 | if (QFileInfo(directory).canonicalFilePath() == m_profileDirectory && m_cache->isOpen()) return true; |
| 91 | 1 | m_profileError = QStringLiteral("A mail session cannot switch its database profile."); | |
| 92 | 1 | return false; | |
| 93 | } | ||
| 94 |
3/4✓ Branch 45 → 46 taken 820 times.
✓ Branch 46 → 47 taken 820 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 57 taken 820 times.
|
820 | if (!QDir().mkpath(directory)) { |
| 95 | ✗ | m_profileError = QStringLiteral("Cannot create the explicit mail profile directory."); | |
| 96 | ✗ | return false; | |
| 97 | } | ||
| 98 |
2/2✓ Branch 57 → 58 taken 820 times.
✓ Branch 58 → 59 taken 820 times.
|
820 | const QString canonical = QFileInfo(directory).canonicalFilePath(); |
| 99 |
3/3✓ Branch 60 → 61 taken 820 times.
✓ Branch 61 → 62 taken 820 times.
✓ Branch 62 → 63 taken 820 times.
|
820 | const QString cachePath = QDir(canonical).filePath("mail_cache.db"); |
| 100 |
5/13✓ Branch 66 → 67 taken 820 times.
✗ Branch 66 → 74 not taken.
✓ Branch 68 → 69 taken 820 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 75 taken 820 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 820 times.
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 86 taken 820 times.
✗ Branch 301 → 302 not taken.
✗ Branch 301 → 303 not taken.
|
820 | if (canonical.isEmpty() || (m_cache->isOpen() && m_cache->databasePath() != cachePath)) { |
| 101 | ✗ | m_profileError = QStringLiteral("The mail session already has a different cache connection."); | |
| 102 | ✗ | return false; | |
| 103 | } | ||
| 104 |
1/1✓ Branch 86 → 87 taken 820 times.
|
820 | const auto started = MailJD::Performance::PerformanceTrace::monotonicNowNs(); |
| 105 |
1/1✓ Branch 88 → 89 taken 820 times.
|
1640 | m_fts = std::make_unique<FtsIndexCoordinator>([cachePath] { |
| 106 | ✗ | MailCache threadCache; | |
| 107 | ✗ | if (!threadCache.open(cachePath)) return false; | |
| 108 | ✗ | return threadCache.search().rebuildSearchIndex(); | |
| 109 | 1640 | }, this); | |
| 110 |
1/1✓ Branch 92 → 93 taken 820 times.
|
820 | m_smtp = std::make_unique<SmtpService>(this); |
| 111 |
1/1✓ Branch 96 → 97 taken 820 times.
|
820 | connect(m_fts.get(), &FtsIndexCoordinator::stateChanged, this, |
| 112 | 820 | [this](FtsIndexCoordinator::State state) { | |
| 113 |
1/4✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 820 times.
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
|
820 | if (state != FtsIndexCoordinator::State::Queued || m_ftsRebuildStartNs != 0) return; |
| 114 | ✗ | m_ftsRebuildStartNs = MailJD::Performance::PerformanceTrace::monotonicNowNs(); | |
| 115 | ✗ | auto &tracker = MailJD::Performance::BackgroundPerformanceTracker::instance(); | |
| 116 | ✗ | tracker.beginBackground({QStringLiteral("fts_rebuild"), QStringLiteral("fts_incremental"), | |
| 117 | ✗ | QStringLiteral("search_during_fts"), QStringLiteral("typing_during_fts")}, m_ftsRebuildStartNs); | |
| 118 | ✗ | tracker.backgroundPhaseStarted(QStringLiteral("fts_index")); | |
| 119 | ✗ | }); | |
| 120 |
1/1✓ Branch 99 → 100 taken 820 times.
|
820 | connect(m_fts.get(), &FtsIndexCoordinator::rebuildFinished, this, [this](bool success, quint64) { |
| 121 | ✗ | MailJD::Performance::StartupPerformanceTracker::instance().recordPhase( | |
| 122 | ✗ | QStringLiteral("background_fts_rebuild"), m_ftsRebuildStartNs, | |
| 123 | MailJD::Performance::PerformanceTrace::monotonicNowNs()); | ||
| 124 | ✗ | auto &tracker = MailJD::Performance::BackgroundPerformanceTracker::instance(); | |
| 125 | ✗ | tracker.backgroundPhaseFinished(QStringLiteral("fts_index")); | |
| 126 | ✗ | tracker.backgroundFinished(success); | |
| 127 | ✗ | m_ftsRebuildStartNs = 0; | |
| 128 | ✗ | }); | |
| 129 |
4/6✓ Branch 102 → 103 taken 820 times.
✓ Branch 103 → 104 taken 820 times.
✗ Branch 103 → 107 not taken.
✓ Branch 105 → 106 taken 820 times.
✓ Branch 106 → 107 taken 820 times.
✗ Branch 106 → 108 not taken.
|
820 | const bool opened = m_cache->isOpen() || m_cache->open(cachePath); |
| 130 |
3/3✓ Branch 109 → 110 taken 820 times.
✓ Branch 110 → 111 taken 820 times.
✓ Branch 114 → 115 taken 820 times.
|
1640 | MailJD::Performance::StartupPerformanceTracker::instance().recordPhase( |
| 131 | 1640 | QStringLiteral("mail_database_open_migrations"), started, | |
| 132 | MailJD::Performance::PerformanceTrace::monotonicNowNs()); | ||
| 133 |
1/2✗ Branch 117 → 118 not taken.
✓ Branch 117 → 123 taken 820 times.
|
820 | if (!opened) { |
| 134 | ✗ | m_profileError = m_cache->lastError(); | |
| 135 | ✗ | return false; | |
| 136 | } | ||
| 137 | // D2: session-owned calendar store and background CalDAV sync. The store | ||
| 138 | // path matches the Widgets frontend, so both see the same data (WAL keeps | ||
| 139 | // the connections safe). The coordinator's timer writes; the Quick | ||
| 140 | // projections reload on its storeChanged() signal. | ||
| 141 | { | ||
| 142 | const QString calendarDb = | ||
| 143 |
2/2✓ Branch 123 → 124 taken 820 times.
✓ Branch 124 → 125 taken 820 times.
|
1640 | QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)) |
| 144 |
1/1✓ Branch 128 → 129 taken 820 times.
|
820 | .filePath(QStringLiteral("mailjd/cache/calendar.db")); |
| 145 |
4/4✓ Branch 134 → 135 taken 820 times.
✓ Branch 135 → 136 taken 820 times.
✓ Branch 136 → 137 taken 820 times.
✓ Branch 137 → 138 taken 820 times.
|
820 | QDir().mkpath(QFileInfo(calendarDb).absolutePath()); |
| 146 |
1/1✓ Branch 142 → 143 taken 820 times.
|
820 | m_calendarStore = std::make_unique<CalendarStore>(this); |
| 147 |
2/3✓ Branch 146 → 147 taken 820 times.
✓ Branch 147 → 148 taken 820 times.
✗ Branch 147 → 187 not taken.
|
820 | if (m_calendarStore->open(calendarDb)) { |
| 148 |
1/1✓ Branch 148 → 149 taken 820 times.
|
820 | m_calendarSync = std::make_unique<CalendarSyncCoordinator>(calendarDb, this); |
| 149 |
2/4✓ Branch 3 → 4 taken 119 times.
✗ Branch 3 → 8 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 119 times.
|
939 | m_calendarSync->setNetworkGate([this] { return m_accounts && m_accounts->networkEnabled(); }); |
| 150 |
2/3✓ Branch 156 → 157 taken 820 times.
✗ Branch 156 → 159 not taken.
✓ Branch 158 → 159 taken 820 times.
|
820 | if (m_accounts) m_calendarSync->startBackgroundSync(); |
| 151 | 820 | DavPushCoordinator::Deps push; | |
| 152 |
2/2✓ Branch 161 → 162 taken 820 times.
✓ Branch 163 → 164 taken 820 times.
|
820 | push.sources.push_back(std::make_unique<CalendarPushSource>(m_calendarSync.get())); |
| 153 |
1/1✓ Branch 166 → 167 taken 820 times.
|
820 | push.secrets = std::make_unique<KeyringDavPushSecretStore>(); |
| 154 |
3/4✓ Branch 3 → 4 taken 1742 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 1741 times.
|
2562 | push.networkAllowed = [this] { return m_accounts && m_accounts->networkEnabled(); }; |
| 155 | 820 | push.resolveCredentials = [](QObject *owner, const QString &id, const QString &url, const QString &user, | |
| 156 | DavCredentials::PasswordCallback callback) { | ||
| 157 | ✗ | DavCredentials::readPasswordAsync(owner, id, url, user, std::move(callback)); | |
| 158 | 820 | }; | |
| 159 |
1/1✓ Branch 171 → 172 taken 820 times.
|
820 | push.lifecycle = NetworkLifecycleCoordinator::instance(); |
| 160 |
3/3✓ Branch 173 → 174 taken 820 times.
✓ Branch 177 → 178 taken 820 times.
✓ Branch 178 → 179 taken 820 times.
|
2460 | m_davPush = std::make_unique<DavPushCoordinator>(QDir(canonical).filePath(QStringLiteral("dav-push.sqlite")), |
| 161 | 1640 | std::move(push), this); | |
| 162 | 820 | } else { | |
| 163 | ✗ | m_calendarStore.reset(); | |
| 164 | ✗ | m_calendarSync.reset(); | |
| 165 | } | ||
| 166 | 820 | } | |
| 167 | // Q09: same runtime ownership for CardDAV contact sync into the shared | ||
| 168 | // contacts database (the Quick contact projection reloads on | ||
| 169 | // contactsChanged()). The shared DAV interval drives both coordinators. | ||
| 170 | { | ||
| 171 | const QString contactsDb = | ||
| 172 |
2/2✓ Branch 190 → 191 taken 820 times.
✓ Branch 191 → 192 taken 820 times.
|
1640 | QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)) |
| 173 |
1/1✓ Branch 195 → 196 taken 820 times.
|
820 | .filePath(QStringLiteral("mailjd/contacts.db")); |
| 174 |
4/4✓ Branch 201 → 202 taken 820 times.
✓ Branch 202 → 203 taken 820 times.
✓ Branch 203 → 204 taken 820 times.
✓ Branch 204 → 205 taken 820 times.
|
820 | QDir().mkpath(QFileInfo(contactsDb).absolutePath()); |
| 175 |
1/1✓ Branch 209 → 210 taken 820 times.
|
820 | m_contactSync = std::make_unique<ContactSyncCoordinator>(contactsDb, this); |
| 176 |
2/3✓ Branch 213 → 214 taken 820 times.
✗ Branch 214 → 215 not taken.
✓ Branch 214 → 216 taken 820 times.
|
820 | if (!m_contactSync->open()) m_contactSync.reset(); |
| 177 | else { | ||
| 178 |
3/4✓ Branch 3 → 4 taken 144 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 142 times.
|
964 | m_contactSync->setNetworkGate([this] { return m_accounts && m_accounts->networkEnabled(); }); |
| 179 |
2/3✓ Branch 221 → 222 taken 820 times.
✗ Branch 221 → 224 not taken.
✓ Branch 223 → 224 taken 820 times.
|
820 | if (m_accounts) m_contactSync->startBackgroundSync(); |
| 180 | } | ||
| 181 | 820 | } | |
| 182 | // Push starts once every collection source exists; address books join | ||
| 183 | // as a second source (S96.6.2). | ||
| 184 |
5/8✓ Branch 226 → 227 taken 820 times.
✗ Branch 226 → 230 not taken.
✓ Branch 228 → 229 taken 820 times.
✗ Branch 228 → 230 not taken.
✓ Branch 231 → 232 taken 820 times.
✗ Branch 231 → 240 not taken.
✓ Branch 234 → 235 taken 820 times.
✓ Branch 236 → 237 taken 820 times.
|
820 | if (m_davPush && m_contactSync) m_davPush->addSource(std::make_unique<ContactPushSource>(m_contactSync.get())); |
| 185 |
6/10✓ Branch 241 → 242 taken 820 times.
✗ Branch 241 → 248 not taken.
✓ Branch 243 → 244 taken 820 times.
✗ Branch 243 → 248 not taken.
✓ Branch 245 → 246 taken 820 times.
✓ Branch 246 → 247 taken 820 times.
✗ Branch 246 → 248 not taken.
✓ Branch 249 → 250 taken 820 times.
✗ Branch 249 → 252 not taken.
✓ Branch 251 → 252 taken 820 times.
|
820 | if (m_davPush && m_accounts && m_davPush->isOpen()) m_davPush->start(); |
| 186 | 820 | m_profileDirectory = canonical; | |
| 187 |
1/1✓ Branch 259 → 260 taken 820 times.
|
1640 | m_settingsSync = std::make_unique<SettingsSyncSession>(*m_accounts, canonical, |
| 188 |
1/2✓ Branch 254 → 255 taken 820 times.
✗ Branch 254 → 257 not taken.
|
2460 | m_calendarStore ? m_calendarStore->databasePath() : QString{}, this); |
| 189 |
2/4✓ Branch 265 → 266 taken 820 times.
✗ Branch 266 → 267 not taken.
✓ Branch 266 → 269 taken 820 times.
✗ Branch 268 → 269 not taken.
|
820 | if (m_cache->search().searchIndexNeedsRebuild()) m_fts->requestRebuild(); |
| 190 | 820 | return true; | |
| 191 | 820 | } | |
| 192 |