app/core/SieveSession.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QDateTime> | ||
| 4 | #include <QList> | ||
| 5 | #include <QObject> | ||
| 6 | #include <QPointer> | ||
| 7 | |||
| 8 | #include <functional> | ||
| 9 | #include <memory> | ||
| 10 | |||
| 11 | #include "data/SieveHistory.h" | ||
| 12 | #include "data/SieveMerge.h" | ||
| 13 | #include "data/SieveRuleCodec.h" | ||
| 14 | #include "service/ManageSieveClient.h" | ||
| 15 | |||
| 16 | // Rule session of one account (Sprint 95 S95.1.9, SIEVE-SESSION). | ||
| 17 | // | ||
| 18 | // Loads the active script (LISTSCRIPTS + GETSCRIPT, parse off the UI thread | ||
| 19 | // above 64 KiB), keeps the base version the user's edits start from, and | ||
| 20 | // uploads through one pipeline: | ||
| 21 | // CHECKSCRIPT (or temp-name PUTSCRIPT on pre-1.0 servers) | ||
| 22 | // → GETSCRIPT + SHA-256 compare with the base (D-7: merge or conflict) | ||
| 23 | // → HAVESPACE → PUTSCRIPT → SETACTIVE (new script only) | ||
| 24 | // → history entry of the replaced version (D-8) | ||
| 25 | // → GETSCRIPT again to detect a lost race. | ||
| 26 | // | ||
| 27 | // Invariants: | ||
| 28 | // - No PUTSCRIPT/SETACTIVE without a preceding successful check of the | ||
| 29 | // exact bytes uploaded (rule 3, gate G5); a rejected script is never | ||
| 30 | // activated. | ||
| 31 | // - A version changed elsewhere is never overwritten silently: disjoint | ||
| 32 | // changes merge, overlapping ones end in SaveOutcome::Conflict unless the | ||
| 33 | // caller passes `keepMine` (gate G6). | ||
| 34 | // - One upload at a time; later saves queue and re-run the check. Uploads | ||
| 35 | // and scripts-panel changes (activate/rename/delete) never overlap. | ||
| 36 | // - Capabilities are read after login(), from the upload's connection. | ||
| 37 | // - Uploads are never retried automatically; a transport failure keeps | ||
| 38 | // the caller's draft ("Not saved to the server"). | ||
| 39 | // - A script that does not parse is published read-only and is only | ||
| 40 | // replaced by an explicit script-editor save. | ||
| 41 | // - Logs carry sizes, hashes, codes and timings, never script text. | ||
| 42 | class SieveSession : public QObject { | ||
| 43 | Q_OBJECT | ||
| 44 | |||
| 45 | public: | ||
| 46 | enum class Status { Idle, Loading, Ready, Saving, Offline, Error }; | ||
| 47 | |||
| 48 | struct Config { | ||
| 49 | QString accountId; | ||
| 50 | SieveServerConfig server; // password is supplied by the provider | ||
| 51 | QString storageDirectory; // ~/.config/mailjd/sieve/<account-id> | ||
| 52 | }; | ||
| 53 | |||
| 54 | using PasswordCallback = std::function<void(bool ok, const QByteArray &password)>; | ||
| 55 | using CredentialProvider = std::function<void(PasswordCallback)>; | ||
| 56 | |||
| 57 | struct SaveOptions { | ||
| 58 | bool keepMine = false; // conflict resolution "Keep mine" | ||
| 59 | QString reason; // for logs: "rule", "script", "rename", … | ||
| 60 | // S95.3.10: stop before uploading a merge result (Kind::MergeReview) | ||
| 61 | // so the caller can show it; the reviewed upload passes the server | ||
| 62 | // version it was merged with as `expectedServer`. | ||
| 63 | bool reviewMerge = false; | ||
| 64 | QByteArray expectedServer; | ||
| 65 | // S106-319: the script the bytes were rendered from (null: the session's | ||
| 66 | // base when the upload runs). An edit rendered while an earlier save was | ||
| 67 | // in flight has the older base; it is merged onto the server version | ||
| 68 | // like a change made elsewhere instead of overwriting the earlier save. | ||
| 69 | QByteArray renderedFrom; | ||
| 70 | }; | ||
| 71 | |||
| 72 | struct SaveOutcome { | ||
| 73 | enum class Kind { Saved, Merged, Conflict, Rejected, Quota, Failed, NotLoaded, MergeReview }; | ||
| 74 | Kind kind = Kind::Failed; | ||
| 75 | QString message; // technical detail ("Details") | ||
| 76 | QList<ManageSieve::ServerError> errors; // CHECKSCRIPT/PUTSCRIPT positions | ||
| 77 | Sieve::MergeResult merge; // Merged / Conflict | ||
| 78 | QByteArray theirs; // server version on Conflict | ||
| 79 | QByteArray uploaded; // bytes now on the server | ||
| 80 | bool raceDetected = false; // changed again during the upload | ||
| 81 | ManageSieveClient::ErrorKind transportError = ManageSieveClient::ErrorKind::None; | ||
| 82 | |||
| 83 |
4/4✓ Branch 2 → 3 taken 75 times.
✓ Branch 2 → 4 taken 542 times.
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 5 taken 68 times.
|
617 | bool ok() const { return kind == Kind::Saved || kind == Kind::Merged; } |
| 84 | }; | ||
| 85 | using SaveCallback = std::function<void(const SaveOutcome &)>; | ||
| 86 | using DoneCallback = std::function<void(bool ok, const QString &message)>; | ||
| 87 | |||
| 88 | explicit SieveSession(const Config &config, QObject *parent = nullptr); | ||
| 89 | ~SieveSession() override; | ||
| 90 | |||
| 91 | 235 | void setCredentialProvider(CredentialProvider provider) { m_credentials = std::move(provider); } | |
| 92 | 40 | void setServer(const SieveServerConfig &server) { m_config.server = server; } | |
| 93 | 234 | ManageSieveClient *client() { return m_client; } | |
| 94 | 41 | SieveHistory &history() { return m_history; } | |
| 95 | |||
| 96 | void load(); | ||
| 97 | void save(const QByteArray &script, const SaveOptions &options, SaveCallback callback); | ||
| 98 | // CHECKSCRIPT only (script editor "Check"); Saved = valid. Servers | ||
| 99 | // without CHECKSCRIPT report Saved with the message "unchecked". | ||
| 100 | void check(const QByteArray &script, SaveCallback callback); | ||
| 101 | |||
| 102 | // Scripts panel (D-2): every call refreshes the script list afterwards. | ||
| 103 | void activateScript(const QString &name, DoneCallback callback); | ||
| 104 | void renameScript(const QString &from, const QString &to, DoneCallback callback); | ||
| 105 | void deleteScript(const QString &name, DoneCallback callback); | ||
| 106 | |||
| 107 | 11570 | Status status() const { return m_status; } | |
| 108 | 68 | QString statusMessage() const { return m_statusMessage; } | |
| 109 | 3 | ManageSieveClient::ErrorKind lastError() const { return m_lastError; } | |
| 110 | 4620 | const Sieve::Document &document() const { return m_document; } | |
| 111 | 111 | QByteArray baseScript() const { return m_base; } | |
| 112 | QString baseHash() const; | ||
| 113 | 213 | QString activeScript() const { return m_activeScript; } | |
| 114 | QString editedScriptName() const; // active, or "mailjd" for a first script | ||
| 115 | 7 | QList<ManageSieve::ScriptEntry> scripts() const { return m_scripts; } | |
| 116 | 1259 | const ManageSieve::Capabilities &capabilities() const { return m_client->capabilities(); } | |
| 117 | bool isOfflineCopy() const { return m_status == Status::Offline; } | ||
| 118 | 1 | QDateTime loadedAt() const { return m_loadedAt; } | |
| 119 | 72 | QDateTime savedAt() const { return m_savedAt; } | |
| 120 | // A save, a load or a scripts-panel change runs; the panel's buttons | ||
| 121 | // are disabled meanwhile (S106-101). | ||
| 122 |
6/6✓ Branch 2 → 3 taken 15 times.
✓ Branch 2 → 5 taken 5 times.
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 13 times.
|
20 | bool busy() const { return m_saving || m_panelBusy || m_status == Status::Loading; } |
| 123 | // Test seam: runs on the worker thread of every off-thread decode. | ||
| 124 | 2 | void setDecodeProbe(std::function<void()> probe) { m_decodeProbe = std::move(probe); } | |
| 125 | |||
| 126 | static constexpr int kOffThreadParseBytes = 64 * 1024; | ||
| 127 | 290 | static QString newScriptName() { return QStringLiteral("mailjd"); } | |
| 128 | |||
| 129 | signals: | ||
| 130 | void statusChanged(); | ||
| 131 | void documentChanged(); | ||
| 132 | void scriptsChanged(); | ||
| 133 | void saved(const QString &reason); | ||
| 134 | |||
| 135 | private: | ||
| 136 | struct SaveJob { | ||
| 137 | QByteArray script; | ||
| 138 | SaveOptions options; | ||
| 139 | SaveCallback callback; | ||
| 140 | }; | ||
| 141 | struct Upload; // pipeline state of the running save | ||
| 142 | |||
| 143 | void withCredentials(std::function<void(bool)> next); | ||
| 144 | void loadActive(const QList<ManageSieve::ScriptEntry> &scripts); | ||
| 145 | void publish(const QByteArray &script, const QString &scriptName, bool offline); | ||
| 146 | void applyDocument(const QByteArray &script, Sieve::Document document); | ||
| 147 | void decodeScript(const QByteArray &script, std::function<void(Sieve::Document)> done); | ||
| 148 | bool beginPanelChange(const DoneCallback &callback); | ||
| 149 | DoneCallback endPanelChange(DoneCallback callback); | ||
| 150 | void loadFailed(const ManageSieveClient::Result &result); | ||
| 151 | void setStatus(Status status, const QString &message = {}); | ||
| 152 | void refreshScripts(DoneCallback callback, bool ok, const QString &message); | ||
| 153 | |||
| 154 | void startNextSave(); | ||
| 155 | void checkStep(const std::shared_ptr<Upload> &upload); | ||
| 156 | void concurrencyStep(const std::shared_ptr<Upload> &upload); | ||
| 157 | void spaceStep(const std::shared_ptr<Upload> &upload); | ||
| 158 | void tempValidationStep(const std::shared_ptr<Upload> &upload); | ||
| 159 | void putStep(const std::shared_ptr<Upload> &upload); | ||
| 160 | void activateStep(const std::shared_ptr<Upload> &upload); | ||
| 161 | void verifyStep(const std::shared_ptr<Upload> &upload); | ||
| 162 | void finishSave(const std::shared_ptr<Upload> &upload, SaveOutcome outcome); | ||
| 163 | SaveOutcome failure(const ManageSieveClient::Result &result) const; | ||
| 164 | |||
| 165 | Config m_config; | ||
| 166 | ManageSieveClient *m_client = nullptr; | ||
| 167 | SieveHistory m_history; | ||
| 168 | CredentialProvider m_credentials; | ||
| 169 | Status m_status = Status::Idle; | ||
| 170 | QString m_statusMessage; | ||
| 171 | ManageSieveClient::ErrorKind m_lastError = ManageSieveClient::ErrorKind::None; | ||
| 172 | QList<ManageSieve::ScriptEntry> m_scripts; | ||
| 173 | QString m_activeScript; | ||
| 174 | QByteArray m_base; | ||
| 175 | Sieve::Document m_document; | ||
| 176 | QDateTime m_loadedAt; | ||
| 177 | QDateTime m_savedAt; | ||
| 178 | QList<SaveJob> m_saveQueue; | ||
| 179 | bool m_saving = false; | ||
| 180 | bool m_panelBusy = false; | ||
| 181 | quint64 m_loadGeneration = 0; | ||
| 182 | std::function<void()> m_decodeProbe; | ||
| 183 | }; | ||
| 184 |