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); assert_eq!(store.emails[sorted[1]].msgnum, 3); assert_eq!(store.emails[sorted[2]].msgnum, 1); }
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 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 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 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 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 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 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 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 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}