1use std::collections::HashMap;
9
10use crate::message::Envelope;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct ThreadedItem {
15 pub index: usize,
17 pub depth: usize,
19 pub root: usize,
21 pub pseudo: bool,
24}
25
26struct Container {
27 message: Option<usize>,
28 parent: Option<usize>,
29 children: Vec<usize>,
30 pseudo: bool,
32}
33
34fn get_or_create(
35 by_id: &mut HashMap<String, usize>,
36 arena: &mut Vec<Container>,
37 id: &str,
38) -> usize {
39 if let Some(&c) = by_id.get(id) {
40 return c;
41 }
42 arena.push(Container {
43 message: None,
44 parent: None,
45 children: Vec::new(),
46 pseudo: false,
47 });
48 let idx = arena.len() - 1;
49 by_id.insert(id.to_string(), idx);
50 idx
51}
52
53fn is_ancestor(arena: &[Container], ancestor: usize, mut node: usize) -> bool {
54 loop {
55 if node == ancestor {
56 return true;
57 }
58 match arena[node].parent {
59 Some(p) => node = p,
60 None => return false,
61 }
62 }
63}
64
65fn link(arena: &mut [Container], rooted: &[bool], parent: usize, child: usize) {
72 if parent == child
73 || arena[child].parent.is_some()
74 || rooted[child]
75 || is_ancestor(arena, child, parent)
76 {
77 return;
78 }
79 arena[child].parent = Some(parent);
80 arena[parent].children.push(child);
81}
82
83fn subtree_date(arena: &[Container], envs: &[&Envelope], node: usize, newest: bool) -> i64 {
86 let own = arena[node]
87 .message
88 .map(|m| envs[m].date)
89 .unwrap_or(if newest { i64::MIN } else { i64::MAX });
90 let fold = if newest { i64::max } else { i64::min };
91 arena[node]
92 .children
93 .iter()
94 .map(|&c| subtree_date(arena, envs, c, newest))
95 .fold(own, fold)
96}
97
98fn real_children(arena: &[Container], node: usize, out: &mut Vec<usize>) {
101 for &child in &arena[node].children {
102 if arena[child].message.is_some() {
103 out.push(child);
104 } else {
105 real_children(arena, child, out);
106 }
107 }
108}
109
110fn emit(
111 arena: &[Container],
112 envs: &[&Envelope],
113 node: usize,
114 depth: usize,
115 root: usize,
116 newest: bool,
117 out: &mut Vec<ThreadedItem>,
118) {
119 let index = arena[node].message.expect("emit called on empty container");
120 out.push(ThreadedItem {
121 index,
122 depth,
123 root,
124 pseudo: arena[node].pseudo,
125 });
126 let mut kids = Vec::new();
127 real_children(arena, node, &mut kids);
128 kids.sort_by_key(|&k| subtree_date(arena, envs, k, newest));
129 for kid in kids {
130 emit(arena, envs, kid, depth + 1, root, newest, out);
131 }
132}
133
134pub fn thread(envs: &[&Envelope]) -> Vec<ThreadedItem> {
135 thread_by(envs, ThreadOrder::default())
136}
137
138#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
144pub struct ThreadOrder {
145 pub newest: bool,
148 pub reverse: bool,
150}
151
152impl ThreadOrder {
153 pub fn parse(spec: &str) -> ThreadOrder {
156 let spec = spec.trim().to_lowercase();
157 let (reverse, rest) = match spec.strip_prefix("reverse-") {
158 Some(rest) => (true, rest.to_string()),
159 None => (false, spec),
160 };
161 ThreadOrder {
162 newest: rest.starts_with("last-"),
163 reverse,
164 }
165 }
166}
167
168#[derive(Clone, Copy)]
174pub struct SubjectFallback<'a> {
175 pub reply_re: &'a regex_lite::Regex,
178 pub sort_re: bool,
182}
183
184fn real_subject<'a>(re: ®ex_lite::Regex, subject: &'a str) -> (&'a str, bool) {
189 match re.find(subject) {
190 Some(m) if m.start() == 0 => (subject[m.end()..].trim(), true),
191 _ => (subject.trim(), false),
192 }
193}
194
195fn message_ancestor(arena: &[Container], node: usize) -> Option<usize> {
198 let mut at = arena[node].parent;
199 while let Some(p) = at {
200 if arena[p].message.is_some() {
201 return Some(p);
202 }
203 at = arena[p].parent;
204 }
205 None
206}
207
208fn group_by_subject(
217 arena: &mut [Container],
218 envs: &[&Envelope],
219 top: &mut Vec<usize>,
220 sub: &SubjectFallback,
221) {
222 let mut candidates: HashMap<&str, Vec<usize>> = HashMap::new();
226 for node in 0..arena.len() {
227 let Some(m) = arena[node].message else {
228 continue;
229 };
230 let subj = real_subject(sub.reply_re, &envs[m].subject).0;
231 let changed = match message_ancestor(arena, node) {
232 Some(p) => {
233 let pm = arena[p]
234 .message
235 .expect("message_ancestor carries a message");
236 real_subject(sub.reply_re, &envs[pm].subject).0 != subj
237 }
238 None => true,
239 };
240 if changed {
241 candidates.entry(subj).or_default().push(node);
242 }
243 }
244
245 let mut roots: Vec<usize> = top.clone();
248 roots.sort_by_key(|&r| {
249 let m = arena[r].message.expect("a thread root carries a message");
250 (envs[m].date, m)
251 });
252
253 for cur in roots {
254 let m = arena[cur].message.expect("a thread root carries a message");
255 if envs[m].broken {
259 continue;
260 }
261 let (subj, is_reply) = real_subject(sub.reply_re, &envs[m].subject);
262 if sub.sort_re && !is_reply {
263 continue;
264 }
265 let here = (envs[m].date, m);
266 let mut best: Option<((i64, usize), usize)> = None;
267 for &t in candidates.get(subj).map(Vec::as_slice).unwrap_or_default() {
268 if t == cur || arena[t].pseudo {
269 continue;
270 }
271 let tm = arena[t].message.expect("a candidate carries a message");
272 let there = (envs[tm].date, tm);
273 if there >= here || is_ancestor(arena, cur, t) {
276 continue;
277 }
278 if best.is_none_or(|(seen, _)| seen < there) {
279 best = Some((there, t));
280 }
281 }
282 if let Some((_, parent)) = best {
283 arena[cur].parent = Some(parent);
284 arena[parent].children.push(cur);
285 arena[cur].pseudo = true;
286 }
287 }
288 top.retain(|&t| arena[t].parent.is_none());
289}
290
291pub fn thread_by(envs: &[&Envelope], order: ThreadOrder) -> Vec<ThreadedItem> {
292 thread_with(envs, order, None)
293}
294
295pub fn thread_with(
299 envs: &[&Envelope],
300 order: ThreadOrder,
301 subject: Option<&SubjectFallback>,
302) -> Vec<ThreadedItem> {
303 let newest = order.newest;
304 let mut arena: Vec<Container> = Vec::new();
305 let mut by_id: HashMap<String, usize> = HashMap::new();
306
307 let mut container_of = Vec::with_capacity(envs.len());
311 for (i, env) in envs.iter().enumerate() {
312 let id = env
313 .msg_id
314 .clone()
315 .unwrap_or_else(|| format!("<rmut-missing-{i}>"));
316 let mut container = get_or_create(&mut by_id, &mut arena, &id);
317 if arena[container].message.is_some() {
318 arena.push(Container {
320 message: None,
321 parent: None,
322 children: Vec::new(),
323 pseudo: false,
324 });
325 container = arena.len() - 1;
326 }
327 arena[container].message = Some(i);
328 container_of.push(container);
329 }
330 let mut rooted: Vec<bool> = arena
331 .iter()
332 .map(|c| c.message.is_some_and(|m| envs[m].references.is_empty()))
333 .collect();
334
335 for (i, env) in envs.iter().enumerate() {
336 let container = container_of[i];
337 let mut prev: Option<usize> = None;
338 for rid in &env.references {
339 let r = get_or_create(&mut by_id, &mut arena, rid);
340 rooted.resize(arena.len(), false);
341 if r == container {
342 continue;
343 }
344 if let Some(p) = prev {
345 link(&mut arena, &rooted, p, r);
346 }
347 prev = Some(r);
348 }
349 if let Some(p) = prev {
350 link(&mut arena, &rooted, p, container);
351 }
352 }
353
354 let mut top = Vec::new();
357 for i in 0..arena.len() {
358 if arena[i].parent.is_none() {
359 if arena[i].message.is_some() {
360 top.push(i);
361 } else {
362 real_children(&arena, i, &mut top);
363 }
364 }
365 }
366 if let Some(sub) = subject {
367 group_by_subject(&mut arena, envs, &mut top, sub);
368 }
369 let envs_ref = envs;
370 top.sort_by_key(|&t| subtree_date(&arena, envs_ref, t, newest));
371 if order.reverse {
372 top.reverse();
375 }
376
377 let mut out = Vec::new();
378 for t in top {
379 let root = arena[t].message.expect("top containers carry messages");
380 emit(&arena, envs_ref, t, 0, root, newest, &mut out);
381 }
382 out
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::maildir::{Flags, MailFile};
389
390 fn env(id: &str, refs: &[&str], date: i64) -> Envelope {
391 Envelope {
392 file: MailFile {
393 path: format!("/mail/{id}").into(),
394 is_new: false,
395 flags: Flags::default(),
396 size: 0,
397 },
398 from: "x".into(),
399 from_full: "x".into(),
400 subject: id.into(),
401 date,
402 msg_id: (!id.is_empty()).then(|| format!("<{id}>")),
403 references: refs.iter().map(|r| format!("<{r}>")).collect(),
404 tagged: false,
405 to: vec![],
406 cc: vec![],
407 lines: Some(0),
408 list: None,
409 label: None,
410 broken: false,
411 }
412 }
413
414 fn subj(id: &str, subject: &str, refs: &[&str], date: i64) -> Envelope {
417 Envelope {
418 subject: subject.into(),
419 ..env(id, refs, date)
420 }
421 }
422
423 fn run_subject(envs: &[Envelope], sort_re: bool) -> Vec<(usize, usize, bool)> {
424 let re = crate::compose::default_reply_regexp();
425 let fallback = SubjectFallback {
426 reply_re: &re,
427 sort_re,
428 };
429 let refs: Vec<&Envelope> = envs.iter().collect();
430 thread_with(&refs, ThreadOrder::default(), Some(&fallback))
431 .iter()
432 .map(|i| (i.index, i.depth, i.pseudo))
433 .collect()
434 }
435
436 fn run(envs: &[Envelope]) -> Vec<(usize, usize)> {
437 let refs: Vec<&Envelope> = envs.iter().collect();
438 thread(&refs).iter().map(|i| (i.index, i.depth)).collect()
439 }
440
441 #[test]
442 fn chain_nests_by_references() {
443 let envs = [
444 env("a", &[], 1),
445 env("b", &["a"], 2),
446 env("c", &["a", "b"], 3),
447 ];
448 assert_eq!(run(&envs), vec![(0, 0), (1, 1), (2, 2)]);
449 let refs: Vec<&Envelope> = envs.iter().collect();
450 assert!(thread(&refs).iter().all(|i| i.root == 0));
451 }
452
453 #[test]
454 fn unrelated_messages_are_separate_threads_by_date() {
455 let envs = [env("b", &[], 5), env("a", &[], 2)];
456 assert_eq!(run(&envs), vec![(1, 0), (0, 0)]);
457 }
458
459 #[test]
460 fn missing_parent_promotes_children() {
461 let envs = [env("b", &["ghost"], 2), env("c", &["ghost"], 3)];
463 assert_eq!(run(&envs), vec![(0, 0), (1, 0)]);
464 }
465
466 #[test]
467 fn missing_middle_of_chain_is_bridged() {
468 let envs = [env("a", &[], 1), env("c", &["a", "b-missing"], 3)];
470 assert_eq!(run(&envs), vec![(0, 0), (1, 1)]);
471 }
472
473 #[test]
474 fn newest_orders_threads_by_their_latest_message() {
475 let envs = [env("a", &[], 10), env("a2", &["a"], 100), env("b", &[], 50)];
477 let refs: Vec<&Envelope> = envs.iter().collect();
478 let order = |spec: &str| -> Vec<usize> {
479 thread_by(&refs, ThreadOrder::parse(spec))
480 .iter()
481 .map(|t| t.index)
482 .collect()
483 };
484 assert_eq!(order("date"), vec![0, 1, 2], "thread A first by its oldest");
485 assert_eq!(
486 order("last-date-sent"),
487 vec![2, 0, 1],
488 "thread B first, A has the newest last"
489 );
490 assert_eq!(order("reverse-date"), vec![2, 0, 1]);
493 assert_eq!(order("reverse-last-date-received"), vec![0, 1, 2]);
494 }
495
496 #[test]
497 fn sort_aux_spellings_parse_the_way_mutt_writes_them() {
498 assert_eq!(ThreadOrder::parse("date"), ThreadOrder::default());
499 assert_eq!(
500 ThreadOrder::parse("last-date-received"),
501 ThreadOrder {
502 newest: true,
503 reverse: false
504 }
505 );
506 assert_eq!(
507 ThreadOrder::parse("reverse-last-date-sent"),
508 ThreadOrder {
509 newest: true,
510 reverse: true
511 }
512 );
513 assert_eq!(
514 ThreadOrder::parse("REVERSE-DATE"),
515 ThreadOrder {
516 newest: false,
517 reverse: true
518 }
519 );
520 }
521
522 #[test]
523 fn siblings_sorted_by_date() {
524 let envs = [
525 env("a", &[], 1),
526 env("late", &["a"], 9),
527 env("early", &["a"], 2),
528 ];
529 assert_eq!(run(&envs), vec![(0, 0), (2, 1), (1, 1)]);
530 }
531
532 #[test]
533 fn duplicates_and_self_references_do_not_panic() {
534 let envs = [
535 env("a", &[], 1),
536 env("a", &[], 2), env("s", &["s"], 3), env("", &[], 4), ];
540 let out = run(&envs);
541 assert_eq!(out.len(), 4);
542 let mut seen: Vec<usize> = out.iter().map(|(i, _)| *i).collect();
543 seen.sort_unstable();
544 assert_eq!(seen, vec![0, 1, 2, 3]);
545 }
546
547 #[test]
548 fn reference_loops_are_broken() {
549 let envs = [env("a", &["b"], 1), env("b", &["a"], 2)];
551 let out = run(&envs);
552 assert_eq!(out.len(), 2);
553 }
554
555 #[test]
556 fn a_message_without_references_is_never_reparented_by_its_replies() {
557 let envs = [env("a", &[], 1), env("b", &[], 2), env("c", &["a", "b"], 3)];
562 assert_eq!(run(&envs), vec![(0, 0), (1, 0), (2, 1)]);
563 let envs = [
566 env("a", &[], 1),
567 env("b", &["a"], 2),
568 env("c", &["a", "b"], 3),
569 ];
570 assert_eq!(run(&envs), vec![(0, 0), (1, 1), (2, 2)]);
571 let envs = [env("p", &[], 1), env("c", &["gone", "p"], 2)];
572 assert_eq!(run(&envs), vec![(0, 0), (1, 1)]);
573 }
574
575 #[test]
576 fn subject_groups_mail_that_carries_no_references() {
577 let envs = [
582 subj("a", "Re: proj | a change (!1661)", &[], 10),
583 subj("b", "Re: proj | a change (!1661)", &[], 20),
584 subj("c", "Re: proj | a change (!1661)", &[], 30),
585 ];
586 assert_eq!(
587 run_subject(&envs, true),
588 vec![(0, 0, false), (1, 1, true), (2, 1, true)]
589 );
590 assert_eq!(run(&envs), vec![(0, 0), (1, 0), (2, 0)]);
592 }
593
594 #[test]
595 fn sort_re_decides_whether_a_plain_subject_joins() {
596 let envs = [
598 subj("a", "hi", &[], 10),
599 subj("b", "Re: hi", &[], 20),
600 subj("c", "hi", &[], 30),
601 subj("d", "Re: other", &[], 40),
602 ];
603 assert_eq!(
605 run_subject(&envs, true),
606 vec![(0, 0, false), (1, 1, true), (2, 0, false), (3, 0, false)]
607 );
608 assert_eq!(
611 run_subject(&envs, false),
612 vec![(0, 0, false), (1, 1, true), (2, 1, true), (3, 0, false)]
613 );
614 }
615
616 #[test]
617 fn a_renamed_reply_is_the_parent_for_its_own_subject() {
618 let envs = [
623 subj("a", "hi", &[], 10),
624 subj("b", "Re: hi", &["a"], 20),
625 subj("m", "Re: newtopic", &["a", "b"], 30),
626 subj("n", "Re: newtopic", &[], 40),
627 ];
628 assert_eq!(
629 run_subject(&envs, true),
630 vec![(0, 0, false), (1, 1, false), (2, 2, false), (3, 3, true)]
631 );
632 }
633
634 #[test]
635 fn a_subject_child_brings_its_own_replies_with_it() {
636 let envs = [
639 subj("a", "hi", &[], 10),
640 subj("b", "Re: hi", &[], 20),
641 subj("c", "Re: hi", &["b"], 30),
642 ];
643 assert_eq!(
644 run_subject(&envs, true),
645 vec![(0, 0, false), (1, 1, true), (2, 2, false)]
646 );
647 }
648
649 #[test]
650 fn the_subject_pass_never_loops_or_reparents_a_real_child() {
651 let envs = [
654 subj("a", "Re: same", &[], 30),
655 subj("b", "Re: same", &["a"], 10),
656 ];
657 let out = run_subject(&envs, true);
658 assert_eq!(out.len(), 2);
659 assert_eq!(out, vec![(0, 0, false), (1, 1, false)]);
662 }
663}