presentation/QuickMailBackendFolders.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Folder operation results (create, delete, rename) of QuickMailBackend, | ||
| 2 | // split from QuickMailBackend.cpp for its line limit. | ||
| 3 | #include "QuickMailBackend.h" | ||
| 4 | |||
| 5 | #include "app/core/FolderPredictionSession.h" | ||
| 6 | #include "app/core/MailSessionCore.h" | ||
| 7 | #include "data/FolderPredictor.h" | ||
| 8 | #include "data/FolderPreferences.h" | ||
| 9 | #include "data/MailCache.h" | ||
| 10 | #include "service/ImapResponseParser.h" | ||
| 11 | #include "service/ImapService.h" | ||
| 12 | |||
| 13 | #include <QCoreApplication> | ||
| 14 | #include <QLoggingCategory> | ||
| 15 | #include <QTimer> | ||
| 16 | #include <algorithm> | ||
| 17 | |||
| 18 | Q_DECLARE_LOGGING_CATEGORY(lcQuickBackend) | ||
| 19 | |||
| 20 | namespace { | ||
| 21 | // S106-211: the learned suggestions follow a renamed folder. The rename | ||
| 22 | // waits for a training batch holding the predictor's writer lock; when it | ||
| 23 | // still finds the database busy it is tried again a little later, so the | ||
| 24 | // rows never stay behind under the old path. | ||
| 25 | ✗ | void renamePredictorData(const QString &predictorPath, const QString &oldPath, const QString &newPath, | |
| 26 | const QString &delimiter, int attempts) { | ||
| 27 | ✗ | FolderPredictor predictor; | |
| 28 | ✗ | if (predictor.open(predictorPath) && predictor.renameFolderData(oldPath, newPath, delimiter)) return; | |
| 29 | ✗ | if (--attempts <= 0) { | |
| 30 | ✗ | qCWarning(lcQuickBackend) << "Folder suggestions could not follow a folder rename"; | |
| 31 | ✗ | return; | |
| 32 | } | ||
| 33 | ✗ | QTimer::singleShot(1000, QCoreApplication::instance(), [=] { | |
| 34 | ✗ | renamePredictorData(predictorPath, oldPath, newPath, delimiter, attempts); | |
| 35 | ✗ | }); | |
| 36 | ✗ | } | |
| 37 | } // namespace | ||
| 38 | |||
| 39 | 795 | void QuickMailBackend::wireFolderOperations() { | |
| 40 | 795 | MailSessionCore &session = m_session; | |
| 41 | // Folder operation results (D1): apply the server answer locally and | ||
| 42 | // surface it as status feedback. | ||
| 43 |
1/1✓ Branch 3 → 4 taken 795 times.
|
795 | connect(session.imap(), &ImapService::folderCreated, this, [this](const QString &path) { |
| 44 | ✗ | FolderInfo info; | |
| 45 | ✗ | info.name = ImapResponseParser::decodeMailboxName(path); | |
| 46 | ✗ | info.path = path; | |
| 47 | ✗ | info.delimiter = m_folderActions.folderDelimiter(); | |
| 48 | ✗ | for (const auto &existing : m_folderInfos) | |
| 49 | ✗ | if (existing.path == path) return; // LIST refresh will carry the real entry. | |
| 50 | ✗ | m_folderInfos.append(info); | |
| 51 | ✗ | m_availableFolders.insert(path); | |
| 52 | ✗ | applyFolderList(m_folderInfos, false); | |
| 53 | ✗ | setStatus(tr("Folder created: %1").arg(info.name)); | |
| 54 | ✗ | }); | |
| 55 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | connect(session.imap(), &ImapService::folderDeleted, this, [this](const QString &path) { |
| 56 | ✗ | m_folderInfos.erase(std::remove_if(m_folderInfos.begin(), m_folderInfos.end(), | |
| 57 | ✗ | [&](const FolderInfo &folder) { | |
| 58 | ✗ | return folder.path == path || | |
| 59 | ✗ | folder.path.startsWith(path + m_folderActions.folderDelimiter()); | |
| 60 | }), | ||
| 61 | ✗ | m_folderInfos.end()); | |
| 62 | ✗ | applyFolderList(m_folderInfos, false); | |
| 63 | ✗ | setStatus(tr("Folder deleted: %1").arg(path)); | |
| 64 | ✗ | }); | |
| 65 |
1/1✓ Branch 9 → 10 taken 795 times.
|
795 | connect(session.imap(), &ImapService::folderRenamed, this, [this](const QString &oldPath, const QString &newPath) { |
| 66 | ✗ | m_rules.folderRenamed(oldPath, newPath); // Sprint 95 S95.4.7: rules follow the folder | |
| 67 | ✗ | const QString delimiter = m_folderActions.folderDelimiter(); | |
| 68 | // F-51 (SPRINT-104): cache, move-learning and path-keyed preferences | ||
| 69 | // move with the folder and its children, so the renamed folder keeps | ||
| 70 | // its cached mail, unread subscription, learned suggestions and | ||
| 71 | // checkpoint instead of being re-downloaded from scratch. | ||
| 72 | ✗ | if (!m_session.cache()->databasePath().isEmpty()) | |
| 73 | ✗ | m_session.cache()->renameFolderPath(accountName(), oldPath, newPath, delimiter); | |
| 74 | ✗ | if (const QString predictorPath = m_session.predictions()->databasePath(); | |
| 75 | ✗ | !predictorPath.isEmpty()) | |
| 76 | ✗ | renamePredictorData(predictorPath, oldPath, newPath, delimiter, 3); | |
| 77 | // Sprint 105 S105.3.4: the evidence model is rebuilt with the new paths. | ||
| 78 | ✗ | FolderPredictionService::shared()->forgetAccount(accountName()); | |
| 79 | ✗ | m_predictions.clear(); | |
| 80 | ✗ | FolderPreferences::renameFolder(m_session.profileDirectory(), accountName(), | |
| 81 | oldPath, newPath, delimiter); | ||
| 82 | ✗ | for (auto &folder : m_folderInfos) { | |
| 83 | ✗ | if (folder.path == oldPath) { | |
| 84 | ✗ | folder.path = newPath; | |
| 85 | ✗ | folder.name = ImapResponseParser::decodeMailboxName(newPath); | |
| 86 | ✗ | } else if (folder.path.startsWith(oldPath + delimiter)) { | |
| 87 | ✗ | folder.path = newPath + folder.path.mid(oldPath.size()); | |
| 88 | } | ||
| 89 | } | ||
| 90 | ✗ | m_availableFolders.remove(oldPath); | |
| 91 | ✗ | m_availableFolders.insert(newPath); | |
| 92 | ✗ | applyFolderList(m_folderInfos, false); | |
| 93 | ✗ | if (m_folderPath == oldPath) { | |
| 94 | ✗ | m_folderPath = newPath; | |
| 95 | ✗ | emit folderChanged(); | |
| 96 | } | ||
| 97 | ✗ | setStatus(tr("Folder renamed: %1").arg(newPath)); | |
| 98 | ✗ | }); | |
| 99 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(session.imap(), &ImapService::folderOperationError, this, |
| 100 | 795 | [this](const QString &operation, const QString &error) { | |
| 101 |
3/3✓ Branch 2 → 3 taken 8 times.
✓ Branch 3 → 4 taken 8 times.
✓ Branch 4 → 5 taken 8 times.
|
8 | setStatus(tr("Folder %1 failed: %2").arg(operation, error)); |
| 102 | 8 | }); | |
| 103 | 795 | } | |
| 104 |