MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - app - FolderOperationsController.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 94.1 % 152 143
Test Date: 2026-07-27 17:53:44 Functions: 94.7 % 19 18
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 55.4 % 280 155

             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 : }
        

Generated by: LCOV version 2.0-1