Skip to main content

hypermail/
structs.rs

1use crate::message::{Body, BodyChain, EmailInfo, Header, HmList, Reply};
2use regex::Regex;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct EmailStore {
7    pub emails: Vec<EmailInfo>,
8    pub subject_list: Option<Box<Header>>,
9    pub author_list: Option<Box<Header>>,
10    pub date_list: Option<Box<Header>>,
11    pub msgid_table: HashMap<String, usize>,
12    pub msgnum_table: HashMap<i32, usize>,
13    pub threadlist: Vec<Reply>,
14    pub threadlist_by_msgnum: Vec<Option<usize>>,
15    pub replylist: Vec<Reply>,
16    pub max_msgnum: i32,
17}
18
19impl Default for EmailStore {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl EmailStore {
26    pub fn new() -> Self {
27        EmailStore {
28            emails: Vec::new(),
29            subject_list: None,
30            author_list: None,
31            date_list: None,
32            msgid_table: HashMap::new(),
33            msgnum_table: HashMap::new(),
34            threadlist: Vec::new(),
35            threadlist_by_msgnum: Vec::new(),
36            replylist: Vec::new(),
37            max_msgnum: -1,
38        }
39    }
40
41    pub fn reinit(&mut self) {
42        self.emails.clear();
43        self.subject_list = None;
44        self.author_list = None;
45        self.date_list = None;
46        self.msgid_table.clear();
47        self.msgnum_table.clear();
48        self.threadlist.clear();
49        self.threadlist_by_msgnum.clear();
50        self.replylist.clear();
51        self.max_msgnum = -1;
52    }
53
54    pub fn find_by_msgid(&self, msgid: &str) -> Option<usize> {
55        self.msgid_table.get(msgid).copied()
56    }
57
58    pub fn find_by_msgnum(&self, msgnum: i32) -> Option<usize> {
59        self.msgnum_table.get(&msgnum).copied()
60    }
61
62    pub fn add_email(&mut self, email: EmailInfo) -> usize {
63        let idx = self.emails.len();
64        let msgnum = email.msgnum;
65
66        if let Some(ref msgid) = email.msgid {
67            self.msgid_table.insert(msgid.clone(), idx);
68        }
69
70        self.msgnum_table.insert(msgnum, idx);
71
72        if msgnum > self.max_msgnum {
73            self.max_msgnum = msgnum;
74        }
75
76        self.emails.push(email);
77        idx
78    }
79
80    pub fn insert_into_subject_list(&mut self, idx: usize) {
81        self.subject_list = Self::insert_into_tree_by_field(
82            self.subject_list.take(),
83            idx,
84            &self.emails,
85            |e| e.unre_subject.as_deref().or(e.subject.as_deref()).unwrap_or("").to_lowercase(),
86            |e| e.msgnum,
87        );
88    }
89
90    pub fn insert_into_author_list(&mut self, idx: usize) {
91        self.author_list = Self::insert_into_tree_by_field(
92            self.author_list.take(),
93            idx,
94            &self.emails,
95            |e| e.name.as_deref().or(e.email_addr.as_deref()).unwrap_or("").to_lowercase(),
96            |e| e.msgnum,
97        );
98    }
99
100    pub fn insert_into_date_list(&mut self, idx: usize) {
101        self.date_list = Self::insert_into_tree_by_field(
102            self.date_list.take(),
103            idx,
104            &self.emails,
105            |e| format!("{:020}", e.date),
106            |e| e.msgnum,
107        );
108    }
109
110    fn insert_into_tree_by_field<F1, F2>(
111        node: Option<Box<Header>>,
112        idx: usize,
113        emails: &[EmailInfo],
114        field_fn: F1,
115        msgnum_fn: F2,
116    ) -> Option<Box<Header>>
117    where
118        F1: Fn(&EmailInfo) -> String,
119        F2: Fn(&EmailInfo) -> i32,
120    {
121        let email = &emails[idx];
122        let key = field_fn(email);
123        let msgnum = msgnum_fn(email);
124
125        match node {
126            None => Some(Box::new(Header { email_index: idx, left: None, right: None })),
127            Some(mut n) => {
128                let node_email = &emails[n.email_index];
129                let node_key = field_fn(node_email);
130                let node_msgnum = msgnum_fn(node_email);
131
132                if key < node_key || (key == node_key && msgnum < node_msgnum) {
133                    n.left =
134                        Self::insert_into_tree_by_field(n.left, idx, emails, field_fn, msgnum_fn);
135                } else {
136                    n.right =
137                        Self::insert_into_tree_by_field(n.right, idx, emails, field_fn, msgnum_fn);
138                }
139                Some(n)
140            },
141        }
142    }
143
144    pub fn traverse_date_list(&self) -> Vec<usize> {
145        let mut result = Vec::new();
146        Self::inorder_traversal(&self.date_list, &mut result);
147        result
148    }
149
150    pub fn traverse_subject_list(&self) -> Vec<usize> {
151        let mut result = Vec::new();
152        Self::inorder_traversal(&self.subject_list, &mut result);
153        result
154    }
155
156    pub fn traverse_author_list(&self) -> Vec<usize> {
157        let mut result = Vec::new();
158        Self::inorder_traversal(&self.author_list, &mut result);
159        result
160    }
161
162    fn inorder_traversal(node: &Option<Box<Header>>, result: &mut Vec<usize>) {
163        if let Some(n) = node {
164            Self::inorder_traversal(&n.left, result);
165            result.push(n.email_index);
166            Self::inorder_traversal(&n.right, result);
167        }
168    }
169}
170
171pub fn add_body(mut bodylist: BodyChain, line: &str, msgnum: i32) -> BodyChain {
172    bodylist.bodies.push(Body {
173        line: line.to_string(),
174        html: false,
175        header: false,
176        parsed_header: false,
177        attached: false,
178        demimed: false,
179        msgnum,
180    });
181    bodylist
182}
183
184pub fn inlist(list: &HmList, val: &str) -> bool {
185    list.values.iter().any(|v| v == val)
186}
187
188pub fn inlist_pos(list: &HmList, val: &str) -> Option<usize> {
189    list.values.iter().position(|v| v == val)
190}
191
192pub fn inlist_regex_pos(list: &HmList, pattern: &str) -> Option<usize> {
193    let re = Regex::new(pattern).ok()?;
194    list.values.iter().position(|v| re.is_match(v))
195}
196
197pub fn add_to_list(list: &mut HmList, val: &str) {
198    if !inlist(list, val) {
199        list.values.push(val.to_string());
200    }
201}
202
203pub fn add_to_list_multi(list: &mut HmList, vals: &str) {
204    for v in vals.split_whitespace() {
205        add_to_list(list, v);
206    }
207}
208
209pub fn link_reply(
210    replylist: &mut Vec<Reply>,
211    from_msgnum: i32,
212    to_msgnum: i32,
213    data: Option<usize>,
214    maybe_reply: bool,
215) {
216    replylist.push(Reply {
217        from_msgnum,
218        msgnum: to_msgnum,
219        data,
220        maybe_reply: if maybe_reply { 1 } else { 0 },
221    });
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    fn make_email(msgnum: i32, msgid: &str, subject: &str, name: &str, date: i64) -> EmailInfo {
229        EmailInfo {
230            msgnum,
231            msgid: Some(msgid.to_string()),
232            subject: Some(subject.to_string()),
233            name: Some(name.to_string()),
234            date,
235            ..Default::default()
236        }
237    }
238
239    #[test]
240    fn test_add_and_find_email() {
241        let mut store = EmailStore::new();
242        let email = make_email(1, "<test@example.com>", "Test", "Alice", 1000000);
243        let idx = store.add_email(email);
244        assert_eq!(idx, 0);
245        assert_eq!(store.find_by_msgid("<test@example.com>"), Some(0));
246        assert_eq!(store.find_by_msgnum(1), Some(0));
247    }
248
249    #[test]
250    fn test_date_sorting() {
251        let mut store = EmailStore::new();
252        let e1 = make_email(1, "<a@e>", "Z", "Zoe", 300);
253        let e2 = make_email(2, "<b@e>", "A", "Alice", 100);
254        let e3 = make_email(3, "<c@e>", "M", "Bob", 200);
255        store.add_email(e1);
256        store.add_email(e2);
257        store.add_email(e3);
258
259        store.insert_into_date_list(0);
260        store.insert_into_date_list(1);
261        store.insert_into_date_list(2);
262
263        let sorted = store.traverse_date_list();
264        assert_eq!(sorted.len(), 3);
265        assert_eq!(store.emails[sorted[0]].msgnum, 2); // date=100
266        assert_eq!(store.emails[sorted[1]].msgnum, 3); // date=200
267        assert_eq!(store.emails[sorted[2]].msgnum, 1); // date=300
268    }
269
270    #[test]
271    fn test_subject_sorting() {
272        let mut store = EmailStore::new();
273        let e1 = make_email(1, "<a@e>", "Zebra", "Zoe", 100);
274        let e2 = make_email(2, "<b@e>", "Alpha", "Alice", 100);
275        let e3 = make_email(3, "<c@e>", "Beta", "Bob", 100);
276        store.add_email(e1);
277        store.add_email(e2);
278        store.add_email(e3);
279
280        store.insert_into_subject_list(0);
281        store.insert_into_subject_list(1);
282        store.insert_into_subject_list(2);
283
284        let sorted = store.traverse_subject_list();
285        assert_eq!(sorted.len(), 3);
286        assert_eq!(store.emails[sorted[0]].subject.as_deref(), Some("Alpha"));
287        assert_eq!(store.emails[sorted[1]].subject.as_deref(), Some("Beta"));
288        assert_eq!(store.emails[sorted[2]].subject.as_deref(), Some("Zebra"));
289    }
290
291    #[test]
292    fn test_inlist() {
293        let list = HmList { values: vec!["a".to_string(), "b".to_string()] };
294        assert!(inlist(&list, "a"));
295        assert!(inlist(&list, "b"));
296        assert!(!inlist(&list, "c"));
297    }
298
299    #[test]
300    fn test_add_to_list() {
301        let mut list = HmList { values: Vec::new() };
302        add_to_list(&mut list, "test");
303        assert!(inlist(&list, "test"));
304        add_to_list(&mut list, "test");
305        assert_eq!(list.values.len(), 1);
306    }
307
308    #[test]
309    fn test_reinit() {
310        let mut store = EmailStore::new();
311        let email = make_email(1, "<a@e>", "T", "A", 100);
312        store.add_email(email);
313        assert_eq!(store.emails.len(), 1);
314        store.reinit();
315        assert_eq!(store.emails.len(), 0);
316        assert!(store.msgid_table.is_empty());
317    }
318
319    #[test]
320    fn test_subject_sorting_uses_unre_subject() {
321        // "Re: Alpha" should sort with "Alpha", not after "Zebra"
322        let mut store = EmailStore::new();
323
324        let mut e1 = make_email(1, "<a@e>", "Zebra", "Alice", 100);
325        e1.unre_subject = Some("zebra".to_string());
326
327        let mut e2 = make_email(2, "<b@e>", "Re: Alpha", "Bob", 200);
328        e2.unre_subject = Some("alpha".to_string());
329
330        let mut e3 = make_email(3, "<c@e>", "Alpha", "Carol", 300);
331        e3.unre_subject = Some("alpha".to_string());
332
333        store.add_email(e1);
334        store.add_email(e2);
335        store.add_email(e3);
336        store.insert_into_subject_list(0);
337        store.insert_into_subject_list(1);
338        store.insert_into_subject_list(2);
339
340        let sorted = store.traverse_subject_list();
341        assert_eq!(sorted.len(), 3);
342        // Both "Alpha" and "Re: Alpha" have unre_subject="alpha" → they sort before "Zebra"
343        let subjects: Vec<_> =
344            sorted.iter().map(|&i| store.emails[i].subject.as_deref().unwrap()).collect();
345        let zebra_pos = subjects.iter().position(|&s| s == "Zebra").unwrap();
346        let re_alpha_pos = subjects.iter().position(|&s| s == "Re: Alpha").unwrap();
347        let alpha_pos = subjects.iter().position(|&s| s == "Alpha").unwrap();
348        assert!(zebra_pos > re_alpha_pos, "Re: Alpha should sort before Zebra");
349        assert!(zebra_pos > alpha_pos, "Alpha should sort before Zebra");
350    }
351
352    #[test]
353    fn test_author_sorting_falls_back_to_email_addr() {
354        let mut store = EmailStore::new();
355
356        // Has a name — sorts by name
357        let e1 = EmailInfo {
358            msgnum: 1,
359            msgid: Some("<a@e>".to_string()),
360            name: Some("Zoe".to_string()),
361            email_addr: Some("zoe@example.com".to_string()),
362            date: 100,
363            ..Default::default()
364        };
365        // No name — should fall back to email_addr "amy@example.com" for sorting
366        let e2 = EmailInfo {
367            msgnum: 2,
368            msgid: Some("<b@e>".to_string()),
369            name: None,
370            email_addr: Some("amy@example.com".to_string()),
371            date: 200,
372            ..Default::default()
373        };
374        // Another no-name — falls back to "mid@example.com"
375        let e3 = EmailInfo {
376            msgnum: 3,
377            msgid: Some("<c@e>".to_string()),
378            name: None,
379            email_addr: Some("mid@example.com".to_string()),
380            date: 300,
381            ..Default::default()
382        };
383
384        store.add_email(e1);
385        store.add_email(e2);
386        store.add_email(e3);
387        store.insert_into_author_list(0);
388        store.insert_into_author_list(1);
389        store.insert_into_author_list(2);
390
391        let sorted = store.traverse_author_list();
392        assert_eq!(sorted.len(), 3);
393        // Expected order by sort key: amy@ < mid@ < Zoe
394        assert_eq!(store.emails[sorted[0]].email_addr.as_deref(), Some("amy@example.com"));
395        assert_eq!(store.emails[sorted[1]].email_addr.as_deref(), Some("mid@example.com"));
396        assert_eq!(store.emails[sorted[2]].name.as_deref(), Some("Zoe"));
397    }
398
399    #[test]
400    fn test_author_sorting_no_name_no_email_sorts_to_front() {
401        // Both name and email_addr are None → key is "" → sorts before everything
402        let mut store = EmailStore::new();
403        let e1 = make_email(1, "<a@e>", "T", "Bob", 100);
404        let e2 = EmailInfo {
405            msgnum: 2,
406            msgid: Some("<b@e>".to_string()),
407            name: None,
408            email_addr: None,
409            date: 200,
410            ..Default::default()
411        };
412        store.add_email(e1);
413        store.add_email(e2);
414        store.insert_into_author_list(0);
415        store.insert_into_author_list(1);
416        let sorted = store.traverse_author_list();
417        // "" < "bob" → nameless/emailless sorts first
418        assert_eq!(store.emails[sorted[0]].msgnum, 2);
419        assert_eq!(store.emails[sorted[1]].name.as_deref(), Some("Bob"));
420    }
421
422    #[test]
423    fn test_add_body() {
424        let chain = crate::message::BodyChain { bodies: Vec::new() };
425        let chain = add_body(chain, "Hello World", 1);
426        assert_eq!(chain.bodies.len(), 1);
427        assert_eq!(chain.bodies[0].line, "Hello World");
428        assert_eq!(chain.bodies[0].msgnum, 1);
429        assert!(!chain.bodies[0].attached);
430        assert!(!chain.bodies[0].header);
431    }
432
433    #[test]
434    fn test_inlist_pos_found() {
435        let list = HmList { values: vec!["a".to_string(), "b".to_string(), "c".to_string()] };
436        assert_eq!(inlist_pos(&list, "b"), Some(1));
437    }
438
439    #[test]
440    fn test_inlist_pos_not_found() {
441        let list = HmList { values: vec!["a".to_string()] };
442        assert_eq!(inlist_pos(&list, "z"), None);
443    }
444
445    #[test]
446    fn test_inlist_regex_pos_found() {
447        let list = HmList { values: vec!["foo".to_string(), "bar123".to_string()] };
448        assert_eq!(inlist_regex_pos(&list, r"bar\d+"), Some(1));
449    }
450
451    #[test]
452    fn test_inlist_regex_pos_not_found() {
453        let list = HmList { values: vec!["foo".to_string()] };
454        assert_eq!(inlist_regex_pos(&list, "xyz"), None);
455    }
456
457    #[test]
458    fn test_add_to_list_multi() {
459        let mut list = HmList { values: Vec::new() };
460        add_to_list_multi(&mut list, "a b c a");
461        assert_eq!(list.values.len(), 3);
462        assert!(inlist(&list, "a"));
463        assert!(inlist(&list, "b"));
464        assert!(inlist(&list, "c"));
465    }
466
467    #[test]
468    fn test_link_reply_adds_to_replylist() {
469        let mut replylist = Vec::new();
470        link_reply(&mut replylist, 1, 2, None, false);
471        assert_eq!(replylist.len(), 1);
472        assert_eq!(replylist[0].from_msgnum, 1);
473        assert_eq!(replylist[0].msgnum, 2);
474        assert_eq!(replylist[0].maybe_reply, 0);
475    }
476
477    #[test]
478    fn test_link_reply_maybe_flag() {
479        let mut replylist = Vec::new();
480        link_reply(&mut replylist, 3, 4, Some(0), true);
481        assert_eq!(replylist[0].maybe_reply, 1);
482    }
483}