imap_codec/
search.rs

1use abnf_core::streaming::sp;
2use imap_types::{command::CommandBody, core::NonEmptyVec, search::SearchKey};
3use nom::{
4    branch::alt,
5    bytes::streaming::{tag, tag_no_case},
6    combinator::{map, map_opt, opt, value},
7    multi::{many1, separated_list1},
8    sequence::{delimited, preceded, tuple},
9};
10
11use crate::{
12    core::{astring, atom, charset, number},
13    datetime::date,
14    decode::{IMAPErrorKind, IMAPParseError, IMAPResult},
15    fetch::header_fld_name,
16    sequence::sequence_set,
17};
18
19/// `search = "SEARCH" [SP "CHARSET" SP charset] 1*(SP search-key)`
20///
21/// Note: CHARSET argument MUST be registered with IANA
22///
23/// errata id: 261
24pub(crate) fn search(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
25    let mut parser = tuple((
26        tag_no_case(b"SEARCH"),
27        opt(map(
28            tuple((sp, tag_no_case(b"CHARSET"), sp, charset)),
29            |(_, _, _, charset)| charset,
30        )),
31        many1(preceded(sp, search_key(9))),
32    ));
33
34    let (remaining, (_, charset, mut criteria)) = parser(input)?;
35
36    let criteria = match criteria.len() {
37        0 => unreachable!(),
38        1 => criteria.pop().unwrap(),
39        _ => SearchKey::And(NonEmptyVec::unvalidated(criteria)),
40    };
41
42    Ok((
43        remaining,
44        CommandBody::Search {
45            charset,
46            criteria,
47            uid: false,
48        },
49    ))
50}
51
52/// `search-key = "ALL" /
53///               "ANSWERED" /
54///               "BCC" SP astring /
55///               "BEFORE" SP date /
56///               "BODY" SP astring /
57///               "CC" SP astring /
58///               "DELETED" /
59///               "FLAGGED" /
60///               "FROM" SP astring /
61///               "KEYWORD" SP flag-keyword /
62///               "NEW" /
63///               "OLD" /
64///               "ON" SP date /
65///               "RECENT" /
66///               "SEEN" /
67///               "SINCE" SP date /
68///               "SUBJECT" SP astring /
69///               "TEXT" SP astring /
70///               "TO" SP astring /
71///               "UNANSWERED" /
72///               "UNDELETED" /
73///               "UNFLAGGED" /
74///               "UNKEYWORD" SP flag-keyword /
75///               "UNSEEN" /
76///                 ; Above this line were in [IMAP2]
77///               "DRAFT" /
78///               "HEADER" SP header-fld-name SP astring /
79///               "LARGER" SP number /
80///               "NOT" SP search-key /
81///               "OR" SP search-key SP search-key /
82///               "SENTBEFORE" SP date /
83///               "SENTON" SP date /
84///               "SENTSINCE" SP date /
85///               "SMALLER" SP number /
86///               "UID" SP sequence-set /
87///               "UNDRAFT" /
88///               sequence-set /
89///               "(" search-key *(SP search-key) ")"`
90///
91/// This parser is recursively defined. Thus, in order to not overflow the stack,
92/// it is needed to limit how may recursions are allowed. (8 should suffice).
93pub(crate) fn search_key(
94    remaining_recursions: usize,
95) -> impl Fn(&[u8]) -> IMAPResult<&[u8], SearchKey> {
96    move |input: &[u8]| search_key_limited(input, remaining_recursions)
97}
98
99fn search_key_limited<'a>(
100    input: &'a [u8],
101    remaining_recursion: usize,
102) -> IMAPResult<&'a [u8], SearchKey> {
103    if remaining_recursion == 0 {
104        return Err(nom::Err::Failure(IMAPParseError {
105            input,
106            kind: IMAPErrorKind::RecursionLimitExceeded,
107        }));
108    }
109
110    let search_key =
111        move |input: &'a [u8]| search_key_limited(input, remaining_recursion.saturating_sub(1));
112
113    alt((
114        alt((
115            value(SearchKey::All, tag_no_case(b"ALL")),
116            value(SearchKey::Answered, tag_no_case(b"ANSWERED")),
117            map(tuple((tag_no_case(b"BCC"), sp, astring)), |(_, _, val)| {
118                SearchKey::Bcc(val)
119            }),
120            map(
121                tuple((tag_no_case(b"BEFORE"), sp, map_opt(date, |date| date))),
122                |(_, _, date)| SearchKey::Before(date),
123            ),
124            map(tuple((tag_no_case(b"BODY"), sp, astring)), |(_, _, val)| {
125                SearchKey::Body(val)
126            }),
127            map(tuple((tag_no_case(b"CC"), sp, astring)), |(_, _, val)| {
128                SearchKey::Cc(val)
129            }),
130            value(SearchKey::Deleted, tag_no_case(b"DELETED")),
131            value(SearchKey::Flagged, tag_no_case(b"FLAGGED")),
132            map(tuple((tag_no_case(b"FROM"), sp, astring)), |(_, _, val)| {
133                SearchKey::From(val)
134            }),
135            map(
136                // Note: `flag_keyword` parser returns `Flag`. Because Rust does not have first-class enum variants
137                // it is not possible to fix SearchKey(Flag::Keyword), but only SearchKey(Flag).
138                // Thus `SearchKey::Keyword(Atom)` is used instead. This is, why we use also `atom` parser here and not `flag_keyword` parser.
139                tuple((tag_no_case(b"KEYWORD"), sp, atom)),
140                |(_, _, val)| SearchKey::Keyword(val),
141            ),
142            value(SearchKey::New, tag_no_case(b"NEW")),
143            value(SearchKey::Old, tag_no_case(b"OLD")),
144            map(
145                tuple((tag_no_case(b"ON"), sp, map_opt(date, |date| date))),
146                |(_, _, date)| SearchKey::On(date),
147            ),
148            value(SearchKey::Recent, tag_no_case(b"RECENT")),
149            value(SearchKey::Seen, tag_no_case(b"SEEN")),
150            map(
151                tuple((tag_no_case(b"SINCE"), sp, map_opt(date, |date| date))),
152                |(_, _, date)| SearchKey::Since(date),
153            ),
154            map(
155                tuple((tag_no_case(b"SUBJECT"), sp, astring)),
156                |(_, _, val)| SearchKey::Subject(val),
157            ),
158            map(tuple((tag_no_case(b"TEXT"), sp, astring)), |(_, _, val)| {
159                SearchKey::Text(val)
160            }),
161            map(tuple((tag_no_case(b"TO"), sp, astring)), |(_, _, val)| {
162                SearchKey::To(val)
163            }),
164        )),
165        alt((
166            value(SearchKey::Unanswered, tag_no_case(b"UNANSWERED")),
167            value(SearchKey::Undeleted, tag_no_case(b"UNDELETED")),
168            value(SearchKey::Unflagged, tag_no_case(b"UNFLAGGED")),
169            map(
170                // Note: `flag_keyword` parser returns `Flag`. Because Rust does not have first-class enum variants
171                // it is not possible to fix SearchKey(Flag::Keyword), but only SearchKey(Flag).
172                // Thus `SearchKey::Keyword(Atom)` is used instead. This is, why we use also `atom` parser here and not `flag_keyword` parser.
173                tuple((tag_no_case(b"UNKEYWORD"), sp, atom)),
174                |(_, _, val)| SearchKey::Unkeyword(val),
175            ),
176            value(SearchKey::Unseen, tag_no_case(b"UNSEEN")),
177            value(SearchKey::Draft, tag_no_case(b"DRAFT")),
178            map(
179                tuple((tag_no_case(b"HEADER"), sp, header_fld_name, sp, astring)),
180                |(_, _, key, _, val)| SearchKey::Header(key, val),
181            ),
182            map(
183                tuple((tag_no_case(b"LARGER"), sp, number)),
184                |(_, _, val)| SearchKey::Larger(val),
185            ),
186            map(
187                tuple((tag_no_case(b"NOT"), sp, search_key)),
188                |(_, _, val)| SearchKey::Not(Box::new(val)),
189            ),
190            map(
191                tuple((tag_no_case(b"OR"), sp, search_key, sp, search_key)),
192                |(_, _, alt1, _, alt2)| SearchKey::Or(Box::new(alt1), Box::new(alt2)),
193            ),
194            map(
195                tuple((tag_no_case(b"SENTBEFORE"), sp, map_opt(date, |date| date))),
196                |(_, _, date)| SearchKey::SentBefore(date),
197            ),
198            map(
199                tuple((tag_no_case(b"SENTON"), sp, map_opt(date, |date| date))),
200                |(_, _, date)| SearchKey::SentOn(date),
201            ),
202            map(
203                tuple((tag_no_case(b"SENTSINCE"), sp, map_opt(date, |date| date))),
204                |(_, _, date)| SearchKey::SentSince(date),
205            ),
206            map(
207                tuple((tag_no_case(b"SMALLER"), sp, number)),
208                |(_, _, val)| SearchKey::Smaller(val),
209            ),
210            map(
211                tuple((tag_no_case(b"UID"), sp, sequence_set)),
212                |(_, _, val)| SearchKey::Uid(val),
213            ),
214            value(SearchKey::Undraft, tag_no_case(b"UNDRAFT")),
215            map(sequence_set, SearchKey::SequenceSet),
216            map(
217                delimited(tag(b"("), separated_list1(sp, search_key), tag(b")")),
218                |val| SearchKey::And(NonEmptyVec::unvalidated(val)),
219            ),
220        )),
221    ))(input)
222}
223
224#[cfg(test)]
225mod tests {
226    use imap_types::{
227        core::{AString, Atom},
228        datetime::NaiveDate,
229        sequence::{Sequence, SequenceSet},
230    };
231
232    use super::*;
233    use crate::testing::known_answer_test_encode;
234
235    #[test]
236    fn test_parse_search() {
237        use imap_types::{
238            search::SearchKey::*,
239            sequence::{SeqOrUid::Value, Sequence::*, SequenceSet as SequenceSetData},
240        };
241
242        let (_rem, val) = search(b"search (uid 5)???").unwrap();
243        assert_eq!(
244            val,
245            CommandBody::Search {
246                charset: None,
247                criteria: And(NonEmptyVec::from(Uid(SequenceSetData(
248                    vec![Single(Value(5.try_into().unwrap()))]
249                        .try_into()
250                        .unwrap()
251                )))),
252                uid: false,
253            }
254        );
255
256        let (_rem, val) = search(b"search (uid 5 or uid 5 (uid 1 uid 2) not uid 5)???").unwrap();
257        let expected = CommandBody::Search {
258            charset: None,
259            criteria: And(vec![
260                Uid(SequenceSetData(
261                    vec![Single(Value(5.try_into().unwrap()))]
262                        .try_into()
263                        .unwrap(),
264                )),
265                Or(
266                    Box::new(Uid(SequenceSetData(
267                        vec![Single(Value(5.try_into().unwrap()))]
268                            .try_into()
269                            .unwrap(),
270                    ))),
271                    Box::new(And(vec![
272                        Uid(SequenceSetData(
273                            vec![Single(Value(1.try_into().unwrap()))]
274                                .try_into()
275                                .unwrap(),
276                        )),
277                        Uid(SequenceSetData(
278                            vec![Single(Value(2.try_into().unwrap()))]
279                                .try_into()
280                                .unwrap(),
281                        )),
282                    ]
283                    .try_into()
284                    .unwrap())),
285                ),
286                Not(Box::new(Uid(SequenceSetData(
287                    vec![Single(Value(5.try_into().unwrap()))]
288                        .try_into()
289                        .unwrap(),
290                )))),
291            ]
292            .try_into()
293            .unwrap()),
294            uid: false,
295        };
296        assert_eq!(val, expected);
297    }
298
299    #[test]
300    fn test_parse_search_key() {
301        assert!(search_key(1)(b"1:5|").is_ok());
302        assert!(search_key(1)(b"(1:5)|").is_err());
303        assert!(search_key(2)(b"(1:5)|").is_ok());
304        assert!(search_key(2)(b"((1:5))|").is_err());
305    }
306
307    #[test]
308    fn test_encode_search_key() {
309        let tests = [
310            (
311                SearchKey::And(NonEmptyVec::try_from(vec![SearchKey::Answered]).unwrap()),
312                b"(ANSWERED)".as_ref(),
313            ),
314            (
315                SearchKey::And(
316                    NonEmptyVec::try_from(vec![SearchKey::Answered, SearchKey::Seen]).unwrap(),
317                ),
318                b"(ANSWERED SEEN)".as_ref(),
319            ),
320            (
321                SearchKey::SequenceSet(SequenceSet::try_from(1).unwrap()),
322                b"1",
323            ),
324            (SearchKey::All, b"ALL"),
325            (SearchKey::Answered, b"ANSWERED"),
326            (SearchKey::Bcc(AString::try_from("A").unwrap()), b"BCC A"),
327            (
328                SearchKey::Before(
329                    NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap())
330                        .unwrap(),
331                ),
332                b"BEFORE \"12-Apr-2023\"",
333            ),
334            (SearchKey::Body(AString::try_from("A").unwrap()), b"BODY A"),
335            (SearchKey::Cc(AString::try_from("A").unwrap()), b"CC A"),
336            (SearchKey::Deleted, b"DELETED"),
337            (SearchKey::Draft, b"DRAFT"),
338            (SearchKey::Flagged, b"FLAGGED"),
339            (SearchKey::From(AString::try_from("A").unwrap()), b"FROM A"),
340            (
341                SearchKey::Header(
342                    AString::try_from("A").unwrap(),
343                    AString::try_from("B").unwrap(),
344                ),
345                b"HEADER A B",
346            ),
347            (
348                SearchKey::Keyword(Atom::try_from("A").unwrap()),
349                b"KEYWORD A",
350            ),
351            (SearchKey::Larger(42), b"LARGER 42"),
352            (SearchKey::New, b"NEW"),
353            (SearchKey::Not(Box::new(SearchKey::New)), b"NOT NEW"),
354            (SearchKey::Old, b"OLD"),
355            (
356                SearchKey::On(
357                    NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap())
358                        .unwrap(),
359                ),
360                b"ON \"12-Apr-2023\"",
361            ),
362            (
363                SearchKey::Or(Box::new(SearchKey::New), Box::new(SearchKey::Recent)),
364                b"OR NEW RECENT",
365            ),
366            (SearchKey::Recent, b"RECENT"),
367            (SearchKey::Seen, b"SEEN"),
368            (
369                SearchKey::SentBefore(
370                    NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap())
371                        .unwrap(),
372                ),
373                b"SENTBEFORE \"12-Apr-2023\"",
374            ),
375            (
376                SearchKey::SentOn(
377                    NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap())
378                        .unwrap(),
379                ),
380                b"SENTON \"12-Apr-2023\"",
381            ),
382            (
383                SearchKey::SentSince(
384                    NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap())
385                        .unwrap(),
386                ),
387                b"SENTSINCE \"12-Apr-2023\"",
388            ),
389            (
390                SearchKey::Since(
391                    NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap())
392                        .unwrap(),
393                ),
394                b"SINCE \"12-Apr-2023\"",
395            ),
396            (SearchKey::Smaller(1337), b"SMALLER 1337"),
397            (
398                SearchKey::Subject(AString::try_from("A").unwrap()),
399                b"SUBJECT A",
400            ),
401            (SearchKey::Text(AString::try_from("A").unwrap()), b"TEXT A"),
402            (SearchKey::To(AString::try_from("A").unwrap()), b"TO A"),
403            (
404                SearchKey::Uid(SequenceSet::try_from(Sequence::try_from(1..).unwrap()).unwrap()),
405                b"UID 1:*",
406            ),
407            (SearchKey::Unanswered, b"UNANSWERED"),
408            (SearchKey::Undeleted, b"UNDELETED"),
409            (SearchKey::Undraft, b"UNDRAFT"),
410            (SearchKey::Unflagged, b"UNFLAGGED"),
411            (
412                SearchKey::Unkeyword(Atom::try_from("A").unwrap()),
413                b"UNKEYWORD A",
414            ),
415            (SearchKey::Unseen, b"UNSEEN"),
416        ];
417
418        for test in tests {
419            known_answer_test_encode(test);
420        }
421    }
422}