Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions imap-codec/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ ext_id = ["imap-types/ext_id"]
ext_login_referrals = ["imap-types/ext_login_referrals"]
ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
ext_metadata = ["imap-types/ext_metadata"]
ext_namespace = ["imap-types/ext_namespace"]
# </Forward to imap-types>

[dependencies]
Expand Down
2 changes: 2 additions & 0 deletions imap-codec/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ext_id = ["imap-codec/ext_id"]
ext_login_referrals = ["imap-codec/ext_login_referrals"]
ext_mailbox_referrals = ["imap-codec/ext_mailbox_referrals"]
ext_metadata = ["imap-codec/ext_metadata"]
ext_namespace = ["imap-codec/ext_namespace"]

# IMAP quirks
quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
Expand All @@ -37,6 +38,7 @@ ext = [
#"ext_login_referrals",
#"ext_mailbox_referrals",
"ext_metadata",
"ext_namespace",
]
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
debug = []
Expand Down
20 changes: 20 additions & 0 deletions imap-codec/src/codec/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ use imap_types::{
utils::escape_quoted,
};
use utils::{List1AttributeValueOrNil, List1OrNil, join_serializable};
#[cfg(feature = "ext_namespace")]
use crate::extensions::namespace::encode_namespaces;

use crate::{AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec};

Expand Down Expand Up @@ -700,6 +702,10 @@ impl EncodeIntoContext for CommandBody<'_> {
ctx.write_all(b")")
}
}
#[cfg(feature = "ext_namespace")]
&CommandBody::Namespace => {
ctx.write_all(b"NAMESPACE")
}
}
}
}
Expand Down Expand Up @@ -1627,6 +1633,20 @@ impl EncodeIntoContext for Data<'_> {
ctx.write_all(b" ")?;
known_uids.encode_ctx(ctx)?;
}

#[cfg(feature = "ext_namespace")]
Data::Namespace {
personal,
other,
shared,
} => {
ctx.write_all(b"* NAMESPACE ")?;
encode_namespaces(ctx, personal)?;
ctx.write_all(b" ")?;
encode_namespaces(ctx, other)?;
ctx.write_all(b" ")?;
encode_namespaces(ctx, shared)?;
}
}

ctx.write_all(b"\r\n")
Expand Down
16 changes: 16 additions & 0 deletions imap-codec/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ pub(crate) fn command_auth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
setmetadata,
#[cfg(feature = "ext_metadata")]
getmetadata,
#[cfg(feature = "ext_namespace")]
namespace,
))(input)
}

Expand Down Expand Up @@ -398,6 +400,20 @@ pub(crate) fn select_param(input: &[u8]) -> IMAPResult<&[u8], SelectParameter> {
))(input)
}

/// FROM RFC 2342:
///
/// ```abnf
/// namespace = "NAMESPACE"
/// ```
#[cfg(feature = "ext_namespace")]
pub(crate) fn namespace(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let parser = tag_no_case(b"NAMESPACE");

let (remaining, _) = parser(input)?;

Ok((remaining, CommandBody::Namespace))
}

/// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"`
pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((
Expand Down
2 changes: 2 additions & 0 deletions imap-codec/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ pub mod sort;
pub mod thread;
pub mod uidplus;
pub mod unselect;
#[cfg(feature = "ext_namespace")]
pub mod namespace;
152 changes: 152 additions & 0 deletions imap-codec/src/extensions/namespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! The IMAP NAMESPACE Extension

use imap_types::{
extensions::namespace::{Namespace, NamespaceResponseExtension, Namespaces},
response::Data,
};
use nom::{
branch::alt,
bytes::{complete::tag, complete::tag_no_case},
combinator::{map, value},
multi::{many0, many1},
sequence::{delimited, preceded, tuple},
};
use std::io::Write;

use crate::{
core::{astring, quoted_char},
decode::IMAPResult,
encode::{EncodeContext, EncodeIntoContext},
};

/// Parses the full NAMESPACE data response.
///
/// ``` abnf
/// Namespace_Response = "*"` SP `"NAMESPACE"` SP `Namespace` SP `Namespace` SP `Namespace`
/// ```
pub(crate) fn namespace_response(input: &[u8]) -> IMAPResult<&[u8], Data> {
let mut parser = tuple((
tag_no_case("NAMESPACE "),
namespaces,
preceded(tag(" "), namespaces),
preceded(tag(" "), namespaces),
));

let (remaining, (_, personal, other, shared)) = parser(input)?;

Ok((
remaining,
Data::Namespace {
personal,
other,
shared,
},
))
}

/// Parses a list of namespaces.
///
/// ```abnf
/// Namespace = nil / "(" 1*( "(" string SP (<"> QUOTED_CHAR <"> / nil) *(Namespace_Response_Extension) ")" ) ")"
/// ```
fn namespaces(input: &[u8]) -> IMAPResult<&[u8], Namespaces> {
alt((
delimited(tag("("), many1(namespace), tag(")")),
map(tag_no_case("NIL"), |_| Vec::new()),
))(input)
}

/// Parses a single namespace description.
fn namespace(input: &[u8]) -> IMAPResult<&[u8], Namespace> {
let delimiter_parser = alt((
map(delimited(tag("\""), quoted_char, tag("\"")), Some),
value(None, tag_no_case("NIL")),
));

map(
delimited(
tag("("),
tuple((
astring,
tag(" "),
delimiter_parser,
many0(namespace_response_extension),
)),
tag(")"),
),
|(prefix, _, delimiter, extensions)| Namespace {
prefix,
delimiter,
extensions,
},
)(input)
}

/// Parses a namespace response extension.
///
/// ```abnf
/// Namespace_Response_Extension = SP string SP "(" string *(SP string) ")"
/// ```
fn namespace_response_extension(input: &[u8]) -> IMAPResult<&[u8], NamespaceResponseExtension> {
map(
preceded(
tag(" "),
tuple((
astring,
tag(" "),
delimited(tag("("), many0(preceded(tag(" "), astring)), tag(")")),
)),
),
|(key, _, values)| NamespaceResponseExtension { key, values },
)(input)
}

impl EncodeIntoContext for Namespace<'_> {
fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
write!(ctx, "(")?;
self.prefix.encode_ctx(ctx)?;
write!(ctx, " ")?;

match &self.delimiter {
Some(delimiter_char) => {
write!(ctx, "\"{}\"", delimiter_char.inner())?;
}
None => {
ctx.write_all(b"NIL")?;
}
}

for ext in &self.extensions {
ext.encode_ctx(ctx)?;
}

write!(ctx, ")")
}
}

