imap_codec/
flag.rs

1use abnf_core::streaming::sp;
2use imap_types::flag::{Flag, FlagFetch, FlagNameAttribute, FlagPerm};
3use nom::{
4    branch::alt,
5    bytes::streaming::tag,
6    character::streaming::char,
7    combinator::{map, recognize, value},
8    multi::{separated_list0, separated_list1},
9    sequence::{delimited, preceded, tuple},
10};
11
12use crate::{core::atom, decode::IMAPResult};
13
14/// ```abnf
15/// flag = "\Answered" /
16///        "\Flagged" /
17///        "\Deleted" /
18///        "\Seen" /
19///        "\Draft" /
20///        flag-keyword /
21///        flag-extension
22/// ```
23///
24/// Note: Does not include "\Recent"
25pub(crate) fn flag(input: &[u8]) -> IMAPResult<&[u8], Flag> {
26    alt((
27        map(preceded(char('\\'), atom), Flag::system),
28        map(atom, Flag::Keyword),
29    ))(input)
30}
31
32// Note(duesee): This was inlined into [`flag`].
33// #[inline]
34// /// `flag-keyword = atom`
35// pub(crate) fn flag_keyword(input: &[u8]) -> IMAPResult<&[u8], Flag> {
36//     map(atom, Flag::Keyword)(input)
37// }
38
39// Note: This was inlined into `mbx_list_flags`.
40// /// ```abnf
41// /// flag-extension = "\" atom
42// /// ```
43// ///
44// /// Future expansion.
45// ///
46// /// Client implementations MUST accept flag-extension flags.
47// /// Server implementations MUST NOT generate flag-extension flags
48// /// except as defined by future standard or standards-track revisions of this specification.
49// pub(crate) fn flag_extension(input: &[u8]) -> IMAPResult<&[u8], Atom> {
50//     preceded(tag(b"\\"), atom)(input)
51// }
52
53/// `flag-list = "(" [flag *(SP flag)] ")"`
54pub(crate) fn flag_list(input: &[u8]) -> IMAPResult<&[u8], Vec<Flag>> {
55    delimited(tag(b"("), separated_list0(sp, flag), tag(b")"))(input)
56}
57
58/// `flag-fetch = flag / "\Recent"`
59pub(crate) fn flag_fetch(input: &[u8]) -> IMAPResult<&[u8], FlagFetch> {
60    if let Ok((rem, peek)) = recognize(tuple((char('\\'), atom)))(input) {
61        if peek.to_ascii_lowercase() == b"\\recent" {
62            return Ok((rem, FlagFetch::Recent));
63        }
64    }
65
66    map(flag, FlagFetch::Flag)(input)
67}
68
69/// `flag-perm = flag / "\*"`
70pub(crate) fn flag_perm(input: &[u8]) -> IMAPResult<&[u8], FlagPerm> {
71    alt((
72        value(FlagPerm::Asterisk, tag("\\*")),
73        map(flag, FlagPerm::Flag),
74    ))(input)
75}
76
77/// ```abnf
78/// mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag *(SP mbx-list-oflag) /
79///                                        mbx-list-oflag *(SP mbx-list-oflag)
80/// ```
81///
82/// TODO(#155): ABNF enforces that sflag is not used more than once.
83///             We could parse any flag and check for multiple occurrences of sflag later.
84pub(crate) fn mbx_list_flags(input: &[u8]) -> IMAPResult<&[u8], Vec<FlagNameAttribute>> {
85    let (remaining, flags) =
86        separated_list1(sp, map(preceded(char('\\'), atom), FlagNameAttribute::from))(input)?;
87
88    // TODO(#155): Do we really want to enforce this?
89    // let sflag_count = flags
90    //     .iter()
91    //     .filter(|&flag| FlagNameAttribute::is_selectability(flag))
92    //     .count();
93    //
94    // if sflag_count > 1 {
95    //     return Err(nom::Err::Failure(nom::error::make_error(
96    //         input,
97    //         nom::error::ErrorKind::Verify,
98    //     )));
99    // }
100
101    Ok((remaining, flags))
102}
103
104// Note: This was inlined into `mbx_list_flags`.
105// /// ```abnf
106// /// mbx-list-oflag = "\Noinferiors" / flag-extension
107// /// ```
108// ///
109// /// Other flags; multiple possible per LIST response
110// pub(crate) fn mbx_list_oflag(input: &[u8]) -> IMAPResult<&[u8], FlagNameAttribute> {
111//     alt((
112//         value(
113//             FlagNameAttribute::Noinferiors,
114//             tag_no_case(b"\\Noinferiors"),
115//         ),
116//         map(flag_extension, FlagNameAttribute::Extension),
117//     ))(input)
118// }
119
120// Note: This was inlined into `mbx_list_flags`.
121// /// ```abnf
122// /// mbx-list-sflag = "\Noselect" / "\Marked" / "\Unmarked"
123// /// ```
124// ///
125// /// Selectability flags; only one per LIST response
126// pub(crate) fn mbx_list_sflag(input: &[u8]) -> IMAPResult<&[u8], FlagNameAttribute> {
127//     alt((
128//         value(FlagNameAttribute::Noselect, tag_no_case(b"\\Noselect")),
129//         value(FlagNameAttribute::Marked, tag_no_case(b"\\Marked")),
130//         value(FlagNameAttribute::Unmarked, tag_no_case(b"\\Unmarked")),
131//     ))(input)
132// }
133
134#[cfg(test)]
135mod tests {
136    use imap_types::{
137        core::Atom,
138        flag::{Flag, FlagFetch, FlagNameAttribute, FlagPerm},
139    };
140
141    use super::*;
142
143    #[test]
144    fn test_parse_flag_fetch() {
145        let tests = [(
146            "iS)",
147            FlagFetch::Flag(Flag::Keyword(Atom::try_from("iS").unwrap())),
148        )];
149
150        for (test, expected) in tests {
151            let (rem, got) = flag_fetch(test.as_bytes()).unwrap();
152            assert_eq!(rem.len(), 1);
153            assert_eq!(expected, got);
154        }
155    }
156
157    #[test]
158    fn test_parse_flag_perm() {
159        let tests = [
160            ("\\Deleted)", FlagPerm::Flag(Flag::Deleted)),
161            (
162                "\\Deletedx)",
163                FlagPerm::Flag(Flag::system(Atom::try_from("Deletedx").unwrap())),
164            ),
165            ("\\Seen ", FlagPerm::Flag(Flag::Seen)),
166            ("\\*)", FlagPerm::Asterisk),
167        ];
168
169        for (test, expected) in tests {
170            let (rem, got) = flag_perm(test.as_bytes()).unwrap();
171            assert_eq!(rem.len(), 1);
172            assert_eq!(expected, got);
173        }
174    }
175
176    #[test]
177    fn test_parse_mbx_list_flags() {
178        let tests = [
179            (
180                "\\Markedm)",
181                vec![FlagNameAttribute::from(Atom::try_from("Markedm").unwrap())],
182            ),
183            ("\\Marked)", vec![FlagNameAttribute::Marked]),
184        ];
185
186        for (test, expected) in tests {
187            let (rem, got) = mbx_list_flags(test.as_bytes()).unwrap();
188            assert_eq!(expected, got);
189            assert_eq!(rem.len(), 1);
190        }
191    }
192}