diff --git a/imap-codec/src/codec.rs b/imap-codec/src/codec.rs index 1944c9c1..89db5b52 100644 --- a/imap-codec/src/codec.rs +++ b/imap-codec/src/codec.rs @@ -111,6 +111,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + modifiers: vec![], }, ) .unwrap(), @@ -122,6 +123,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + modifiers: vec![], }, ) .unwrap(), @@ -135,12 +137,12 @@ mod tests { ( b"* SEARCH 1\r\n".as_ref(), b"".as_ref(), - Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), + Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()], None)), ), ( b"* SEARCH 1\r\n???", b"???", - Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), + Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()], None)), ), ( b"* 1 FETCH (RFC822 {5}\r\nhello)\r\n", diff --git a/imap-codec/src/codec/decode.rs b/imap-codec/src/codec/decode.rs index 44b6c4f6..fc476a48 100644 --- a/imap-codec/src/codec/decode.rs +++ b/imap-codec/src/codec/decode.rs @@ -433,6 +433,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + modifiers: vec![], }, ) .unwrap(), @@ -446,6 +447,7 @@ mod tests { "a", CommandBody::Select { mailbox: Mailbox::Inbox, + modifiers: vec![], }, ) .unwrap(), @@ -637,14 +639,14 @@ mod tests { b"* SEARCH 1\r\n".as_ref(), Ok(( b"".as_ref(), - Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), + Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()], None)), )), ), ( b"* SEARCH 1\r\n???".as_ref(), Ok(( b"???".as_ref(), - Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), + Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()], None)), )), ), ( diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs index 9a5834b5..cc1309a0 100644 --- a/imap-codec/src/codec/encode.rs +++ b/imap-codec/src/codec/encode.rs @@ -45,7 +45,7 @@ //! C: Pa²²W0rD //! ``` -use std::{borrow::Borrow, io::Write, num::NonZeroU32}; +use std::{borrow::Borrow, io::Write, num::{NonZeroU32, NonZeroU64}}; use base64::{engine::general_purpose::STANDARD as base64, Engine}; use chrono::{DateTime as ChronoDateTime, FixedOffset}; @@ -57,7 +57,7 @@ use imap_types::{ BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location, MultiPartExtensionData, SinglePartExtensionData, SpecificFields, }, - command::{Command, CommandBody}, + command::{Command, CommandBody, StoreModifier, FetchModifier, SelectExamineModifier, ListReturnItem}, core::{ AString, Atom, AtomExt, Charset, IString, Literal, LiteralMode, NString, Quoted, QuotedChar, Tag, Text, @@ -74,7 +74,7 @@ use imap_types::{ Bye, Capability, Code, CodeOther, CommandContinuationRequest, Data, Greeting, GreetingKind, Response, Status, StatusBody, StatusKind, Tagged, }, - search::SearchKey, + search::{SearchKey, MetadataItemType}, sequence::{SeqOrUid, Sequence, SequenceSet}, status::{StatusDataItem, StatusDataItemName}, utils::escape_quoted, @@ -327,16 +327,30 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { ctx.write_all(b" ")?; password.declassify().encode_ctx(ctx) } - CommandBody::Select { mailbox } => { + CommandBody::Select { mailbox, modifiers } => { ctx.write_all(b"SELECT")?; ctx.write_all(b" ")?; - mailbox.encode_ctx(ctx) + mailbox.encode_ctx(ctx)?; + if !modifiers.is_empty() { + ctx.write_all(b" (")?; + join_serializable(modifiers, b" ", ctx)?; + ctx.write_all(b")")?; + } + + Ok(()) } CommandBody::Unselect => ctx.write_all(b"UNSELECT"), - CommandBody::Examine { mailbox } => { + CommandBody::Examine { mailbox, modifiers } => { ctx.write_all(b"EXAMINE")?; ctx.write_all(b" ")?; - mailbox.encode_ctx(ctx) + mailbox.encode_ctx(ctx)?; + if !modifiers.is_empty() { + ctx.write_all(b" (")?; + join_serializable(modifiers, b" ", ctx)?; + ctx.write_all(b")")?; + } + + Ok(()) } CommandBody::Create { mailbox } => { ctx.write_all(b"CREATE")?; @@ -371,12 +385,20 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { CommandBody::List { reference, mailbox_wildcard, + r#return, } => { ctx.write_all(b"LIST")?; ctx.write_all(b" ")?; reference.encode_ctx(ctx)?; ctx.write_all(b" ")?; - mailbox_wildcard.encode_ctx(ctx) + mailbox_wildcard.encode_ctx(ctx)?; + if !r#return.is_empty() { + ctx.write_all(b"(")?; + join_serializable(r#return, b" ", ctx)?; + ctx.write_all(b")")?; + } + + Ok(()) } CommandBody::Lsub { reference, @@ -427,7 +449,14 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { } CommandBody::Check => ctx.write_all(b"CHECK"), CommandBody::Close => ctx.write_all(b"CLOSE"), - CommandBody::Expunge => ctx.write_all(b"EXPUNGE"), + CommandBody::Expunge { uid_sequence_set } => { + if let Some(seqset) = uid_sequence_set { + ctx.write_all(b"UID EXPUNGE ")?; + seqset.encode_ctx(ctx) + } else { + ctx.write_all(b"EXPUNGE") + } + } CommandBody::Search { charset, criteria, @@ -483,6 +512,7 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { } CommandBody::Fetch { sequence_set, + modifiers, macro_or_item_names, uid, } => { @@ -492,6 +522,12 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { ctx.write_all(b"FETCH ")?; } + if !modifiers.is_empty() { + ctx.write_all(b" (")?; + join_serializable(modifiers, b" ", ctx)?; + ctx.write_all(b")")?; + } + sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; macro_or_item_names.encode_ctx(ctx) @@ -501,6 +537,7 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { kind, response, flags, + modifiers, uid, } => { if *uid { @@ -512,6 +549,12 @@ impl<'a> EncodeIntoContext for CommandBody<'a> { sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; + if !modifiers.is_empty() { + ctx.write_all(b" (")?; + join_serializable(modifiers, b" ", ctx)?; + ctx.write_all(b")")?; + } + match kind { StoreType::Add => ctx.write_all(b"+")?, StoreType::Remove => ctx.write_all(b"-")?, @@ -758,6 +801,36 @@ impl<'a> EncodeIntoContext for ListCharString<'a> { } } +impl<'a> EncodeIntoContext for SelectExamineModifier { + fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { + match self { + SelectExamineModifier::Condstore => ctx.write_all(b"CONDSTORE"), + } + } +} + +impl<'a> EncodeIntoContext for FetchModifier { + fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { + match self { + FetchModifier::ChangedSince(val) => { + ctx.write_all(b"CHANGEDSINCE ")?; + val.encode_ctx(ctx) + } + } + } +} + +impl<'a> EncodeIntoContext for StoreModifier { + fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { + match self { + StoreModifier::UnchangedSince(val) => { + ctx.write_all(b"UNCHANGEDSINCE ")?; + val.encode_ctx(ctx) + } + } + } +} + impl EncodeIntoContext for StatusDataItemName { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { @@ -768,12 +841,25 @@ impl EncodeIntoContext for StatusDataItemName { Self::Unseen => ctx.write_all(b"UNSEEN"), Self::Deleted => ctx.write_all(b"DELETED"), Self::DeletedStorage => ctx.write_all(b"DELETED-STORAGE"), - #[cfg(feature = "ext_condstore_qresync")] Self::HighestModSeq => ctx.write_all(b"HIGHESTMODSEQ"), } } } +impl EncodeIntoContext for ListReturnItem { + fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { + match self { + Self::Subscribed => ctx.write_all(b"SUBSCRIBED"), + Self::Children => ctx.write_all(b"CHILDREN"), + Self::Status(attr) => { + ctx.write_all(b"STATUS (")?; + join_serializable(attr, b" ", ctx)?; + ctx.write_all(b")") + } + } + } +} + impl<'a> EncodeIntoContext for Flag<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) @@ -917,7 +1003,29 @@ impl<'a> EncodeIntoContext for SearchKey<'a> { ctx.write_all(b"(")?; join_serializable(search_keys.as_ref(), b" ", ctx)?; ctx.write_all(b")") - } + }, + SearchKey::ModSeq { metadata_item, modseq } => { + ctx.write_all(b"MODSEQ ")?; + if let Some(entry) = metadata_item { + ctx.write_all(b"(")?; + entry.entry_name.encode_ctx(ctx)?; + ctx.write_all(b" ")?; + entry.entry_type.encode_ctx(ctx)?; + ctx.write_all(b") ")?; + } + modseq.encode_ctx(ctx) + }, + } + } +} + +impl EncodeIntoContext for MetadataItemType { + fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { + use MetadataItemType::*; + match self { + Private => ctx.write_all(b"priv"), + Shared => ctx.write_all(b"shared"), + All => ctx.write_all(b"all"), } } } @@ -1012,6 +1120,7 @@ impl<'a> EncodeIntoContext for MessageDataItemName<'a> { Self::Rfc822Size => ctx.write_all(b"RFC822.SIZE"), Self::Rfc822Text => ctx.write_all(b"RFC822.TEXT"), Self::Uid => ctx.write_all(b"UID"), + Self::ModSeq => ctx.write_all(b"MODSEQ"), #[cfg(feature = "ext_binary")] MessageDataItemName::Binary { section, @@ -1109,6 +1218,12 @@ impl EncodeIntoContext for NonZeroU32 { } } +impl EncodeIntoContext for NonZeroU64 { + fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { + write!(ctx, "{self}") + } +} + impl<'a> EncodeIntoContext for Capability<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) @@ -1323,12 +1438,18 @@ impl<'a> EncodeIntoContext for Data<'a> { join_serializable(items, b" ", ctx)?; ctx.write_all(b")")?; } - Data::Search(seqs) => { + Data::Search(seqs, maybe_modseq) => { if seqs.is_empty() { ctx.write_all(b"* SEARCH")?; } else { ctx.write_all(b"* SEARCH ")?; join_serializable(seqs, b" ", ctx)?; + if let Some(modseq) = maybe_modseq { + ctx.write_all(b" (MODSEQ ")?; + modseq.encode_ctx(ctx)?; + ctx.write_all(b")")?; + } + } } #[cfg(feature = "ext_sort_thread")] @@ -1387,6 +1508,7 @@ impl<'a> EncodeIntoContext for Data<'a> { root.encode_ctx(ctx)?; } } + #[cfg(feature = "ext_id")] Data::Id { parameters } => { ctx.write_all(b"* ID ")?; @@ -1484,6 +1606,10 @@ impl EncodeIntoContext for StatusDataItem { ctx.write_all(b"DELETED-STORAGE ")?; count.encode_ctx(ctx) } + Self::HighestModSeq(modseq) => { + ctx.write_all(b"HIGHESTMODSEQ ")?; + modseq.encode_ctx(ctx) + } } } } @@ -1543,6 +1669,11 @@ impl<'a> EncodeIntoContext for MessageDataItem<'a> { nstring.encode_ctx(ctx) } Self::Uid(uid) => write!(ctx, "UID {uid}"), + Self::ModSeq(modseq) => { + ctx.write_all(b"MODSEQ (")?; + modseq.encode_ctx(ctx)?; + ctx.write_all(b")") + } #[cfg(feature = "ext_binary")] Self::Binary { section, value } => { ctx.write_all(b"BINARY[")?; diff --git a/imap-codec/src/command.rs b/imap-codec/src/command.rs index c3fa9ad8..4a07aae8 100644 --- a/imap-codec/src/command.rs +++ b/imap-codec/src/command.rs @@ -9,7 +9,7 @@ use abnf_core::streaming::sp; use imap_types::extensions::binary::LiteralOrLiteral8; use imap_types::{ auth::AuthMechanism, - command::{Command, CommandBody}, + command::{Command, CommandBody, FetchModifier, SelectExamineModifier, StoreModifier, ListReturnItem}, core::AString, fetch::{Macro, MacroOrMessageDataItemNames}, flag::{Flag, StoreResponse, StoreType}, @@ -33,7 +33,7 @@ use crate::extensions::metadata::{getmetadata, setmetadata}; use crate::extensions::{sort::sort, thread::thread}; use crate::{ auth::auth_type, - core::{astring, base64, literal, tag_imap}, + core::{astring, base64, literal, nz_number64, tag_imap}, datetime::date_time, decode::{IMAPErrorKind, IMAPResult}, extensions::{ @@ -215,24 +215,55 @@ 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 modifier = alt(( + value(SelectExamineModifier::Condstore, tag_no_case(b"CONDSTORE")), + )); - let (remaining, (_, _, mailbox)) = parser(input)?; + let mut parser = tuple(( + tag_no_case(b"EXAMINE"), + sp, + mailbox, + opt(preceded(sp, delimited(tag(b"("), separated_list1(sp, modifier), tag(b")")))), + )); + + let (remaining, (_, _, mailbox, maybe_modifiers)) = parser(input)?; + let modifiers = maybe_modifiers.unwrap_or(vec![]); - Ok((remaining, CommandBody::Examine { mailbox })) + Ok((remaining, CommandBody::Examine { mailbox, modifiers })) } /// `list = "LIST" SP mailbox SP list-mailbox` pub(crate) fn list(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { - let mut parser = tuple((tag_no_case(b"LIST"), sp, mailbox, sp, list_mailbox)); + let return_item = alt(( + value(ListReturnItem::Subscribed, tag_no_case(b"SUBSCRIBED")), + value(ListReturnItem::Children, tag_no_case(b"CHILDREN")), + map(preceded( + tuple((tag_no_case(b"STATUS"), sp)), + delimited( + tag(b"("), + separated_list0(sp, status_att), + tag(b")"), + ), + ), |status_att| ListReturnItem::Status(status_att)), + )); - let (remaining, (_, _, reference, _, mailbox_wildcard)) = parser(input)?; + let return_parser = preceded( + tuple((sp, tag_no_case(b"RETURN"), sp)), + delimited( + tag(b"("), + separated_list1(sp, return_item), + tag(b")") + )); + let mut parser = tuple((tag_no_case(b"LIST"), sp, mailbox, sp, list_mailbox, opt(return_parser))); + + let (remaining, (_, _, reference, _, mailbox_wildcard, maybe_return)) = parser(input)?; Ok(( remaining, CommandBody::List { reference, mailbox_wildcard, + r#return: maybe_return.unwrap_or(vec![]).into(), }, )) } @@ -271,11 +302,21 @@ 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 modifier = alt(( + value(SelectExamineModifier::Condstore, tag_no_case(b"CONDSTORE")), + )); - let (remaining, (_, _, mailbox)) = parser(input)?; + let mut parser = tuple(( + tag_no_case(b"SELECT"), + sp, + mailbox, + opt(preceded(sp, delimited(tag(b"("), separated_list1(sp, modifier), tag(b")")))), + )); + + let (remaining, (_, _, mailbox, maybe_modifiers)) = parser(input)?; + let modifiers = maybe_modifiers.unwrap_or(vec![]); - Ok((remaining, CommandBody::Select { mailbox })) + Ok((remaining, CommandBody::Select { mailbox, modifiers })) } /// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"` @@ -418,7 +459,7 @@ pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { alt(( value(CommandBody::Check, tag_no_case(b"CHECK")), value(CommandBody::Close, tag_no_case(b"CLOSE")), - value(CommandBody::Expunge, tag_no_case(b"EXPUNGE")), + value(CommandBody::Expunge { uid_sequence_set: None }, tag_no_case(b"EXPUNGE")), copy, fetch, store, @@ -454,6 +495,14 @@ pub(crate) fn copy(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { /// "FAST" / /// fetch-att / "(" fetch-att *(SP fetch-att) ")")` pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { + let modifier = alt(( + map( + tuple((tag_no_case(b"CHANGEDSINCE"), sp, nz_number64)), + |(_, _, val)| FetchModifier::ChangedSince(val), + ), + )); + + let mut parser = tuple(( tag_no_case(b"FETCH"), sp, @@ -480,15 +529,18 @@ pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { MacroOrMessageDataItemNames::MessageDataItemNames, ), )), + opt(preceded(sp, delimited(tag(b"("), separated_list1(sp, modifier), tag(b")")))), )); - let (remaining, (_, _, sequence_set, _, macro_or_item_names)) = parser(input)?; + let (remaining, (_, _, sequence_set, _, macro_or_item_names, maybe_modifiers)) = parser(input)?; + let modifiers = maybe_modifiers.unwrap_or(vec![]); Ok(( remaining, CommandBody::Fetch { sequence_set, macro_or_item_names, + modifiers, uid: false, }, )) @@ -496,9 +548,24 @@ 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 modifiers = alt(( + map( + tuple((tag_no_case(b"UNCHANGEDSINCE"), sp, nz_number64)), + |(_, _, val)| StoreModifier::UnchangedSince(val), + ), + )); - let (remaining, (_, _, sequence_set, _, (kind, response, flags))) = parser(input)?; + let mut parser = tuple(( + tag_no_case(b"STORE"), + sp, + sequence_set, + opt(preceded(sp, delimited(tag("("), separated_list1(sp, modifiers), tag(")")))), + sp, + store_att_flags + )); + + let (remaining, (_, _, sequence_set, maybe_modifiers, _, (kind, response, flags))) = parser(input)?; + let modifiers = maybe_modifiers.unwrap_or(vec![]); Ok(( remaining, @@ -507,6 +574,7 @@ pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { kind, response, flags, + modifiers, uid: false, }, )) @@ -543,14 +611,25 @@ pub(crate) fn store_att_flags( Ok((remaining, (store_type, store_response, flag_list))) } -/// `uid = "UID" SP (copy / fetch / search / store)` +// uid-expunge = "UID" SP "EXPUNGE" SP sequence-set +pub(crate) fn expunge_range(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { + let mut parser = preceded( + tuple((tag_no_case(b"EXPUNGE"), sp)), + map(sequence_set, |seq| CommandBody::Expunge { + uid_sequence_set: Some(seq), + }), + ); + parser(input) +} + +/// `uid = "UID" SP (copy / fetch / search / store / expunge)` /// /// Note: Unique identifiers used instead of message sequence numbers pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case(b"UID"), sp, - alt((copy, fetch, search, store, r#move)), + alt((copy, fetch, search, store, r#move, expunge_range)), )); let (remaining, (_, _, mut cmd)) = parser(input)?; @@ -561,6 +640,7 @@ pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { | CommandBody::Search { ref mut uid, .. } | CommandBody::Store { ref mut uid, .. } | CommandBody::Move { ref mut uid, .. } => *uid = true, + CommandBody::Expunge { .. } => (), _ => unreachable!(), } diff --git a/imap-codec/src/core.rs b/imap-codec/src/core.rs index 90a810b7..664fae41 100644 --- a/imap-codec/src/core.rs +++ b/imap-codec/src/core.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, num::NonZeroU32, str::from_utf8}; +use std::{borrow::Cow, num::{NonZeroU32, NonZeroU64}, str::from_utf8}; #[cfg(not(feature = "quirk_crlf_relaxed"))] use abnf_core::streaming::crlf; @@ -65,6 +65,10 @@ pub(crate) fn nz_number(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> { map_res(number, NonZeroU32::try_from)(input) } +pub(crate) fn nz_number64(input: &[u8]) -> IMAPResult<&[u8], NonZeroU64> { + map_res(number64, NonZeroU64::try_from)(input) +} + // ----- string ----- /// `string = quoted / literal` diff --git a/imap-codec/src/fetch.rs b/imap-codec/src/fetch.rs index d2d4178d..9b71b2cb 100644 --- a/imap-codec/src/fetch.rs +++ b/imap-codec/src/fetch.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU32}; use abnf_core::streaming::sp; #[cfg(feature = "ext_binary")] @@ -21,7 +21,7 @@ use nom::{ use crate::extensions::binary::{literal8, partial, section_binary}; use crate::{ body::body, - core::{astring, nstring, number, nz_number}, + core::{astring, nstring, number, nz_number64, nz_number}, datetime::date_time, decode::IMAPResult, envelope::envelope, @@ -117,6 +117,7 @@ pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName> value(MessageDataItemName::Rfc822Size, tag_no_case(b"RFC822.SIZE")), value(MessageDataItemName::Rfc822Text, tag_no_case(b"RFC822.TEXT")), value(MessageDataItemName::Rfc822, tag_no_case(b"RFC822")), + value(MessageDataItemName::ModSeq, tag_no_case(b"MODSEQ")), ))(input) } @@ -213,6 +214,9 @@ pub(crate) fn msg_att_static(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> map(tuple((tag_no_case(b"UID"), sp, uniqueid)), |(_, _, uid)| { MessageDataItem::Uid(uid) }), + map(tuple((tag_no_case(b"MODSEQ "), delimited(tag("("), nz_number64, tag(")")))), |(_, modseq)| { + MessageDataItem::ModSeq(modseq) + }), #[cfg(feature = "ext_binary")] map( tuple(( diff --git a/imap-codec/src/mailbox.rs b/imap-codec/src/mailbox.rs index 52f19698..ae2a6673 100644 --- a/imap-codec/src/mailbox.rs +++ b/imap-codec/src/mailbox.rs @@ -19,7 +19,7 @@ use crate::extensions::metadata::metadata_resp; #[cfg(feature = "ext_sort_thread")] use crate::extensions::thread::thread_data; use crate::{ - core::{astring, nil, number, nz_number, quoted_char, string}, + core::{astring, nil, number, nz_number, nz_number64, quoted_char, string}, decode::IMAPResult, extensions::quota::{quota_response, quotaroot_response}, flag::{flag_list, mbx_list_flags}, @@ -89,8 +89,12 @@ pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> { }, ), map( - tuple((tag_no_case(b"SEARCH"), many0(preceded(sp, nz_number)))), - |(_, nums)| Data::Search(nums), + tuple(( + tag_no_case(b"SEARCH"), + many0(preceded(sp, nz_number)), + opt(delimited(tag_no_case(b"(MODSEQ "), nz_number64, tag(b")"))) + )), + |(_, nums, modseq)| Data::Search(nums, modseq), ), #[cfg(feature = "ext_sort_thread")] map( diff --git a/imap-codec/src/response.rs b/imap-codec/src/response.rs index aabbd2c2..d89c4896 100644 --- a/imap-codec/src/response.rs +++ b/imap-codec/src/response.rs @@ -472,7 +472,7 @@ mod tests { 2.try_into().unwrap(), 3.try_into().unwrap(), 42.try_into().unwrap(), - ])), + ], None)), ), (b"* 42 EXISTS\r\n", b"", Response::Data(Data::Exists(42))), ( diff --git a/imap-codec/src/search.rs b/imap-codec/src/search.rs index 2884f32f..dbbb31d7 100644 --- a/imap-codec/src/search.rs +++ b/imap-codec/src/search.rs @@ -1,7 +1,7 @@ use abnf_core::streaming::sp; +use imap_types::{command::CommandBody, core::Vec1, search::{SearchKey, MetadataItemSearch, MetadataItemType}}; #[cfg(feature = "ext_sort_thread")] use imap_types::core::Charset; -use imap_types::{command::CommandBody, core::Vec1, search::SearchKey}; #[cfg(feature = "ext_sort_thread")] use nom::sequence::separated_pair; use nom::{ @@ -13,7 +13,7 @@ use nom::{ }; use crate::{ - core::{astring, atom, charset, number}, + core::{astring, atom, charset, number, nz_number64, quoted}, datetime::date, decode::{IMAPErrorKind, IMAPParseError, IMAPResult}, fetch::header_fld_name, @@ -109,6 +109,12 @@ fn search_key_limited<'a>( let search_key = move |input: &'a [u8]| search_key_limited(input, remaining_recursion.saturating_sub(1)); + let metadata_item_type = alt(( + value(MetadataItemType::Private, tag_no_case(b"priv")), + value(MetadataItemType::Shared, tag_no_case(b"shared")), + value(MetadataItemType::All, tag_no_case(b"all")), + )); + alt(( alt(( value(SearchKey::All, tag_no_case(b"ALL")), @@ -211,6 +217,18 @@ fn search_key_limited<'a>( |(_, _, val)| SearchKey::Uid(val), ), value(SearchKey::Undraft, tag_no_case(b"UNDRAFT")), + map( + tuple(( + tag_no_case(b"MODSEQ"), + sp, + opt(map( + delimited(tag(b"("), tuple((quoted, sp, metadata_item_type)), tag(b") ")), + |(entry_name, _, entry_type)| MetadataItemSearch { entry_name, entry_type } , + )), + nz_number64 + )), + |(_, _, _opt, modseq)| SearchKey::ModSeq { metadata_item: None, modseq }, + ), map(sequence_set, SearchKey::SequenceSet), map( delimited(tag(b"("), separated_list1(sp, search_key), tag(b")")), diff --git a/imap-codec/tests/trace.rs b/imap-codec/tests/trace.rs index 1c3d9d9d..f37e98b7 100644 --- a/imap-codec/tests/trace.rs +++ b/imap-codec/tests/trace.rs @@ -947,7 +947,7 @@ fn test_transcript_from_rfc() { Message::Command( Command::new( "a003", - CommandBody::fetch("12", Macro::Full, false).unwrap(), + CommandBody::fetch("12", vec![], Macro::Full, false).unwrap(), ) .unwrap(), ), @@ -1078,6 +1078,7 @@ fn test_transcript_from_rfc() { "a004", CommandBody::fetch( "12", + vec![], vec![MessageDataItemName::BodyExt { section: Some(Section::Header(None)), peek: false, @@ -1150,6 +1151,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 401f07a6..9f94b640 100644 --- a/imap-types/src/command.rs +++ b/imap-types/src/command.rs @@ -3,6 +3,7 @@ //! See . use std::borrow::Cow; +use std::num::NonZeroU64; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; @@ -392,6 +393,8 @@ pub enum CommandBody<'a> { Select { /// Mailbox. mailbox: Mailbox<'a>, + /// Optional parameters according to RFC466 section 2.1 + modifiers: Vec, }, /// Unselect a mailbox. @@ -421,6 +424,8 @@ pub enum CommandBody<'a> { Examine { /// Mailbox. mailbox: Mailbox<'a>, + /// Optional parameters according to RFC466 section 2.1 + modifiers: Vec, }, /// ### 6.3.3. CREATE Command @@ -737,6 +742,29 @@ pub enum CommandBody<'a> { reference: Mailbox<'a>, /// Mailbox (wildcard). mailbox_wildcard: ListMailbox<'a>, + /// Return Options + /// + /// --- + /// + /// The return options defined in this specification (RFC5258) are as follows. + /// SUBSCRIBED - causes the LIST command to return subscription state + /// for all matching mailbox names. The "\Subscribed" attribute MUST + /// be supported and MUST be accurately computed when the SUBSCRIBED + /// return option is specified. Further, all mailbox flags MUST be + /// accurately computed (this differs from the behavior of the LSUB + /// command). + /// CHILDREN - requests mailbox child information as originally proposed + /// in [CMbox]. See Section 4, below, for details. This option MUST + /// be supported by all servers. + /// + /// --- + /// + /// STATUS Return Option to LIST Command + /// + /// In order to achieve this goal, this document + /// is extending the LIST command with a new return option, STATUS. This + /// option takes STATUS data items as parameters. + r#return: Cow<'a, [ListReturnItem]>, }, /// ### 6.3.9. LSUB Command @@ -967,7 +995,9 @@ pub enum CommandBody<'a> { /// Note: In this example, messages 3, 4, 7, and 11 had the /// \Deleted flag set. See the description of the EXPUNGE /// response for further explanation. - Expunge, + Expunge { + uid_sequence_set: Option, + }, /// ### 6.4.4. SEARCH Command /// @@ -1103,6 +1133,8 @@ pub enum CommandBody<'a> { Fetch { /// Set of messages. sequence_set: SequenceSet, + /// Fetch modifiers + modifiers: Vec, /// Message data items (or a macro). macro_or_item_names: MacroOrMessageDataItemNames<'a>, /// Use UID variant. @@ -1168,6 +1200,8 @@ pub enum CommandBody<'a> { response: StoreResponse, /// Flags. flags: Vec>, // FIXME(misuse): must not accept "\*" or "\Recent" + /// Modifiers. + modifiers: Vec, /// Use UID variant. uid: bool, }, @@ -1444,6 +1478,40 @@ pub enum 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 ListReturnItem { + Subscribed, + Children, + Status(Vec), +} + +#[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 SelectExamineModifier { + Condstore, +} + +#[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 FetchModifier { + ChangedSince(NonZeroU64), +} + +#[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 { + UnchangedSince(NonZeroU64), +} + impl<'a> CommandBody<'a> { /// Prepend a tag to finalize the command body to a command. pub fn tag(self, tag: T) -> Result, T::Error> @@ -1498,6 +1566,7 @@ impl<'a> CommandBody<'a> { { Ok(CommandBody::Select { mailbox: mailbox.try_into()?, + modifiers: vec![], }) } @@ -1508,6 +1577,7 @@ impl<'a> CommandBody<'a> { { Ok(CommandBody::Examine { mailbox: mailbox.try_into()?, + modifiers: vec![], }) } @@ -1575,6 +1645,7 @@ impl<'a> CommandBody<'a> { Ok(CommandBody::List { reference: reference.try_into().map_err(ListError::Reference)?, mailbox_wildcard: mailbox_wildcard.try_into().map_err(ListError::Mailbox)?, + r#return: [][..].into(), }) } @@ -1639,7 +1710,7 @@ impl<'a> CommandBody<'a> { } /// Construct a FETCH command. - pub fn fetch(sequence_set: S, macro_or_item_names: I, uid: bool) -> Result + pub fn fetch(sequence_set: S, modifiers: Vec, macro_or_item_names: I, uid: bool) -> Result where S: TryInto, I: Into>, @@ -1649,6 +1720,7 @@ impl<'a> CommandBody<'a> { Ok(CommandBody::Fetch { sequence_set, macro_or_item_names: macro_or_item_names.into(), + modifiers, uid, }) } @@ -1659,6 +1731,7 @@ impl<'a> CommandBody<'a> { kind: StoreType, response: StoreResponse, flags: Vec>, + modifiers: Vec, uid: bool, ) -> Result where @@ -1671,6 +1744,7 @@ impl<'a> CommandBody<'a> { kind, response, flags, + modifiers, uid, }) } @@ -1720,7 +1794,7 @@ impl<'a> CommandBody<'a> { Self::Append { .. } => "APPEND", Self::Check => "CHECK", Self::Close => "CLOSE", - Self::Expunge => "EXPUNGE", + Self::Expunge { .. } => "EXPUNGE", Self::Search { .. } => "SEARCH", Self::Fetch { .. } => "FETCH", Self::Store { .. } => "STORE", @@ -1890,7 +1964,7 @@ mod tests { .unwrap(), CommandBody::Check, CommandBody::Close, - CommandBody::Expunge, + CommandBody::Expunge { uid_sequence_set: None }, CommandBody::search( None, Vec1::from(SearchKey::And( @@ -1950,6 +2024,7 @@ mod tests { ), CommandBody::fetch( "1", + vec![], vec![MessageDataItemName::BodyExt { partial: None, section: Some(Section::Part(Part( @@ -1962,12 +2037,13 @@ mod tests { false, ) .unwrap(), - CommandBody::fetch("1:*,2,3", Macro::Full, true).unwrap(), + CommandBody::fetch("1:*,2,3", vec![], Macro::Full, true).unwrap(), CommandBody::store( "1,2:*", StoreType::Remove, StoreResponse::Answer, vec![Flag::Seen, Flag::Draft], + vec![], false, ) .unwrap(), @@ -1976,6 +2052,7 @@ mod tests { StoreType::Add, StoreResponse::Answer, vec![Flag::Keyword("TEST".try_into().unwrap())], + vec![], true, ) .unwrap(), @@ -2015,6 +2092,7 @@ mod tests { ( CommandBody::Select { mailbox: Mailbox::Inbox, + modifiers: vec![], }, "SELECT", ), @@ -2022,6 +2100,7 @@ mod tests { ( CommandBody::Examine { mailbox: Mailbox::Inbox, + modifiers: vec![], }, "EXAMINE", ), @@ -2060,6 +2139,7 @@ mod tests { CommandBody::List { reference: Mailbox::Inbox, mailbox_wildcard: ListMailbox::try_from("").unwrap(), + r#return: [][..].into(), }, "LIST", ), @@ -2104,7 +2184,9 @@ mod tests { ), (CommandBody::Check, "CHECK"), (CommandBody::Close, "CLOSE"), - (CommandBody::Expunge, "EXPUNGE"), + (CommandBody::Expunge { + uid_sequence_set: None, + }, "EXPUNGE"), ( CommandBody::Search { charset: None, @@ -2117,6 +2199,7 @@ mod tests { CommandBody::Fetch { sequence_set: SequenceSet::try_from(1u32).unwrap(), macro_or_item_names: MacroOrMessageDataItemNames::Macro(Macro::Full), + modifiers: vec![], uid: true, }, "FETCH", @@ -2127,6 +2210,7 @@ mod tests { flags: vec![], response: StoreResponse::Silent, kind: StoreType::Add, + modifiers: vec![], uid: true, }, "STORE", diff --git a/imap-types/src/fetch.rs b/imap-types/src/fetch.rs index 16d80fdb..5ae9b171 100644 --- a/imap-types/src/fetch.rs +++ b/imap-types/src/fetch.rs @@ -2,7 +2,7 @@ use std::{ fmt::{Display, Formatter}, - num::NonZeroU32, + num::{NonZeroU32, NonZeroU64}, }; #[cfg(feature = "arbitrary")] @@ -232,6 +232,8 @@ pub enum MessageDataItemName<'a> { /// ``` Uid, + /// The ModSeq of CONDSTORE + ModSeq, #[cfg(feature = "ext_binary")] Binary { section: Vec, @@ -369,6 +371,8 @@ pub enum MessageDataItem<'a> { /// ``` Uid(NonZeroU32), + /// The ModSeq value described in CONDSTORE + ModSeq(NonZeroU64), #[cfg(feature = "ext_binary")] Binary { section: Vec, diff --git a/imap-types/src/response.rs b/imap-types/src/response.rs index 4287be42..63a12bee 100644 --- a/imap-types/src/response.rs +++ b/imap-types/src/response.rs @@ -3,7 +3,7 @@ use std::{ borrow::Cow, fmt::{Debug, Display, Formatter}, - num::{NonZeroU32, TryFromIntError}, + num::{NonZeroU32, NonZeroU64, TryFromIntError}, }; #[cfg(feature = "arbitrary")] @@ -432,7 +432,10 @@ pub enum Data<'a> { /// search criteria. For SEARCH, these are message sequence numbers; /// for UID SEARCH, these are unique identifiers. Each number is /// delimited by a space. - Search(Vec), + /// + /// Optionally, a modseq value can be set to inform the client the highest + /// modset in the set of returned messages. + Search(Vec, Option), #[cfg(feature = "ext_sort_thread")] Sort(Vec), diff --git a/imap-types/src/search.rs b/imap-types/src/search.rs index e18791b7..2bdaa8a1 100644 --- a/imap-types/src/search.rs +++ b/imap-types/src/search.rs @@ -5,12 +5,30 @@ use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::num::NonZeroU64; use crate::{ - core::{AString, Atom, Vec1}, + core::{AString, Atom, Quoted, Vec1}, datetime::NaiveDate, sequence::SequenceSet, }; +#[cfg_attr(feature = "bounded-static", derive(ToStatic))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MetadataItemType { + Private, + Shared, + All, +} + +#[cfg_attr(feature = "bounded-static", derive(ToStatic))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MetadataItemSearch<'a> { + pub entry_name: Quoted<'a>, + pub entry_type: MetadataItemType, +} + /// The defined search keys. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -82,6 +100,14 @@ pub enum SearchKey<'a> { /// number of octets. Larger(u32), + /// Messages with a modseq that have a value equal or greater than the given one. + /// Search can optionally be restricted to a specific metadata item type (eg. the Draft flag) + /// and/or a visibility scope (private, shared, or both). + ModSeq { + metadata_item: Option>, + modseq: NonZeroU64, + }, + /// Messages that have the \Recent flag set but not the \Seen flag. /// This is functionally equivalent to "(RECENT UNSEEN)". New, diff --git a/imap-types/src/status.rs b/imap-types/src/status.rs index 92aa44c1..455cbd56 100644 --- a/imap-types/src/status.rs +++ b/imap-types/src/status.rs @@ -35,8 +35,6 @@ pub enum StatusDataItemName { /// The amount of storage space that can be reclaimed by performing EXPUNGE on the mailbox. DeletedStorage, - #[cfg(feature = "ext_condstore_qresync")] - #[cfg_attr(docsrs, doc(cfg(feature = "ext_condstore_qresync")))] HighestModSeq, } @@ -69,4 +67,7 @@ pub enum StatusDataItem { /// The amount of storage space that can be reclaimed by performing EXPUNGE on the mailbox. DeletedStorage(u64), + + /// The highest mod sequence in the mailbox + HighestModSeq(u64), }