Skip to content

Commit 7ba64ef

Browse files
qbit-0duesee
andcommitted
feat: add NAMESPACE support
Co-Authored-by: Damian Poddebniak <[email protected]>
1 parent 1a6984b commit 7ba64ef

File tree

14 files changed

+364
-2
lines changed

14 files changed

+364
-2
lines changed

imap-codec/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ ext_id = ["imap-types/ext_id"]
7474
ext_login_referrals = ["imap-types/ext_login_referrals"]
7575
ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
7676
ext_metadata = ["imap-types/ext_metadata"]
77+
ext_namespace = ["imap-types/ext_namespace"]
7778
ext_utf8 = ["imap-types/ext_utf8"]
7879
# </Forward to imap-types>
7980

imap-codec/fuzz/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ ext_id = ["imap-codec/ext_id"]
2424
ext_login_referrals = ["imap-codec/ext_login_referrals"]
2525
ext_mailbox_referrals = ["imap-codec/ext_mailbox_referrals"]
2626
ext_metadata = ["imap-codec/ext_metadata"]
27+
ext_namespace = ["imap-codec/ext_namespace"]
2728
ext_utf8 = ["imap-codec/ext_utf8"]
2829

2930
# IMAP quirks
@@ -39,6 +40,7 @@ ext = [
3940
#"ext_login_referrals",
4041
#"ext_mailbox_referrals",
4142
"ext_metadata",
43+
"ext_namespace",
4244
"ext_utf8",
4345
]
4446
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.

imap-codec/src/codec/encode.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ use imap_types::{
8383
};
8484
use utils::{List1AttributeValueOrNil, List1OrNil, join_serializable};
8585

86+
#[cfg(feature = "ext_namespace")]
87+
use crate::extensions::namespace::encode_namespaces;
8688
use crate::{AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec};
8789

8890
/// Encoder.
@@ -685,6 +687,8 @@ impl EncodeIntoContext for CommandBody<'_> {
685687
ctx.write_all(b")")
686688
}
687689
}
690+
#[cfg(feature = "ext_namespace")]
691+
&CommandBody::Namespace => ctx.write_all(b"NAMESPACE"),
688692
}
689693
}
690694
}
@@ -1614,6 +1618,19 @@ impl EncodeIntoContext for Data<'_> {
16141618
ctx.write_all(b" ")?;
16151619
known_uids.encode_ctx(ctx)?;
16161620
}
1621+
#[cfg(feature = "ext_namespace")]
1622+
Data::Namespace {
1623+
personal,
1624+
other,
1625+
shared,
1626+
} => {
1627+
ctx.write_all(b"* NAMESPACE ")?;
1628+
encode_namespaces(ctx, personal)?;
1629+
ctx.write_all(b" ")?;
1630+
encode_namespaces(ctx, other)?;
1631+
ctx.write_all(b" ")?;
1632+
encode_namespaces(ctx, shared)?;
1633+
}
16171634
}
16181635

16191636
ctx.write_all(b"\r\n")

imap-codec/src/command.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ use crate::extensions::condstore_qresync::mod_sequence_valzer;
3838
use crate::extensions::id::id;
3939
#[cfg(feature = "ext_metadata")]
4040
use crate::extensions::metadata::{getmetadata, setmetadata};
41+
#[cfg(feature = "ext_namespace")]
42+
use crate::extensions::namespace::namespace_command;
4143
use crate::{
4244
auth::auth_type,
4345
core::{astring, base64, literal, tag_imap},
@@ -168,6 +170,8 @@ pub(crate) fn command_auth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
168170
setmetadata,
169171
#[cfg(feature = "ext_metadata")]
170172
getmetadata,
173+
#[cfg(feature = "ext_namespace")]
174+
namespace_command,
171175
))(input)
172176
}
173177

