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