diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs index 8d9f9916..4d8c9a6b 100644 --- a/imap-codec/src/codec/encode.rs +++ b/imap-codec/src/codec/encode.rs @@ -45,6 +45,8 @@ //! C: Pa²²W0rD //! ``` +#[cfg(feature = "ext_condstore_qresync")] +use std::num::NonZeroU64; use std::{borrow::Borrow, io::Write, num::NonZeroU32}; use base64::{engine::general_purpose::STANDARD as base64, Engine}; @@ -1036,6 +1038,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 +1180,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..f14af58b 100644 --- a/imap-codec/src/core.rs +++ b/imap-codec/src/core.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "ext_condstore_qresync")] +use std::num::NonZeroU64; use std::{borrow::Cow, num::NonZeroU32, str::from_utf8}; #[cfg(not(feature = "quirk_crlf_relaxed"))] @@ -65,6 +67,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..c7382397 100644 --- a/imap-codec/src/response.rs +++ b/imap-codec/src/response.rs @@ -21,6 +21,8 @@ use nom::{ sequence::{delimited, preceded, terminated, tuple}, }; +#[cfg(feature = "ext_condstore_qresync")] +use crate::core::mod_sequence_value; #[cfg(feature = "ext_id")] use crate::extensions::id::id_response; use crate::{ @@ -124,7 +126,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 +182,22 @@ 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 +722,15 @@ 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..19ad03da 100644 --- a/imap-types/src/response.rs +++ b/imap-types/src/response.rs @@ -1,5 +1,7 @@ //! # 7. Server Responses +#[cfg(feature = "ext_condstore_qresync")] +use std::num::NonZeroU64; use std::{ borrow::Cow, fmt::{Debug, Display, Formatter}, @@ -832,6 +834,34 @@ 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