email/email/envelope/list/
imap.rs1use 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; #[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 drop(client);
92
93 if uids.is_empty() {
94 return Ok(Envelopes::default());
95 }
96
97 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 !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 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
329fn 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}