Branch data Line data Source code
1 : : #include "FolderOperationsController.h"
2 : :
3 : : #include <QInputDialog>
4 : : #include <QLineEdit>
5 : : #include <QMessageBox>
6 : : #include <algorithm>
7 : : #include <memory>
8 : :
9 : : #include "service/ImapService.h"
10 : : #include "ui/CommandBar.h"
11 : : #include "ui/FolderTree.h"
12 : : #include "ui/PlainTextMessageBox.h"
13 : :
14 : 59 : FolderOperationsController::FolderOperationsController(const Deps &deps,
15 : 59 : QObject *parent)
16 : 59 : : QObject(parent), m_d(deps) {
17 : : // Modal test seams: real dialogs by default (unit tests override).
18 : : // SEC-2026-07-21-16: Escape the label text to prevent UI spoofing via
19 : : // server-delivered IMAP folder names interpolated into QInputDialog labels.
20 : 1 : m_promptText = [this](const QString &title, const QString &label,
21 : : const QString &initial, bool *ok) {
22 [ + - ]: 2 : return QInputDialog::getText(m_d.folderTree, title, label.toHtmlEscaped(),
23 [ + - ]: 2 : QLineEdit::Normal, initial, ok);
24 : 59 : };
25 : 119 : m_confirm = [this](const QString &title, const QString &text) {
26 : 1 : return PlainTextMessageBox::question(m_d.folderTree, title, text) ==
27 : 1 : QMessageBox::Yes;
28 : 59 : };
29 : :
30 : : // T-290: FolderTree context-menu requests
31 : 59 : connect(m_d.folderTree, &FolderTree::createFolderRequested, this,
32 [ + - ]: 59 : &FolderOperationsController::createFolder);
33 : 59 : connect(m_d.folderTree, &FolderTree::deleteFolderRequested, this,
34 [ + - ]: 59 : &FolderOperationsController::deleteFolder);
35 : 59 : connect(m_d.folderTree, &FolderTree::renameFolderRequested, this,
36 [ + - ]: 59 : &FolderOperationsController::renameFolder);
37 : 59 : connect(m_d.folderTree, &FolderTree::moveFolderRequested, this,
38 [ + - ]: 59 : &FolderOperationsController::moveFolder);
39 : 59 : }
40 : :
41 : 33 : QStringList FolderOperationsController::folderFlagsForPath(
42 : : const QString &folderPath) const {
43 [ + + ]: 55 : for (const auto &folder : m_folders) {
44 [ + + ]: 44 : if (folder.path == folderPath)
45 : 22 : return folder.flags;
46 : : }
47 : 11 : return {};
48 : : }
49 : :
50 : 30 : bool FolderOperationsController::isProtectedFolderPath(
51 : : const QString &folderPath) const {
52 [ + - ]: 30 : return FolderTree::isSpecialFolder(folderPath,
53 [ + - ]: 60 : folderFlagsForPath(folderPath));
54 : : }
55 : :
56 : 9 : void FolderOperationsController::createFolder(const QString &parentPath) {
57 : 9 : bool ok = false;
58 : : QString name = m_promptText(
59 : 18 : QStringLiteral("Neuer Ordner"),
60 : 9 : parentPath.isEmpty()
61 [ + + + - : 32 : ? QStringLiteral("Name des neuen Ordners:")
- - ]
62 [ + + + + : 13 : : QStringLiteral("Name des neuen Unterordners von '%1':")
+ + - - -
- ]
63 : : .arg(parentPath),
64 [ + - ]: 27 : QString(), &ok);
65 : :
66 [ + + + - : 9 : if (!ok || name.trimmed().isEmpty())
+ + + + +
+ - - ]
67 : 4 : return;
68 : :
69 [ + - ]: 5 : name = name.trimmed();
70 : 5 : QString fullPath = parentPath.isEmpty()
71 : 5 : ? name
72 [ + + + - : 5 : : parentPath + effectiveDelimiter() + name;
+ - + - +
+ + + - -
- - ]
73 : :
74 [ + - + - ]: 5 : m_d.imap->executeAfterIdle(
75 : 15 : [this, fullPath]() { m_d.imap->createFolder(fullPath); });
76 [ + + ]: 9 : }
77 : :
78 : 9 : void FolderOperationsController::deleteFolder(const QString &folderPath) {
79 [ + - + + ]: 9 : if (isProtectedFolderPath(folderPath)) {
80 [ + - ]: 2 : emit keyedStatusMessage(
81 : 4 : QStringLiteral("folder"),
82 : 4 : QStringLiteral("Spezialordner koennen nicht geloescht werden"), 3000);
83 : 5 : return;
84 : : }
85 : :
86 : : // Count children for warning
87 [ + - ]: 7 : const QString delimiter = effectiveDelimiter();
88 : 7 : int childCount = 0;
89 : 7 : QStringList childPaths;
90 [ + - + - : 26 : for (const auto &f : m_folders) {
+ + ]
91 [ + - + - : 19 : if (f.path.startsWith(folderPath + delimiter)) {
+ + ]
92 : 4 : childCount++;
93 [ + - ]: 4 : childPaths.append(f.path);
94 : : }
95 : : }
96 : :
97 : : QString msg =
98 [ + - ]: 14 : QStringLiteral("Ordner '%1' wirklich loeschen?").arg(folderPath);
99 [ + + ]: 7 : if (childCount > 0)
100 : 4 : msg += QStringLiteral("\n\nDieser Ordner hat %1 Unterordner, die "
101 : : "ebenfalls geloescht werden.")
102 [ + - + - ]: 4 : .arg(childCount);
103 : :
104 [ + - + + ]: 14 : if (!m_confirm(QStringLiteral("Ordner loeschen"), msg))
105 : 3 : return;
106 : :
107 : : // Build delete list: children first (bottom-up by path depth)
108 : 4 : QStringList deleteList = childPaths;
109 [ + - + - : 4 : std::sort(deleteList.begin(), deleteList.end(),
+ - ]
110 : 2 : [&delimiter](const QString &a, const QString &b) {
111 : 2 : return a.count(delimiter) > b.count(delimiter);
112 : : });
113 [ + - ]: 4 : deleteList.append(folderPath); // parent last
114 : :
115 : : // FIX: IMAP servers send BYE when deleting the currently SELECTed folder.
116 : : // We must switch to INBOX before starting the delete chain.
117 : : // Check if current folder is the folder being deleted or a child of it.
118 : 4 : QString currentFolder = m_d.imap->selectedFolder();
119 [ + - ]: 8 : bool needSwitch = (currentFolder == folderPath) ||
120 [ + - + - : 8 : currentFolder.startsWith(folderPath + delimiter);
- + + - -
- ]
121 : :
122 : : // Chain sequential deletes
123 [ + - ]: 4 : auto remaining = std::make_shared<QStringList>(deleteList);
124 [ + - ]: 4 : auto deleteNext = std::make_shared<std::function<void()>>();
125 [ - - ]: 8 : *deleteNext = [this, remaining, deleteNext]() {
126 [ - + ]: 6 : if (remaining->isEmpty())
127 : 0 : return;
128 [ + - ]: 6 : QString next = remaining->takeFirst();
129 [ + + ]: 6 : if (!remaining->isEmpty()) {
130 [ + - ]: 2 : auto conn = std::make_shared<QMetaObject::Connection>();
131 : 4 : *conn = connect(m_d.imap, &ImapService::folderDeleted, this,
132 [ + - - - ]: 4 : [deleteNext, conn](const QString &) {
133 : 2 : QObject::disconnect(*conn);
134 : 2 : (*deleteNext)();
135 : 2 : });
136 : 2 : }
137 [ + - + - ]: 6 : m_d.imap->executeAfterIdle(
138 : 18 : [this, next]() { m_d.imap->deleteFolder(next); });
139 [ + - ]: 10 : };
140 : :
141 [ - + ]: 4 : if (needSwitch) {
142 : : // Switch to INBOX first, then start deleting
143 [ # # ]: 0 : m_d.folderTree->selectFolder(QStringLiteral("INBOX"));
144 : : // Wait for SELECT INBOX to complete before deleting
145 [ # # ]: 0 : auto conn = std::make_shared<QMetaObject::Connection>();
146 : 0 : *conn = connect(m_d.imap, &ImapService::folderSelected, this,
147 [ # # # # ]: 0 : [deleteNext, conn](const QString &, int, quint32, quint64) {
148 : 0 : QObject::disconnect(*conn);
149 : 0 : (*deleteNext)();
150 : 0 : });
151 : 0 : } else {
152 [ + - ]: 4 : (*deleteNext)();
153 : : }
154 [ + + + + : 13 : }
+ + ]
155 : :
156 : 5 : void FolderOperationsController::renameFolder(const QString &folderPath) {
157 [ + - + + ]: 5 : if (isProtectedFolderPath(folderPath)) {
158 [ + - ]: 1 : emit keyedStatusMessage(
159 : 2 : QStringLiteral("folder"),
160 : 2 : QStringLiteral("Spezialordner koennen nicht umbenannt werden"), 3000);
161 : 2 : return;
162 : : }
163 : :
164 : : // Extract current name (last segment after delimiter)
165 [ + - ]: 4 : const QString delimiter = effectiveDelimiter();
166 [ + - ]: 4 : int lastSep = folderPath.lastIndexOf(delimiter);
167 : : QString currentName = (lastSep >= 0)
168 [ + + ]: 4 : ? folderPath.mid(lastSep + delimiter.length())
169 [ + - ]: 4 : : folderPath;
170 [ + + + - ]: 4 : QString parentPath = (lastSep >= 0) ? folderPath.left(lastSep) : QString();
171 : :
172 : 4 : bool ok = false;
173 : : QString newName = m_promptText(
174 : 8 : QStringLiteral("Ordner umbenennen"),
175 [ + - ]: 12 : QStringLiteral("Neuer Name für '%1':").arg(currentName),
176 [ + - ]: 8 : currentName, &ok);
177 : :
178 [ + - + - : 4 : if (!ok || newName.trimmed().isEmpty() || newName.trimmed() == currentName)
+ - + - +
+ + - + -
+ + - - -
- ]
179 : 1 : return;
180 : :
181 [ + - ]: 3 : newName = newName.trimmed();
182 : : QString newPath =
183 [ + + + - : 3 : parentPath.isEmpty() ? newName : parentPath + delimiter + newName;
+ - + + -
- ]
184 : :
185 [ + - + - : 3 : m_d.imap->executeAfterIdle([this, folderPath, newPath]() {
- - ]
186 : 3 : m_d.imap->renameFolder(folderPath, newPath);
187 : 3 : });
188 [ + + + + : 7 : }
+ + + + ]
189 : :
190 : 6 : void FolderOperationsController::moveFolder(const QString &folderPath) {
191 [ + - + + ]: 6 : if (isProtectedFolderPath(folderPath)) {
192 [ + - ]: 1 : emit keyedStatusMessage(
193 : 2 : QStringLiteral("folder"),
194 : 2 : QStringLiteral("Spezialordner koennen nicht verschoben werden"), 3000);
195 : 1 : return;
196 : : }
197 : :
198 : : // Activate CommandBar in FolderSwitch mode to pick target
199 [ + - ]: 5 : m_d.commandBar->activate(CommandBar::FolderSwitch);
200 : :
201 : : // One-shot connection: when folder is selected, perform RENAME
202 [ + - ]: 5 : auto conn = std::make_shared<QMetaObject::Connection>();
203 : 10 : *conn = connect(
204 : 5 : m_d.commandBar, &CommandBar::folderSelected, this,
205 [ + - - - ]: 10 : [this, folderPath, conn](CommandBar::Mode mode,
206 : : const QString &targetFolder) {
207 [ + - ]: 3 : QObject::disconnect(*conn);
208 [ + + ]: 3 : if (mode != CommandBar::FolderSwitch)
209 : 1 : return;
210 : :
211 : : // Build new path: targetFolder + delimiter + folderName
212 [ + - ]: 2 : const QString delimiter = effectiveDelimiter();
213 [ + - ]: 2 : int lastSep = folderPath.lastIndexOf(delimiter);
214 : : QString folderName = (lastSep >= 0)
215 [ - + ]: 2 : ? folderPath.mid(lastSep + delimiter.length())
216 [ - - ]: 2 : : folderPath;
217 [ + - + - ]: 2 : QString newPath = targetFolder + delimiter + folderName;
218 : :
219 [ + - + - : 2 : m_d.imap->executeAfterIdle([this, folderPath, newPath]() {
- - ]
220 : 2 : m_d.imap->renameFolder(folderPath, newPath);
221 : 2 : });
222 : 7 : });
223 : :
224 : : // Clean up if cancelled
225 [ + - ]: 5 : auto cancelConn = std::make_shared<QMetaObject::Connection>();
226 : 10 : *cancelConn = connect(m_d.commandBar, &CommandBar::cancelled, this,
227 [ + - - - ]: 10 : [conn, cancelConn]() {
228 : 3 : QObject::disconnect(*conn);
229 : 3 : QObject::disconnect(*cancelConn);
230 : 5 : });
231 : 5 : }
|