imap_proto/parser/
rfc5464.rs

1//!
2//! https://tools.ietf.org/html/rfc5464
3//!
4//! IMAP METADATA extension
5//!
6
7use nom::{
8    branch::alt,
9    bytes::streaming::{tag, tag_no_case},
10    combinator::map,
11    multi::separated_list0,
12    sequence::tuple,
13    IResult,
14};
15use std::borrow::Cow;
16
17use crate::{parser::core::*, types::*};
18
19fn is_entry_component_char(c: u8) -> bool {
20    c < 0x80 && c > 0x19 && c != b'*' && c != b'%' && c != b'/'
21}
22
23enum EntryParseStage<'a> {
24    PrivateShared,
25    Admin(usize),
26    VendorComment(usize),
27    Path(usize),
28    Done(usize),
29    Fail(nom::Err<&'a [u8]>),
30}
31
32fn check_private_shared(i: &[u8]) -> EntryParseStage<'_> {
33    if i.starts_with(b"/private") {
34        EntryParseStage::VendorComment(8)
35    } else if i.starts_with(b"/shared") {
36        EntryParseStage::Admin(7)
37    } else {
38        EntryParseStage::Fail(nom::Err::Error(
39            b"Entry Name doesn't start with /private or /shared",
40        ))
41    }
42}
43
44fn check_admin(i: &[u8], l: usize) -> EntryParseStage<'_> {
45    if i[l..].starts_with(b"/admin") {
46        EntryParseStage::Path(l + 6)
47    } else {
48        EntryParseStage::VendorComment(l)
49    }
50}
51
52fn check_vendor_comment(i: &[u8], l: usize) -> EntryParseStage<'_> {
53    if i[l..].starts_with(b"/comment") {
54        EntryParseStage::Path(l + 8)
55    } else if i[l..].starts_with(b"/vendor") {
56        //make sure vendor name is present
57        if i.len() < l + 9 || i[l + 7] != b'/' || !is_entry_component_char(i[l + 8]) {
58            EntryParseStage::Fail(nom::Err::Incomplete(nom::Needed::Unknown))
59        } else {
60            EntryParseStage::Path(l + 7)
61        }
62    } else {
63        EntryParseStage::Fail(nom::Err::Error(
64            b"Entry name is not continued with /admin, /vendor or /comment",
65        ))
66    }
67}
68
69fn check_path(i: &[u8], l: usize) -> EntryParseStage<'_> {
70    if i.len() == l || i[l] == b' ' || i[l] == b'\r' {
71        return EntryParseStage::Done(l);
72    } else if i[l] != b'/' {
73        return EntryParseStage::Fail(nom::Err::Error(b"Entry name path is corrupted"));
74    }
75    for j in 1..(i.len() - l) {
76        if !is_entry_component_char(i[l + j]) {
77            return EntryParseStage::Path(l + j);
78        }
79    }
80    EntryParseStage::Done(i.len())
81}
82
83fn check_entry_name(i: &[u8]) -> IResult<&[u8], &[u8]> {
84    let mut stage = EntryParseStage::PrivateShared;
85    loop {
86        match stage {
87            EntryParseStage::PrivateShared => {
88                stage = check_private_shared(i);
89            }
90            EntryParseStage::Admin(l) => {
91                stage = check_admin(i, l);
92            }
93            EntryParseStage::VendorComment(l) => {
94                stage = check_vendor_comment(i, l);
95            }
96            EntryParseStage::Path(l) => {
97                stage = check_path(i, l);
98            }
99            EntryParseStage::Done(l) => {
100                return Ok((&i[l..], &i[..l]));
101            }
102            EntryParseStage::Fail(nom::Err::Error(err_msg)) => {
103                return std::result::Result::Err(nom::Err::Error(nom::error::Error::new(
104                    err_msg,
105                    nom::error::ErrorKind::Verify,
106                )));
107            }
108            EntryParseStage::Fail(nom::Err::Incomplete(reason)) => {
109                return std::result::Result::Err(nom::Err::Incomplete(reason));
110            }
111            _ => panic!("Entry name verification failure"),
112        }
113    }
114}
115
116fn entry_name(i: &[u8]) -> IResult<&[u8], &[u8]> {
117    let astring_res = astring(i)?;
118    check_entry_name(astring_res.1)?;
119    Ok(astring_res)
120}
121
122fn slice_to_str(i: &[u8]) -> &str {
123    std::str::from_utf8(i).unwrap()
124}
125
126fn nil_value(i: &[u8]) -> IResult<&[u8], Option<String>> {
127    map(tag_no_case("NIL"), |_| None)(i)
128}
129
130fn string_value(i: &[u8]) -> IResult<&[u8], Option<String>> {
131    map(alt((quoted, literal)), |s| {
132        Some(slice_to_str(s).to_string())
133    })(i)
134}
135
136fn keyval_list(i: &[u8]) -> IResult<&[u8], Vec<Metadata>> {
137    parenthesized_nonempty_list(map(
138        tuple((
139            map(entry_name, slice_to_str),
140            tag(" "),
141            alt((nil_value, string_value)),
142        )),
143        |(key, _, value)| Metadata {
144            entry: key.to_string(),
145            value,
146        },
147    ))(i)
148}
149
150fn entry_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<'_, str>>> {
151    separated_list0(tag(" "), map(map(entry_name, slice_to_str), Cow::Borrowed))(i)
152}
153
154fn metadata_common(i: &[u8]) -> IResult<&[u8], &[u8]> {
155    let (i, (_, mbox, _)) = tuple((tag_no_case("METADATA "), quoted, tag(" ")))(i)?;
156    Ok((i, mbox))
157}
158
159// [RFC5464 - 4.4.1 METADATA Response with values]
160pub(crate) fn metadata_solicited(i: &[u8]) -> IResult<&[u8], Response<'_>> {
161    let (i, (mailbox, values)) = tuple((metadata_common, keyval_list))(i)?;
162    Ok((
163        i,
164        Response::MailboxData(MailboxDatum::MetadataSolicited {
165            mailbox: Cow::Borrowed(slice_to_str(mailbox)),
166            values,
167        }),
168    ))
169}
170
171// [RFC5464 - 4.4.2 Unsolicited METADATA Response without values]
172pub(crate) fn metadata_unsolicited(i: &[u8]) -> IResult<&[u8], Response<'_>> {
173    let (i, (mailbox, values)) = tuple((metadata_common, entry_list))(i)?;
174    Ok((
175        i,
176        Response::MailboxData(MailboxDatum::MetadataUnsolicited {
177            mailbox: Cow::Borrowed(slice_to_str(mailbox)),
178            values,
179        }),
180    ))
181}
182
183// There are any entries with values larger than the MAXSIZE limit given to GETMETADATA.
184// Extends resp-test-code defined in rfc3501.
185// [RFC5464 - 4.2.1 MAXSIZE GETMETADATA Command Option](https://tools.ietf.org/html/rfc5464#section-4.2.1)
186// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
187pub(crate) fn resp_text_code_metadata_long_entries(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
188    let (i, (_, num)) = tuple((tag_no_case("METADATA LONGENTRIES "), number_64))(i)?;
189    Ok((i, ResponseCode::MetadataLongEntries(num)))
190}
191
192// Server is unable to set an annotation because the size of its value is too large.
193// Extends resp-test-code defined in rfc3501.
194// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
195// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
196pub(crate) fn resp_text_code_metadata_max_size(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
197    let (i, (_, num)) = tuple((tag_no_case("METADATA MAXSIZE "), number_64))(i)?;
198    Ok((i, ResponseCode::MetadataMaxSize(num)))
199}
200
201// Server is unable to set a new annotation because the maximum number of allowed annotations has already been reached.
202// Extends resp-test-code defined in rfc3501.
203// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
204// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
205pub(crate) fn resp_text_code_metadata_too_many(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
206    let (i, _) = tag_no_case("METADATA TOOMANY")(i)?;
207    Ok((i, ResponseCode::MetadataTooMany))
208}
209
210// Server is unable to set a new annotation because it does not support private annotations on one of the specified mailboxes.
211// Extends resp-test-code defined in rfc3501.
212// [RFC5464 - 4.3 SETMETADATA Command](https://tools.ietf.org/html/rfc5464#section-4.3)
213// [RFC5464 - 5. Formal Syntax - resp-text-code](https://tools.ietf.org/html/rfc5464#section-5)
214pub(crate) fn resp_text_code_metadata_no_private(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
215    let (i, _) = tag_no_case("METADATA NOPRIVATE")(i)?;
216    Ok((i, ResponseCode::MetadataNoPrivate))
217}
218
219#[cfg(test)]
220mod tests {
221    use super::{metadata_solicited, metadata_unsolicited};
222    use crate::types::*;
223    use std::borrow::Cow;
224
225    #[test]
226    fn test_solicited_fail_1() {
227        match metadata_solicited(b"METADATA \"\" (/asdfg \"asdf\")\r\n") {
228            Err(_) => {}
229            _ => panic!("Error required when entry name is not starting with /private or /shared"),
230        }
231    }
232
233    #[test]
234    fn test_solicited_fail_2() {
235        match metadata_solicited(b"METADATA \"\" (/shared/asdfg \"asdf\")\r\n") {
236            Err(_) => {}
237            _ => panic!(
238                "Error required when in entry name /shared \
239                 is not continued with /admin, /comment or /vendor"
240            ),
241        }
242    }
243
244    #[test]
245    fn test_solicited_fail_3() {
246        match metadata_solicited(b"METADATA \"\" (/private/admin \"asdf\")\r\n") {
247            Err(_) => {}
248            _ => panic!(
249                "Error required when in entry name /private \
250                 is not continued with /comment or /vendor"
251            ),
252        }
253    }
254
255    #[test]
256    fn test_solicited_fail_4() {
257        match metadata_solicited(b"METADATA \"\" (/shared/vendor \"asdf\")\r\n") {
258            Err(_) => {}
259            _ => panic!("Error required when vendor name is not provided."),
260        }
261    }
262
263    #[test]
264    fn test_solicited_success() {
265        match metadata_solicited(
266            b"METADATA \"mbox\" (/shared/vendor/vendorname \"asdf\" \
267              /private/comment/a \"bbb\")\r\n",
268        ) {
269            Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
270                assert_eq!(mailbox, "mbox");
271                assert_eq!(i, b"\r\n");
272                assert_eq!(values.len(), 2);
273                assert_eq!(values[0].entry, "/shared/vendor/vendorname");
274                assert_eq!(
275                    values[0]
276                        .value
277                        .as_ref()
278                        .expect("None value is not expected"),
279                    "asdf"
280                );
281                assert_eq!(values[1].entry, "/private/comment/a");
282                assert_eq!(
283                    values[1]
284                        .value
285                        .as_ref()
286                        .expect("None value is not expected"),
287                    "bbb"
288                );
289            }
290            _ => panic!("Correct METADATA response is not parsed properly."),
291        }
292    }
293
294    #[test]
295    fn test_literal_success() {
296        // match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a \"AAA\")\r\n")
297        match metadata_solicited(b"METADATA \"\" (/shared/vendor/vendor.coi/a {3}\r\nAAA)\r\n") {
298            Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
299                assert_eq!(mailbox, "");
300                assert_eq!(i, b"\r\n");
301                assert_eq!(values.len(), 1);
302                assert_eq!(values[0].entry, "/shared/vendor/vendor.coi/a");
303                assert_eq!(
304                    values[0]
305                        .value
306                        .as_ref()
307                        .expect("None value is not expected"),
308                    "AAA"
309                );
310            }
311            Err(e) => panic!("ERR: {e:?}"),
312            _ => panic!("Strange failure"),
313        }
314    }
315
316    #[test]
317    fn test_nil_success() {
318        match metadata_solicited(b"METADATA \"\" (/shared/comment NIL /shared/admin NIL)\r\n") {
319            Ok((i, Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }))) => {
320                assert_eq!(mailbox, "");
321                assert_eq!(i, b"\r\n");
322                assert_eq!(values.len(), 2);
323                assert_eq!(values[0].entry, "/shared/comment");
324                assert_eq!(values[0].value, None);
325                assert_eq!(values[1].entry, "/shared/admin");
326                assert_eq!(values[1].value, None);
327            }
328            Err(e) => panic!("ERR: {e:?}"),
329            _ => panic!("Strange failure"),
330        }
331    }
332
333    #[test]
334    fn test_unsolicited_success() {
335        match metadata_unsolicited(b"METADATA \"theBox\" /shared/admin/qwe /private/comment/a\r\n")
336        {
337            Ok((
338                i,
339                Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }),
340            )) => {
341                assert_eq!(i, b"\r\n");
342                assert_eq!(mailbox, "theBox");
343                assert_eq!(values.len(), 2);
344                assert_eq!(values[0], "/shared/admin/qwe");
345                assert_eq!(values[1], "/private/comment/a");
346            }
347            _ => panic!("Correct METADATA response is not parsed properly."),
348        }
349    }
350
351    #[test]
352    fn test_response_codes() {
353        use crate::parser::parse_response;
354
355        match parse_response(b"* OK [METADATA LONGENTRIES 123] Some entries omitted.\r\n") {
356            Ok((
357                _,
358                Response::Data {
359                    status: Status::Ok,
360                    code: Some(ResponseCode::MetadataLongEntries(123)),
361                    information: Some(Cow::Borrowed("Some entries omitted.")),
362                },
363            )) => {}
364            rsp => panic!("unexpected response {rsp:?}"),
365        }
366
367        match parse_response(b"* NO [METADATA MAXSIZE 123] Annotation too large.\r\n") {
368            Ok((
369                _,
370                Response::Data {
371                    status: Status::No,
372                    code: Some(ResponseCode::MetadataMaxSize(123)),
373                    information: Some(Cow::Borrowed("Annotation too large.")),
374                },
375            )) => {}
376            rsp => panic!("unexpected response {rsp:?}"),
377        }
378
379        match parse_response(b"* NO [METADATA TOOMANY] Too many annotations.\r\n") {
380            Ok((
381                _,
382                Response::Data {
383                    status: Status::No,
384                    code: Some(ResponseCode::MetadataTooMany),
385                    information: Some(Cow::Borrowed("Too many annotations.")),
386                },
387            )) => {}
388            rsp => panic!("unexpected response {rsp:?}"),
389        }
390
391        match parse_response(b"* NO [METADATA NOPRIVATE] Private annotations not supported.\r\n") {
392            Ok((
393                _,
394                Response::Data {
395                    status: Status::No,
396                    code: Some(ResponseCode::MetadataNoPrivate),
397                    information: Some(Cow::Borrowed("Private annotations not supported.")),
398                },
399            )) => {}
400            rsp => panic!("unexpected response {rsp:?}"),
401        }
402    }
403}