diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs index 8d9f9916..7c13872a 100644 --- a/imap-codec/src/codec/encode.rs +++ b/imap-codec/src/codec/encode.rs @@ -79,6 +79,9 @@ use imap_types::{ }; use utils::{join_serializable, List1AttributeValueOrNil, List1OrNil}; +#[cfg(feature = "ext_condstore_qresync")] +use std::num::NonZeroU64; + use crate::{AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec}; /// Encoder. @@ -1036,6 +1039,13 @@ impl EncodeIntoContext for NonZeroU32 { } } +#[cfg(feature = "ext_condstore_qresync")] +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) @@ -1171,6 +1181,21 @@ impl<'a> EncodeIntoContext for Code<'a> { ctx.write_all(b"REFERRAL ")?; ctx.write_all(url.as_bytes()) } + + // RFC 4551 + #[cfg(feature = "ext_condstore_qresync")] + Code::HighestModSeq(modseq) => { + ctx.write_all(b"HIGHESTMODSEQ ")?; + modseq.encode_ctx(ctx) + }, + #[cfg(feature = "ext_condstore_qresync")] + Code::NoModSeq => ctx.write_all(b"NOMODSEQ"), + #[cfg(feature = "ext_condstore_qresync")] + Code::Modified(seq_or_uid_list) => { + ctx.write_all(b"MODIFIED ")?; + join_serializable(seq_or_uid_list.as_ref(), b",", ctx) + }, + Code::CompressionActive => ctx.write_all(b"COMPRESSIONACTIVE"), Code::OverQuota => ctx.write_all(b"OVERQUOTA"), Code::TooBig => ctx.write_all(b"TOOBIG"), diff --git a/imap-codec/src/core.rs b/imap-codec/src/core.rs index 90a810b7..9d2d616d 100644 --- a/imap-codec/src/core.rs +++ b/imap-codec/src/core.rs @@ -24,6 +24,9 @@ use nom::{ sequence::{delimited, terminated, tuple}, }; +#[cfg(feature = "ext_condstore_qresync")] +use std::num::NonZeroU64; + use crate::decode::{IMAPErrorKind, IMAPParseError, IMAPResult}; // ----- number ----- @@ -65,6 +68,15 @@ pub(crate) fn nz_number(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> { map_res(number, NonZeroU32::try_from)(input) } +/// mod-sequence-value = 1*DIGIT +/// ;; Positive unsigned 64-bit integer +/// ;; (mod-sequence) +/// ;; (1 <= n < 18,446,744,073,709,551,615) +#[cfg(feature = "ext_condstore_qresync")] +pub(crate) fn mod_sequence_value(input: &[u8]) -> IMAPResult<&[u8], NonZeroU64> { + map_res(number64, NonZeroU64::try_from)(input) +} + // ----- string ----- /// `string = quoted / literal` diff --git a/imap-codec/src/response.rs b/imap-codec/src/response.rs index ab6e1ff4..d1aed476 100644 --- a/imap-codec/src/response.rs +++ b/imap-codec/src/response.rs @@ -32,6 +32,9 @@ use crate::{ mailbox::mailbox_data, }; +#[cfg(feature = "ext_condstore_qresync")] +use crate::core::mod_sequence_value; + // ----- greeting ----- /// `greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF` @@ -124,7 +127,10 @@ pub(crate) fn resp_text(input: &[u8]) -> IMAPResult<&[u8], (Option, Text)> /// "UIDNEXT" SP nz-number / /// "UIDVALIDITY" SP nz-number / /// "UNSEEN" SP nz-number / -/// "COMPRESSIONACTIVE" ; RFC 4978 +/// "COMPRESSIONACTIVE" ; RFC 4978 / +/// "HIGHESTMODSEQ" SP mod-sequence-value ; RFC4551 / +/// "NOMODSEQ" ; RFC4551 / +/// "MODIFIED" SP set ; RFC4551 / /// atom [SP 1*]` /// /// Note: See errata id: 261 @@ -177,6 +183,18 @@ pub(crate) fn resp_text_code(input: &[u8]) -> IMAPResult<&[u8], Code> { value(Code::CompressionActive, tag_no_case(b"COMPRESSIONACTIVE")), value(Code::OverQuota, tag_no_case(b"OVERQUOTA")), value(Code::TooBig, tag_no_case(b"TOOBIG")), + #[cfg(feature = "ext_condstore_qresync")] + map( + tuple((tag_no_case(b"HIGHESTMODSEQ"), sp, mod_sequence_value)), + |(_, _, modseq)| Code::HighestModSeq(modseq) + ), + #[cfg(feature = "ext_condstore_qresync")] + value(Code::NoModSeq, tag_no_case(b"NOMODSEQ")), + #[cfg(feature = "ext_condstore_qresync")] + map( + tuple((tag_no_case(b"MODIFIED"), sp, separated_list1(tag(","), nz_number))), + |(_, _, set)| Code::Modified(Vec1::unvalidated(set)), + ), ))(input) } @@ -701,4 +719,12 @@ mod tests { assert!(resp_text(b"[IMAP4rev1] \r\n").is_ok()); } } + + #[cfg(feature = "ext_condstore_qresync")] + #[test] + fn test_condstore_qresync_codes() { + assert!(resp_text(b"[MODIFIED 7,9] Conditional STORE failed\r\n").is_ok()); + assert!(resp_text(b"[NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n").is_ok()); + assert!(resp_text(b"[HIGHESTMODSEQ 715194045007] Highest\r\n").is_ok()); + } } diff --git a/imap-types/src/response.rs b/imap-types/src/response.rs index 83f319f4..f2de6dda 100644 --- a/imap-types/src/response.rs +++ b/imap-types/src/response.rs @@ -6,6 +6,9 @@ use std::{ num::{NonZeroU32, TryFromIntError}, }; +#[cfg(feature = "ext_condstore_qresync")] +use std::num::NonZeroU64; + #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use base64::{engine::general_purpose::STANDARD as _base64, Engine}; @@ -832,6 +835,35 @@ pub enum Code<'a> { /// Server got a non-synchronizing literal larger than 4096 bytes. TooBig, + + /// IMAP4 Extension for Conditional STORE Operation (RFC 4551) + /// A server supporting the persistent storage of mod-sequences for the mailbox + /// MUST send the OK untagged response including HIGHESTMODSEQ response + /// code with every successful SELECT or EXAMINE command + #[cfg(feature = "ext_condstore_qresync")] + #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))] + HighestModSeq(NonZeroU64), + + /// IMAP4 Extension for Conditional STORE Operation (RFC 4551) + /// When the server finished performing the operation on all the + /// messages in the message set, it checks for a non-empty list of + /// messages that failed the UNCHANGESINCE test. If this list is + /// non-empty, the server MUST return in the tagged response a + /// MODIFIED response code. The MODIFIED response code includes the + /// message set (for STORE) or set of UIDs (for UID STORE) of all + /// messages that failed the UNCHANGESINCE test. + #[cfg(feature = "ext_condstore_qresync")] + #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))] + Modified(Vec1), + + /// IMAP4 Extension for Conditional STORE Operation (RFC 4551) + /// A server that doesn't support the persistent storage of mod-sequences + /// for the mailbox MUST send the OK untagged response including NOMODSEQ + /// response code with every successful SELECT or EXAMINE command. + #[cfg(feature = "ext_condstore_qresync")] + #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))] + NoModSeq, + /// Additional response codes defined by particular client or server /// implementations SHOULD be prefixed with an "X" until they are /// added to a revision of this protocol. Client implementations