Skip to content

Commit 332de09

Browse files
authored
add support for RFC2971 IMAP4 ID extension in imap-proto (#141)
1 parent a6c1855 commit 332de09

File tree

4 files changed

+192
-2
lines changed

4 files changed

+192
-2
lines changed

imap-proto/src/parser/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod core;
66
pub mod bodystructure;
77
pub mod gmail;
88
pub mod rfc2087;
9+
pub mod rfc2971;
910
pub mod rfc3501;
1011
pub mod rfc4315;
1112
pub mod rfc4551;

imap-proto/src/parser/rfc2971.rs

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//!
2+
//!
3+
//! https://tools.ietf.org/html/rfc2971
4+
//!
5+
//! The IMAP4 ID extension
6+
//!
7+
8+
use std::{borrow::Cow, collections::HashMap};
9+
10+
use nom::{
11+
branch::alt,
12+
bytes::complete::tag_no_case,
13+
character::complete::{char, space1},
14+
combinator::map,
15+
multi::many0,
16+
sequence::{separated_pair, tuple},
17+
IResult,
18+
};
19+
20+
use 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)
28+
fn 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)
35+
fn 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+
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)
61+
fn 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)
67+
pub(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)]
84+
mod 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:[email protected]")"##),
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:[email protected]"),
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:[email protected]")"##),
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:[email protected]"),
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:[email protected]")"##),
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:[email protected]"),
167+
].into_iter()
168+
.map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
169+
.collect()
170+
);
171+
}
172+
);
173+
174+
assert_matches!(
175+
resp_id(br##"ID NIL"##),
176+
Ok((_, Response::Id(id_info))) => {
177+
assert_eq!(id_info, None);
178+
}
179+
);
180+
}
181+
}

imap-proto/src/parser/rfc3501/mod.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use nom::{
1919

2020
use crate::{
2121
parser::{
22-
core::*, rfc2087, rfc3501::body::*, rfc3501::body_structure::*, rfc4315, rfc4551, rfc5161,
23-
rfc5256, rfc5464, rfc7162,
22+
core::*, rfc2087, rfc2971, rfc3501::body::*, rfc3501::body_structure::*, rfc4315, rfc4551,
23+
rfc5161, rfc5256, rfc5464, rfc7162,
2424
},
2525
types::*,
2626
};
@@ -679,6 +679,7 @@ pub(crate) fn response_data(i: &[u8]) -> IResult<&[u8], Response> {
679679
rfc7162::resp_vanished,
680680
rfc2087::quota,
681681
rfc2087::quota_root,
682+
rfc2971::resp_id,
682683
)),
683684
tag(b"\r\n"),
684685
)(i)

imap-proto/src/types.rs

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::collections::HashMap;
23
use std::ops::RangeInclusive;
34

45
fn to_owned_cow<'a, T: ?Sized + ToOwned>(c: Cow<'a, T>) -> Cow<'static, T> {
@@ -43,6 +44,7 @@ pub enum Response<'a> {
4344
MailboxData(MailboxDatum<'a>),
4445
Quota(Quota<'a>),
4546
QuotaRoot(QuotaRoot<'a>),
47+
Id(Option<HashMap<Cow<'a, str>, Cow<'a, str>>>),
4648
}
4749

4850
impl<'a> Response<'a> {
@@ -91,6 +93,11 @@ impl<'a> Response<'a> {
9193
Response::MailboxData(datum) => Response::MailboxData(datum.into_owned()),
9294
Response::Quota(quota) => Response::Quota(quota.into_owned()),
9395
Response::QuotaRoot(quota_root) => Response::QuotaRoot(quota_root.into_owned()),
96+
Response::Id(map) => Response::Id(map.map(|m| {
97+
m.into_iter()
98+
.map(|(k, v)| (to_owned_cow(k), to_owned_cow(v)))
99+
.collect()
100+
})),
94101
}
95102
}
96103
}

0 commit comments

Comments
 (0)