MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - controller - ThreadBuilder.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 100.0 % 82 82
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 7 7
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 72.8 % 136 99

             Branch data     Line data    Source code
       1                 :             : #include "ThreadBuilder.h"
       2                 :             : 
       3                 :             : #include <QHash>
       4                 :             : #include <QLoggingCategory>
       5                 :             : #include <QRegularExpression>
       6                 :             : 
       7                 :             : #include <algorithm>
       8                 :             : 
       9   [ +  +  +  -  :         104 : Q_LOGGING_CATEGORY(lcThreadBuilder, "mailjd.threading")
             +  -  -  - ]
      10                 :             : 
      11                 :             : std::vector<std::unique_ptr<ThreadNode>>
      12                 :         121 : ThreadBuilder::buildThreads(const QList<MailHeader> &headers) {
      13         [ +  + ]:         121 :   if (headers.isEmpty())
      14                 :          17 :     return {};
      15                 :             : 
      16                 :             :   // Step 1: Build messageId → ThreadNode index
      17                 :             :   // All nodes are created as unique_ptr and stored in allNodes.
      18                 :             :   // Raw pointers in nodeById are non-owning references for linking.
      19                 :         104 :   QHash<QString, ThreadNode *> nodeById;
      20                 :         104 :   std::vector<std::unique_ptr<ThreadNode>> allNodes;
      21                 :             : 
      22         [ +  + ]:         340 :   for (int i = 0; i < headers.size(); ++i) {
      23         [ +  - ]:         236 :     auto node = std::make_unique<ThreadNode>();
      24                 :         236 :     node->header = &headers[i];
      25                 :         236 :     auto *raw = node.get();
      26         [ +  - ]:         236 :     allNodes.push_back(std::move(node));
      27                 :             : 
      28         [ +  + ]:         236 :     if (!headers[i].messageId.isEmpty()) {
      29         [ +  - ]:         158 :       nodeById.insert(headers[i].messageId, raw);
      30                 :             :     }
      31                 :         236 :   }
      32                 :             : 
      33                 :             :   // Step 2: Link parents via In-Reply-To, then References fallback
      34         [ +  + ]:         340 :   for (auto &node : allNodes) {
      35                 :             :     // Try In-Reply-To first (most reliable)
      36         [ +  + ]:         236 :     if (!node->header->inReplyTo.isEmpty()) {
      37                 :          18 :       auto *parent = nodeById.value(node->header->inReplyTo, nullptr);
      38   [ +  +  +  +  :          18 :       if (parent && parent != node.get()) { // avoid self-reference
                   +  + ]
      39                 :          16 :         node->parent = parent;
      40                 :             :         // Transfer ownership: move from allNodes to parent->children later
      41                 :          16 :         continue; // linked via In-Reply-To
      42                 :             :       }
      43                 :             :     }
      44                 :             :     // T-432: Fallback — try References header (last entry = direct parent)
      45         [ +  + ]:         220 :     if (!node->header->references.isEmpty()) {
      46         [ +  + ]:          11 :       for (int r = node->header->references.size() - 1; r >= 0; --r) {
      47                 :             :         auto *refParent =
      48                 :          10 :             nodeById.value(node->header->references[r], nullptr);
      49   [ +  +  +  +  :          10 :         if (refParent && refParent != node.get()) {
                   +  + ]
      50                 :           7 :           node->parent = refParent;
      51                 :           7 :           break;
      52                 :             :         }
      53                 :             :       }
      54                 :             :     }
      55                 :             :   }
      56                 :             : 
      57                 :             :   // Step 3: Subject-based fallback for orphaned replies
      58                 :             :   // Group by normalized subject → if a Re:/Fwd: mail has no parent,
      59                 :             :   // try to attach it to the oldest non-reply mail with the same subject.
      60                 :         104 :   QHash<QString, ThreadNode *> subjectRoots;
      61         [ +  + ]:         340 :   for (auto &node : allNodes) {
      62         [ +  + ]:         236 :     if (node->parent)
      63                 :          24 :       continue; // already threaded
      64                 :             : 
      65         [ +  - ]:         213 :     QString normSubject = normalizeSubject(node->header->subject);
      66         [ +  + ]:         213 :     if (normSubject.isEmpty())
      67                 :           1 :       continue;
      68                 :             : 
      69         [ +  - ]:         212 :     bool isReply = (normSubject != node->header->subject.trimmed());
      70         [ +  + ]:         212 :     if (!isReply) {
      71                 :             :       // This is a root-level subject (not a reply)
      72         [ +  + ]:         202 :       if (!subjectRoots.contains(normSubject)) {
      73         [ +  - ]:         197 :         subjectRoots.insert(normSubject, node.get());
      74                 :             :       }
      75                 :             :     }
      76         [ +  + ]:         213 :   }
      77                 :             : 
      78                 :             :   // Now attach orphaned replies to subject roots
      79         [ +  + ]:         340 :   for (auto &node : allNodes) {
      80         [ +  + ]:         236 :     if (node->parent)
      81                 :          23 :       continue;
      82                 :             : 
      83         [ +  - ]:         213 :     QString normSubject = normalizeSubject(node->header->subject);
      84         [ +  - ]:         213 :     bool isReply = (normSubject != node->header->subject.trimmed());
      85   [ +  +  +  +  :         213 :     if (isReply && subjectRoots.contains(normSubject)) {
                   +  + ]
      86                 :           7 :       auto *root = subjectRoots.value(normSubject);
      87         [ +  - ]:           7 :       if (root != node.get()) {
      88                 :           7 :         node->parent = root;
      89                 :             :       }
      90                 :             :     }
      91                 :         213 :   }
      92                 :             : 
      93                 :             :   // Step 4: Transfer children ownership from allNodes to parent->children
      94                 :             :   // and collect root nodes.
      95                 :             :   // We must iterate with index because we move elements out of allNodes.
      96                 :         104 :   std::vector<std::unique_ptr<ThreadNode>> roots;
      97         [ +  + ]:         340 :   for (size_t i = 0; i < allNodes.size(); ++i) {
      98         [ +  + ]:         236 :     if (allNodes[i]->parent) {
      99         [ +  - ]:          30 :       allNodes[i]->parent->children.push_back(std::move(allNodes[i]));
     100                 :             :     } else {
     101         [ +  - ]:         206 :       roots.push_back(std::move(allNodes[i]));
     102                 :             :     }
     103                 :             :   }
     104                 :             : 
     105                 :             :   // Set depths recursively
     106                 :         444 :   std::function<void(ThreadNode *, int)> setDepths = [&](ThreadNode *node,
     107                 :             :                                                          int depth) {
     108                 :         236 :     node->depth = depth;
     109         [ +  + ]:         266 :     for (auto &child : node->children) {
     110         [ +  - ]:          30 :       setDepths(child.get(), depth + 1);
     111                 :             :     }
     112                 :         340 :   };
     113         [ +  + ]:         310 :   for (auto &root : roots) {
     114         [ +  - ]:         206 :     setDepths(root.get(), 0);
     115                 :             :   }
     116                 :             : 
     117                 :             :   // Step 5: Sort children by date (oldest first)
     118                 :         444 :   std::function<void(ThreadNode *)> sortChildren = [&](ThreadNode *node) {
     119                 :         236 :     std::sort(node->children.begin(), node->children.end(),
     120                 :          15 :               [](const std::unique_ptr<ThreadNode> &a,
     121                 :             :                  const std::unique_ptr<ThreadNode> &b) {
     122                 :          15 :                 return a->header->date < b->header->date;
     123                 :             :               });
     124         [ +  + ]:         266 :     for (auto &child : node->children) {
     125         [ +  - ]:          30 :       sortChildren(child.get());
     126                 :             :     }
     127                 :         340 :   };
     128         [ +  + ]:         310 :   for (auto &root : roots) {
     129         [ +  - ]:         206 :     sortChildren(root.get());
     130                 :             :   }
     131                 :             : 
     132                 :             :   // Step 6: Sort root nodes by newest descendant date (DESC)
     133         [ +  - ]:         104 :   std::sort(roots.begin(), roots.end(),
     134                 :         198 :             [](const std::unique_ptr<ThreadNode> &a,
     135                 :             :                const std::unique_ptr<ThreadNode> &b) {
     136   [ +  -  +  -  :         198 :               return a->newestDate() > b->newestDate();
                   +  - ]
     137                 :             :             });
     138                 :             : 
     139   [ +  -  +  -  :         208 :   qCInfo(lcThreadBuilder) << "Built" << roots.size() << "threads from"
          +  -  +  -  +  
                -  +  + ]
     140   [ +  -  +  - ]:         104 :                           << headers.size() << "headers";
     141                 :         104 :   return roots;
     142                 :         104 : }
     143                 :             : 
     144                 :         426 : QString ThreadBuilder::normalizeSubject(const QString &subject) {
     145                 :             :   // Strip Re:/Fwd:/AW:/WG: prefixes (case-insensitive, repeating)
     146                 :             :   static QRegularExpression prefixRx(
     147                 :             :       R"(^(?:\s*(?:Re|Fwd|Fw|AW|WG)\s*(?:\[\d+\])?\s*:\s*)+)",
     148   [ +  +  +  -  :         426 :       QRegularExpression::CaseInsensitiveOption);
          +  -  +  -  -  
                      - ]
     149                 :         426 :   QString result = subject;
     150         [ +  - ]:         426 :   result.replace(prefixRx, QString());
     151         [ +  - ]:         852 :   return result.trimmed();
     152                 :         426 : }
        

Generated by: LCOV version 2.0-1