diff --git a/src/client.rs b/src/client.rs index a6f63d6..149bc24 100644 --- a/src/client.rs +++ b/src/client.rs @@ -687,7 +687,7 @@ impl Session { /// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently /// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox. /// The message sequence number of each message that is removed is returned. - pub fn expunge(&mut self) -> Result> { + pub fn expunge(&mut self) -> Result { self.run_command_and_read_response("EXPUNGE") .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } @@ -714,7 +714,7 @@ impl Session { /// /// Alternatively, the client may fall back to using just [`Session::expunge`], risking the /// unintended removal of some messages. - pub fn uid_expunge>(&mut self, uid_set: S) -> Result> { + pub fn uid_expunge>(&mut self, uid_set: S) -> Result { self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref())) .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } diff --git a/src/parse.rs b/src/parse.rs index 862bb45..0b57f52 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -127,13 +127,42 @@ pub fn parse_fetches( pub fn parse_expunge( lines: Vec, unsolicited: &mut mpsc::Sender, -) -> Result> { - let f = |resp| match resp { - Response::Expunge(id) => Ok(MapOrNot::Map(id)), - resp => Ok(MapOrNot::Not(resp)), - }; +) -> Result { + let mut lines: &[u8] = &lines; + let mut expunged = Vec::new(); + let mut vanished = Vec::new(); - unsafe { parse_many(lines, f, unsolicited).map(|ids| ids.take()) } + loop { + if lines.is_empty() { + break; + } + + match imap_proto::parser::parse_response(lines) { + Ok((rest, Response::Expunge(seq))) => { + lines = rest; + expunged.push(seq); + } + Ok((rest, Response::Vanished { earlier: _, uids })) => { + lines = rest; + vanished.extend(uids); + } + Ok((rest, data)) => { + lines = rest; + if let Some(resp) = handle_unilateral(data, unsolicited) { + return Err(resp.into()); + } + } + _ => { + return Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } + } + + if !vanished.is_empty() { + Ok(Deleted::from_vanished(vanished)) + } else { + Ok(Deleted::from_expunged(expunged)) + } } pub fn parse_capabilities( @@ -561,31 +590,25 @@ mod tests { fn parse_vanished_test() { // VANISHED can appear if the user has enabled QRESYNC (RFC 7162), in response to // SELECT/EXAMINE (QRESYNC); UID FETCH (VANISHED); or EXPUNGE commands. In the first - // two cases the VANISHED respone will be a different type than the expected response - // and so goes into the unsolicited respones channel. In the last case, VANISHED is - // explicitly a response to an EXPUNGE command, but the semantics of EXPUNGE (one ID - // per response, multiple responses) vs VANISHED (a sequence-set of UIDs in a single - // response) are different enough that is isn't obvious what parse_expunge() should do. - // If we do nothing special, then the VANISHED response ends up in the unsolicited - // responses channel, which is at least consistent with the other cases where VANISHED - // can show up. + // two cases the VANISHED response will be a different type than expected + // and so goes into the unsolicited responses channel. let lines = b"* VANISHED 3\r\n"; let (mut send, recv) = mpsc::channel(); let resp = parse_expunge(lines.to_vec(), &mut send).unwrap(); - assert!(resp.is_empty()); - match recv.try_recv().unwrap() { - UnsolicitedResponse::Vanished { earlier, uids } => { - assert!(!earlier); - assert_eq!(uids.len(), 1); - assert_eq!(*uids[0].start(), 3); - assert_eq!(*uids[0].end(), 3); - } - what => panic!("Unexpected response in unsolicited responses: {:?}", what), - } + // Should be not empty, and have no seqs + assert!(!resp.is_empty()); + assert_eq!(None, resp.seqs().next()); + + // Should have one UID response + let mut uids = resp.uids(); + assert_eq!(Some(3), uids.next()); + assert_eq!(None, uids.next()); + + // Should be nothing in the unsolicited responses channel assert!(recv.try_recv().is_err()); - // test VANISHED mixed with FETCH + // Test VANISHED mixed with FETCH let lines = b"* VANISHED (EARLIER) 3:8,12,50:60\r\n\ * 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n"; diff --git a/src/types/deleted.rs b/src/types/deleted.rs new file mode 100644 index 0000000..90945c7 --- /dev/null +++ b/src/types/deleted.rs @@ -0,0 +1,180 @@ +use super::{Seq, Uid}; +use std::ops::RangeInclusive; + +/// An enum representing message sequence numbers or UID sequence sets returned +/// in response to a `EXPUNGE` command. +/// +/// The `EXPUNGE` command may return several `EXPUNGE` responses referencing +/// message sequence numbers, or it may return a `VANISHED` response referencing +/// multiple UID values in a sequence set if the client has enabled +/// [QRESYNC](https://tools.ietf.org/html/rfc7162#section-3.2.7). +/// +/// `Deleted` implements some iterators to make it easy to use. If the caller +/// knows that they should be receiving an `EXPUNGE` or `VANISHED` response, +/// then they can use [`seqs()`](#method.seqs) to get an iterator over `EXPUNGE` +/// message sequence numbers, or [`uids()`](#method.uids) to get an iterator over +/// the `VANISHED` UIDs. As a convenience `Deleted` also implents `IntoIterator` +/// which just returns an iterator over whatever is contained within. +/// +/// # Examples +/// ```no_run +/// # let domain = "imap.example.com"; +/// # let tls = native_tls::TlsConnector::builder().build().unwrap(); +/// # let client = imap::connect((domain, 993), domain, &tls).unwrap(); +/// # let mut session = client.login("name", "pw").unwrap(); +/// // Iterate over whatever is returned +/// if let Ok(deleted) = session.expunge() { +/// for id in &deleted { +/// // Do something with id +/// } +/// } +/// +/// // Expect a VANISHED response with UIDs +/// if let Ok(deleted) = session.expunge() { +/// for uid in deleted.uids() { +/// // Do something with uid +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +pub enum Deleted { + /// Message sequence numbers given in an `EXPUNGE` response. + Expunged(Vec), + /// Message UIDs given in a `VANISHED` response. + Vanished(Vec>), +} + +impl Deleted { + /// Construct a new `Deleted` value from a vector of message sequence + /// numbers returned in one or more `EXPUNGE` responses. + pub fn from_expunged(v: Vec) -> Self { + Deleted::Expunged(v) + } + + /// Construct a new `Deleted` value from a sequence-set of UIDs + /// returned in a `VANISHED` response + pub fn from_vanished(v: Vec>) -> Self { + Deleted::Vanished(v) + } + + /// Return an iterator over message sequence numbers from an `EXPUNGE` + /// response. If the client is expecting sequence numbers this function + /// can be used to ensure only sequence numbers returned in an `EXPUNGE` + /// response are processed. + pub fn seqs(&self) -> impl Iterator + '_ { + match self { + Deleted::Expunged(s) => s.iter(), + Deleted::Vanished(_) => [].iter(), + } + .copied() + } + + /// Return an iterator over UIDs returned in a `VANISHED` response. + /// If the client is expecting UIDs this function can be used to ensure + /// only UIDs are processed. + pub fn uids(&self) -> impl Iterator + '_ { + match self { + Deleted::Expunged(_) => [].iter(), + Deleted::Vanished(s) => s.iter(), + } + .flat_map(|range| range.clone()) + } + + /// Return if the set is empty + pub fn is_empty(&self) -> bool { + match self { + Deleted::Expunged(v) => v.is_empty(), + Deleted::Vanished(v) => v.is_empty(), + } + } +} + +impl<'a> IntoIterator for &'a Deleted { + type Item = u32; + type IntoIter = Box + 'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + Deleted::Expunged(_) => Box::new(self.seqs()), + Deleted::Vanished(_) => Box::new(self.uids()), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn seq() { + let seqs = Deleted::from_expunged(vec![3, 6, 9, 12]); + let mut i = seqs.into_iter(); + assert_eq!(Some(3), i.next()); + assert_eq!(Some(6), i.next()); + assert_eq!(Some(9), i.next()); + assert_eq!(Some(12), i.next()); + assert_eq!(None, i.next()); + + let seqs = Deleted::from_expunged(vec![]); + let mut i = seqs.into_iter(); + assert_eq!(None, i.next()); + } + + #[test] + fn seq_set() { + let uids = Deleted::from_vanished(vec![1..=1, 3..=5, 8..=9, 12..=12]); + let mut i = uids.into_iter(); + assert_eq!(Some(1), i.next()); + assert_eq!(Some(3), i.next()); + assert_eq!(Some(4), i.next()); + assert_eq!(Some(5), i.next()); + assert_eq!(Some(8), i.next()); + assert_eq!(Some(9), i.next()); + assert_eq!(Some(12), i.next()); + assert_eq!(None, i.next()); + + let uids = Deleted::from_vanished(vec![]); + assert_eq!(None, uids.into_iter().next()); + } + + #[test] + fn seqs() { + let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]); + let mut count: u32 = 0; + for seq in seqs.seqs() { + count += 3; + assert_eq!(seq, count); + } + assert_eq!(count, 12); + } + + #[test] + fn uids() { + let uids: Deleted = Deleted::from_vanished(vec![1..=6]); + let mut count: u32 = 0; + for uid in uids.uids() { + count += 1; + assert_eq!(uid, count); + } + assert_eq!(count, 6); + } + + #[test] + fn generic_iteration() { + let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]); + let mut count: u32 = 0; + for seq in &seqs { + count += 3; + assert_eq!(seq, count); + } + assert_eq!(count, 12); + + let uids: Deleted = Deleted::from_vanished(vec![1..=6]); + let mut count: u32 = 0; + for uid in &uids { + count += 1; + assert_eq!(uid, count); + } + assert_eq!(count, 6); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 5f6f647..5dbaec9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -216,6 +216,9 @@ pub use self::name::{Name, NameAttribute}; mod capabilities; pub use self::capabilities::Capabilities; +mod deleted; +pub use self::deleted::Deleted; + /// re-exported from imap_proto; pub use imap_proto::StatusAttribute; @@ -350,6 +353,7 @@ impl 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 }