imap-codec/src/extensions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub mod literal;
1010
#[cfg(feature = "ext_metadata")]
1111
pub mod metadata;
1212
pub mod r#move;
13+
#[cfg(feature = "ext_namespace")]
14+
pub mod namespace;
1315
pub mod quota;
1416
pub mod sort;
1517
pub mod thread;
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//! The IMAP NAMESPACE Extension
2+
3+
use std::io::Write;
4+
5+
use abnf_core::streaming::{dquote, sp};
6+
use imap_types::{
7+
command::CommandBody,
8+
core::Vec1,
9+
extensions::namespace::{Namespace, NamespaceResponseExtension, Namespaces},
10+
response::Data,
11+
};
12+
use nom::{
13+
branch::alt,
14+
bytes::streaming::tag_no_case,
15+
character::streaming::char,
16+
combinator::{map, value},
17+
multi::{many0, many1, separated_list1},
18+
sequence::{delimited, preceded, tuple},
19+
};
20+
21+
use crate::{
22+
core::{nil, quoted_char, string},
23+
decode::IMAPResult,
24+
encode::{EncodeContext, EncodeIntoContext},
25+
};
26+
27+
/// ```abnf
28+
/// namespace = "NAMESPACE"
29+
/// ```
30+
pub(crate) fn namespace_command(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
31+
value(CommandBody::Namespace, tag_no_case(b"NAMESPACE"))(input)
32+
}
33+
34+
/// ```abnf
35+
/// ;; The first Namespace is the Personal Namespace(s)
36+
/// ;; The second Namespace is the Other Users' Namespace(s)
37+
/// ;; The third Namespace is the Shared Namespace(s)
38+
/// Namespace-Response = "NAMESPACE" SP Namespace SP Namespace SP Namespace
39+
/// ```
40+
pub(crate) fn namespace_response(input: &[u8]) -> IMAPResult<&[u8], Data> {
41+
let mut parser = tuple((
42+
tag_no_case("NAMESPACE "),
43+
namespace,
44+
preceded(sp, namespace),
45+
preceded(sp, namespace),
46+
));
47+
48+
let (remaining, (_, personal, other, shared)) = parser(input)?;
49+
50+
Ok((
51+
remaining,
52+
Data::Namespace {
53+
personal,
54+
other,
55+
shared,
56+
},
57+
))
58+
}
59+
60+
/// ```abnf
61+
/// Namespace = nil / "(" 1*Namespace-Descr ")"
62+
/// ```
63+
fn namespace(input: &[u8]) -> IMAPResult<&[u8], Namespaces> {
64+
alt((
65+
map(nil, |_| Vec::new()),
66+
delimited(char('('), many1(namespace_descr), char(')')),
67+
))(input)
68+
}
69+
70+
/// ```abnf
71+
/// Namespace-Descr = "("
72+
/// string
73+
/// SP
74+
/// (DQUOTE QUOTED-CHAR DQUOTE / nil)
75+
/// *(Namespace-Response-Extension)
76+
/// ")"
77+
/// ```
78+
fn namespace_descr(input: &[u8]) -> IMAPResult<&[u8], Namespace> {
79+
map(
80+
delimited(
81+
char('('),
82+
tuple((
83+
string,
84+
sp,
85+
alt((
86+
map(delimited(dquote, quoted_char, dquote), Some),
87+
value(None, nil),
88+
)),
89+
many0(namespace_response_extension),
90+
)),
91+
char(')'),
92+
),
93+
|(prefix, _, delimiter, extensions)| Namespace {
94+
prefix,
95+
delimiter,
96+
extensions,
97+
},
98+
)(input)
99+
}
100+
101+
/// ```abnf
102+
/// Namespace-Response-Extension = SP string SP "(" string *(SP string) ")"
103+
/// ```
104+
fn namespace_response_extension(input: &[u8]) -> IMAPResult<&[u8], NamespaceResponseExtension> {
105+
map(
106+
tuple((
107+
preceded(sp, string),
108+
preceded(
109+
sp,
110+
delimited(char('('), separated_list1(sp, string), char(')')),
111+
),
112+
)),
113+
|(key, values)| NamespaceResponseExtension {
114+
key,
115+
// We can use `unvalidated` because we know the vector has at least one element due to the `separated_list1` call above.
116+
values: Vec1::unvalidated(values),
117+
},
118+
)(input)
119+
}
120+
121+
impl EncodeIntoContext for Namespace<'_> {
122+
fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
123+
write!(ctx, "(")?;
124+
self.prefix.encode_ctx(ctx)?;
125+
write!(ctx, " ")?;
126+
127+
match &self.delimiter {
128+
Some(delimiter_char) => {
129+
write!(ctx, "\"")?;
130+
delimiter_char.encode_ctx(ctx)?;
131+
write!(ctx, "\"")?;
132+
}
133+
None => {
134+
ctx.write_all(b"NIL")?;
135+
}
136+
}
137+
138+
for ext in &self.extensions {
139+
ext.encode_ctx(ctx)?;
140+
}
141+
142+
write!(ctx, ")")
143+
}
144+
}
145+
146+
impl EncodeIntoContext for NamespaceResponseExtension<'_> {
147+
fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
148+
write!(ctx, " ")?;
149+
self.key.encode_ctx(ctx)?;
150+
151+
write!(ctx, " (")?;
152+
if let Some((last, head)) = self.values.as_ref().split_last() {
153+
for value in head {
154+
value.encode_ctx(ctx)?;
155+
write!(ctx, " ")?;
156+
}
157+
last.encode_ctx(ctx)?;
158+
}
159+
write!(ctx, ")")
160+
}
161+
}
162+
163+
pub fn encode_namespaces(ctx: &mut EncodeContext, list: &Namespaces<'_>) -> std::io::Result<()> {
164+
if list.is_empty() {
165+
ctx.write_all(b"NIL")
166+
} else {
167+
ctx.write_all(b"(")?;
168+
for desc in list {
169+
desc.encode_ctx(ctx)?;
170+
}
171+
ctx.write_all(b")")
172+
}
173+
}
174+
175+
#[cfg(test)]
176+
mod tests {
177+
use super::namespace_response;
178+
179+
#[test]
180+
fn parse_namespace_response() {
181+
let tests = [
182+
b"NAMESPACE ((\"0\" \"\\\"\")) NIL NIL\r\n".as_ref(),
183+
#[cfg(feature = "ext_utf8")]
184+
b"NAMESPACE ((\"^^\x00\" \"\x07\")) NIL NIL\r\n",
185+
];
186+
187+
for test in tests.into_iter() {
188+
namespace_response(test).unwrap();
189+
}
190+
}
191+
}

