1use alloc::{collections::BTreeSet, string::String, vec::Vec};
16use core::{cmp::Ordering, mem};
17use std::path::PathBuf;
18
19use chrono::{DateTime, FixedOffset, NaiveDate};
20use io_m2dir::{
21 coroutine::*,
22 entry::{
23 list::{
24 M2dirEntryList as InnerList, M2dirEntryListError as InnerErr,
25 M2dirEntryListOptions as InnerOpts,
26 },
27 types::M2dirEntry,
28 },
29 flag::types::M2dirFlags,
30 m2dir::types::M2dir,
31 path::M2dirPath,
32};
33use log::trace;
34use mail_parser::MessageParser;
35use thiserror::Error;
36
37use crate::{
38 address::Address,
39 envelope::types::Envelope,
40 m2dir::convert::{InvalidMailboxName, envelope_from, paginate, resolve_mailbox},
41 search::{
42 filter::query::SearchEmailsFilterQuery,
43 query::SearchEmailsQuery,
44 sort::query::{SearchEmailsSorter, SearchEmailsSorterKind, SearchEmailsSorterOrder},
45 },
46};
47
48#[derive(Debug, Error)]
50pub enum M2dirEnvelopeSearchError {
51 #[error(transparent)]
52 List(#[from] InnerErr),
53 #[error(transparent)]
54 InvalidMailbox(#[from] InvalidMailboxName),
55 #[error("coroutine was resumed with an M2dirArg variant it did not request")]
56 UnexpectedArg,
57 #[error("coroutine was resumed after completion")]
58 ResumedAfterDone,
59 #[error("failed to parse m2dir message at {0:?}")]
60 Parse(M2dirPath),
61}
62
63pub struct M2dirEnvelopeSearch {
66 state: State,
67 m2dir: M2dir,
68 filter: Option<SearchEmailsFilterQuery>,
69 sort: Option<Vec<SearchEmailsSorter>>,
70 page: Option<u32>,
71 page_size: Option<u32>,
72 with_attachment: bool,
73}
74
75impl M2dirEnvelopeSearch {
76 pub fn new(
77 root: impl Into<PathBuf>,
78 mailbox: &str,
79 query: Option<&SearchEmailsQuery>,
80 page: Option<u32>,
81 page_size: Option<u32>,
82 with_attachment: bool,
83 ) -> Result<Self, M2dirEnvelopeSearchError> {
84 trace!("prepare m2dir envelope search");
85 let m2dir = resolve_mailbox(root, mailbox)?;
86 let inner = InnerList::new(m2dir.clone(), InnerOpts::default());
87 Ok(Self {
88 state: State::Listing(inner),
89 m2dir,
90 filter: query.and_then(|q| q.filter.clone()),
91 sort: query.and_then(|q| q.sort.clone()),
92 page,
93 page_size,
94 with_attachment,
95 })
96 }
97}
98
99enum State {
100 Listing(InnerList),
101 Reading(Vec<M2dirEntry>),
102 Done,
103}
104
105fn parse_meta_flags(bytes: &[u8]) -> M2dirFlags {
107 let Ok(text) = core::str::from_utf8(bytes) else {
108 return M2dirFlags::default();
109 };
110 text.lines()
111 .map(str::trim)
112 .filter(|line| !line.is_empty())
113 .collect()
114}
115
116fn matches_filter(envelope: &Envelope, raw: &[u8], filter: &SearchEmailsFilterQuery) -> bool {
118 use SearchEmailsFilterQuery as Q;
119
120 match filter {
121 Q::And(left, right) => {
122 matches_filter(envelope, raw, left) && matches_filter(envelope, raw, right)
123 }
124 Q::Or(left, right) => {
125 matches_filter(envelope, raw, left) || matches_filter(envelope, raw, right)
126 }
127 Q::Not(inner) => !matches_filter(envelope, raw, inner),
128 Q::Date(target) => same_day(envelope.date, *target),
129 Q::AfterDate(target) => after_day(envelope.date, *target),
130 Q::From(pattern) => addresses_contain(&envelope.from, pattern),
131 Q::To(pattern) => addresses_contain(&envelope.to, pattern),
132 Q::Subject(pattern) => contains_ci(&envelope.subject, pattern),
133 Q::Body(pattern) => body_contains(raw, pattern),
134 Q::Flag(flag) => envelope.flags.contains(flag),
135 }
136}
137
138fn body_contains(raw: &[u8], pattern: &str) -> bool {
139 let Some(msg) = MessageParser::new().parse(raw) else {
140 return false;
141 };
142 let needle = pattern.as_bytes();
143 for part in msg.text_bodies() {
144 if contains_ignore_ascii_case(part.contents(), needle) {
145 return true;
146 }
147 }
148 for part in msg.html_bodies() {
149 if contains_ignore_ascii_case(part.contents(), needle) {
150 return true;
151 }
152 }
153 false
154}
155
156fn contains_ignore_ascii_case(haystack: &[u8], needle: &[u8]) -> bool {
157 if needle.is_empty() {
158 return true;
159 }
160 if needle.len() > haystack.len() {
161 return false;
162 }
163 haystack
164 .windows(needle.len())
165 .any(|w| w.eq_ignore_ascii_case(needle))
166}
167
168fn same_day(date: Option<DateTime<FixedOffset>>, target: NaiveDate) -> bool {
169 date.map(|d| d.date_naive() == target).unwrap_or(false)
170}
171
172fn after_day(date: Option<DateTime<FixedOffset>>, target: NaiveDate) -> bool {
173 date.map(|d| d.date_naive() > target).unwrap_or(false)
174}
175
176fn addresses_contain(addrs: &[Address], pattern: &str) -> bool {
177 let needle = pattern.to_lowercase();
178 addrs.iter().any(|addr| {
179 let email_hit = addr.email.to_lowercase().contains(&needle);
180 let name_hit = addr
181 .name
182 .as_deref()
183 .map(|n| n.to_lowercase().contains(&needle))
184 .unwrap_or(false);
185 email_hit || name_hit
186 })
187}
188
189fn contains_ci(haystack: &str, needle: &str) -> bool {
190 haystack.to_lowercase().contains(&needle.to_lowercase())
191}
192
193fn compare_with(left: &Envelope, right: &Envelope, sort: &[SearchEmailsSorter]) -> Ordering {
194 for SearchEmailsSorter(kind, order) in sort {
195 let cmp = match kind {
196 SearchEmailsSorterKind::Date => left.date.cmp(&right.date),
197 SearchEmailsSorterKind::From => {
198 first_addr_key(&left.from).cmp(&first_addr_key(&right.from))
199 }
200 SearchEmailsSorterKind::To => first_addr_key(&left.to).cmp(&first_addr_key(&right.to)),
201 SearchEmailsSorterKind::Subject => left.subject.cmp(&right.subject),
202 };
203 let cmp = match order {
204 SearchEmailsSorterOrder::Ascending => cmp,
205 SearchEmailsSorterOrder::Descending => cmp.reverse(),
206 };
207 if cmp != Ordering::Equal {
208 return cmp;
209 }
210 }
211 Ordering::Equal
212}
213
214fn first_addr_key(addrs: &[Address]) -> Option<String> {
215 addrs.first().map(|a| {
216 a.name
217 .as_deref()
218 .map(str::to_lowercase)
219 .unwrap_or_else(|| a.email.to_lowercase())
220 })
221}
222
223impl M2dirCoroutine for M2dirEnvelopeSearch {
224 type Yield = M2dirYield;
225 type Return = Result<Vec<Envelope>, M2dirEnvelopeSearchError>;
226
227 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
228 match mem::replace(&mut self.state, State::Done) {
229 State::Listing(mut inner) => match inner.resume(arg) {
230 M2dirCoroutineState::Yielded(y) => {
231 self.state = State::Listing(inner);
232 M2dirCoroutineState::Yielded(y)
233 }
234 M2dirCoroutineState::Complete(Ok(entries)) => {
235 if entries.is_empty() {
236 return M2dirCoroutineState::Complete(Ok(Vec::new()));
237 }
238 let mut paths: BTreeSet<M2dirPath> = BTreeSet::new();
239 for entry in &entries {
240 paths.insert(entry.path().clone());
241 paths.insert(self.m2dir.flags_path(entry.id()));
242 }
243 self.state = State::Reading(entries);
244 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths))
245 }
246 M2dirCoroutineState::Complete(Err(err)) => {
247 M2dirCoroutineState::Complete(Err(err.into()))
248 }
249 },
250 State::Reading(entries) => {
251 let Some(M2dirArg::FileRead(mut contents)) = arg else {
252 self.state = State::Reading(entries);
253 return M2dirCoroutineState::Complete(Err(
254 M2dirEnvelopeSearchError::UnexpectedArg,
255 ));
256 };
257 let parser = MessageParser::default();
258 let mut hits: Vec<Envelope> = Vec::with_capacity(entries.len());
259
260 for entry in entries {
261 let Some(body) = contents.remove(entry.path()) else {
262 continue;
263 };
264 let flags_bytes = contents
265 .remove(&self.m2dir.flags_path(entry.id()))
266 .unwrap_or_default();
267 let flags = parse_meta_flags(&flags_bytes);
268
269 let parsed = if self.with_attachment {
270 parser.parse(&body)
271 } else {
272 parser.parse_headers(&body)
273 };
274 let Some(parsed) = parsed else {
275 return M2dirCoroutineState::Complete(Err(
276 M2dirEnvelopeSearchError::Parse(entry.path().clone()),
277 ));
278 };
279
280 let mut envelope = envelope_from(&entry, &flags, &parsed);
281 if self.with_attachment {
282 envelope.has_attachment = Some(parsed.attachment_count() > 0);
283 }
284
285 let keep = match self.filter.as_ref() {
286 Some(f) => matches_filter(&envelope, &body, f),
287 None => true,
288 };
289 if keep {
290 hits.push(envelope);
291 }
292 }
293
294 match self.sort.as_deref() {
295 Some(sort) if !sort.is_empty() => {
296 hits.sort_by(|a, b| compare_with(a, b, sort));
297 }
298 _ => hits.sort_by(|a, b| b.date.cmp(&a.date)),
299 }
300
301 M2dirCoroutineState::Complete(Ok(paginate(hits, self.page, self.page_size)))
302 }
303 State::Done => {
304 M2dirCoroutineState::Complete(Err(M2dirEnvelopeSearchError::ResumedAfterDone))
305 }
306 }
307 }
308}