From 753e1b9db138ca4f56a9f7bf94c508d7a3abe613 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 11 Nov 2020 11:25:21 -0500 Subject: [PATCH 1/9] Fix trivial clippy warnings. --- src/client.rs | 2 -- src/types/mod.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index a47c401..6d54e58 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,6 @@ -use base64; use bufstream::BufStream; #[cfg(feature = "tls")] use native_tls::{TlsConnector, TlsStream}; -use nom; use std::collections::HashSet; use std::io::{Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; diff --git a/src/types/mod.rs b/src/types/mod.rs index fb57e4e..a91fa25 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -172,13 +172,13 @@ impl Flag<'static> { 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::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), } } From b11b08954c6cefd16c4356cd3ec094c4bf29a6ca Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 11 Nov 2020 11:34:42 -0500 Subject: [PATCH 2/9] Fix clippy manual_non_exhaustive lint. --- src/error.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index c062d56..dc57422 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,7 @@ pub type Result = result::Result; /// A set of errors that can occur in the IMAP client #[derive(Debug)] +#[non_exhaustive] pub enum Error { /// An `io::Error` that occurred while trying to read or write to a network stream. Io(IoError), @@ -43,8 +44,6 @@ pub enum Error { Validate(ValidateError), /// Error appending an e-mail. Append, - #[doc(hidden)] - __Nonexhaustive, } impl From for Error { @@ -99,7 +98,6 @@ impl fmt::Display for Error { Error::Bad(ref data) => write!(f, "Bad Response: {}", data), Error::ConnectionLost => f.write_str("Connection Lost"), Error::Append => f.write_str("Could not append mail to mailbox"), - Error::__Nonexhaustive => f.write_str("Unknown"), } } } @@ -119,7 +117,6 @@ impl StdError for Error { Error::No(_) => "No Response", Error::ConnectionLost => "Connection lost", Error::Append => "Could not append mail to mailbox", - Error::__Nonexhaustive => "Unknown", } } From c49e78b4d06ae374548a22ff3dda0d152e98e7ea Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 11 Nov 2020 11:37:33 -0500 Subject: [PATCH 3/9] Update imap-proto and nom dependencies. Add support for HIGHESTMODSEQ (RFC 4551) and VANISHED (RFC 7162), which allows users to quickly synchronize to a mailbox by fetching only changes since the last known highest mod sequence. --- Cargo.toml | 4 +-- src/client.rs | 6 ++-- src/parse.rs | 78 ++++++++++++++++++++++++++++++++++++++++---- src/types/mailbox.rs | 10 ++++-- src/types/mod.rs | 26 +++++++++++++++ 5 files changed, 111 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e4f5388..4d4be82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ default = ["tls"] native-tls = { version = "0.2.2", optional = true } regex = "1.0" bufstream = "0.1" -imap-proto = "0.10.0" -nom = "5.0" +imap-proto = "0.12.0" +nom = "6.0" base64 = "0.12" chrono = "0.4" lazy_static = "1.4" diff --git a/src/client.rs b/src/client.rs index 6d54e58..a6f63d6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1252,10 +1252,10 @@ impl Connection { }; let break_with = { - use imap_proto::{parse_response, Response, Status}; + use imap_proto::{Response, Status}; let line = &data[line_start..]; - match parse_response(line) { + match imap_proto::parser::parse_response(line) { Ok(( _, Response::Done { @@ -1606,6 +1606,7 @@ mod tests { permanent_flags: vec![], uid_next: Some(2), uid_validity: Some(1257842737), + highest_mod_seq: None, }; let mailbox_name = "INBOX"; let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name)); @@ -1652,6 +1653,7 @@ mod tests { ], uid_next: Some(2), uid_validity: Some(1257842737), + highest_mod_seq: None, }; let mailbox_name = "INBOX"; let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name)); diff --git a/src/parse.rs b/src/parse.rs index b2dc002..862bb45 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -44,7 +44,7 @@ where break Ok(things); } - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, resp)) => { lines = rest; @@ -143,7 +143,7 @@ pub fn parse_capabilities( let f = |mut lines| { let mut caps = HashSet::new(); loop { - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, Response::Capabilities(c))) => { lines = rest; caps.extend(c); @@ -179,7 +179,7 @@ pub fn parse_noop( break Ok(()); } - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, data)) => { lines = rest; if let Some(resp) = handle_unilateral(data, unsolicited) { @@ -200,7 +200,7 @@ pub fn parse_mailbox( let mut mailbox = Mailbox::default(); loop { - match imap_proto::parse_response(lines) { + match imap_proto::parser::parse_response(lines) { Ok((rest, Response::Data { status, code, .. })) => { lines = rest; @@ -212,6 +212,9 @@ pub fn parse_mailbox( use imap_proto::ResponseCode; match code { + Some(ResponseCode::HighestModSeq(seq)) => { + mailbox.highest_mod_seq = Some(seq); + } Some(ResponseCode::UidValidity(uid)) => { mailbox.uid_validity = Some(uid); } @@ -254,7 +257,8 @@ pub fn parse_mailbox( } MailboxDatum::List { .. } | MailboxDatum::MetadataSolicited { .. } - | MailboxDatum::MetadataUnsolicited { .. } => {} + | MailboxDatum::MetadataUnsolicited { .. } + | MailboxDatum::Search { .. } => {} } } Ok((rest, Response::Expunge(n))) => { @@ -286,8 +290,8 @@ pub fn parse_ids( break Ok(ids); } - match imap_proto::parse_response(lines) { - Ok((rest, Response::IDs(c))) => { + match imap_proto::parser::parse_response(lines) { + Ok((rest, Response::MailboxData(MailboxDatum::Search(c)))) => { lines = rest; ids.extend(c); } @@ -331,6 +335,11 @@ fn handle_unilateral<'a>( Response::Expunge(n) => { unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); } + Response::Vanished { earlier, uids } => { + unsolicited + .send(UnsolicitedResponse::Vanished { earlier, uids }) + .unwrap(); + } res => { return Some(res); } @@ -547,4 +556,59 @@ mod tests { let ids: HashSet = ids.iter().cloned().collect(); assert_eq!(ids, HashSet::::new()); } + + #[test] + 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. + 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), + } + assert!(recv.try_recv().is_err()); + + // 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"; + + let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + match recv.try_recv().unwrap() { + UnsolicitedResponse::Vanished { earlier, uids } => { + assert!(earlier); + assert_eq!(uids.len(), 3); + assert_eq!(*uids[0].start(), 3); + assert_eq!(*uids[0].end(), 8); + assert_eq!(*uids[1].start(), 12); + assert_eq!(*uids[1].end(), 12); + assert_eq!(*uids[2].start(), 50); + assert_eq!(*uids[2].end(), 60); + } + what => panic!("Unexpected response in unsolicited responses: {:?}", what), + } + 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); + } } diff --git a/src/types/mailbox.rs b/src/types/mailbox.rs index 1b87720..cec4166 100644 --- a/src/types/mailbox.rs +++ b/src/types/mailbox.rs @@ -35,6 +35,10 @@ pub struct Mailbox { /// The unique identifier validity value. See [`Uid`] for more details. If this is missing, /// the server does not support unique identifiers. pub uid_validity: Option, + + /// The highest mod sequence for this mailboxr. Used with + /// [Conditional STORE](https://tools.ietf.org/html/rfc4551#section-3.1.1). + pub highest_mod_seq: Option, } impl Default for Mailbox { @@ -47,6 +51,7 @@ impl Default for Mailbox { permanent_flags: Vec::new(), uid_next: None, uid_validity: None, + highest_mod_seq: None, } } } @@ -56,14 +61,15 @@ impl fmt::Display for Mailbox { write!( f, "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ - uid_next: {:?}, uid_validity: {:?}", + uid_next: {:?}, uid_validity: {:?}, highest_mod_seq: {:?}", self.flags, self.exists, self.recent, self.unseen, self.permanent_flags, self.uid_next, - self.uid_validity + self.uid_validity, + self.highest_mod_seq, ) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index a91fa25..5f6f647 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -277,6 +277,32 @@ pub enum UnsolicitedResponse { /// sequence numbers 9, 8, 7, 6, and 5. // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? Expunge(Seq), + + /// An unsolicited [`VANISHED` response](https://tools.ietf.org/html/rfc7162#section-3.2.10) + /// that reports a sequence-set of `UID`s that have been expunged from the mailbox. + /// + /// The `VANISHED` response is similar to the `EXPUNGE` response and can be sent wherever + /// an `EXPUNGE` response can be sent. It can only be sent by the server if the client + /// has enabled [`QRESYNC`](https://tools.ietf.org/html/rfc7162). + /// + /// The `VANISHED` response has two forms, one with the `EARLIER` tag which is used to + /// respond to a `UID FETCH` or `SELECT/EXAMINE` command, and one without an `EARLIER` + /// tag, which is used to announce removals within an already selected mailbox. + /// + /// If using `QRESYNC`, the client can fetch new, updated and deleted `UID`s in a + /// single round trip by including the `(CHANGEDSINCE VANISHED)` + /// modifier to the `UID SEARCH` command, as described in + /// [RFC7162](https://tools.ietf.org/html/rfc7162#section-3.1.4). For example + /// `UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 1234 VANISHED)` would return `FETCH` + /// results for all `UID`s added or modified since `MODSEQ` `1234`. Deleted `UID`s + /// will be present as a `VANISHED` response in the `Session::unsolicited_responses` + /// channel. + Vanished { + /// Whether the `EARLIER` tag was set on the response + earlier: bool, + /// The list of `UID`s which have been removed + uids: Vec>, + }, } /// This type wraps an input stream and a type that was constructed by parsing that input stream, From 22dae40ab5857fd78798f454d3d927014182f832 Mon Sep 17 00:00:00 2001 From: mordak Date: Sat, 14 Nov 2020 16:45:50 -0500 Subject: [PATCH 4/9] Fix typo in doc comment Co-authored-by: Jon Gjengset --- src/types/mailbox.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/mailbox.rs b/src/types/mailbox.rs index cec4166..962e05d 100644 --- a/src/types/mailbox.rs +++ b/src/types/mailbox.rs @@ -36,7 +36,7 @@ pub struct Mailbox { /// the server does not support unique identifiers. pub uid_validity: Option, - /// The highest mod sequence for this mailboxr. Used with + /// The highest mod sequence for this mailbox. Used with /// [Conditional STORE](https://tools.ietf.org/html/rfc4551#section-3.1.1). pub highest_mod_seq: Option, } From d381723deba922a2f28456ecd2d73240c09235db Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 14 Nov 2020 16:52:03 -0500 Subject: [PATCH 5/9] Add non_exhaustive to Mailbox. --- src/types/mailbox.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mailbox.rs b/src/types/mailbox.rs index 962e05d..1cadba6 100644 --- a/src/types/mailbox.rs +++ b/src/types/mailbox.rs @@ -4,6 +4,7 @@ use std::fmt; /// Meta-information about an IMAP mailbox, as returned by /// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends. #[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] pub struct Mailbox { /// Defined flags in the mailbox. See the description of the [FLAGS /// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail. From a9788ad1e07508a4e9f3d18db7b8038d51303cae Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 21 Nov 2020 14:35:47 -0500 Subject: [PATCH 6/9] Add a Deleted type to wrap EXPUNGE and VANISHED responses. EXPUNGE may return either a series of EXPUNGE responses each with a single message sequence number, or a VANISHED response with a sequence set of UIDs. This adds a wrapper enum and some associated iterators to make it easy to handle these in the client. --- src/client.rs | 4 +- src/parse.rs | 73 ++++++++++++------ src/types/deleted.rs | 180 +++++++++++++++++++++++++++++++++++++++++++ src/types/mod.rs | 4 + 4 files changed, 234 insertions(+), 27 deletions(-) create mode 100644 src/types/deleted.rs 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 } From b87083c5b9b50ef5ea8cfdaf5f616a08efa6dc3f Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 5 Dec 2020 21:12:53 -0500 Subject: [PATCH 7/9] Bump minimum version. Via nom6 dependency bitvec. --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index de1917f..c9ac219 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,12 +47,12 @@ jobs: - job: msrv pool: vmImage: ubuntu-latest - displayName: "Minimum supported Rust version: 1.40.0" + displayName: "Minimum supported Rust version: 1.43.0" dependsOn: [] steps: - template: install-rust.yml@templates parameters: - rust: 1.40.0 # static-assertions (1.37+) and base64 (1.40+) + rust: 1.43.0 # nom6 depends on bitvec (1.43+) - script: cargo check displayName: cargo check - job: integration From 5bb1500d1484f21ad42ebd8e46375af8b7b26c6a Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 5 Dec 2020 21:26:28 -0500 Subject: [PATCH 8/9] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48ebcb..ade21d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - VANISHED support in EXPUNGE responses and unsolicited responses (#172). ### Changed + - MSRV increased to 1.43 for nom6 and bitvec + - `expunge` and `uid_expunge` return `Result` instead of `Result>`. ### Removed From 166a0cb6b3a9d630557e04e41344ed477d5c084c Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sun, 6 Dec 2020 15:49:11 -0500 Subject: [PATCH 9/9] Link to QRESYNC RFC section describing VANISHED vs EXPUNGE responses. --- src/parse.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parse.rs b/src/parse.rs index 0b57f52..9bf61b9 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -158,6 +158,10 @@ pub fn parse_expunge( } } + // If the server sends a VANISHED response then they must only send VANISHED + // in lieu of EXPUNGE responses for the rest of this connection, so it is + // always one or the other. + // https://tools.ietf.org/html/rfc7162#section-3.2.10 if !vanished.is_empty() { Ok(Deleted::from_vanished(vanished)) } else {