imap_proto/parser/
rfc2087.rs

1//!
2//! https://tools.ietf.org/html/rfc2087
3//!
4//! IMAP4 QUOTA extension
5//!
6
7use std::borrow::Cow;
8
9use nom::{
10    branch::alt,
11    bytes::streaming::{tag, tag_no_case},
12    character::streaming::space1,
13    combinator::map,
14    multi::many0,
15    multi::separated_list0,
16    sequence::{delimited, preceded, tuple},
17    IResult,
18};
19
20use crate::parser::core::astring_utf8;
21use crate::types::*;
22
23use super::core::number_64;
24
25/// 5.1. QUOTA Response
26/// ```ignore
27/// quota_response  ::= "QUOTA" SP astring SP quota_list
28/// ```
29pub(crate) fn quota(i: &[u8]) -> IResult<&[u8], Response<'_>> {
30    let (rest, (_, _, root_name, _, resources)) = tuple((
31        tag_no_case("QUOTA"),
32        space1,
33        map(astring_utf8, Cow::Borrowed),
34        space1,
35        quota_list,
36    ))(i)?;
37
38    Ok((
39        rest,
40        Response::Quota(Quota {
41            root_name,
42            resources,
43        }),
44    ))
45}
46
47/// ```ignore
48/// quota_list  ::= "(" #quota_resource ")"
49/// ```
50pub(crate) fn quota_list(i: &[u8]) -> IResult<&[u8], Vec<QuotaResource<'_>>> {
51    delimited(tag("("), separated_list0(space1, quota_resource), tag(")"))(i)
52}
53
54/// ```ignore
55/// quota_resource  ::= atom SP number SP number
56/// ```
57pub(crate) fn quota_resource(i: &[u8]) -> IResult<&[u8], QuotaResource<'_>> {
58    let (rest, (name, _, usage, _, limit)) =
59        tuple((quota_resource_name, space1, number_64, space1, number_64))(i)?;
60
61    Ok((rest, QuotaResource { name, usage, limit }))
62}
63
64pub(crate) fn quota_resource_name(i: &[u8]) -> IResult<&[u8], QuotaResourceName<'_>> {
65    alt((
66        map(tag_no_case("STORAGE"), |_| QuotaResourceName::Storage),
67        map(tag_no_case("MESSAGE"), |_| QuotaResourceName::Message),
68        map(map(astring_utf8, Cow::Borrowed), QuotaResourceName::Atom),
69    ))(i)
70}
71
72/// 5.2. QUOTAROOT Response
73/// ```ignore
74/// quotaroot_response ::= "QUOTAROOT" SP astring *(SP astring)
75/// ```
76pub(crate) fn quota_root(i: &[u8]) -> IResult<&[u8], Response<'_>> {
77    let (rest, (_, _, mailbox_name, quota_root_names)) = tuple((
78        tag_no_case("QUOTAROOT"),
79        space1,
80        map(astring_utf8, Cow::Borrowed),
81        many0(preceded(space1, map(astring_utf8, Cow::Borrowed))),
82    ))(i)?;
83
84    Ok((
85        rest,
86        Response::QuotaRoot(QuotaRoot {
87            mailbox_name,
88            quota_root_names,
89        }),
90    ))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use assert_matches::assert_matches;
97    use std::borrow::Cow;
98
99    #[test]
100    fn test_quota() {
101        assert_matches!(
102            quota(b"QUOTA \"\" (STORAGE 10 512)"),
103            Ok((_, r)) => {
104                assert_eq!(
105                    r,
106                    Response::Quota(Quota {
107                        root_name: Cow::Borrowed(""),
108                        resources: vec![QuotaResource {
109                            name: QuotaResourceName::Storage,
110                            usage: 10,
111                            limit: 512
112                        }]
113                    })
114                );
115            }
116        );
117    }
118
119    #[test]
120    fn test_quota_spaces() {
121        // Archiveopteryx 3.2.0 generates QUOTA resources with double space.
122        // This is a test of a workaround for such incorrect implementation of QUOTA.
123        assert_matches!(
124            quota(b"QUOTA \"\" (STORAGE 0 2147483647 MESSAGE 0  2147483647)"),
125            Ok((_, r)) => {
126                assert_eq!(
127                    r,
128                    Response::Quota(Quota {
129                        root_name: Cow::Borrowed(""),
130                        resources: vec![QuotaResource {
131                            name: QuotaResourceName::Storage,
132                            usage: 0,
133                            limit: 2147483647
134                        }, QuotaResource {
135                            name: QuotaResourceName::Message,
136                            usage: 0,
137                            limit: 2147483647
138                        }]
139                    })
140                );
141            }
142        );
143    }
144
145    #[test]
146    fn test_quota_response_data() {
147        assert_matches!(
148            crate::parser::rfc3501::response_data(b"* QUOTA \"\" (STORAGE 10 512)\r\n"),
149            Ok((_, r)) => {
150                assert_eq!(
151                    r,
152                    Response::Quota(Quota {
153                        root_name: Cow::Borrowed(""),
154                        resources: vec![QuotaResource {
155                            name: QuotaResourceName::Storage,
156                            usage: 10,
157                            limit: 512
158                        }]
159                    })
160                );
161            }
162        );
163    }
164
165    #[test]
166    fn test_quota_list() {
167        assert_matches!(
168            quota_list(b"(STORAGE 10 512)"),
169            Ok((_, r)) => {
170                assert_eq!(
171                    r,
172                    vec![QuotaResource {
173                        name: QuotaResourceName::Storage,
174                        usage: 10,
175                        limit: 512
176                    }]
177                );
178            }
179        );
180
181        assert_matches!(
182            quota_list(b"(MESSAGE 100 512)"),
183            Ok((_, r)) => {
184                assert_eq!(
185                    r,
186                    vec![QuotaResource {
187                        name: QuotaResourceName::Message,
188                        usage: 100,
189                        limit: 512
190                    }]
191                );
192            }
193        );
194
195        assert_matches!(
196            quota_list(b"(DAILY 55 200)"),
197            Ok((_, r)) => {
198                assert_eq!(
199                    r,
200                    vec![QuotaResource {
201                        name: QuotaResourceName::Atom(Cow::Borrowed("DAILY")),
202                        usage: 55,
203                        limit: 200
204                    }]
205                );
206            }
207        );
208    }
209
210    #[test]
211    fn test_quota_root_response_data() {
212        assert_matches!(
213            crate::parser::rfc3501::response_data("* QUOTAROOT INBOX \"\"\r\n".as_bytes()),
214            Ok((_, r)) => {
215                assert_eq!(
216                    r,
217                    Response::QuotaRoot(QuotaRoot{
218                        mailbox_name: Cow::Borrowed("INBOX"),
219                        quota_root_names: vec![Cow::Borrowed("")]
220                    })
221                );
222            }
223        );
224    }
225
226    fn terminated_quota_root(i: &[u8]) -> IResult<&[u8], Response<'_>> {
227        nom::sequence::terminated(quota_root, nom::bytes::streaming::tag("\r\n"))(i)
228    }
229
230    #[test]
231    fn test_quota_root_without_root_names() {
232        assert_matches!(
233            terminated_quota_root(b"QUOTAROOT comp.mail.mime\r\n"),
234            Ok((_, r)) => {
235                assert_eq!(
236                    r,
237                    Response::QuotaRoot(QuotaRoot{
238                        mailbox_name: Cow::Borrowed("comp.mail.mime"),
239                        quota_root_names: vec![]
240                    })
241                );
242            }
243        );
244    }
245
246    #[test]
247    fn test_quota_root2() {
248        assert_matches!(
249            terminated_quota_root(b"QUOTAROOT INBOX HU\r\n"),
250            Ok((_, r)) => {
251                assert_eq!(
252                    r,
253                    Response::QuotaRoot(QuotaRoot{
254                        mailbox_name: Cow::Borrowed("INBOX"),
255                        quota_root_names: vec![Cow::Borrowed("HU")]
256                    })
257                );
258            }
259        );
260
261        assert_matches!(
262            terminated_quota_root(b"QUOTAROOT INBOX \"\"\r\n"),
263            Ok((_, r)) => {
264                assert_eq!(
265                    r,
266                    Response::QuotaRoot(QuotaRoot{
267                        mailbox_name: Cow::Borrowed("INBOX"),
268                        quota_root_names: vec![Cow::Borrowed("")]
269                    })
270                );
271            }
272        );
273
274        assert_matches!(
275            terminated_quota_root(b"QUOTAROOT \"Inbox\" \"#Account\"\r\n"),
276            Ok((_, r)) => {
277                assert_eq!(
278                    r,
279                    Response::QuotaRoot(QuotaRoot{
280                        mailbox_name: Cow::Borrowed("Inbox"),
281                        quota_root_names: vec![Cow::Borrowed("#Account")]
282                    })
283                );
284            }
285        );
286
287        assert_matches!(
288            terminated_quota_root(b"QUOTAROOT \"Inbox\" \"#Account\" \"#Mailbox\"\r\n"),
289            Ok((_, r)) => {
290                assert_eq!(
291                    r,
292                    Response::QuotaRoot(QuotaRoot{
293                        mailbox_name: Cow::Borrowed("Inbox"),
294                        quota_root_names: vec![Cow::Borrowed("#Account"), Cow::Borrowed("#Mailbox")]
295                    })
296                );
297            }
298        );
299    }
300}