1use crate::message::BodyChain;
2use crate::structs::EmailStore;
3use std::collections::HashMap;
4
5pub fn link_quotes(store: &mut EmailStore, ignore_types: bool, linkquotes: bool) {
15 if !linkquotes || ignore_types {
16 return;
17 }
18
19 let msgid_to_msgnum: HashMap<&str, i32> = store
20 .emails
21 .iter()
22 .filter_map(|e| e.msgid.as_deref().map(|mid| (mid, e.msgnum)))
23 .collect();
24
25 let mut new_replies = Vec::new();
26 for email in &store.emails {
27 let to_msgnum = email.msgnum;
28 let from_text = collect_body_text(&email.bodylist);
29 for candidate in extract_angle_bracket_tokens(&from_text) {
30 if let Some(&from_msgnum) = msgid_to_msgnum.get(candidate) {
31 if from_msgnum != to_msgnum {
32 new_replies.push(crate::message::Reply {
33 from_msgnum,
34 msgnum: to_msgnum,
35 data: None,
36 maybe_reply: 1,
37 });
38 }
39 }
40 }
41 }
42 store.replylist.extend(new_replies);
43}
44
45fn extract_angle_bracket_tokens(text: &str) -> impl Iterator<Item = &str> {
47 let mut rest = text;
48 std::iter::from_fn(move || {
49 let start = rest.find('<')?;
50 let after_start = &rest[start + 1..];
51 match after_start.find('>') {
52 Some(end) => {
53 let consumed = start + 1 + end + 1;
54 let token = &rest[start..consumed];
55 rest = &rest[consumed..];
56 Some(token)
57 },
58 None => None,
59 }
60 })
61}
62
63fn collect_body_text(body_chain: &BodyChain) -> String {
64 let mut text = String::new();
65 for body in &body_chain.bodies {
66 if !body.attached && !body.header {
67 text.push_str(&body.line);
68 text.push(' ');
69 }
70 }
71 text
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use crate::message::{Body, BodyChain, EmailInfo};
78
79 #[test]
80 fn test_link_quotes_empty_store() {
81 let mut store = EmailStore::new();
82 link_quotes(&mut store, false, true);
83 assert!(store.replylist.is_empty());
84 }
85
86 #[test]
87 fn test_link_quotes_disabled() {
88 let mut store = EmailStore::new();
89 let e1 = EmailInfo {
90 msgnum: 1,
91 msgid: Some("<a@b>".to_string()),
92 bodylist: BodyChain {
93 bodies: vec![Body {
94 line: "reply to <a@b>".to_string(),
95 html: false,
96 header: false,
97 parsed_header: false,
98 attached: false,
99 demimed: false,
100 msgnum: 0,
101 }],
102 },
103 ..Default::default()
104 };
105 let e2 = EmailInfo {
106 msgnum: 2,
107 msgid: Some("<b@c>".to_string()),
108 bodylist: BodyChain { bodies: Vec::new() },
109 ..Default::default()
110 };
111 store.add_email(e1);
112 store.add_email(e2);
113 link_quotes(&mut store, false, false);
114 assert!(store.replylist.is_empty());
115 }
116
117 #[test]
118 fn test_link_quotes_finds_quoted_msgid() {
119 let mut store = EmailStore::new();
120 let e1 = EmailInfo {
121 msgnum: 1,
122 msgid: Some("<a@b>".to_string()),
123 bodylist: BodyChain { bodies: Vec::new() },
124 ..Default::default()
125 };
126 let e2 = EmailInfo {
127 msgnum: 2,
128 msgid: Some("<c@d>".to_string()),
129 bodylist: BodyChain {
130 bodies: vec![Body {
131 line: "On Mon, someone wrote: <a@b>".to_string(),
132 html: false,
133 header: false,
134 parsed_header: false,
135 attached: false,
136 demimed: false,
137 msgnum: 0,
138 }],
139 },
140 ..Default::default()
141 };
142 store.add_email(e1);
143 store.add_email(e2);
144 link_quotes(&mut store, false, true);
145 assert_eq!(store.replylist.len(), 1);
146 assert_eq!(store.replylist[0].from_msgnum, 1);
147 assert_eq!(store.replylist[0].msgnum, 2);
148 }
149
150 #[test]
151 fn test_extract_angle_bracket_tokens() {
152 let text = "reply to <a@b.com> and also <c@d.com> end";
153 let tokens: Vec<&str> = extract_angle_bracket_tokens(text).collect();
154 assert_eq!(tokens, vec!["<a@b.com>", "<c@d.com>"]);
155 }
156
157 #[test]
158 fn test_extract_angle_bracket_tokens_unclosed() {
159 let text = "no closing bracket <a@b.com";
160 let tokens: Vec<&str> = extract_angle_bracket_tokens(text).collect();
161 assert!(tokens.is_empty());
162 }
163
164 #[test]
165 fn test_collect_body_text() {
166 let chain = BodyChain {
167 bodies: vec![
168 Body {
169 line: "hello".to_string(),
170 html: false,
171 header: false,
172 parsed_header: false,
173 attached: false,
174 demimed: false,
175 msgnum: 0,
176 },
177 Body {
178 line: "world".to_string(),
179 html: false,
180 header: false,
181 parsed_header: false,
182 attached: true,
183 demimed: false,
184 msgnum: 0,
185 },
186 ],
187 };
188 let text = collect_body_text(&chain);
189 assert!(!text.contains("world"));
190 assert!(text.contains("hello"));
191 }
192}