From 247e36bb0f3a82fbab365af0ea4a138a90f7dce2 Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Tue, 13 Dec 2022 10:41:46 +0100 Subject: [PATCH] =?UTF-8?q?Add=20support=20for=20RFC=C2=A05819:=20LIST-STA?= =?UTF-8?q?TUS=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://tools.ietf.org/html/rfc5819 --- src/client.rs | 1 + src/extensions/list_status.rs | 197 ++++++++++++++++++++++++++++++++++ src/extensions/mod.rs | 1 + tests/imap_integration.rs | 12 +++ 4 files changed, 211 insertions(+) create mode 100644 src/extensions/list_status.rs diff --git a/src/client.rs b/src/client.rs index e06d09e..f629b3e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1113,6 +1113,7 @@ impl Session { /// - `UIDNEXT`: The next [`Uid`] of the mailbox. /// - `UIDVALIDITY`: The unique identifier validity value of the mailbox (see [`Uid`]). /// - `UNSEEN`: The number of messages which do not have [`Flag::Seen`] set. + /// - `HIGHESTMODSEQ`: The highest mod sequence of the mailbox, counting modifications made to it. /// /// `data_items` is a space-separated list enclosed in parentheses. pub fn status( diff --git a/src/extensions/list_status.rs b/src/extensions/list_status.rs new file mode 100644 index 0000000..3715288 --- /dev/null +++ b/src/extensions/list_status.rs @@ -0,0 +1,197 @@ +//! Adds support for the IMAP LIST-STATUS extension specificed in [RFC +//! 5819](https://tools.ietf.org/html/rfc5819). + +use crate::client::{validate_str, Session}; +use crate::error::{Error, ParseError, Result}; +use crate::parse::try_handle_unilateral; +use crate::types::{Mailbox, Name, UnsolicitedResponse}; +use imap_proto::types::{MailboxDatum, Response, StatusAttribute}; +use ouroboros::self_referencing; +use std::io::{Read, Write}; +use std::slice::Iter; +use std::sync::mpsc; + +/// A wrapper for one or more [`Name`] responses paired with optional [`Mailbox`] responses. +/// +/// This structure represents responses to a LIST-STATUS command, as implemented in +/// [`Session::list_status`]. See [RFC 5819, section 2](https://tools.ietf.org/html/rfc5819.html#section-2). +#[self_referencing] +pub struct ExtendedNames { + data: Vec, + #[borrows(data)] + #[covariant] + pub(crate) extended_names: Vec<(Name<'this>, Option)>, +} + +impl ExtendedNames { + /// Parse one or more LIST-STATUS responses from a response buffer + pub(crate) fn parse( + owned: Vec, + unsolicited: &mut mpsc::Sender, + ) -> core::result::Result { + ExtendedNamesTryBuilder { + data: owned, + extended_names_builder: |input| { + let mut lines: &[u8] = &input; + let mut names = Vec::new(); + let mut current_name: Option> = None; + let mut current_mailbox: Option = None; + + loop { + if lines.is_empty() { + if let Some(cur_name) = current_name { + names.push((cur_name, current_mailbox)); + } + break; + } + + match imap_proto::parser::parse_response(lines) { + Ok(( + rest, + Response::MailboxData(MailboxDatum::List { + name_attributes, + delimiter, + name, + }), + )) => { + lines = rest; + if let Some(cur_name) = current_name { + names.push((cur_name, current_mailbox)); + current_mailbox = None; + } + current_name = Some(Name { + attributes: name_attributes, + delimiter, + name, + }); + } + Ok(( + rest, + Response::MailboxData(MailboxDatum::Status { mailbox: _, status }), + )) => { + lines = rest; + let mut mb = Mailbox::default(); + for attr in status { + match attr { + StatusAttribute::HighestModSeq(v) => { + mb.highest_mod_seq = Some(v) + } + StatusAttribute::Messages(v) => mb.exists = v, + StatusAttribute::Recent(v) => mb.recent = v, + StatusAttribute::UidNext(v) => mb.uid_next = Some(v), + StatusAttribute::UidValidity(v) => mb.uid_validity = Some(v), + StatusAttribute::Unseen(v) => mb.unseen = Some(v), + _ => {} // needed because StatusAttribute is #[non_exhaustive] + } + } + current_mailbox = Some(mb); + } + Ok((rest, resp)) => { + lines = rest; + if let Some(unhandled) = try_handle_unilateral(resp, unsolicited) { + return Err(unhandled.into()); + } + } + Err(_) => { + return Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } + } + + Ok(names) + }, + } + .try_build() + } + + /// Iterate over the contained elements + pub fn iter(&self) -> Iter<'_, (Name<'_>, Option)> { + self.borrow_extended_names().iter() + } + + /// Get the number of elements in this container. + pub fn len(&self) -> usize { + self.borrow_extended_names().len() + } + + /// Return true of there are no elements in the container. + pub fn is_empty(&self) -> bool { + self.borrow_extended_names().is_empty() + } + + /// Get the element at the given index + pub fn get(&self, index: usize) -> Option<&(Name<'_>, Option)> { + self.borrow_extended_names().get(index) + } +} + +impl Session { + /// The [extended `LIST` command](https://tools.ietf.org/html/rfc5819.html#section-2) returns + /// a subset of names from the complete set of all names available to the client. Each name + /// _should_ be paired with a STATUS response, though the server _may_ drop it if it encounters + /// problems looking up the required information. + /// + /// This version of the command is also often referred to as `LIST-STATUS` command, as that is + /// the name of the extension and it is a combination of the two. + /// + /// The `reference_name` and `mailbox_pattern` arguments have the same semantics as they do in + /// [`Session::list`]. + /// + /// The `data_items` argument has the same semantics as it does in [`Session::status`]. + pub fn list_status( + &mut self, + reference_name: Option<&str>, + mailbox_pattern: Option<&str>, + data_items: &str, + ) -> Result { + let reference = validate_str("LIST-STATUS", "reference", reference_name.unwrap_or(""))?; + self.run_command_and_read_response(&format!( + "LIST {} {} RETURN (STATUS {})", + &reference, + mailbox_pattern.unwrap_or("\"\""), + data_items + )) + .and_then(|lines| ExtendedNames::parse(lines, &mut self.unsolicited_responses_tx)) + } +} + +#[cfg(test)] +mod tests { + use imap_proto::NameAttribute; + + use super::*; + + #[test] + fn parse_list_status_test() { + let lines = b"\ + * LIST () \".\" foo\r\n\ + * STATUS foo (HIGHESTMODSEQ 122)\r\n\ + * LIST () \".\" foo.bar\r\n\ + * STATUS foo.bar (HIGHESTMODSEQ 132)\r\n\ + * LIST (\\UnMarked) \".\" feeds\r\n\ + * LIST () \".\" feeds.test\r\n\ + * STATUS feeds.test (HIGHESTMODSEQ 757)\r\n"; + let (mut send, recv) = mpsc::channel(); + let fetches = ExtendedNames::parse(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); + assert!(!fetches.is_empty()); + assert_eq!(fetches.len(), 4); + let (name, status) = fetches.get(0).unwrap(); + assert_eq!(&name.name, "foo"); + assert!(status.is_some()); + assert_eq!(status.as_ref().unwrap().highest_mod_seq, Some(122)); + let (name, status) = fetches.get(1).unwrap(); + assert_eq!(&name.name, "foo.bar"); + assert!(status.is_some()); + assert_eq!(status.as_ref().unwrap().highest_mod_seq, Some(132)); + let (name, status) = fetches.get(2).unwrap(); + assert_eq!(&name.name, "feeds"); + assert_eq!(name.attributes.len(), 1); + assert_eq!(name.attributes.get(0).unwrap(), &NameAttribute::Unmarked); + assert!(status.is_none()); + let (name, status) = fetches.get(3).unwrap(); + assert_eq!(&name.name, "feeds.test"); + assert!(status.is_some()); + assert_eq!(status.as_ref().unwrap().highest_mod_seq, Some(757)); + } +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index d2f548c..fd3a91e 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -1,4 +1,5 @@ //! Implementations of various IMAP extensions. pub mod idle; +pub mod list_status; pub mod metadata; pub mod sort; diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index 5394d38..65e5eaf 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -383,6 +383,18 @@ fn list() { // TODO: make a subdir } +#[test] +#[cfg(feature = "test-full-imap")] +fn list_status() { + let mut s = session("readonly-test@localhost"); + let extnames = s.list_status(None, Some("*"), "(HIGHESTMODSEQ)").unwrap(); + assert_eq!(extnames.len(), 1); + let (name, status) = extnames.get(0).unwrap(); + assert_eq!(name.name(), "INBOX"); + assert!(status.is_some()); + assert!(status.as_ref().unwrap().highest_mod_seq.is_some()); +} + #[test] fn append() { let to = "inbox-append1@localhost";