impl EncodeIntoContext for NamespaceResponseExtension<'_> {
fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
write!(ctx, " ")?;
self.key.encode_ctx(ctx)?;
write!(ctx, " (")?;
for (i, value) in self.values.iter().enumerate() {
if i > 0 {
write!(ctx, " ")?;
}
value.encode_ctx(ctx)?;
}
write!(ctx, ")")
}
}

pub fn encode_namespaces(ctx: &mut EncodeContext, list: &Namespaces<'_>) -> std::io::Result<()> {
if list.is_empty() {
ctx.write_all(b"NIL")
} else {
ctx.write_all(b"(")?;
for desc in list {
desc.encode_ctx(ctx)?;
}
ctx.write_all(b")")
}
}
4 changes: 4 additions & 0 deletions imap-codec/src/mailbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use nom::{
use crate::extensions::condstore_qresync::search_sort_mod_seq;
#[cfg(feature = "ext_metadata")]
use crate::extensions::metadata::metadata_resp;
#[cfg(feature = "ext_namespace")]
use crate::extensions::namespace::namespace_response;
use crate::{
core::{astring, nil, number, nz_number, quoted_char, string},
decode::IMAPResult,
Expand Down Expand Up @@ -168,6 +170,8 @@ pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
),
#[cfg(feature = "ext_metadata")]
metadata_resp,
#[cfg(feature = "ext_namespace")]
namespace_response,
map(terminated(number, tag_no_case(b" EXISTS")), Data::Exists),
map(terminated(number, tag_no_case(b" RECENT")), Data::Recent),
quotaroot_response,
Expand Down
1 change: 1 addition & 0 deletions imap-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ ext_id = []
ext_login_referrals = []
ext_mailbox_referrals = []
ext_metadata = []
ext_namespace = []

[dependencies]
arbitrary = { version = "1.4.2", optional = true, default-features = false, features = ["derive"] }
Expand Down
2 changes: 2 additions & 0 deletions imap-types/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ext_id = ["imap-types/ext_id"]
ext_login_referrals = ["imap-types/ext_login_referrals"]
ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
ext_metadata = ["imap-types/ext_metadata"]
ext_namespace = ["imap-types/ext_namespace"]
# </Forward to imap-types>

# Use (most) IMAP extensions.
Expand All @@ -30,6 +31,7 @@ ext = [
#"ext_login_referrals",
#"ext_mailbox_referrals",
"ext_metadata",
"ext_namespace",
]
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
debug = []
Expand Down
10 changes: 10 additions & 0 deletions imap-types/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,14 @@ pub enum CommandBody<'a> {
mailbox: Mailbox<'a>,
entries: Vec1<Entry<'a>>,
},

#[cfg(feature = "ext_namespace")]
/// Retrieve the namespaces available to the client.
///
/// <div class="warning">
/// This extension must only be used when the server advertised support for it sending the NAMESPACE capability.
/// </div>
Namespace,
}

impl<'a> CommandBody<'a> {
Expand Down Expand Up @@ -1839,6 +1847,8 @@ impl<'a> CommandBody<'a> {
Self::SetMetadata { .. } => "SETMETADATA",
#[cfg(feature = "ext_metadata")]
Self::GetMetadata { .. } => "GETMETADATA",
#[cfg(feature = "ext_namespace")]
Self::Namespace => "NAMESPACE",
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions imap-types/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ pub mod sort;
pub mod thread;
pub mod uidplus;
pub mod unselect;
#[cfg(feature = "ext_namespace")]
pub mod namespace;
Loading
Loading