service/SettingsSyncService.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QObject> | ||
| 4 | #include <QString> | ||
| 5 | #include <QTimer> | ||
| 6 | #include <functional> | ||
| 7 | |||
| 8 | #include "data/AccountConfig.h" | ||
| 9 | #include "data/Models.h" | ||
| 10 | #include "data/SettingsSyncModels.h" | ||
| 11 | #include "service/ImapService.h" | ||
| 12 | |||
| 13 | class ConnectionHealthMonitor; | ||
| 14 | |||
| 15 | // T-312/T-314: Manages settings synchronisation via a dedicated IMAP connection. | ||
| 16 | // Stores settings as a virtual mail (JSON body) in a configurable IMAP folder. | ||
| 17 | // Uses IMAP IDLE for real-time change detection from other clients. | ||
| 18 | // | ||
| 19 | // Architecture: owns its own ImapService instance (separate from the main | ||
| 20 | // connection) to avoid interference with regular mail operations. | ||
| 21 | // | ||
| 22 | // T-720: auto-reconnect (and dead-socket detection) for the dedicated IMAP | ||
| 23 | // connection is delegated to ConnectionHealthMonitor (Sprint 72). The | ||
| 24 | // m_reconnectTimer/m_reconnectAttempts members and handleDisconnect()/ | ||
| 25 | // onReconnectTimer() methods have been removed. | ||
| 26 | class SettingsSyncService : public QObject { | ||
| 27 | 3 | Q_OBJECT | |
| 28 | #ifdef MAILJD_UNIT_TEST | ||
| 29 | friend class TestSettingsSyncService; | ||
| 30 | friend class TestSprint79Services; | ||
| 31 | #endif | ||
| 32 | |||
| 33 | public: | ||
| 34 | explicit SettingsSyncService(QObject *parent = nullptr); | ||
| 35 | ~SettingsSyncService() override; | ||
| 36 | |||
| 37 | // Configure and connect | ||
| 38 | void configure(const ImapConfig &config, const QString &syncFolder); | ||
| 39 | void setEnabled(bool enabled); | ||
| 40 | 4 | bool isEnabled() const { return m_enabled; } | |
| 41 | 2 | QString syncFolder() const { return m_syncFolder; } | |
| 42 | bool readyForOperation() const; | ||
| 43 | |||
| 44 | #ifdef MAILJD_E2E_TESTING | ||
| 45 | // Read-only transport seam for platform lifecycle assertions. Production | ||
| 46 | // callers cannot reach the owned IMAP connection. | ||
| 47 | ImapService *transportForE2e() const { return m_imap; } | ||
| 48 | #endif | ||
| 49 | |||
| 50 | // Upload current settings to IMAP | ||
| 51 | void uploadSettings(const SyncPayload &payload); | ||
| 52 | |||
| 53 | // Fetch latest settings from IMAP | ||
| 54 | void fetchLatestSettings(); | ||
| 55 | |||
| 56 | // Start/stop IDLE-watching on the sync folder | ||
| 57 | void startWatching(); | ||
| 58 | void stopWatching(); | ||
| 59 | |||
| 60 | // Disconnect and clean up | ||
| 61 | void shutdown(); | ||
| 62 | |||
| 63 | signals: | ||
| 64 | void settingsReceived(const SyncPayload &payload); | ||
| 65 | void fetchComplete(bool settingsFound); | ||
| 66 | void uploadComplete(); | ||
| 67 | void syncError(const QString &error); | ||
| 68 | void syncFolderReady(); | ||
| 69 | void connected(); | ||
| 70 | void connectionLost(); | ||
| 71 | |||
| 72 | private slots: | ||
| 73 | void onStateChanged(ImapService::State newState); | ||
| 74 | void onFolderListReceived(const QList<FolderInfo> &folders); | ||
| 75 | void onFolderSelected(const QString &path, int messageCount, | ||
| 76 | quint32 uidValidity, quint64 highestModseq); | ||
| 77 | void onFolderCreated(const QString &folderPath); | ||
| 78 | void onSearchResultReceived(const QList<qint64> &uids); | ||
| 79 | void onRawBodyReceived(qint64 uid, const QByteArray &rawBody); | ||
| 80 | void fetchNextCandidate(); | ||
| 81 | void onMessageAppended(const QString &folder, qint64 uid); | ||
| 82 | void onIdleNewMessages(int newCount); | ||
| 83 | void onAppendError(const QString &error); | ||
| 84 | void onFolderOperationError(const QString &op, const QString &error); | ||
| 85 | void onExpungeComplete(); | ||
| 86 | void onExpungeFailed(const QString &error); | ||
| 87 | |||
| 88 | private: | ||
| 89 | enum class Phase { | ||
| 90 | Idle, | ||
| 91 | CheckingFolder, // LIST to check if sync folder exists | ||
| 92 | CreatingFolder, // CREATE sync folder | ||
| 93 | SelectingFolder, // SELECT sync folder | ||
| 94 | Fetching, // SEARCH + FETCH latest settings | ||
| 95 | Uploading, // APPEND new settings | ||
| 96 | CleaningUp, // Recheck UIDVALIDITY, remove one validated previous version | ||
| 97 | WatchingIdle, // IDLE on sync folder | ||
| 98 | }; | ||
| 99 | |||
| 100 | void retireTransport(ImapService::DisconnectReason reason); | ||
| 101 | void afterIdle(std::function<void()> command); | ||
| 102 | quint64 m_generation = 0; | ||
| 103 | void checkOrCreateFolder(); | ||
| 104 | void doFetch(); | ||
| 105 | void doUpload(); | ||
| 106 | void cleanupOldSettings(qint64 keepUid); | ||
| 107 | void finishUpload(); | ||
| 108 | void startIdleWatch(); | ||
| 109 | |||
| 110 | ImapService *m_imap = nullptr; | ||
| 111 | // T-720: Health monitor for the dedicated IMAP connection. Periodic | ||
| 112 | // liveness probes + exponential-backoff reconnect + suspend/network | ||
| 113 | // reactivity — same code path as the main + body + search connections. | ||
| 114 | ConnectionHealthMonitor *m_health = nullptr; | ||
| 115 | ImapConfig m_config; | ||
| 116 | QString m_syncFolder; | ||
| 117 | bool m_enabled = false; | ||
| 118 | Phase m_phase = Phase::Idle; | ||
| 119 | |||
| 120 | // State tracking | ||
| 121 | qint64 m_currentSettingsUid = -1; // UID of the current settings mail | ||
| 122 | QList<qint64> m_candidateUids; // remaining search hits, newest first | ||
| 123 | int m_messageCount = 0; // Messages in sync folder | ||
| 124 | QByteArray m_pendingUpload; // RFC-2822 message to upload | ||
| 125 | qint64 m_newAppendedUid = -1; // UID of just-appended message | ||
| 126 | qint64 m_validatedSettingsUid = -1; | ||
| 127 | quint32 m_validatedUidValidity = 0; | ||
| 128 | qint64 m_previousSettingsUid = -1; | ||
| 129 | quint32 m_uploadUidValidity = 0; | ||
| 130 | }; | ||
| 131 |