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,