email/email/envelope/list/
imap.rs

1use std::{collections::HashMap, num::NonZeroU32, result};
2
3use async_trait::async_trait;
4use chrono::TimeDelta;
5use futures::{stream::FuturesUnordered, StreamExt};
6use imap_client::imap_next::imap_types::{
7    core::Vec1,
8    extensions::sort::{SortCriterion, SortKey},
9    search::SearchKey,
10    sequence::{SeqOrUid, Sequence, SequenceSet},
11};
12use tracing::{debug, info, instrument, trace};
13use utf7_imap::encode_utf7_imap as encode_utf7;
14
15use super::{Envelopes, ListEnvelopes, ListEnvelopesOptions};
16use crate::{
17    email::error::Error,
18    envelope::Envelope,
19    imap,
20    imap::ImapContext,
21    search_query::{
22        filter::SearchEmailsFilterQuery,
23        sort::{SearchEmailsSorter, SearchEmailsSorterKind, SearchEmailsSorterOrder},
24        SearchEmailsQuery,
25    },
26    AnyResult, Result,
27};
28
29static MAX_SEQUENCE_SIZE: u8 = u8::MAX; // 255
30
31#[derive(Clone, Debug)]
32pub struct ListImapEnvelopes {
33    ctx: ImapContext,
34}
35
36impl ListImapEnvelopes {
37    pub fn new(ctx: &ImapContext) -> Self {
38        Self { ctx: ctx.clone() }
39    }
40
41    pub fn new_boxed(ctx: &ImapContext) -> Box<dyn ListEnvelopes> {
42        Box::new(Self::new(ctx))
43    }
44
45    pub fn some_new_boxed(ctx: &ImapContext) -> Option<Box<dyn ListEnvelopes>> {
46        Some(Self::new_boxed(ctx))
47    }
48}
49
50#[async_trait]
51impl ListEnvelopes for ListImapEnvelopes {
52    #[instrument(skip(self), level = "trace")]
53    async fn list_envelopes(
54        &self,
55        folder: &str,
56        opts: ListEnvelopesOptions,
57    ) -> AnyResult<Envelopes> {
58        info!("listing IMAP envelopes from mailbox {folder}");
59
60        let config = &self.ctx.account_config;
61        let mut client = self.ctx.client().await;
62
63        let folder = config.get_folder_alias(folder);
64        let folder_encoded = encode_utf7(folder.clone());
65        debug!(name = folder_encoded, "UTF7-encoded mailbox");
66
67        let data = client.select_mailbox(folder_encoded.clone()).await?;
68        let folder_size = data.exists.unwrap_or_default() as usize;
69        debug!(name = folder_encoded, ?data, "mailbox selected");
70
71        if folder_size == 0 {
72            return Ok(Envelopes::default());
73        }
74
75        let envelopes = if let Some(query) = opts.query.as_ref() {
76            let sort_supported = client.ext_sort_supported();
77            let sort_criteria = query.to_imap_sort_criteria();
78            let search_criteria = query.to_imap_search_criteria();
79
80            let uids = if sort_supported {
81                client
82                    .sort_uids(sort_criteria.clone(), search_criteria.clone())
83                    .await
84            } else {
85                client.search_uids(search_criteria.clone()).await
86            }?;
87
88            // this client is not used anymore, so we can drop it now
89            // in order to free one client slot from the clients
90            // connection pool
91            drop(client);
92
93            if uids.is_empty() {
94                return Ok(Envelopes::default());
95            }
96
97            // if the SORT extension is supported by the client,
98            // envelopes can be paginated straight away
99            let uids = if sort_supported {
100                paginate(&uids, opts.page, opts.page_size)?
101            } else {
102                &uids
103            };
104
105            let uids_chunks = uids.chunks(MAX_SEQUENCE_SIZE as usize);
106            let uids_chunks_len = uids_chunks.len();
107
108            debug!(?uids, "fetching envelopes using {uids_chunks_len} chunks");
109
110            let mut fetches = FuturesUnordered::from_iter(uids_chunks.map(|uids| {
111                let ctx = self.ctx.clone();
112                let mbox = folder_encoded.clone();
113                let uids = SequenceSet::try_from(uids.to_vec()).unwrap();
114
115                tokio::spawn(async move {
116                    let mut client = ctx.client().await;
117                    client.select_mailbox(mbox).await?;
118                    client.fetch_envelopes(uids).await
119                })
120            }))
121            .enumerate()
122            .fold(
123                Ok(HashMap::<String, Envelope>::default()),
124                |all_envelopes, (n, envelopes)| async move {
125                    let Ok(mut all_envelopes) = all_envelopes else {
126                        return all_envelopes;
127                    };
128
129                    match envelopes {
130                        Err(err) => {
131                            return Err(imap::Error::JoinClientError(err));
132                        }
133                        Ok(Err(err)) => {
134                            return Err(err);
135                        }
136                        Ok(Ok(envelopes)) => {
137                            debug!("fetched envelopes chunk {}/{uids_chunks_len}", n + 1);
138
139                            for envelope in envelopes {
140                                all_envelopes.insert(envelope.id.clone(), envelope);
141                            }
142
143                            Ok(all_envelopes)
144                        }
145                    }
146                },
147            )
148            .await?;
149
150            let mut envelopes: Envelopes = uids
151                .iter()
152                .flat_map(|uid| fetches.remove(&uid.to_string()))
153                .collect();
154
155            // if the SORT extension is NOT supported by the client,
156            // envelopes are sorted and paginated only now
157            if !sort_supported {
158                opts.sort_envelopes(&mut envelopes);
159                apply_pagination(&mut envelopes, opts.page, opts.page_size)?;
160            }
161
162            envelopes
163        } else {
164            let seq = build_sequence(opts.page, opts.page_size, folder_size)?;
165            let mut envelopes = client.fetch_envelopes_by_sequence(seq.into()).await?;
166            envelopes.sort_by(|a, b| b.date.cmp(&a.date));
167            envelopes
168        };
169
170        debug!("found {} imap envelopes", envelopes.len());
171        trace!("{envelopes:#?}");
172
173        Ok(envelopes)
174    }
175}
176
177impl SearchEmailsQuery {
178    pub fn to_imap_search_criteria(&self) -> Vec1<SearchKey<'static>> {
179        self.filter
180            .as_ref()
181            .map(|f| f.to_imap_search_criterion())
182            .unwrap_or(SearchKey::All)
183            .into()
184    }
185
186    pub fn to_imap_sort_criteria(&self) -> Vec1<SortCriterion> {
187        let criteria: Vec<_> = self
188            .sort
189            .as_ref()
190            .map(|sorters| {
191                sorters
192                    .iter()
193                    .map(|sorter| sorter.to_imap_sort_criterion())
194                    .collect()
195            })
196            .unwrap_or_default();
197
198        Vec1::try_from(criteria).unwrap_or_else(|_| {
199            Vec1::from(SortCriterion {
200                reverse: true,
201                key: SortKey::Date,
202            })
203        })
204    }
205}
206
207impl SearchEmailsFilterQuery {
208    pub fn to_imap_search_criterion(&self) -> SearchKey<'static> {
209        match self {
210            SearchEmailsFilterQuery::And(left, right) => {
211                let criteria = vec![
212                    left.to_imap_search_criterion(),
213                    right.to_imap_search_criterion(),
214                ];
215                SearchKey::And(criteria.try_into().unwrap())
216            }
217            SearchEmailsFilterQuery::Or(left, right) => {
218                let left = left.to_imap_search_criterion();
219                let right = right.to_imap_search_criterion();
220                SearchKey::Or(Box::new(left), Box::new(right))
221            }
222            SearchEmailsFilterQuery::Not(filter) => {
223                let criterion = filter.to_imap_search_criterion();
224                SearchKey::Not(Box::new(criterion))
225            }
226            SearchEmailsFilterQuery::Date(date) => SearchKey::SentOn((*date).try_into().unwrap()),
227            SearchEmailsFilterQuery::BeforeDate(date) => {
228                SearchKey::SentBefore((*date).try_into().unwrap())
229            }
230            SearchEmailsFilterQuery::AfterDate(date) => {
231                // imap sentsince is inclusive, so we add one day to
232                // the date filter.
233                let date = *date + TimeDelta::try_days(1).unwrap();
234                SearchKey::SentSince(date.try_into().unwrap())
235            }
236            SearchEmailsFilterQuery::From(pattern) => {
237                SearchKey::From(pattern.clone().try_into().unwrap())
238            }
239            SearchEmailsFilterQuery::To(pattern) => {
240                SearchKey::To(pattern.clone().try_into().unwrap())
241            }
242            SearchEmailsFilterQuery::Subject(pattern) => {
243                SearchKey::Subject(pattern.clone().try_into().unwrap())
244            }
245            SearchEmailsFilterQuery::Body(pattern) => {
246                SearchKey::Body(pattern.clone().try_into().unwrap())
247            }
248            SearchEmailsFilterQuery::Flag(flag) => flag.clone().try_into().unwrap(),
249        }
250    }
251}
252
253impl SearchEmailsSorter {
254    pub fn to_imap_sort_criterion(&self) -> SortCriterion {
255        use SearchEmailsSorterKind::*;
256        use SearchEmailsSorterOrder::*;
257
258        match self {
259            SearchEmailsSorter(Date, Ascending) => SortCriterion {
260                reverse: false,
261                key: SortKey::Date,
262            },
263            SearchEmailsSorter(Date, Descending) => SortCriterion {
264                reverse: true,
265                key: SortKey::Date,
266            },
267            SearchEmailsSorter(From, Ascending) => SortCriterion {
268                reverse: false,
269                key: SortKey::From,
270            },
271            SearchEmailsSorter(From, Descending) => SortCriterion {
272                reverse: true,
273                key: SortKey::From,
274            },
275            SearchEmailsSorter(To, Ascending) => SortCriterion {
276                reverse: false,
277                key: SortKey::To,
278            },
279            SearchEmailsSorter(To, Descending) => SortCriterion {
280                reverse: true,
281                key: SortKey::To,
282            },
283            SearchEmailsSorter(Subject, Ascending) => SortCriterion {
284                reverse: false,
285                key: SortKey::Subject,
286            },
287            SearchEmailsSorter(Subject, Descending) => SortCriterion {
288                reverse: true,
289                key: SortKey::Subject,
290            },
291        }
292    }
293}
294
295fn paginate<T>(items: &[T], page: usize, page_size: usize) -> Result<&[T]> {
296    if page_size == 0 {
297        return Ok(items);
298    }
299
300    let total = items.len();
301    let page_cursor = page * page_size;
302    if page_cursor >= total {
303        Err(Error::BuildPageRangeOutOfBoundsImapError(page + 1))?
304    }
305
306    Ok(&items[0..page_size.min(total)])
307}
308
309fn apply_pagination(
310    envelopes: &mut Envelopes,
311    page: usize,
312    page_size: usize,
313) -> result::Result<(), Error> {
314    let total = envelopes.len();
315    let page_cursor = page * page_size;
316    if page_cursor >= total {
317        Err(Error::BuildPageRangeOutOfBoundsImapError(page + 1))?
318    }
319
320    if page_size == 0 {
321        return Ok(());
322    }
323
324    let page_size = page_size.min(total);
325    *envelopes = Envelopes(envelopes[0..page_size].to_vec());
326    Ok(())
327}
328
329/// Builds the IMAP sequence set for the give page, page size and
330/// total size.
331fn build_sequence(page: usize, page_size: usize, total: usize) -> Result<Sequence> {
332    let seq = if page_size == 0 {
333        Sequence::Range(SeqOrUid::try_from(1).unwrap(), SeqOrUid::Asterisk)
334    } else {
335        let page_cursor = page * page_size;
336        if page_cursor >= total {
337            Err(Error::BuildPageRangeOutOfBoundsImapError(page + 1))?
338        }
339
340        let mut count = 1;
341        let mut cursor = total - (total.min(page_cursor));
342
343        let page_size = page_size.min(total);
344        let from = SeqOrUid::Value(NonZeroU32::new(cursor as u32).unwrap());
345        while cursor > 1 && count < page_size {
346            count += 1;
347            cursor -= 1;
348        }
349        let to = SeqOrUid::Value(NonZeroU32::new(cursor as u32).unwrap());
350        Sequence::Range(from, to)
351    };
352
353    Ok(seq)
354}