diff --git a/imap-codec/Cargo.toml b/imap-codec/Cargo.toml index aef25190..e9b7f06d 100644 --- a/imap-codec/Cargo.toml +++ b/imap-codec/Cargo.toml @@ -78,6 +78,7 @@ 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"] +ext_status_size = ["imap-types/ext_status_size"] ext_utf8 = ["imap-types/ext_utf8"] # diff --git a/imap-codec/fuzz/Cargo.toml b/imap-codec/fuzz/Cargo.toml index f82d1719..a2881d49 100644 --- a/imap-codec/fuzz/Cargo.toml +++ b/imap-codec/fuzz/Cargo.toml @@ -25,6 +25,7 @@ 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"] +ext_status_size = ["imap-codec/ext_status_size"] ext_utf8 = ["imap-codec/ext_utf8"] # IMAP quirks @@ -41,6 +42,7 @@ ext = [ #"ext_mailbox_referrals", "ext_metadata", "ext_namespace", + "ext_status_size", "ext_utf8", ] # Enable `Debug`-printing during parsing. This is useful to analyze crashes. diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs index edbaddb5..9e57c914 100644 --- a/imap-codec/src/codec/encode.rs +++ b/imap-codec/src/codec/encode.rs @@ -859,6 +859,8 @@ 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_status_size")] + Self::Size => ctx.write_all(b"SIZE"), #[cfg(feature = "ext_condstore_qresync")] Self::HighestModSeq => ctx.write_all(b"HIGHESTMODSEQ"), } @@ -1700,6 +1702,11 @@ impl EncodeIntoContext for StatusDataItem { ctx.write_all(b"DELETED-STORAGE ")?; count.encode_ctx(ctx) } + #[cfg(feature = "ext_status_size")] + Self::Size(size) => { + ctx.write_all(b"SIZE ")?; + size.encode_ctx(ctx) + } #[cfg(feature = "ext_condstore_qresync")] Self::HighestModSeq(value) => { ctx.write_all(b"HIGHESTMODSEQ ")?; diff --git a/imap-codec/src/response.rs b/imap-codec/src/response.rs index 0dd5e8a1..898a15e7 100644 --- a/imap-codec/src/response.rs +++ b/imap-codec/src/response.rs @@ -510,6 +510,14 @@ mod tests { b"".as_ref(), Response::Data(Data::Capability(Vec1::from(Capability::Imap4Rev1))), ), + #[cfg(feature = "ext_status_size")] + ( + b"* CAPABILITY IMAP4REV1 STATUS=SIZE\r\n".as_ref(), + b"".as_ref(), + Response::Data( + Data::capability(vec![Capability::Imap4Rev1, Capability::StatusSize]).unwrap(), + ), + ), ( b"* LIST (\\Noselect) \"/\" bbb\r\n", b"", diff --git a/imap-codec/src/status.rs b/imap-codec/src/status.rs index 75a659e7..70ba448c 100644 --- a/imap-codec/src/status.rs +++ b/imap-codec/src/status.rs @@ -32,6 +32,8 @@ pub(crate) fn status_att(input: &[u8]) -> IMAPResult<&[u8], StatusDataItemName> tag_no_case(b"DELETED-STORAGE"), ), value(StatusDataItemName::Deleted, tag_no_case(b"DELETED")), + #[cfg(feature = "ext_status_size")] + value(StatusDataItemName::Size, tag_no_case(b"SIZE")), #[cfg(feature = "ext_condstore_qresync")] value( StatusDataItemName::HighestModSeq, @@ -87,6 +89,11 @@ fn status_att_val(input: &[u8]) -> IMAPResult<&[u8], StatusDataItem> { preceded(tag_no_case(b"DELETED "), number), StatusDataItem::Deleted, ), + #[cfg(feature = "ext_status_size")] + map( + preceded(tag_no_case(b"SIZE "), number64), + StatusDataItem::Size, + ), #[cfg(feature = "ext_condstore_qresync")] map( preceded(tag_no_case(b"HIGHESTMODSEQ "), mod_sequence_valzer), @@ -99,8 +106,51 @@ fn status_att_val(input: &[u8]) -> IMAPResult<&[u8], StatusDataItem> { mod tests { use std::num::NonZeroU32; + #[cfg(feature = "ext_status_size")] + use imap_types::{ + command::CommandBody, + mailbox::Mailbox, + response::{Data, Response}, + }; + use super::*; use crate::testing::known_answer_test_encode; + #[cfg(feature = "ext_status_size")] + use crate::testing::{kat_inverse_command, kat_inverse_response}; + + #[cfg(feature = "ext_status_size")] + #[test] + fn test_kat_inverse_status_size() { + kat_inverse_command(&[( + b"A01 STATUS frop (MESSAGES SIZE UIDNEXT)\r\n", + b"", + CommandBody::status( + "frop", + vec![ + StatusDataItemName::Messages, + StatusDataItemName::Size, + StatusDataItemName::UidNext, + ], + ) + .unwrap() + .tag("A01") + .unwrap(), + )]); + + kat_inverse_response(&[( + b"* STATUS frop (MESSAGES 8 SIZE 44421 UIDNEXT 242344)\r\n", + b"", + Response::Data(Data::Status { + mailbox: Mailbox::try_from("frop").unwrap(), + items: vec![ + StatusDataItem::Messages(8), + StatusDataItem::Size(44421), + StatusDataItem::UidNext(NonZeroU32::new(242344).unwrap()), + ] + .into(), + }), + )]); + } #[test] fn test_encode_status_data_item_name() { @@ -112,6 +162,8 @@ mod tests { (StatusDataItemName::Unseen, b"UNSEEN"), (StatusDataItemName::Deleted, b"DELETED"), (StatusDataItemName::DeletedStorage, b"DELETED-STORAGE"), + #[cfg(feature = "ext_status_size")] + (StatusDataItemName::Size, b"SIZE"), ]; for test in tests { @@ -138,6 +190,8 @@ mod tests { StatusDataItem::DeletedStorage(u64::MAX), b"DELETED-STORAGE 18446744073709551615", ), + #[cfg(feature = "ext_status_size")] + (StatusDataItem::Size(u64::MAX), b"SIZE 18446744073709551615"), ]; for test in tests { diff --git a/imap-types/Cargo.toml b/imap-types/Cargo.toml index 1c3136ba..c8ce2c3c 100644 --- a/imap-types/Cargo.toml +++ b/imap-types/Cargo.toml @@ -27,6 +27,7 @@ ext_login_referrals = [] ext_mailbox_referrals = [] ext_metadata = [] ext_namespace = [] +ext_status_size = [] ext_utf8 = [] [dependencies] diff --git a/imap-types/fuzz/Cargo.toml b/imap-types/fuzz/Cargo.toml index 1c79fedd..20e0a67d 100644 --- a/imap-types/fuzz/Cargo.toml +++ b/imap-types/fuzz/Cargo.toml @@ -21,6 +21,7 @@ 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"] +ext_status_size = ["imap-types/ext_status_size"] ext_utf8 = ["imap-types/ext_utf8"] # @@ -33,6 +34,7 @@ ext = [ #"ext_mailbox_referrals", "ext_metadata", "ext_namespace", + "ext_status_size", "ext_utf8", ] # Enable `Debug`-printing during parsing. This is useful to analyze crashes. diff --git a/imap-types/src/response.rs b/imap-types/src/response.rs index 3fb43582..c71ce498 100644 --- a/imap-types/src/response.rs +++ b/imap-types/src/response.rs @@ -1106,6 +1106,9 @@ pub enum Capability<'a> { /// NAMESPACE extension (RFC 2342) #[cfg(feature = "ext_namespace")] Namespace, + /// STATUS=SIZE extension (RFC 8438) + #[cfg(feature = "ext_status_size")] + StatusSize, #[cfg(feature = "ext_utf8")] Utf8(Utf8Kind), /// Other/Unknown @@ -1153,6 +1156,8 @@ impl Display for Capability<'_> { Self::QResync => write!(f, "QRESYNC"), #[cfg(feature = "ext_namespace")] Self::Namespace => write!(f, "NAMESPACE"), + #[cfg(feature = "ext_status_size")] + Self::StatusSize => write!(f, "STATUS=SIZE"), #[cfg(feature = "ext_utf8")] Self::Utf8(kind) => write!(f, "UTF8={kind}"), Self::Other(other) => write!(f, "{}", other.0), @@ -1223,6 +1228,8 @@ impl<'a> From> for Capability<'a> { "condstore" => Self::CondStore, #[cfg(feature = "ext_condstore_qresync")] "qresync" => Self::QResync, + #[cfg(feature = "ext_status_size")] + "status=size" => Self::StatusSize, "uidplus" => Self::UidPlus, #[cfg(feature = "ext_utf8")] "utf8=accept" => Self::Utf8(Utf8Kind::Accept), @@ -1321,6 +1328,16 @@ mod tests { assert_eq!(Capability::ListExtended.to_string(), "LIST-EXTENDED"); } + #[cfg(feature = "ext_status_size")] + #[test] + fn test_capability_status_size() { + assert_eq!( + Capability::from(Atom::try_from("status=size").unwrap()), + Capability::StatusSize, + ); + assert_eq!(Capability::StatusSize.to_string(), "STATUS=SIZE"); + } + #[test] fn test_conversion_continue_failing() { let tests = [ diff --git a/imap-types/src/status.rs b/imap-types/src/status.rs index 0408bc94..5af6064b 100644 --- a/imap-types/src/status.rs +++ b/imap-types/src/status.rs @@ -33,6 +33,10 @@ pub enum StatusDataItemName { /// The amount of storage space that can be reclaimed by performing EXPUNGE on the mailbox. DeletedStorage, + #[cfg(feature = "ext_status_size")] + /// The total size of the mailbox in octets. + Size, + #[cfg(feature = "ext_condstore_qresync")] HighestModSeq, } @@ -67,6 +71,11 @@ pub enum StatusDataItem { /// The amount of storage space that can be reclaimed by performing EXPUNGE on the mailbox. DeletedStorage(u64), + #[cfg(feature = "ext_status_size")] + /// The total size of the mailbox in octets. Refer to RFC 8438 and RFC 9051, + /// Section 6.3.11 for more information. + Size(u64), + #[cfg(feature = "ext_condstore_qresync")] /// The highest mod-sequence value of all messages in the mailbox. /// This is the same value that is returned by the server in the HIGHESTMODSEQ response code in diff --git a/justfile b/justfile index 1054f849..b2f6a282 100644 --- a/justfile +++ b/justfile @@ -65,6 +65,7 @@ cargo_hack mode: install_cargo_hack ext_mailbox_referrals,\ ext_metadata,\ ext_namespace,\ + ext_status_size,\ ext_utf8 \ --group-features \ quirk_crlf_relaxed,\ @@ -96,6 +97,7 @@ cargo_hack mode: install_cargo_hack ext_mailbox_referrals,\ ext_metadata,\ ext_namespace,\ + ext_status_size,\ ext_utf8\ {{ mode }}