diff --git a/Cargo.toml b/Cargo.toml index c64a903..caa9fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "3.0.0-alpha.4" +version = "3.0.0-alpha.5" authors = ["Jon Gjengset ", "Matt McCoy "] documentation = "https://docs.rs/imap/" @@ -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/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs index a323f13..62df292 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -42,7 +42,7 @@ fn main() { match imap_session.fetch("2", "body[text]") { Ok(msgs) => { - for msg in &msgs { + for msg in msgs.iter() { print!("{:?}", msg); } } diff --git a/src/client.rs b/src/client.rs index c88f048..78d1a2f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -565,39 +565,39 @@ impl Session { /// - `RFC822.HEADER`: Functionally equivalent to `BODY.PEEK[HEADER]`. /// - `RFC822.SIZE`: The [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message. /// - `UID`: The unique identifier for the message. - pub fn fetch(&mut self, sequence_set: S1, query: S2) -> ZeroCopyResult> + pub fn fetch(&mut self, sequence_set: S1, query: S2) -> Result where S1: AsRef, S2: AsRef, { if sequence_set.as_ref().is_empty() { - parse_fetches(vec![], &mut self.unsolicited_responses_tx) + Fetches::parse(vec![], &mut self.unsolicited_responses_tx) } else { self.run_command_and_read_response(&format!( "FETCH {} {}", validate_sequence_set(sequence_set.as_ref())?, validate_str_noquote(query.as_ref())? )) - .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } } /// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8). - pub fn uid_fetch(&mut self, uid_set: S1, query: S2) -> ZeroCopyResult> + pub fn uid_fetch(&mut self, uid_set: S1, query: S2) -> Result where S1: AsRef, S2: AsRef, { if uid_set.as_ref().is_empty() { - parse_fetches(vec![], &mut self.unsolicited_responses_tx) + Fetches::parse(vec![], &mut self.unsolicited_responses_tx) } else { self.run_command_and_read_response(&format!( "UID FETCH {} {}", validate_sequence_set(uid_set.as_ref())?, validate_str_noquote(query.as_ref())? )) - .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } } @@ -728,9 +728,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 @@ -845,7 +845,7 @@ impl Session { /// Ok(()) /// } /// ``` - pub fn store(&mut self, sequence_set: S1, query: S2) -> ZeroCopyResult> + pub fn store(&mut self, sequence_set: S1, query: S2) -> Result where S1: AsRef, S2: AsRef, @@ -855,12 +855,12 @@ impl Session { sequence_set.as_ref(), query.as_ref() )) - .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } /// Equivalent to [`Session::store`], except that all identifiers in `sequence_set` are /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8). - pub fn uid_store(&mut self, uid_set: S1, query: S2) -> ZeroCopyResult> + pub fn uid_store(&mut self, uid_set: S1, query: S2) -> Result where S1: AsRef, S2: AsRef, @@ -870,7 +870,7 @@ impl Session { uid_set.as_ref(), query.as_ref() )) - .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Fetches::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`COPY` command](https://tools.ietf.org/html/rfc3501#section-6.4.7) copies the @@ -999,13 +999,13 @@ impl Session { &mut self, reference_name: Option<&str>, mailbox_pattern: Option<&str>, - ) -> ZeroCopyResult> { + ) -> Result { self.run_command_and_read_response(&format!( "LIST {} {}", quote!(reference_name.unwrap_or("")), mailbox_pattern.unwrap_or("\"\"") )) - .and_then(|lines| parse_names(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Names::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`LSUB` command](https://tools.ietf.org/html/rfc3501#section-6.3.9) returns a subset of @@ -1027,13 +1027,13 @@ impl Session { &mut self, reference_name: Option<&str>, mailbox_pattern: Option<&str>, - ) -> ZeroCopyResult> { + ) -> Result { self.run_command_and_read_response(&format!( "LSUB {} {}", quote!(reference_name.unwrap_or("")), mailbox_pattern.unwrap_or("") )) - .and_then(|lines| parse_names(lines, &mut self.unsolicited_responses_tx)) + .and_then(|lines| Names::parse(lines, &mut self.unsolicited_responses_tx)) } /// The [`STATUS` command](https://tools.ietf.org/html/rfc3501#section-6.3.10) requests the diff --git a/src/parse.rs b/src/parse.rs index fc56674..0df2a0a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,6 +3,7 @@ use lazy_static::lazy_static; use regex::Regex; use std::collections::HashSet; use std::convert::TryFrom; +use std::iter::Extend; use std::sync::mpsc; use super::error::{Error, ParseError, Result}; @@ -23,105 +24,51 @@ pub fn parse_authenticate_response(line: &str) -> Result<&str> { ))) } -enum MapOrNot { +pub(crate) enum MapOrNot<'a, T> { Map(T), - Not(Response<'static>), + MapVec(Vec), + Not(Response<'a>), #[allow(dead_code)] Ignore, } -unsafe fn parse_many( - lines: Vec, +/// Parse many `T` Responses with `F` and extend `into` with them. +/// Responses other than `T` go into the `unsolicited` channel. +pub(crate) fn parse_many_into<'input, T, F>( + input: &'input [u8], + into: &mut impl Extend, + unsolicited: &mut mpsc::Sender, mut map: F, - unsolicited: &mut mpsc::Sender, -) -> ZeroCopyResult> +) -> Result<()> where - F: FnMut(Response<'static>) -> Result>, + F: FnMut(Response<'input>) -> Result>, { - let f = |mut lines: &'static [u8]| { - let mut things = Vec::new(); - loop { - if lines.is_empty() { - break Ok(things); + let mut lines = input; + loop { + if lines.is_empty() { + break Ok(()); + } + + match imap_proto::parser::parse_response(lines) { + Ok((rest, resp)) => { + lines = rest; + + match map(resp)? { + MapOrNot::Map(t) => into.extend(std::iter::once(t)), + MapOrNot::MapVec(t) => into.extend(t), + MapOrNot::Not(resp) => match try_handle_unilateral(resp, unsolicited) { + Some(Response::Fetch(..)) => continue, + Some(resp) => break Err(resp.into()), + None => {} + }, + MapOrNot::Ignore => continue, + } } - - match imap_proto::parser::parse_response(lines) { - Ok((rest, resp)) => { - lines = rest; - - match map(resp)? { - MapOrNot::Map(t) => things.push(t), - MapOrNot::Not(resp) => match try_handle_unilateral(resp, unsolicited) { - Some(Response::Fetch(..)) => continue, - Some(resp) => break Err(resp.into()), - None => {} - }, - MapOrNot::Ignore => continue, - } - } - _ => { - break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); - } + _ => { + break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); } } - }; - - ZeroCopy::make(lines, f) -} - -pub fn parse_names( - lines: Vec, - unsolicited: &mut mpsc::Sender, -) -> ZeroCopyResult> { - let f = |resp| match resp { - // https://github.com/djc/imap-proto/issues/4 - Response::MailboxData(MailboxDatum::List { - flags, - delimiter, - name, - }) => Ok(MapOrNot::Map(Name { - attributes: flags.into_iter().map(NameAttribute::from).collect(), - delimiter, - name, - })), - resp => Ok(MapOrNot::Not(resp)), - }; - - unsafe { parse_many(lines, f, unsolicited) } -} - -pub fn parse_fetches( - lines: Vec, - unsolicited: &mut mpsc::Sender, -) -> ZeroCopyResult> { - let f = |resp| match resp { - Response::Fetch(num, attrs) => { - let mut fetch = Fetch { - message: num, - flags: vec![], - uid: None, - size: None, - fetch: attrs, - }; - - // set some common fields eaglery - for attr in &fetch.fetch { - match attr { - AttributeValue::Flags(flags) => { - fetch.flags.extend(Flag::from_strs(flags)); - } - AttributeValue::Uid(uid) => fetch.uid = Some(*uid), - AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), - _ => {} - } - } - - Ok(MapOrNot::Map(fetch)) - } - resp => Ok(MapOrNot::Not(resp)), - }; - - unsafe { parse_many(lines, f, unsolicited) } + } } pub fn parse_expunge( @@ -169,38 +116,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 +371,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 +389,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 +403,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()); } @@ -496,22 +411,23 @@ mod tests { fn parse_names_test() { let lines = b"* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"; let (mut send, recv) = mpsc::channel(); - let names = parse_names(lines.to_vec(), &mut send).unwrap(); + let names = Names::parse(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); assert_eq!(names.len(), 1); + let first = names.get(0).unwrap(); assert_eq!( - names[0].attributes(), + first.attributes(), &[NameAttribute::from("\\HasNoChildren")] ); - assert_eq!(names[0].delimiter(), Some(".")); - assert_eq!(names[0].name(), "INBOX"); + assert_eq!(first.delimiter(), Some(".")); + assert_eq!(first.name(), "INBOX"); } #[test] fn parse_fetches_empty() { let lines = b""; let (mut send, recv) = mpsc::channel(); - let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + let fetches = Fetches::parse(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); assert!(fetches.is_empty()); } @@ -522,19 +438,21 @@ mod tests { * 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n\ * 25 FETCH (FLAGS (\\Seen))\r\n"; let (mut send, recv) = mpsc::channel(); - let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + let fetches = Fetches::parse(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); assert_eq!(fetches.len(), 2); - assert_eq!(fetches[0].message, 24); - assert_eq!(fetches[0].flags(), &[Flag::Seen]); - assert_eq!(fetches[0].uid, Some(4827943)); - assert_eq!(fetches[0].body(), None); - assert_eq!(fetches[0].header(), None); - assert_eq!(fetches[1].message, 25); - assert_eq!(fetches[1].flags(), &[Flag::Seen]); - assert_eq!(fetches[1].uid, None); - assert_eq!(fetches[1].body(), None); - assert_eq!(fetches[1].header(), None); + let first = fetches.get(0).unwrap(); + assert_eq!(first.message, 24); + assert_eq!(first.flags(), &[Flag::Seen]); + assert_eq!(first.uid, Some(4827943)); + assert_eq!(first.body(), None); + assert_eq!(first.header(), None); + let second = fetches.get(1).unwrap(); + assert_eq!(second.message, 25); + assert_eq!(second.flags(), &[Flag::Seen]); + assert_eq!(second.uid, None); + assert_eq!(second.body(), None); + assert_eq!(second.header(), None); } #[test] @@ -544,11 +462,12 @@ mod tests { * 37 FETCH (UID 74)\r\n\ * 1 RECENT\r\n"; let (mut send, recv) = mpsc::channel(); - let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + let fetches = Fetches::parse(lines.to_vec(), &mut send).unwrap(); assert_eq!(recv.try_recv(), Ok(UnsolicitedResponse::Recent(1))); assert_eq!(fetches.len(), 1); - assert_eq!(fetches[0].message, 37); - assert_eq!(fetches[0].uid, Some(74)); + let first = fetches.get(0).unwrap(); + assert_eq!(first.message, 37); + assert_eq!(first.uid, Some(74)); } #[test] @@ -560,7 +479,7 @@ mod tests { * OK Searched 91% of the mailbox, ETA 0:01\r\n\ * 37 FETCH (UID 74)\r\n"; let (mut send, recv) = mpsc::channel(); - let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + let fetches = Fetches::parse(lines.to_vec(), &mut send).unwrap(); assert_eq!( recv.try_recv(), Ok(UnsolicitedResponse::Ok { @@ -569,8 +488,9 @@ mod tests { }) ); assert_eq!(fetches.len(), 1); - assert_eq!(fetches[0].message, 37); - assert_eq!(fetches[0].uid, Some(74)); + let first = fetches.get(0).unwrap(); + assert_eq!(first.message, 37); + assert_eq!(first.uid, Some(74)); } #[test] @@ -579,17 +499,18 @@ mod tests { * LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n\ * 4 EXPUNGE\r\n"; let (mut send, recv) = mpsc::channel(); - let names = parse_names(lines.to_vec(), &mut send).unwrap(); + let names = Names::parse(lines.to_vec(), &mut send).unwrap(); assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Expunge(4)); assert_eq!(names.len(), 1); + let first = names.get(0).unwrap(); assert_eq!( - names[0].attributes(), + first.attributes(), &[NameAttribute::from("\\HasNoChildren")] ); - assert_eq!(names[0].delimiter(), Some(".")); - assert_eq!(names[0].name(), "INBOX"); + assert_eq!(first.delimiter(), Some(".")); + assert_eq!(first.name(), "INBOX"); } #[test] @@ -605,7 +526,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 { @@ -720,7 +641,7 @@ mod tests { let lines = b"* VANISHED (EARLIER) 3:8,12,50:60\r\n\ * 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n"; - let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + let fetches = Fetches::parse(lines.to_vec(), &mut send).unwrap(); match recv.try_recv().unwrap() { UnsolicitedResponse::Vanished { earlier, uids } => { assert!(earlier); @@ -736,10 +657,11 @@ mod tests { } assert!(recv.try_recv().is_err()); assert_eq!(fetches.len(), 1); - assert_eq!(fetches[0].message, 49); - assert_eq!(fetches[0].flags(), &[Flag::Seen, Flag::Answered]); - assert_eq!(fetches[0].uid, Some(117)); - assert_eq!(fetches[0].body(), None); - assert_eq!(fetches[0].header(), None); + let first = fetches.get(0).unwrap(); + assert_eq!(first.message, 49); + assert_eq!(first.flags(), &[Flag::Seen, Flag::Answered]); + assert_eq!(first.uid, Some(117)); + assert_eq!(first.body(), None); + assert_eq!(first.header(), None); } } diff --git a/src/types/capabilities.rs b/src/types/capabilities.rs index 13238d9..563c75f 100644 --- a/src/types/capabilities.rs +++ b/src/types/capabilities.rs @@ -1,6 +1,11 @@ -use imap_proto::types::Capability; +use crate::error::Error; +use crate::parse::{parse_many_into, MapOrNot}; +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,37 @@ 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 caps = HashSet::new(); + parse_many_into(input, &mut caps, unsolicited, |response| match response { + Response::Capabilities(c) => Ok(MapOrNot::MapVec(c)), + resp => Ok(MapOrNot::Not(resp)), + })?; + 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 +85,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() } } diff --git a/src/types/fetch.rs b/src/types/fetch.rs index 809664f..8ea6370 100644 --- a/src/types/fetch.rs +++ b/src/types/fetch.rs @@ -1,17 +1,98 @@ use super::{Flag, Seq, Uid}; +use crate::error::Error; +use crate::parse::{parse_many_into, MapOrNot}; +use crate::types::UnsolicitedResponse; use chrono::{DateTime, FixedOffset}; -use imap_proto::types::{AttributeValue, BodyStructure, Envelope, MessageSection, SectionPath}; +use imap_proto::types::{ + AttributeValue, BodyStructure, Envelope, MessageSection, Response, SectionPath, +}; +use ouroboros::self_referencing; +use std::slice::Iter; +use std::sync::mpsc; /// Format of Date and Time as defined RFC3501. /// See `date-time` element in [Formal Syntax](https://tools.ietf.org/html/rfc3501#section-9) /// chapter of this RFC. const DATE_TIME_FORMAT: &str = "%d-%b-%Y %H:%M:%S %z"; +/// A wrapper for one or more [`Fetch`] responses. +#[self_referencing] +pub struct Fetches { + data: Vec, + #[borrows(data)] + #[covariant] + pub(crate) fetches: Vec>, +} + +impl Fetches { + /// Parse one or more [`Fetch`] responses from a response buffer. + pub fn parse( + owned: Vec, + unsolicited: &mut mpsc::Sender, + ) -> Result { + FetchesTryBuilder { + data: owned, + fetches_builder: |input| { + let mut fetches = Vec::new(); + parse_many_into(input, &mut fetches, unsolicited, |response| { + match response { + Response::Fetch(num, attrs) => { + let mut fetch = Fetch { + message: num, + flags: vec![], + uid: None, + size: None, + fetch: attrs, + }; + + // set some common fields eagerly + for attr in &fetch.fetch { + match attr { + AttributeValue::Flags(flags) => { + fetch.flags.extend(Flag::from_strs(flags)); + } + AttributeValue::Uid(uid) => fetch.uid = Some(*uid), + AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), + _ => {} + } + } + Ok(MapOrNot::Map(fetch)) + } + resp => Ok(MapOrNot::Not(resp)), + } + })?; + Ok(fetches) + }, + } + .try_build() + } + + /// Iterate over the contained [`Fetch`]es. + pub fn iter(&self) -> Iter<'_, Fetch<'_>> { + self.borrow_fetches().iter() + } + + /// Get the number of [`Fetch`]es in this container. + pub fn len(&self) -> usize { + self.borrow_fetches().len() + } + + /// Return true if there are no [`Fetch`]es in the container. + pub fn is_empty(&self) -> bool { + self.borrow_fetches().is_empty() + } + + /// Get the element at the given index + pub fn get(&self, index: usize) -> Option<&Fetch<'_>> { + self.borrow_fetches().get(index) + } +} + /// An IMAP [`FETCH` response](https://tools.ietf.org/html/rfc3501#section-7.4.2) that contains /// data about a particular message. This response occurs as the result of a `FETCH` or `STORE` /// command, as well as by unilateral server decision (e.g., flag updates). #[derive(Debug, Eq, PartialEq)] -pub struct Fetch { +pub struct Fetch<'a> { /// The ordinal number of this message in its containing mailbox. pub message: Seq, @@ -24,16 +105,13 @@ pub struct Fetch { /// Only present if `RFC822.SIZE` was specified in the query argument to `FETCH`. pub size: Option, - // Note that none of these fields are *actually* 'static. Rather, they are tied to the lifetime - // of the `ZeroCopy` that contains this `Name`. That's also why they can't be public -- we can - // only return them with a lifetime tied to self. - pub(crate) fetch: Vec>, + pub(crate) fetch: Vec>, pub(crate) flags: Vec>, } -impl Fetch { +impl<'a> Fetch<'a> { /// A list of flags that are set for this message. - pub fn flags(&self) -> &[Flag<'_>] { + pub fn flags(&self) -> &[Flag<'a>] { &self.flags[..] } @@ -134,10 +212,21 @@ impl Fetch { /// /// See [section 2.3.6 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.6) for /// details. - pub fn bodystructure<'a>(&self) -> Option<&BodyStructure<'a>> { + pub fn bodystructure(&self) -> Option<&BodyStructure<'a>> { self.fetch.iter().find_map(|av| match av { AttributeValue::BodyStructure(bs) => Some(bs), _ => None, }) } + + /// Get an owned copy of the [`Fetch`]. + pub fn into_owned(self) -> Fetch<'static> { + Fetch { + message: self.message, + uid: self.uid, + size: self.size, + fetch: self.fetch.into_iter().map(|av| av.into_owned()).collect(), + flags: self.flags.clone(), + } + } } diff --git a/src/types/flag.rs b/src/types/flag.rs new file mode 100644 index 0000000..a1bb9a9 --- /dev/null +++ b/src/types/flag.rs @@ -0,0 +1,124 @@ +use std::borrow::Cow; + +/// With the exception of [`Flag::Custom`], these flags are system flags that are pre-defined in +/// [RFC 3501 section 2.3.2](https://tools.ietf.org/html/rfc3501#section-2.3.2). All system flags +/// begin with `\` in the IMAP protocol. Certain system flags (`\Deleted` and `\Seen`) have +/// special semantics described elsewhere. +/// +/// A flag can be permanent or session-only on a per-flag basis. Permanent flags are those which +/// the client can add or remove from the message flags permanently; that is, concurrent and +/// subsequent sessions will see any change in permanent flags. Changes to session flags are valid +/// only in that session. +/// +/// > Note: The `\Recent` system flag is a special case of a session flag. `\Recent` can not be +/// > used as an argument in a `STORE` or `APPEND` command, and thus can not be changed at all. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[non_exhaustive] +pub enum Flag<'a> { + /// Message has been read + Seen, + + /// Message has been answered + Answered, + + /// Message is "flagged" for urgent/special attention + Flagged, + + /// Message is "deleted" for removal by later EXPUNGE + Deleted, + + /// Message has not completed composition (marked as a draft). + Draft, + + /// Message is "recently" arrived in this mailbox. This session is the first session to have + /// been notified about this message; if the session is read-write, subsequent sessions will + /// not see `\Recent` set for this message. This flag can not be altered by the client. + /// + /// If it is not possible to determine whether or not this session is the first session to be + /// notified about a message, then that message will generally be considered recent. + /// + /// If multiple connections have the same mailbox selected simultaneously, it is undefined + /// which of these connections will see newly-arrived messages with `\Recent` set and which + /// will see it without `\Recent` set. + Recent, + + /// The [`Mailbox::permanent_flags`] can include this special flag (`\*`), which indicates that + /// it is possible to create new keywords by attempting to store those flags in the mailbox. + MayCreate, + + /// A non-standard user- or server-defined flag. + Custom(Cow<'a, str>), +} + +impl Flag<'static> { + fn system(s: &str) -> Option { + match s { + "\\Seen" => Some(Flag::Seen), + "\\Answered" => Some(Flag::Answered), + "\\Flagged" => Some(Flag::Flagged), + "\\Deleted" => Some(Flag::Deleted), + "\\Draft" => Some(Flag::Draft), + "\\Recent" => Some(Flag::Recent), + "\\*" => Some(Flag::MayCreate), + _ => None, + } + } + + /// Helper function to transform Strings into owned Flags + pub fn from_strs( + v: impl IntoIterator, + ) -> impl Iterator> { + v.into_iter().map(|s| Flag::from(s.to_string())) + } +} + +impl<'a> Flag<'a> { + /// Get an owned version of the [`Flag`]. + pub fn into_owned(self) -> Flag<'static> { + match self { + Flag::Custom(cow) => Flag::Custom(Cow::Owned(cow.into_owned())), + Flag::Seen => Flag::Seen, + Flag::Answered => Flag::Answered, + Flag::Flagged => Flag::Flagged, + Flag::Deleted => Flag::Deleted, + Flag::Draft => Flag::Draft, + Flag::Recent => Flag::Recent, + Flag::MayCreate => Flag::MayCreate, + } + } +} + +impl<'a> std::fmt::Display for Flag<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Flag::Seen => write!(f, "\\Seen"), + Flag::Answered => write!(f, "\\Answered"), + Flag::Flagged => write!(f, "\\Flagged"), + Flag::Deleted => write!(f, "\\Deleted"), + Flag::Draft => write!(f, "\\Draft"), + Flag::Recent => write!(f, "\\Recent"), + Flag::MayCreate => write!(f, "\\*"), + Flag::Custom(ref s) => write!(f, "{}", s), + } + } +} + +impl<'a> From for Flag<'a> { + fn from(s: String) -> Self { + if let Some(f) = Flag::system(&s) { + f + } else { + Flag::Custom(Cow::Owned(s)) + } + } +} + +impl<'a> From<&'a str> for Flag<'a> { + fn from(s: &'a str) -> Self { + if let Some(f) = Flag::system(s) { + f + } else { + Flag::Custom(Cow::Borrowed(s)) + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index d175f43..ba04668 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,7 +1,5 @@ //! This module contains types used throughout the IMAP protocol. -use std::borrow::Cow; - /// From section [2.3.1.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1). /// /// A 32-bit value assigned to each message, which when used with the unique identifier validity @@ -105,121 +103,17 @@ pub type Uid = u32; /// messages which have greater UIDs. pub type Seq = u32; -/// With the exception of [`Flag::Custom`], these flags are system flags that are pre-defined in -/// [RFC 3501 section 2.3.2](https://tools.ietf.org/html/rfc3501#section-2.3.2). All system flags -/// begin with `\` in the IMAP protocol. Certain system flags (`\Deleted` and `\Seen`) have -/// special semantics described elsewhere. -/// -/// A flag can be permanent or session-only on a per-flag basis. Permanent flags are those which -/// the client can add or remove from the message flags permanently; that is, concurrent and -/// subsequent sessions will see any change in permanent flags. Changes to session flags are valid -/// only in that session. -/// -/// > Note: The `\Recent` system flag is a special case of a session flag. `\Recent` can not be -/// > used as an argument in a `STORE` or `APPEND` command, and thus can not be changed at all. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -#[non_exhaustive] -pub enum Flag<'a> { - /// Message has been read - Seen, +mod fetch; +pub use self::fetch::{Fetch, Fetches}; - /// Message has been answered - Answered, - - /// Message is "flagged" for urgent/special attention - Flagged, - - /// Message is "deleted" for removal by later EXPUNGE - Deleted, - - /// Message has not completed composition (marked as a draft). - Draft, - - /// Message is "recently" arrived in this mailbox. This session is the first session to have - /// been notified about this message; if the session is read-write, subsequent sessions will - /// not see `\Recent` set for this message. This flag can not be altered by the client. - /// - /// If it is not possible to determine whether or not this session is the first session to be - /// notified about a message, then that message will generally be considered recent. - /// - /// If multiple connections have the same mailbox selected simultaneously, it is undefined - /// which of these connections will see newly-arrived messages with `\Recent` set and which - /// will see it without `\Recent` set. - Recent, - - /// The [`Mailbox::permanent_flags`] can include this special flag (`\*`), which indicates that - /// it is possible to create new keywords by attempting to store those flags in the mailbox. - MayCreate, - - /// A non-standard user- or server-defined flag. - Custom(Cow<'a, str>), -} - -impl Flag<'static> { - fn system(s: &str) -> Option { - match s { - "\\Seen" => Some(Flag::Seen), - "\\Answered" => Some(Flag::Answered), - "\\Flagged" => Some(Flag::Flagged), - "\\Deleted" => Some(Flag::Deleted), - "\\Draft" => Some(Flag::Draft), - "\\Recent" => Some(Flag::Recent), - "\\*" => Some(Flag::MayCreate), - _ => None, - } - } - - /// Helper function to transform Strings into owned Flags - pub fn from_strs( - v: impl IntoIterator, - ) -> impl Iterator> { - v.into_iter().map(|s| Flag::from(s.to_string())) - } -} - -impl<'a> fmt::Display for Flag<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Flag::Seen => write!(f, "\\Seen"), - Flag::Answered => write!(f, "\\Answered"), - Flag::Flagged => write!(f, "\\Flagged"), - Flag::Deleted => write!(f, "\\Deleted"), - Flag::Draft => write!(f, "\\Draft"), - Flag::Recent => write!(f, "\\Recent"), - Flag::MayCreate => write!(f, "\\*"), - Flag::Custom(ref s) => write!(f, "{}", s), - } - } -} - -impl<'a> From for Flag<'a> { - fn from(s: String) -> Self { - if let Some(f) = Flag::system(&s) { - f - } else { - Flag::Custom(Cow::Owned(s)) - } - } -} - -impl<'a> From<&'a str> for Flag<'a> { - fn from(s: &'a str) -> Self { - if let Some(f) = Flag::system(s) { - f - } else { - Flag::Custom(Cow::Borrowed(s)) - } - } -} +mod flag; +pub use self::flag::Flag; mod mailbox; pub use self::mailbox::Mailbox; -mod fetch; -pub use self::fetch::Fetch; - mod name; -pub use self::name::{Name, NameAttribute}; +pub use self::name::{Name, NameAttribute, Names}; mod capabilities; pub use self::capabilities::Capabilities; @@ -229,129 +123,3 @@ pub use self::deleted::Deleted; mod unsolicited_response; pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse}; - -/// This type wraps an input stream and a type that was constructed by parsing that input stream, -/// which allows the parsed type to refer to data in the underlying stream instead of copying it. -/// -/// Any references given out by a `ZeroCopy` should never be used after the `ZeroCopy` is dropped. -pub struct ZeroCopy { - _owned: Box<[u8]>, - derived: D, -} - -impl ZeroCopy { - /// Derive a new `ZeroCopy` view of the byte data stored in `owned`. - /// - /// # Safety - /// - /// The `derive` callback will be passed a `&'static [u8]`. However, this reference is not, in - /// fact `'static`. Instead, it is only valid for as long as the `ZeroCopy` lives. Therefore, - /// it is *only* safe to call this function if *every* accessor on `D` returns either a type - /// that does not contain any borrows, *or* where the return type is bound to the lifetime of - /// `&self`. - /// - /// It is *not* safe for the error type `E` to borrow from the passed reference. - pub(crate) unsafe fn make(owned: Vec, derive: F) -> Result - where - F: FnOnce(&'static [u8]) -> Result, - { - use std::mem; - - // the memory pointed to by `owned` now has a stable address (on the heap). - // even if we move the `Box` (i.e., into `ZeroCopy`), a slice to it will remain valid. - let _owned = owned.into_boxed_slice(); - - // this is the unsafe part -- the implementor of `derive` must be aware that the reference - // they are passed is not *really* 'static, but rather the lifetime of `&self`. - let static_owned_ref: &'static [u8] = mem::transmute(&*_owned); - - Ok(ZeroCopy { - _owned, - derived: derive(static_owned_ref)?, - }) - } - - /// Take out the derived value of this `ZeroCopy`. - /// - /// Only safe if `D` contains no references into the underlying input stream (i.e., the `owned` - /// passed to `ZeroCopy::new`). - #[allow(dead_code)] - pub(crate) unsafe fn take(self) -> D { - self.derived - } -} - -use super::error::Error; -pub(crate) type ZeroCopyResult = Result, Error>; - -use std::ops::Deref; -impl Deref for ZeroCopy { - type Target = D; - fn deref(&self) -> &Self::Target { - &self.derived - } -} - -// re-implement standard traits -// basically copied from Rc - -impl PartialEq for ZeroCopy { - fn eq(&self, other: &ZeroCopy) -> bool { - **self == **other - } -} -impl Eq for ZeroCopy {} - -use std::cmp::Ordering; -impl PartialOrd for ZeroCopy { - fn partial_cmp(&self, other: &ZeroCopy) -> Option { - (**self).partial_cmp(&**other) - } - fn lt(&self, other: &ZeroCopy) -> bool { - **self < **other - } - fn le(&self, other: &ZeroCopy) -> bool { - **self <= **other - } - fn gt(&self, other: &ZeroCopy) -> bool { - **self > **other - } - fn ge(&self, other: &ZeroCopy) -> bool { - **self >= **other - } -} -impl Ord for ZeroCopy { - fn cmp(&self, other: &ZeroCopy) -> Ordering { - (**self).cmp(&**other) - } -} - -use std::hash::{Hash, Hasher}; -impl Hash for ZeroCopy { - fn hash(&self, state: &mut H) { - (**self).hash(state); - } -} - -use std::fmt; -impl fmt::Display for ZeroCopy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&**self, f) - } -} -impl fmt::Debug for ZeroCopy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - -impl<'a, D> IntoIterator for &'a ZeroCopy -where - &'a D: IntoIterator, -{ - type Item = <&'a D as IntoIterator>::Item; - type IntoIter = <&'a D as IntoIterator>::IntoIter; - fn into_iter(self) -> Self::IntoIter { - (**self).into_iter() - } -} diff --git a/src/types/name.rs b/src/types/name.rs index 9196738..e9808fd 100644 --- a/src/types/name.rs +++ b/src/types/name.rs @@ -1,13 +1,76 @@ +use crate::error::Error; +use crate::parse::{parse_many_into, MapOrNot}; +use crate::types::UnsolicitedResponse; +use imap_proto::{MailboxDatum, Response}; +use ouroboros::self_referencing; use std::borrow::Cow; +use std::slice::Iter; +use std::sync::mpsc; + +/// A wrapper for one or more [`Name`] responses. +#[self_referencing] +pub struct Names { + data: Vec, + #[borrows(data)] + #[covariant] + pub(crate) names: Vec>, +} + +impl Names { + /// Parse one or more [`Name`] from a response buffer + pub fn parse( + owned: Vec, + unsolicited: &mut mpsc::Sender, + ) -> Result { + NamesTryBuilder { + data: owned, + names_builder: |input| { + let mut names = Vec::new(); + parse_many_into(input, &mut names, unsolicited, |response| match response { + Response::MailboxData(MailboxDatum::List { + flags, + delimiter, + name, + }) => Ok(MapOrNot::Map(Name { + attributes: flags.into_iter().map(NameAttribute::from).collect(), + delimiter, + name, + })), + resp => Ok(MapOrNot::Not(resp)), + })?; + Ok(names) + }, + } + .try_build() + } + + /// Iterate over the contained [`Name`]s + pub fn iter(&self) -> Iter<'_, Name<'_>> { + self.borrow_names().iter() + } + + /// Get the number of [`Name`]s in this container. + pub fn len(&self) -> usize { + self.borrow_names().len() + } + + /// Return true of there are no [`Name`]s in the container. + pub fn is_empty(&self) -> bool { + self.borrow_names().is_empty() + } + + /// Get the element at the given index + pub fn get(&self, index: usize) -> Option<&Name<'_>> { + self.borrow_names().get(index) + } +} /// A name that matches a `LIST` or `LSUB` command. #[derive(Debug, Eq, PartialEq)] -pub struct Name { - // Note that none of these fields are *actually* 'static. - // Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`. - pub(crate) attributes: Vec>, - pub(crate) delimiter: Option>, - pub(crate) name: Cow<'static, str>, +pub struct Name<'a> { + pub(crate) attributes: Vec>, + pub(crate) delimiter: Option>, + pub(crate) name: Cow<'a, str>, } /// An attribute set for an IMAP name. @@ -46,6 +109,18 @@ impl NameAttribute<'static> { } } +impl<'a> NameAttribute<'a> { + fn into_owned(self) -> NameAttribute<'static> { + match self { + NameAttribute::NoInferiors => NameAttribute::NoInferiors, + NameAttribute::NoSelect => NameAttribute::NoSelect, + NameAttribute::Marked => NameAttribute::Marked, + NameAttribute::Unmarked => NameAttribute::Unmarked, + NameAttribute::Custom(cow) => NameAttribute::Custom(Cow::Owned(cow.into_owned())), + } + } +} + impl<'a> From for NameAttribute<'a> { fn from(s: String) -> Self { if let Some(f) = NameAttribute::system(&s) { @@ -76,9 +151,9 @@ impl<'a> From<&'a str> for NameAttribute<'a> { } } -impl Name { +impl<'a> Name<'a> { /// Attributes of this name. - pub fn attributes(&self) -> &[NameAttribute<'_>] { + pub fn attributes(&self) -> &[NameAttribute<'a>] { &self.attributes[..] } @@ -97,4 +172,17 @@ impl Name { pub fn name(&self) -> &str { &*self.name } + + /// Get an owned version of this [`Name`]. + pub fn into_owned(self) -> Name<'static> { + Name { + attributes: self + .attributes + .into_iter() + .map(|av| av.into_owned()) + .collect(), + delimiter: self.delimiter.map(|cow| Cow::Owned(cow.into_owned())), + name: Cow::Owned(self.name.into_owned()), + } + } } diff --git a/tests/imap_integration.rs b/tests/imap_integration.rs index c3eea0e..81a5efa 100644 --- a/tests/imap_integration.rs +++ b/tests/imap_integration.rs @@ -147,7 +147,7 @@ fn inbox() { // let's see that we can also fetch the e-mails let fetch = c.fetch("1", "(ALL UID)").unwrap(); assert_eq!(fetch.len(), 1); - let fetch = &fetch[0]; + let fetch = fetch.iter().next().unwrap(); assert_eq!(fetch.message, 1); assert_ne!(fetch.uid, None); assert_eq!(fetch.size, Some(138)); @@ -265,7 +265,7 @@ fn inbox_uid() { // let's see that we can also fetch the e-mail let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); assert_eq!(fetch.len(), 1); - let fetch = &fetch[0]; + let fetch = fetch.iter().next().unwrap(); assert_eq!(fetch.uid, Some(uid)); let e = fetch.envelope().unwrap(); assert_eq!(e.subject, Some(b"My first e-mail"[..].into())); @@ -325,7 +325,7 @@ fn append() { // fetch the e-mail let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); assert_eq!(fetch.len(), 1); - let fetch = &fetch[0]; + let fetch = fetch.iter().next().unwrap(); assert_eq!(fetch.uid, Some(uid)); let e = fetch.envelope().unwrap(); assert_eq!(e.subject, Some(b"My second e-mail"[..].into())); @@ -376,7 +376,7 @@ fn append_with_flags() { // fetch the e-mail let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); assert_eq!(fetch.len(), 1); - let fetch = &fetch[0]; + let fetch = fetch.iter().next().unwrap(); assert_eq!(fetch.uid, Some(uid)); let e = fetch.envelope().unwrap(); assert_eq!(e.subject, Some(b"My third e-mail"[..].into())); @@ -436,7 +436,7 @@ fn append_with_flags_and_date() { // fetch the e-mail let fetch = c.uid_fetch(format!("{}", uid), "(ALL UID)").unwrap(); assert_eq!(fetch.len(), 1); - let fetch = &fetch[0]; + let fetch = fetch.iter().next().unwrap(); assert_eq!(fetch.uid, Some(uid)); assert_eq!(fetch.internal_date(), Some(date));