imap-codec/src/mailbox.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use nom::{
2020
use crate::extensions::condstore_qresync::search_sort_mod_seq;
2121
#[cfg(feature = "ext_metadata")]
2222
use crate::extensions::metadata::metadata_resp;
23+
#[cfg(feature = "ext_namespace")]
24+
use crate::extensions::namespace::namespace_response;
2325
use crate::{
2426
core::{astring, nil, number, nz_number, quoted_char, string},
2527
decode::IMAPResult,
@@ -168,6 +170,8 @@ pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
168170
),
169171
#[cfg(feature = "ext_metadata")]
170172
metadata_resp,
173+
#[cfg(feature = "ext_namespace")]
174+
namespace_response,
171175
map(terminated(number, tag_no_case(b" EXISTS")), Data::Exists),
172176
map(terminated(number, tag_no_case(b" RECENT")), Data::Recent),
173177
quotaroot_response,

imap-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ext_id = []
2626
ext_login_referrals = []
2727
ext_mailbox_referrals = []
2828
ext_metadata = []
29+
ext_namespace = []
2930
ext_utf8 = []
3031

3132
[dependencies]

imap-types/fuzz/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ext_id = ["imap-types/ext_id"]
2020
ext_login_referrals = ["imap-types/ext_login_referrals"]
2121
ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
2222
ext_metadata = ["imap-types/ext_metadata"]
23+
ext_namespace = ["imap-types/ext_namespace"]
2324
ext_utf8 = ["imap-types/ext_utf8"]
2425
# </Forward to imap-types>
2526

@@ -31,6 +32,7 @@ ext = [
3132
#"ext_login_referrals",
3233
#"ext_mailbox_referrals",
3334
"ext_metadata",
35+
"ext_namespace",
3436
"ext_utf8",
3537
]
3638
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.

imap-types/src/command.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,13 @@ pub enum CommandBody<'a> {
15361536
mailbox: Mailbox<'a>,
15371537
entries: Vec1<Entry<'a>>,
15381538
},
1539+
#[cfg(feature = "ext_namespace")]
1540+
/// Retrieve the namespaces available to the client.
1541+
///
1542+
/// <div class="warning">
1543+
/// This extension must only be used when the server advertised support for it sending the NAMESPACE capability.
1544+
/// </div>
1545+
Namespace,
15391546
}
15401547

15411548
impl<'a> CommandBody<'a> {
@@ -1840,6 +1847,8 @@ impl<'a> CommandBody<'a> {
18401847
Self::SetMetadata { .. } => "SETMETADATA",
18411848
#[cfg(feature = "ext_metadata")]
18421849
Self::GetMetadata { .. } => "GETMETADATA",
1850+
#[cfg(feature = "ext_namespace")]
1851+
Self::Namespace => "NAMESPACE",
18431852
}
18441853
}
18451854
}

0 commit comments

Comments
 (0)