diff --git a/Cargo.toml b/Cargo.toml index c64a903..a30a60f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ nom = { version = "6.0", default-features = false } base64 = "0.13" chrono = { version = "0.4", default-features = false, features = ["std"]} lazy_static = "1.4" +ouroboros = "0.9.5" [dev-dependencies] lettre = "0.9" diff --git a/src/client.rs b/src/client.rs index 2b1400e..bfb6c8b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -717,9 +717,9 @@ impl Session { /// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a /// listing of capabilities that the server supports. The server will include "IMAP4rev1" as /// one of the listed capabilities. See [`Capabilities`] for further details. - pub fn capabilities(&mut self) -> ZeroCopyResult { + pub fn capabilities(&mut self) -> Result { self.run_command_and_read_response("CAPABILITY") - .and_then(|lines| parse_capabilities(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Capabilities::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently diff --git a/src/parse.rs b/src/parse.rs index fc56674..dee40f2 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -169,38 +169,6 @@ pub fn parse_expunge( } } -pub fn parse_capabilities( - lines: Vec, - unsolicited: &mut mpsc::Sender, -) -> ZeroCopyResult { - let f = |mut lines| { - let mut caps = HashSet::new(); - loop { - match imap_proto::parser::parse_response(lines) { - Ok((rest, Response::Capabilities(c))) => { - lines = rest; - caps.extend(c); - } - Ok((rest, data)) => { - lines = rest; - if let Some(resp) = try_handle_unilateral(data, unsolicited) { - break Err(resp.into()); - } - } - _ => { - break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); - } - } - - if lines.is_empty() { - break Ok(Capabilities(caps)); - } - } - }; - - unsafe { ZeroCopy::make(lines, f) } -} - pub fn parse_noop( lines: Vec, unsolicited: &mut mpsc::Sender, @@ -456,7 +424,7 @@ mod tests { ]; let lines = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; let (mut send, recv) = mpsc::channel(); - let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); + let capabilities = Capabilities::parse(lines.to_vec(), &mut send).unwrap(); // shouldn't be any unexpected responses parsed assert!(recv.try_recv().is_err()); assert_eq!(capabilities.len(), 4); @@ -474,7 +442,7 @@ mod tests { ]; let lines = b"* CAPABILITY IMAP4REV1 STARTTLS\r\n"; let (mut send, recv) = mpsc::channel(); - let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); + let capabilities = Capabilities::parse(lines.to_vec(), &mut send).unwrap(); // shouldn't be any unexpected responses parsed assert!(recv.try_recv().is_err()); assert_eq!(capabilities.len(), 2); @@ -488,7 +456,7 @@ mod tests { fn parse_capability_invalid_test() { let (mut send, recv) = mpsc::channel(); let lines = b"* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; - parse_capabilities(lines.to_vec(), &mut send).unwrap(); + Capabilities::parse(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); } @@ -605,7 +573,7 @@ mod tests { * STATUS dev.github (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n\ * 4 EXISTS\r\n"; let (mut send, recv) = mpsc::channel(); - let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); + let capabilities = Capabilities::parse(lines.to_vec(), &mut send).unwrap(); assert_eq!(capabilities.len(), 4); for e in expected_capabilities { diff --git a/src/types/capabilities.rs b/src/types/capabilities.rs index 13238d9..df723fb 100644 --- a/src/types/capabilities.rs +++ b/src/types/capabilities.rs @@ -1,6 +1,11 @@ -use imap_proto::types::Capability; +use crate::error::{Error, ParseError}; +use crate::parse::try_handle_unilateral; +use crate::types::UnsolicitedResponse; +use imap_proto::{Capability, Response}; +use ouroboros::self_referencing; use std::collections::hash_set::Iter; use std::collections::HashSet; +use std::sync::mpsc; const IMAP4REV1_CAPABILITY: &str = "IMAP4rev1"; const AUTH_CAPABILITY_PREFIX: &str = "AUTH="; @@ -30,16 +35,54 @@ const AUTH_CAPABILITY_PREFIX: &str = "AUTH="; /// /// Client implementations SHOULD NOT require any capability name other than `IMAP4rev1`, and MUST /// ignore any unknown capability names. -pub struct Capabilities( - // Note that this field isn't *actually* 'static. - // Rather, it is tied to the lifetime of the `ZeroCopy` that contains this `Name`. - pub(crate) HashSet>, -); +#[self_referencing] +pub struct Capabilities { + data: Vec, + #[borrows(data)] + #[covariant] + pub(crate) capabilities: HashSet>, +} impl Capabilities { + /// Parse the given input into one or more [`Capabilitity`] responses. + pub fn parse( + owned: Vec, + unsolicited: &mut mpsc::Sender, + ) -> Result { + CapabilitiesTryBuilder { + data: owned, + capabilities_builder: |input| { + let mut lines = input; + let mut caps = HashSet::new(); + loop { + match imap_proto::parser::parse_response(lines) { + Ok((rest, Response::Capabilities(c))) => { + lines = rest; + caps.extend(c); + } + Ok((rest, data)) => { + lines = rest; + if let Some(resp) = try_handle_unilateral(data, unsolicited) { + break Err(resp.into()); + } + } + _ => { + break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } + + if lines.is_empty() { + break Ok(caps); + } + } + }, + } + .try_build() + } + /// Check if the server has the given capability. pub fn has<'a>(&self, cap: &Capability<'a>) -> bool { - self.0.contains(cap) + self.borrow_capabilities().contains(cap) } /// Check if the server has the given capability via str. @@ -59,16 +102,16 @@ impl Capabilities { /// Iterate over all the server's capabilities pub fn iter(&self) -> Iter<'_, Capability<'_>> { - self.0.iter() + self.borrow_capabilities().iter() } /// Returns how many capabilities the server has. pub fn len(&self) -> usize { - self.0.len() + self.borrow_capabilities().len() } /// Returns true if the server purports to have no capabilities. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.borrow_capabilities().is_empty() } }