1use std::collections::HashMap;
8
9use crate::message::Envelope;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ThreadedItem {
14 pub index: usize,
16 pub depth: usize,
18 pub root: usize,
20}
21
22struct Container {
23 message: Option<usize>,
24 parent: Option<usize>,
25 children: Vec<usize>,
26}
27
28fn get_or_create(
29 by_id: &mut HashMap<String, usize>,
30 arena: &mut Vec<Container>,
31 id: &str,
32) -> usize {
33 if let Some(&c) = by_id.get(id) {
34 return c;
35 }
36 arena.push(Container {
37 message: None,
38 parent: None,
39 children: Vec::new(),
40 });
41 let idx = arena.len() - 1;
42 by_id.insert(id.to_string(), idx);
43 idx
44}
45
46fn is_ancestor(arena: &[Container], ancestor: usize, mut node: usize) -> bool {
47 loop {
48 if node == ancestor {
49 return true;
50 }
51 match arena[node].parent {
52 Some(p) => node = p,
53 None => return false,
54 }
55 }
56}
57
58fn link(arena: &mut [Container], rooted: &[bool], parent: usize, child: usize) {
65 if parent == child
66 || arena[child].parent.is_some()
67 || rooted[child]
68 || is_ancestor(arena, child, parent)
69 {
70 return;
71 }
72 arena[child].parent = Some(parent);
73 arena[parent].children.push(child);
74}
75
76fn subtree_date(arena: &[Container], envs: &[&Envelope], node: usize, newest: bool) -> i64 {
79 let own = arena[node]
80 .message
81 .map(|m| envs[m].date)
82 .unwrap_or(if newest { i64::MIN } else { i64::MAX });
83 let fold = if newest { i64::max } else { i64::min };
84 arena[node]
85 .children
86 .iter()
87 .map(|&c| subtree_date(arena, envs, c, newest))
88 .fold(own, fold)
89}
90
91fn real_children(arena: &[Container], node: usize, out: &mut Vec<usize>) {
94 for &child in &arena[node].children {
95 if arena[child].message.is_some() {
96 out.push(child);
97 } else {
98 real_children(arena, child, out);
99 }
100 }
101}
102
103fn emit(
104 arena: &[Container],
105 envs: &[&Envelope],
106 node: usize,
107 depth: usize,
108 root: usize,
109 newest: bool,
110 out: &mut Vec<ThreadedItem>,
111) {
112 let index = arena[node].message.expect("emit called on empty container");
113 out.push(ThreadedItem { index, depth, root });
114 let mut kids = Vec::new();
115 real_children(arena, node, &mut kids);
116 kids.sort_by_key(|&k| subtree_date(arena, envs, k, newest));
117 for kid in kids {
118 emit(arena, envs, kid, depth + 1, root, newest, out);
119 }
120}
121
122pub fn thread(envs: &[&Envelope]) -> Vec<ThreadedItem> {
123 thread_by(envs, ThreadOrder::default())
124}
125
126#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
132pub struct ThreadOrder {
133 pub newest: bool,
136 pub reverse: bool,
138}
139
140impl ThreadOrder {
141 pub fn parse(spec: &str) -> ThreadOrder {
144 let spec = spec.trim().to_lowercase();
145 let (reverse, rest) = match spec.strip_prefix("reverse-") {
146 Some(rest) => (true, rest.to_string()),
147 None => (false, spec),
148 };
149 ThreadOrder {
150 newest: rest.starts_with("last-"),
151 reverse,
152 }
153 }
154}
155
156pub fn thread_by(envs: &[&Envelope], order: ThreadOrder) -> Vec<ThreadedItem> {
157 let newest = order.newest;
158 let mut arena: Vec<Container> = Vec::new();
159 let mut by_id: HashMap<String, usize> = HashMap::new();
160
161 let mut container_of = Vec::with_capacity(envs.len());
165 for (i, env) in envs.iter().enumerate() {
166 let id = env
167 .msg_id
168 .clone()
169 .unwrap_or_else(|| format!("<rmut-missing-{i}>"));
170 let mut container = get_or_create(&mut by_id, &mut arena, &id);
171 if arena[container].message.is_some() {
172 arena.push(Container {
174 message: None,
175 parent: None,
176 children: Vec::new(),
177 });
178 container = arena.len() - 1;
179 }
180 arena[container].message = Some(i);
181 container_of.push(container);
182 }
183 let mut rooted: Vec<bool> = arena
184 .iter()
185 .map(|c| c.message.is_some_and(|m| envs[m].references.is_empty()))
186 .collect();
187
188 for (i, env) in envs.iter().enumerate() {
189 let container = container_of[i];
190 let mut prev: Option<usize> = None;
191 for rid in &env.references {
192 let r = get_or_create(&mut by_id, &mut arena, rid);
193 rooted.resize(arena.len(), false);
194 if r == container {
195 continue;
196 }
197 if let Some(p) = prev {
198 link(&mut arena, &rooted, p, r);
199 }
200 prev = Some(r);
201 }
202 if let Some(p) = prev {
203 link(&mut arena, &rooted, p, container);
204 }
205 }
206
207 let mut top = Vec::new();
210 for i in 0..arena.len() {
211 if arena[i].parent.is_none() {
212 if arena[i].message.is_some() {
213 top.push(i);
214 } else {
215 real_children(&arena, i, &mut top);
216 }
217 }
218 }
219 let envs_ref = envs;
220 top.sort_by_key(|&t| subtree_date(&arena, envs_ref, t, newest));
221 if order.reverse {
222 top.reverse();
225 }
226
227 let mut out = Vec::new();
228 for t in top {
229 let root = arena[t].message.expect("top containers carry messages");
230 emit(&arena, envs_ref, t, 0, root, newest, &mut out);
231 }
232 out
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::maildir::{Flags, MailFile};
239
240 fn env(id: &str, refs: &[&str], date: i64) -> Envelope {
241 Envelope {
242 file: MailFile {
243 path: format!("/mail/{id}").into(),
244 is_new: false,
245 flags: Flags::default(),
246 size: 0,
247 },
248 from: "x".into(),
249 from_full: "x".into(),
250 subject: id.into(),
251 date,
252 msg_id: (!id.is_empty()).then(|| format!("<{id}>")),
253 references: refs.iter().map(|r| format!("<{r}>")).collect(),
254 tagged: false,
255 to: vec![],
256 cc: vec![],
257 lines: Some(0),
258 list: None,
259 label: None,
260 }
261 }
262
263 fn run(envs: &[Envelope]) -> Vec<(usize, usize)> {
264 let refs: Vec<&Envelope> = envs.iter().collect();
265 thread(&refs).iter().map(|i| (i.index, i.depth)).collect()
266 }
267
268 #[test]
269 fn chain_nests_by_references() {
270 let envs = [
271 env("a", &[], 1),
272 env("b", &["a"], 2),
273 env("c", &["a", "b"], 3),
274 ];
275 assert_eq!(run(&envs), vec![(0, 0), (1, 1), (2, 2)]);
276 let refs: Vec<&Envelope> = envs.iter().collect();
277 assert!(thread(&refs).iter().all(|i| i.root == 0));
278 }
279
280 #[test]
281 fn unrelated_messages_are_separate_threads_by_date() {
282 let envs = [env("b", &[], 5), env("a", &[], 2)];
283 assert_eq!(run(&envs), vec![(1, 0), (0, 0)]);
284 }
285
286 #[test]
287 fn missing_parent_promotes_children() {
288 let envs = [env("b", &["ghost"], 2), env("c", &["ghost"], 3)];
290 assert_eq!(run(&envs), vec![(0, 0), (1, 0)]);
291 }
292
293 #[test]
294 fn missing_middle_of_chain_is_bridged() {
295 let envs = [env("a", &[], 1), env("c", &["a", "b-missing"], 3)];
297 assert_eq!(run(&envs), vec![(0, 0), (1, 1)]);
298 }
299
300 #[test]
301 fn newest_orders_threads_by_their_latest_message() {
302 let envs = [env("a", &[], 10), env("a2", &["a"], 100), env("b", &[], 50)];
304 let refs: Vec<&Envelope> = envs.iter().collect();
305 let order = |spec: &str| -> Vec<usize> {
306 thread_by(&refs, ThreadOrder::parse(spec))
307 .iter()
308 .map(|t| t.index)
309 .collect()
310 };
311 assert_eq!(order("date"), vec![0, 1, 2], "thread A first by its oldest");
312 assert_eq!(
313 order("last-date-sent"),
314 vec![2, 0, 1],
315 "thread B first, A has the newest last"
316 );
317 assert_eq!(order("reverse-date"), vec![2, 0, 1]);
320 assert_eq!(order("reverse-last-date-received"), vec![0, 1, 2]);
321 }
322
323 #[test]
324 fn sort_aux_spellings_parse_the_way_mutt_writes_them() {
325 assert_eq!(ThreadOrder::parse("date"), ThreadOrder::default());
326 assert_eq!(
327 ThreadOrder::parse("last-date-received"),
328 ThreadOrder {
329 newest: true,
330 reverse: false
331 }
332 );
333 assert_eq!(
334 ThreadOrder::parse("reverse-last-date-sent"),
335 ThreadOrder {
336 newest: true,
337 reverse: true
338 }
339 );
340 assert_eq!(
341 ThreadOrder::parse("REVERSE-DATE"),
342 ThreadOrder {
343 newest: false,
344 reverse: true
345 }
346 );
347 }
348
349 #[test]
350 fn siblings_sorted_by_date() {
351 let envs = [
352 env("a", &[], 1),
353 env("late", &["a"], 9),
354 env("early", &["a"], 2),
355 ];
356 assert_eq!(run(&envs), vec![(0, 0), (2, 1), (1, 1)]);
357 }
358
359 #[test]
360 fn duplicates_and_self_references_do_not_panic() {
361 let envs = [
362 env("a", &[], 1),
363 env("a", &[], 2), env("s", &["s"], 3), env("", &[], 4), ];
367 let out = run(&envs);
368 assert_eq!(out.len(), 4);
369 let mut seen: Vec<usize> = out.iter().map(|(i, _)| *i).collect();
370 seen.sort_unstable();
371 assert_eq!(seen, vec![0, 1, 2, 3]);
372 }
373
374 #[test]
375 fn reference_loops_are_broken() {
376 let envs = [env("a", &["b"], 1), env("b", &["a"], 2)];
378 let out = run(&envs);
379 assert_eq!(out.len(), 2);
380 }
381
382 #[test]
383 fn a_message_without_references_is_never_reparented_by_its_replies() {
384 let envs = [env("a", &[], 1), env("b", &[], 2), env("c", &["a", "b"], 3)];
389 assert_eq!(run(&envs), vec![(0, 0), (1, 0), (2, 1)]);
390 let envs = [
393 env("a", &[], 1),
394 env("b", &["a"], 2),
395 env("c", &["a", "b"], 3),
396 ];
397 assert_eq!(run(&envs), vec![(0, 0), (1, 1), (2, 2)]);
398 let envs = [env("p", &[], 1), env("c", &["gone", "p"], 2)];
399 assert_eq!(run(&envs), vec![(0, 0), (1, 1)]);
400 }
401}