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.
This commit is contained in:
Todd Mortimer 2020-11-11 11:37:33 -05:00
parent b11b08954c
commit c49e78b4d0
5 changed files with 111 additions and 13 deletions

View file

@ -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"

View file

@ -1252,10 +1252,10 @@ impl<T: Read + Write> Connection<T> {
};
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));

View file

@ -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<u32> = ids.iter().cloned().collect();
assert_eq!(ids, HashSet::<u32>::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);
}
}

View file

@ -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<u32>,
/// 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<u64>,
}
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,
)
}
}

View file

@ -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 <MODSEQ> 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<std::ops::RangeInclusive<u32>>,
},
}
/// This type wraps an input stream and a type that was constructed by parsing that input stream,