app/core/BackgroundAccountSession.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QHash> | ||
| 4 | #include <QObject> | ||
| 5 | #include <QPointer> | ||
| 6 | #include <QStringList> | ||
| 7 | #include <QTimer> | ||
| 8 | |||
| 9 | #include <functional> | ||
| 10 | #include <vector> | ||
| 11 | |||
| 12 | #include "data/AccountConfig.h" | ||
| 13 | #include "service/ImapService.h" | ||
| 14 | |||
| 15 | class ConnectionHealthMonitor; | ||
| 16 | class MailCache; | ||
| 17 | struct StatusResult; | ||
| 18 | |||
| 19 | // F-50 (SPRINT-104, D-11): every configured account stays online, not only | ||
| 20 | // the one selected in the account selector. One connection per non-selected | ||
| 21 | // account watches its folders with a slow STATUS round and publishes the | ||
| 22 | // unread badges into the shared cache, so background arrivals can notify | ||
| 23 | // (the account-scoped arrival summaries of F-36) and the counts are fresh | ||
| 24 | // when the user switches. | ||
| 25 | // | ||
| 26 | // v1 scope (SPRINT-104-F50-BACKGROUND-ACCOUNTS.md): badges and arrival | ||
| 27 | // signals only — no background header sync and no replay of queued journal | ||
| 28 | // actions; those stay with the selected account. Watching uses STATUS | ||
| 29 | // polling (RFC 3501): RFC 5461 NOTIFY requires a selected mailbox in this | ||
| 30 | // client's push session, which a background connection never has; the | ||
| 31 | // deviation from the package's "NOTIFY where available" is recorded there. | ||
| 32 | // | ||
| 33 | // Deliberately defensive about startup: the first connect happens seconds | ||
| 34 | // after the session loaded (never on the critical path) and accounts | ||
| 35 | // stagger, so a profile with many accounts cannot produce a connection | ||
| 36 | // burst against one server. | ||
| 37 | class BackgroundAccountSession final : public QObject { | ||
| 38 | Q_OBJECT | ||
| 39 | public: | ||
| 40 | struct Deps { | ||
| 41 | MailCache *cache = nullptr; | ||
| 42 | // Non-secret account configurations (passwords empty). | ||
| 43 | std::function<std::vector<AccountConfig>()> configurations; | ||
| 44 | std::function<QString()> selectedAccountId; | ||
| 45 | std::function<bool()> networkEnabled; | ||
| 46 | // Resolves the passwords of the given accounts asynchronously; tests | ||
| 47 | // inject a synchronous filler. | ||
| 48 | std::function<void(std::vector<AccountConfig> &, std::function<void()>)> resolveCredentials; | ||
| 49 | int startDelayMs = 8000; // after startup — off the critical path | ||
| 50 | int staggerMs = 3000; // between accounts | ||
| 51 | int pollIntervalMs = 600000; // one STATUS round every 10 minutes | ||
| 52 | int maxFolders = 50; // bounded rounds for huge folder trees | ||
| 53 | bool connectToServer = true; // test seam: no socket without it | ||
| 54 | }; | ||
| 55 | |||
| 56 | explicit BackgroundAccountSession(Deps deps, QObject *parent = nullptr); | ||
| 57 | ~BackgroundAccountSession() override; | ||
| 58 | |||
| 59 | // Production wiring: AccountSession::accountsLoaded / configurationChanged | ||
| 60 | // / configurationRemoved. reconcile() starts the missing watches (staggered) | ||
| 61 | // and stops the removed and the now-selected ones. | ||
| 62 | void onAccountsLoaded(); | ||
| 63 | void onConfigurationChanged(const QString &id); | ||
| 64 | void onConfigurationRemoved(const QString &id); | ||
| 65 | void setNetworkEnabled(bool enabled); | ||
| 66 | |||
| 67 | // Production handlers for the per-account service signals; also the test | ||
| 68 | // seams — state transitions and STATUS answers are injected directly. | ||
| 69 | void onStateChanged(const QString &accountId, ImapService::State state); | ||
| 70 | void onFolderStatus(const QString &accountId, const StatusResult &result); | ||
| 71 | // One STATUS round of one account (timer target; tests call it directly). | ||
| 72 | void pollNow(const QString &accountId); | ||
| 73 | |||
| 74 | 64 | bool isWatching(const QString &accountId) const { return m_watches.contains(accountId); } | |
| 75 | 3 | bool networkEnabled() const { return m_networkEnabled; } | |
| 76 | bool isReady(const QString &accountId) const; // authenticated once | ||
| 77 | 1 | QStringList watchedAccountIds() const { return m_watches.keys(); } | |
| 78 | // Test seam: the account's service (the watch owns it). | ||
| 79 | ImapService *serviceForTests(const QString &accountId) const; | ||
| 80 | |||
| 81 | signals: | ||
| 82 | // One folder's unread count changed while its account was not selected. | ||
| 83 | void backgroundUnreadChanged(const QString &accountId, const QString &accountName, | ||
| 84 | const QString &folderPath, int unread); | ||
| 85 | // A watch could not start (no password) or its account vanished. | ||
| 86 | void watchFailed(const QString &accountId, const QString &reason); | ||
| 87 | |||
| 88 | private: | ||
| 89 | struct Watch { | ||
| 90 | QString name; | ||
| 91 | ImapConfig config; // password resolved before connecting | ||
| 92 | ImapService *service = nullptr; | ||
| 93 | ConnectionHealthMonitor *monitor = nullptr; | ||
| 94 | QStringList folders; | ||
| 95 | bool ready = false; | ||
| 96 | }; | ||
| 97 | |||
| 98 | void reconcile(); | ||
| 99 | void startWatch(const QString &accountId, const AccountConfig &config, int delayMs); | ||
| 100 | void stopWatch(const QString &accountId); | ||
| 101 | void pollWatch(const QString &accountId, Watch &watch); | ||
| 102 | |||
| 103 | Deps m_deps; | ||
| 104 | QHash<QString, Watch> m_watches; | ||
| 105 | QTimer m_pollTimer; | ||
| 106 | quint64 m_epoch = 0; // invalidates scheduled starts after a reload | ||
| 107 | bool m_networkEnabled = false; | ||
| 108 | }; | ||
| 109 |