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}