imap_proto/parser/
rfc2971.rs

1//!
2//!
3//! https://tools.ietf.org/html/rfc2971
4//!
5//! The IMAP4 ID extension
6//!
7
8use std::{borrow::Cow, collections::HashMap};
9
10use nom::{
11    branch::alt,
12    bytes::complete::tag_no_case,
13    character::complete::{char, space0, space1},
14    combinator::map,
15    multi::many0,
16    sequence::{preceded, separated_pair, tuple},
17    IResult,
18};
19
20use crate::{
21    parser::core::{nil, nstring_utf8, string_utf8},
22    Response,
23};
24
25// A single id parameter (field and value).
26// Format: string SPACE nstring
27// [RFC2971 - Formal Syntax](https://tools.ietf.org/html/rfc2971#section-4)
28fn id_param(i: &[u8]) -> IResult<&[u8], (&str, Option<&str>)> {
29    separated_pair(string_utf8, space1, nstring_utf8)(i)
30}
31
32// The non-nil case of id parameter list.
33// Format: "(" #(string SPACE nstring) ")"
34// [RFC2971 - Formal Syntax](https://tools.ietf.org/html/rfc2971#section-4)
35fn id_param_list_not_nil(i: &[u8]) -> IResult<&[u8], HashMap<&str, &str>> {
36    map(
37        tuple((
38            char('('),
39            id_param,
40            many0(tuple((space1, id_param))),
41            preceded(space0, char(')')),
42        )),
43        |(_, first_param, rest_params, _)| {
44            let mut params = vec![first_param];
45            for (_, p) in rest_params {
46                params.push(p)
47            }
48
49            params
50                .into_iter()
51                .filter(|(_k, v)| v.is_some())
52                .map(|(k, v)| (k, v.unwrap()))
53                .collect()
54        },
55    )(i)
56}
57
58// The id parameter list of all cases
59// id_params_list ::= "(" #(string SPACE nstring) ")" / nil
60// [RFC2971 - Formal Syntax](https://tools.ietf.org/html/rfc2971#section-4)
61fn id_param_list(i: &[u8]) -> IResult<&[u8], Option<HashMap<&str, &str>>> {
62    alt((map(id_param_list_not_nil, Some), map(nil, |_| None)))(i)
63}
64
65// id_response ::= "ID" SPACE id_params_list
66// [RFC2971 - Formal Syntax](https://tools.ietf.org/html/rfc2971#section-4)
67pub(crate) fn resp_id(i: &[u8]) -> IResult<&[u8], Response<'_>> {
68    let (rest, map) = map(
69        tuple((tag_no_case("ID"), space1, id_param_list)),
70        |(_id, _sp, p)| p,
71    )(i)?;
72
73    Ok((
74        rest,
75        Response::Id(map.map(|m| {
76            m.into_iter()
77                .map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
78                .collect()
79        })),
80    ))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use assert_matches::assert_matches;
87
88    #[test]
89    fn test_id_param() {
90        assert_matches!(
91            id_param(br#""name" "Cyrus""#),
92            Ok((_, (name, value))) => {
93                assert_eq!(name, "name");
94                assert_eq!(value, Some("Cyrus"));
95            }
96        );
97
98        assert_matches!(
99            id_param(br#""name" NIL"#),
100            Ok((_, (name, value))) => {
101                assert_eq!(name, "name");
102                assert_eq!(value, None);
103            }
104        );
105    }
106
107    #[test]
108    fn test_id_param_list_not_nil() {
109        assert_matches!(
110            id_param_list_not_nil(br#"("name" "Cyrus" "version" "1.5" "os" "sunos" "os-version" "5.5" "support-url" "mailto:cyrus-bugs+@andrew.cmu.edu")"#),
111            Ok((_, params)) => {
112                assert_eq!(
113                    params,
114                    vec![
115                        ("name", "Cyrus"),
116                        ("version", "1.5"),
117                        ("os", "sunos"),
118                        ("os-version", "5.5"),
119                        ("support-url", "mailto:cyrus-bugs+@andrew.cmu.edu"),
120                    ].into_iter()
121                    .collect()
122                );
123            }
124        );
125    }
126
127    #[test]
128    fn test_id_param_list() {
129        assert_matches!(
130            id_param_list(br#"("name" "Cyrus" "version" "1.5" "os" "sunos" "os-version" "5.5" "support-url" "mailto:cyrus-bugs+@andrew.cmu.edu")"#),
131            Ok((_, Some(params))) => {
132                assert_eq!(
133                    params,
134                    vec![
135                        ("name", "Cyrus"),
136                        ("version", "1.5"),
137                        ("os", "sunos"),
138                        ("os-version", "5.5"),
139                        ("support-url", "mailto:cyrus-bugs+@andrew.cmu.edu"),
140                    ].into_iter()
141                    .collect()
142                );
143            }
144        );
145
146        assert_matches!(
147            id_param_list(br##"NIL"##),
148            Ok((_, params)) => {
149                assert_eq!(params, None);
150            }
151        );
152    }
153
154    #[test]
155    fn test_resp_id() {
156        assert_matches!(
157            resp_id(br#"ID ("name" "Cyrus" "version" "1.5" "os" "sunos" "os-version" "5.5" "support-url" "mailto:cyrus-bugs+@andrew.cmu.edu")"#),
158            Ok((_, Response::Id(Some(id_info)))) => {
159                assert_eq!(
160                    id_info,
161                    vec![
162                        ("name", "Cyrus"),
163                        ("version", "1.5"),
164                        ("os", "sunos"),
165                        ("os-version", "5.5"),
166                        ("support-url", "mailto:cyrus-bugs+@andrew.cmu.edu"),
167                    ].into_iter()
168                    .map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
169                    .collect()
170                );
171            }
172        );
173
174        // Test that NILs inside parameter list don't crash the parser.
175        // RFC2971 allows NILs as parameter values.
176        assert_matches!(
177            resp_id(br#"ID ("name" "Cyrus" "version" "1.5" "os" NIL "os-version" NIL "support-url" "mailto:cyrus-bugs+@andrew.cmu.edu")"#),
178            Ok((_, Response::Id(Some(id_info)))) => {
179                assert_eq!(
180                    id_info,
181                    vec![
182                        ("name", "Cyrus"),
183                        ("version", "1.5"),
184                        ("support-url", "mailto:cyrus-bugs+@andrew.cmu.edu"),
185                    ].into_iter()
186                    .map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
187                    .collect()
188                );
189            }
190        );
191
192        assert_matches!(
193            resp_id(br##"ID NIL"##),
194            Ok((_, Response::Id(id_info))) => {
195                assert_eq!(id_info, None);
196            }
197        );
198
199        assert_matches!(
200            resp_id(br#"ID ("name" "Archiveopteryx" "version" "3.2.0" "compile-time" "Feb  6 2023 19:59:14" "homepage-url" "http://archiveopteryx.org" "release-url" "http://archiveopteryx.org/3.2.0" )"#),
201            Ok((_, Response::Id(Some(id_info)))) => {
202                assert_eq!(
203                    id_info,
204                    vec![
205                        ("name", "Archiveopteryx"),
206                        ("version", "3.2.0"),
207                        ("compile-time", "Feb  6 2023 19:59:14"),
208                        ("homepage-url", "http://archiveopteryx.org"),
209                        ("release-url", "http://archiveopteryx.org/3.2.0"),
210                    ].into_iter()
211                    .map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
212                    .collect()
213                );
214            }
215        );
216    }
217}