diff --git a/imap-codec/src/codec.rs b/imap-codec/src/codec.rs index 9a768c55..35b3d8bc 100644 --- a/imap-codec/src/codec.rs +++ b/imap-codec/src/codec.rs @@ -112,6 +112,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + parameters: None, }, ) .unwrap(), @@ -123,6 +124,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + parameters: None, }, ) .unwrap(), diff --git a/imap-codec/src/codec/decode.rs b/imap-codec/src/codec/decode.rs index 531e95e8..a79df38e 100644 --- a/imap-codec/src/codec/decode.rs +++ b/imap-codec/src/codec/decode.rs @@ -434,6 +434,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + parameters: None, }, ) .unwrap(), @@ -447,6 +448,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + parameters: None, }, ) .unwrap(), diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs index 4f304b9e..37dcc3aa 100644 --- a/imap-codec/src/codec/encode.rs +++ b/imap-codec/src/codec/encode.rs @@ -55,7 +55,7 @@ use imap_types::{ BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location, MultiPartExtensionData, SinglePartExtensionData, SpecificFields, }, - command::{Command, CommandBody}, + command::{Command, CommandBody, StoreModifier}, core::{ AString, Atom, AtomExt, Charset, IString, Literal, LiteralMode, NString, Quoted, QuotedChar, Tag, Text, @@ -325,16 +325,34 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { ctx.write_all(b" ")?; password.declassify().encode_ctx(ctx) } - CommandBody::Select { mailbox } => { + CommandBody::Select { mailbox, parameters } => { ctx.write_all(b"SELECT")?; ctx.write_all(b" ")?; - mailbox.encode_ctx(ctx) + mailbox.encode_ctx(ctx)?; + if let Some(p) = parameters { + ctx.write_all(b" (")?; + for atom in p.as_ref().iter() { + atom.encode_ctx(ctx)?; + } + ctx.write_all(b")")?; + } + + Ok(()) } CommandBody::Unselect => ctx.write_all(b"UNSELECT"), - CommandBody::Examine { mailbox } => { + CommandBody::Examine { mailbox, parameters } => { ctx.write_all(b"EXAMINE")?; ctx.write_all(b" ")?; - mailbox.encode_ctx(ctx) + mailbox.encode_ctx(ctx)?; + if let Some(p) = parameters { + ctx.write_all(b" (")?; + for atom in p.as_ref().iter() { + atom.encode_ctx(ctx)?; + } + ctx.write_all(b")")?; + } + + Ok(()) } CommandBody::Create { mailbox } => { ctx.write_all(b"CREATE")?; @@ -463,6 +481,7 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { kind, response, flags, + modifiers, uid, } => { if *uid { @@ -474,6 +493,32 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; + if !modifiers.is_empty() { + ctx.write_all(b" (")?; + let mut mod_iter = modifiers.iter().peekable(); + while let Some((k, x)) = mod_iter.next() { + k.encode_ctx(ctx)?; + ctx.write_all(b" ")?; + match x { + StoreModifier::Value(num) => { + num.encode_ctx(ctx)?; + }, + StoreModifier::SequenceSet(seq) => { + seq.encode_ctx(ctx)?; + }, + StoreModifier::Arbitrary(val) => { + ctx.write_all(b"(")?; + val.encode_ctx(ctx)?; + ctx.write_all(b")")?; + } + } + if mod_iter.peek().is_some() { + ctx.write_all(b" ")?; + } + } + ctx.write_all(b") ")?; + } + match kind { StoreType::Add => ctx.write_all(b"+")?, StoreType::Remove => ctx.write_all(b"-")?, diff --git a/imap-codec/src/command.rs b/imap-codec/src/command.rs index 8f6435b5..1a3fa17c 100644 --- a/imap-codec/src/command.rs +++ b/imap-codec/src/command.rs @@ -7,8 +7,8 @@ use abnf_core::streaming::crlf_relaxed as crlf; use abnf_core::streaming::sp; use imap_types::{ auth::AuthMechanism, - command::{Command, CommandBody}, - core::AString, + command::{Command, CommandBody, StoreModifier}, + core::{AString, NonEmptyVec}, fetch::{Macro, MacroOrMessageDataItemNames}, flag::{Flag, StoreResponse, StoreType}, secret::Secret, @@ -25,7 +25,7 @@ use nom::{ use crate::extensions::id::id; use crate::{ auth::auth_type, - core::{astring, base64, literal, tag_imap}, + core::{astring, atom, base64, literal, number, tag_imap}, datetime::date_time, decode::{IMAPErrorKind, IMAPResult}, extensions::{ @@ -190,11 +190,17 @@ pub(crate) fn delete(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { /// `examine = "EXAMINE" SP mailbox` pub(crate) fn examine(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { - let mut parser = tuple((tag_no_case(b"EXAMINE"), sp, mailbox)); + let mut parser = tuple(( + tag_no_case(b"EXAMINE"), + sp, + mailbox, + opt(tuple((sp, delimited(tag(b"("), separated_list1(sp, atom), tag(b")"))))), + )); - let (remaining, (_, _, mailbox)) = parser(input)?; + let (remaining, (_, _, mailbox, maybe_params)) = parser(input)?; + let final_params = maybe_params.map(|(_, elems)| NonEmptyVec::unvalidated(elems)); - Ok((remaining, CommandBody::Examine { mailbox })) + Ok((remaining, CommandBody::Examine { mailbox, parameters: final_params })) } /// `list = "LIST" SP mailbox SP list-mailbox` @@ -246,11 +252,17 @@ pub(crate) fn rename(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { /// `select = "SELECT" SP mailbox` pub(crate) fn select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { - let mut parser = tuple((tag_no_case(b"SELECT"), sp, mailbox)); + let mut parser = tuple(( + tag_no_case(b"SELECT"), + sp, + mailbox, + opt(tuple((sp, delimited(tag(b"("), separated_list1(sp, atom), tag(b")"))))), + )); - let (remaining, (_, _, mailbox)) = parser(input)?; + let (remaining, (_, _, mailbox, maybe_params)) = parser(input)?; + let final_params = maybe_params.map(|(_, elems)| NonEmptyVec::unvalidated(elems)); - Ok((remaining, CommandBody::Select { mailbox })) + Ok((remaining, CommandBody::Select { mailbox, parameters: final_params })) } /// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"` @@ -467,9 +479,19 @@ pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { /// `store = "STORE" SP sequence-set SP store-att-flags` pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { - let mut parser = tuple((tag_no_case(b"STORE"), sp, sequence_set, sp, store_att_flags)); + let modifier_val_parser = alt(( + map(number, |num| StoreModifier::Value(num)), + map(sequence_set, |sq| StoreModifier::SequenceSet(sq)), + map(astring, |astr| StoreModifier::Arbitrary(astr)), + )); + let modifiers_parser = opt(delimited( + tag(b"("), + separated_list1(sp, map(tuple((atom, sp, modifier_val_parser)), |(k, _, v)| (k, v))), + tag(b") "))); + let mut parser = tuple((tag_no_case(b"STORE"), sp, sequence_set, sp, modifiers_parser, store_att_flags)); - let (remaining, (_, _, sequence_set, _, (kind, response, flags))) = parser(input)?; + let (remaining, (_, _, sequence_set, _, maybe_modifiers, (kind, response, flags))) = parser(input)?; + let modifiers = maybe_modifiers.unwrap_or(vec![]); Ok(( remaining, @@ -478,6 +500,7 @@ pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { kind, response, flags, + modifiers, uid: false, }, )) diff --git a/imap-codec/tests/trace.rs b/imap-codec/tests/trace.rs index 42a8cbb6..81524430 100644 --- a/imap-codec/tests/trace.rs +++ b/imap-codec/tests/trace.rs @@ -1150,6 +1150,7 @@ Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r StoreType::Add, StoreResponse::Answer, vec![Flag::Deleted], + vec![], false, ) .unwrap(), diff --git a/imap-types/src/command.rs b/imap-types/src/command.rs index 98d7d6cf..34e8deae 100644 --- a/imap-types/src/command.rs +++ b/imap-types/src/command.rs @@ -16,7 +16,7 @@ use crate::core::{IString, NString}; use crate::{ auth::AuthMechanism, command::error::{AppendError, CopyError, ListError, LoginError, RenameError}, - core::{AString, Charset, Literal, NonEmptyVec, Tag}, + core::{Atom, AString, Charset, Literal, NonEmptyVec, Tag}, datetime::DateTime, extensions::{compress::CompressionAlgorithm, enable::CapabilityEnable, quota::QuotaSet}, fetch::MacroOrMessageDataItemNames, @@ -386,6 +386,8 @@ pub enum CommandBody<'a> { Select { /// Mailbox. mailbox: Mailbox<'a>, + /// Optional parameters according to RFC466 section 2.1 + parameters: Option>>, }, /// Unselect a mailbox. @@ -415,6 +417,8 @@ pub enum CommandBody<'a> { Examine { /// Mailbox. mailbox: Mailbox<'a>, + /// Optional parameters according to RFC466 section 2.1 + parameters: Option>>, }, /// ### 6.3.3. CREATE Command @@ -1110,6 +1114,8 @@ pub enum CommandBody<'a> { response: StoreResponse, /// Flags. flags: Vec>, // FIXME(misuse): must not accept "\*" or "\Recent" + /// Modifiers. + modifiers: Vec<(Atom<'a>, StoreModifier<'a>)>, /// Use UID variant. uid: bool, }, @@ -1427,6 +1433,7 @@ impl<'a> CommandBody<'a> { { Ok(CommandBody::Select { mailbox: mailbox.try_into()?, + parameters: None, }) } @@ -1437,6 +1444,7 @@ impl<'a> CommandBody<'a> { { Ok(CommandBody::Examine { mailbox: mailbox.try_into()?, + parameters: None, }) } @@ -1585,6 +1593,7 @@ impl<'a> CommandBody<'a> { kind: StoreType, response: StoreResponse, flags: Vec>, + modifiers: Vec<(Atom<'a>, StoreModifier<'a>)>, uid: bool, ) -> Result where @@ -1597,6 +1606,7 @@ impl<'a> CommandBody<'a> { kind, response, flags, + modifiers, uid, }) } @@ -1659,6 +1669,15 @@ impl<'a> CommandBody<'a> { } } } +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[cfg_attr(feature = "bounded-static", derive(ToStatic))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum StoreModifier<'a> { + Value(u32), + SequenceSet(SequenceSet), + Arbitrary(AString<'a>), +} /// Error-related types. pub mod error { @@ -1870,6 +1889,7 @@ mod tests { StoreType::Remove, StoreResponse::Answer, vec![Flag::Seen, Flag::Draft], + vec![], false, ) .unwrap(), @@ -1878,6 +1898,7 @@ mod tests { StoreType::Add, StoreResponse::Answer, vec![Flag::Keyword("TEST".try_into().unwrap())], + vec![], true, ) .unwrap(), @@ -1917,6 +1938,7 @@ mod tests { ( CommandBody::Select { mailbox: Mailbox::Inbox, + parameters: None, }, "SELECT", ), @@ -1924,6 +1946,7 @@ mod tests { ( CommandBody::Examine { mailbox: Mailbox::Inbox, + parameters: None, }, "EXAMINE", ), @@ -2013,6 +2036,7 @@ mod tests { flags: vec![], response: StoreResponse::Silent, kind: StoreType::Add, + modifiers: vec![], uid: true, }, "STORE",