From 81ed9ff1cf83bad1f2702e0eb7ab3702996d308d Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Thu, 14 Jul 2022 11:59:44 +0200 Subject: [PATCH 1/3] Expose HIGHESTMODSEQ value in EXPUNGE response If the `QRESYNC` extension (RFC 7162) is being used, `EXPUNGE` responses will return the new highest mod sequence for the mailbox after the expunge operation. Access to this value is quite valuable for caching clients. --- src/client.rs | 10 +++--- src/parse.rs | 12 ++++++-- src/types/deleted.rs | 73 +++++++++++++++++++++++++++----------------- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1965a01..a619700 100644 --- a/src/client.rs +++ b/src/client.rs @@ -784,8 +784,9 @@ impl Session { /// 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 { - self.run_command_and_read_response("EXPUNGE") - .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) + self.run_command("EXPUNGE")?; + self.read_response() + .and_then(|(lines, _)| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } /// The [`UID EXPUNGE` command](https://tools.ietf.org/html/rfc4315#section-2.1) permanently @@ -811,8 +812,9 @@ 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 { - self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref())) - .and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) + self.run_command(&format!("UID EXPUNGE {}", uid_set.as_ref()))?; + self.read_response() + .and_then(|(lines, _)| parse_expunge(lines, &mut self.unsolicited_responses_tx)) } /// The [`CHECK` command](https://tools.ietf.org/html/rfc3501#section-6.4.1) requests a diff --git a/src/parse.rs b/src/parse.rs index 32ddccc..c0350d1 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -78,6 +78,7 @@ pub fn parse_expunge( let mut lines: &[u8] = &lines; let mut expunged = Vec::new(); let mut vanished = Vec::new(); + let mut mod_seq: Option = None; loop { if lines.is_empty() { @@ -85,6 +86,13 @@ pub fn parse_expunge( } match imap_proto::parser::parse_response(lines) { + Ok((rest, Response::Done { status, code, .. })) => { + assert_eq!(status, imap_proto::Status::Ok); + lines = rest; + if let Some(ResponseCode::HighestModSeq(ms)) = code { + mod_seq = Some(ms); + }; + } Ok((rest, Response::Expunge(seq))) => { lines = rest; expunged.push(seq); @@ -110,9 +118,9 @@ pub fn parse_expunge( // always one or the other. // https://tools.ietf.org/html/rfc7162#section-3.2.10 if !vanished.is_empty() { - Ok(Deleted::from_vanished(vanished)) + Ok(Deleted::from_vanished(vanished, mod_seq)) } else { - Ok(Deleted::from_expunged(expunged)) + Ok(Deleted::from_expunged(expunged, mod_seq)) } } diff --git a/src/types/deleted.rs b/src/types/deleted.rs index 98e8ad9..6a604ed 100644 --- a/src/types/deleted.rs +++ b/src/types/deleted.rs @@ -1,13 +1,15 @@ 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. +/// A struct containing message sequence numbers or UID sequence sets and a mod +/// sequence 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). +/// [QRESYNC](https://tools.ietf.org/html/rfc7162#section-3.2.7). If `QRESYNC` is +/// enabled, the server will also return the mod sequence of the completed +/// operation. /// /// `Deleted` implements some iterators to make it easy to use. If the caller /// knows that they should be receiving an `EXPUNGE` or `VANISHED` response, @@ -39,7 +41,16 @@ use std::ops::RangeInclusive; /// # } /// ``` #[derive(Debug, Clone)] -pub enum Deleted { +pub struct Deleted { + /// The list of messages that were expunged + pub messages: DeletedMessages, + /// The mod sequence of the performed operation, if the `QRESYNC` extension + /// is enabled. + pub mod_seq: Option, +} + +#[derive(Debug, Clone)] +pub enum DeletedMessages { /// Message sequence numbers given in an `EXPUNGE` response. Expunged(Vec), /// Message UIDs given in a `VANISHED` response. @@ -49,14 +60,20 @@ pub enum Deleted { 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) + pub fn from_expunged(v: Vec, mod_seq: Option) -> Self { + Self { + messages: DeletedMessages::Expunged(v), + mod_seq: mod_seq, + } } /// 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) + pub fn from_vanished(v: Vec>, mod_seq: Option) -> Self { + Self { + messages: DeletedMessages::Vanished(v), + mod_seq: mod_seq, + } } /// Return an iterator over message sequence numbers from an `EXPUNGE` @@ -64,9 +81,9 @@ impl Deleted { /// 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(), + match &self.messages { + DeletedMessages::Expunged(s) => s.iter(), + DeletedMessages::Vanished(_) => [].iter(), } .copied() } @@ -75,18 +92,18 @@ impl Deleted { /// 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(), + match &self.messages { + DeletedMessages::Expunged(_) => [].iter(), + DeletedMessages::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(), + match &self.messages { + DeletedMessages::Expunged(v) => v.is_empty(), + DeletedMessages::Vanished(v) => v.is_empty(), } } } @@ -96,9 +113,9 @@ impl<'a> IntoIterator for &'a Deleted { type IntoIter = Box + 'a>; fn into_iter(self) -> Self::IntoIter { - match self { - Deleted::Expunged(_) => Box::new(self.seqs()), - Deleted::Vanished(_) => Box::new(self.uids()), + match &self.messages { + DeletedMessages::Expunged(_) => Box::new(self.seqs()), + DeletedMessages::Vanished(_) => Box::new(self.uids()), } } } @@ -109,7 +126,7 @@ mod test { #[test] fn seq() { - let seqs = Deleted::from_expunged(vec![3, 6, 9, 12]); + let seqs = Deleted::from_expunged(vec![3, 6, 9, 12], None); let mut i = seqs.into_iter(); assert_eq!(Some(3), i.next()); assert_eq!(Some(6), i.next()); @@ -117,14 +134,14 @@ mod test { assert_eq!(Some(12), i.next()); assert_eq!(None, i.next()); - let seqs = Deleted::from_expunged(vec![]); + let seqs = Deleted::from_expunged(vec![], None); 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 uids = Deleted::from_vanished(vec![1..=1, 3..=5, 8..=9, 12..=12], None); let mut i = uids.into_iter(); assert_eq!(Some(1), i.next()); assert_eq!(Some(3), i.next()); @@ -135,13 +152,13 @@ mod test { assert_eq!(Some(12), i.next()); assert_eq!(None, i.next()); - let uids = Deleted::from_vanished(vec![]); + let uids = Deleted::from_vanished(vec![], None); assert_eq!(None, uids.into_iter().next()); } #[test] fn seqs() { - let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]); + let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12], None); let mut count: u32 = 0; for seq in seqs.seqs() { count += 3; @@ -152,7 +169,7 @@ mod test { #[test] fn uids() { - let uids: Deleted = Deleted::from_vanished(vec![1..=6]); + let uids: Deleted = Deleted::from_vanished(vec![1..=6], None); let mut count: u32 = 0; for uid in uids.uids() { count += 1; @@ -163,7 +180,7 @@ mod test { #[test] fn generic_iteration() { - let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12]); + let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12], None); let mut count: u32 = 0; for seq in &seqs { count += 3; @@ -171,7 +188,7 @@ mod test { } assert_eq!(count, 12); - let uids: Deleted = Deleted::from_vanished(vec![1..=6]); + let uids: Deleted = Deleted::from_vanished(vec![1..=6], None); let mut count: u32 = 0; for uid in &uids { count += 1; From 564063561d9c6be62ebe4cd89a001d94823a8f0a Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Thu, 14 Jul 2022 14:55:24 +0200 Subject: [PATCH 2/3] Mark struct types::Deleted non-exhaustive --- src/types/deleted.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/deleted.rs b/src/types/deleted.rs index 6a604ed..8e08be0 100644 --- a/src/types/deleted.rs +++ b/src/types/deleted.rs @@ -41,6 +41,7 @@ use std::ops::RangeInclusive; /// # } /// ``` #[derive(Debug, Clone)] +#[non_exhaustive] pub struct Deleted { /// The list of messages that were expunged pub messages: DeletedMessages, From b0682088770636887ca506ad4c3ff07c204937fe Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Thu, 14 Jul 2022 14:55:46 +0200 Subject: [PATCH 3/3] Add test case for EXPUNGE response w/ mod sequence --- src/parse.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/parse.rs b/src/parse.rs index c0350d1..eb81845 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -672,4 +672,30 @@ mod tests { assert_eq!(first.body(), None); assert_eq!(first.header(), None); } + + #[test] + fn parse_expunged_mod_seq_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 latter + // case, the VANISHED responses will be parsed with the response and the list of + // expunged message is included in the returned struct. + let (mut send, recv) = mpsc::channel(); + + // Test VANISHED mixed with FETCH + let lines = b"* VANISHED 3:5,12\r\n\ + B202 OK [HIGHESTMODSEQ 20010715194045319] expunged\r\n"; + + let deleted = parse_expunge(lines.to_vec(), &mut send).unwrap(); + + // No unsolicited responses, they are aggregated in the returned type + assert!(recv.try_recv().is_err()); + + assert_eq!(deleted.mod_seq, Some(20010715194045319)); + let mut del = deleted.uids(); + assert_eq!(del.next(), Some(3)); + assert_eq!(del.next(), Some(4)); + assert_eq!(del.next(), Some(5)); + assert_eq!(del.next(), Some(12)); + assert_eq!(del.next(), None); + } }