imap_codec/
mailbox.rs

1use abnf_core::streaming::{dquote, sp};
2use imap_types::{
3    core::QuotedChar,
4    flag::FlagNameAttribute,
5    mailbox::{ListCharString, ListMailbox, Mailbox},
6    response::Data,
7    utils::indicators::is_list_char,
8};
9use nom::{
10    branch::alt,
11    bytes::streaming::{tag, tag_no_case, take_while1},
12    combinator::{map, opt, value},
13    multi::many0,
14    sequence::{delimited, preceded, tuple},
15};
16
17use crate::{
18    core::{astring, nil, number, nz_number, quoted_char, string},
19    decode::IMAPResult,
20    extensions::quota::{quota_response, quotaroot_response},
21    flag::{flag_list, mbx_list_flags},
22    status::status_att_list,
23};
24
25/// `list-mailbox = 1*list-char / string`
26pub(crate) fn list_mailbox(input: &[u8]) -> IMAPResult<&[u8], ListMailbox> {
27    alt((
28        map(take_while1(is_list_char), |bytes: &[u8]| {
29            // # Safety
30            //
31            // `unwrap` is safe here, because `is_list_char` enforces that the bytes ...
32            //   * contain ASCII-only characters, i.e., `from_utf8` will return `Ok`.
33            //   * are valid according to `ListCharString::verify()`, i.e., `unvalidated` is safe.
34            ListMailbox::Token(ListCharString::unvalidated(
35                std::str::from_utf8(bytes).unwrap(),
36            ))
37        }),
38        map(string, ListMailbox::String),
39    ))(input)
40}
41
42/// `mailbox = "INBOX" / astring`
43///
44/// INBOX is case-insensitive. All case variants of INBOX (e.g., "iNbOx")
45/// MUST be interpreted as INBOX not as an astring.
46///
47/// An astring which consists of the case-insensitive sequence
48/// "I" "N" "B" "O" "X" is considered to be INBOX and not an astring.
49///
50/// Refer to section 5.1 for further semantic details of mailbox names.
51pub(crate) fn mailbox(input: &[u8]) -> IMAPResult<&[u8], Mailbox> {
52    map(astring, Mailbox::from)(input)
53}
54
55/// `mailbox-data = "FLAGS" SP flag-list /
56///                 "LIST" SP mailbox-list /
57///                 "LSUB" SP mailbox-list /
58///                 "SEARCH" *(SP nz-number) /
59///                 "STATUS" SP mailbox SP "(" [status-att-list] ")" /
60///                 number SP "EXISTS" /
61///                 number SP "RECENT"`
62pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
63    alt((
64        map(
65            tuple((tag_no_case(b"FLAGS"), sp, flag_list)),
66            |(_, _, flags)| Data::Flags(flags),
67        ),
68        map(
69            tuple((tag_no_case(b"LIST"), sp, mailbox_list)),
70            |(_, _, (items, delimiter, mailbox))| Data::List {
71                items: items.unwrap_or_default(),
72                mailbox,
73                delimiter,
74            },
75        ),
76        map(
77            tuple((tag_no_case(b"LSUB"), sp, mailbox_list)),
78            |(_, _, (items, delimiter, mailbox))| Data::Lsub {
79                items: items.unwrap_or_default(),
80                mailbox,
81                delimiter,
82            },
83        ),
84        map(
85            tuple((tag_no_case(b"SEARCH"), many0(preceded(sp, nz_number)))),
86            |(_, nums)| Data::Search(nums),
87        ),
88        map(
89            tuple((
90                tag_no_case(b"STATUS"),
91                sp,
92                mailbox,
93                sp,
94                delimited(tag(b"("), opt(status_att_list), tag(b")")),
95            )),
96            |(_, _, mailbox, _, items)| Data::Status {
97                mailbox,
98                items: items.unwrap_or_default().into(),
99            },
100        ),
101        map(
102            tuple((number, sp, tag_no_case(b"EXISTS"))),
103            |(num, _, _)| Data::Exists(num),
104        ),
105        map(
106            tuple((number, sp, tag_no_case(b"RECENT"))),
107            |(num, _, _)| Data::Recent(num),
108        ),
109        quotaroot_response,
110        quota_response,
111    ))(input)
112}
113
114/// `mailbox-list = "(" [mbx-list-flags] ")" SP
115///                 (DQUOTE QUOTED-CHAR DQUOTE / nil) SP
116///                 mailbox`
117#[allow(clippy::type_complexity)]
118pub(crate) fn mailbox_list(
119    input: &[u8],
120) -> IMAPResult<&[u8], (Option<Vec<FlagNameAttribute>>, Option<QuotedChar>, Mailbox)> {
121    let mut parser = tuple((
122        delimited(tag(b"("), opt(mbx_list_flags), tag(b")")),
123        sp,
124        alt((
125            map(delimited(dquote, quoted_char, dquote), Option::Some),
126            value(None, nil),
127        )),
128        sp,
129        mailbox,
130    ));
131
132    let (remaining, (mbx_list_flags, _, maybe_delimiter, _, mailbox)) = parser(input)?;
133
134    Ok((remaining, (mbx_list_flags, maybe_delimiter, mailbox)))
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_mailbox() {
143        assert!(mailbox(b"\"iNbOx\"").is_ok());
144        assert!(mailbox(b"{3}\r\naaa\r\n").is_ok());
145        assert!(mailbox(b"inbox ").is_ok());
146        assert!(mailbox(b"inbox.sent ").is_ok());
147        assert!(mailbox(b"aaa").is_err());
148    }
149}