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:
parent
b11b08954c
commit
c49e78b4d0
5 changed files with 111 additions and 13 deletions
|
|
@ -29,8 +29,8 @@ default = ["tls"]
|
||||||
native-tls = { version = "0.2.2", optional = true }
|
native-tls = { version = "0.2.2", optional = true }
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
bufstream = "0.1"
|
bufstream = "0.1"
|
||||||
imap-proto = "0.10.0"
|
imap-proto = "0.12.0"
|
||||||
nom = "5.0"
|
nom = "6.0"
|
||||||
base64 = "0.12"
|
base64 = "0.12"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
|
|
||||||
|
|
@ -1252,10 +1252,10 @@ impl<T: Read + Write> Connection<T> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let break_with = {
|
let break_with = {
|
||||||
use imap_proto::{parse_response, Response, Status};
|
use imap_proto::{Response, Status};
|
||||||
let line = &data[line_start..];
|
let line = &data[line_start..];
|
||||||
|
|
||||||
match parse_response(line) {
|
match imap_proto::parser::parse_response(line) {
|
||||||
Ok((
|
Ok((
|
||||||
_,
|
_,
|
||||||
Response::Done {
|
Response::Done {
|
||||||
|
|
@ -1606,6 +1606,7 @@ mod tests {
|
||||||
permanent_flags: vec![],
|
permanent_flags: vec![],
|
||||||
uid_next: Some(2),
|
uid_next: Some(2),
|
||||||
uid_validity: Some(1257842737),
|
uid_validity: Some(1257842737),
|
||||||
|
highest_mod_seq: None,
|
||||||
};
|
};
|
||||||
let mailbox_name = "INBOX";
|
let mailbox_name = "INBOX";
|
||||||
let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name));
|
let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name));
|
||||||
|
|
@ -1652,6 +1653,7 @@ mod tests {
|
||||||
],
|
],
|
||||||
uid_next: Some(2),
|
uid_next: Some(2),
|
||||||
uid_validity: Some(1257842737),
|
uid_validity: Some(1257842737),
|
||||||
|
highest_mod_seq: None,
|
||||||
};
|
};
|
||||||
let mailbox_name = "INBOX";
|
let mailbox_name = "INBOX";
|
||||||
let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name));
|
let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name));
|
||||||
|
|
|
||||||
78
src/parse.rs
78
src/parse.rs
|
|
@ -44,7 +44,7 @@ where
|
||||||
break Ok(things);
|
break Ok(things);
|
||||||
}
|
}
|
||||||
|
|
||||||
match imap_proto::parse_response(lines) {
|
match imap_proto::parser::parse_response(lines) {
|
||||||
Ok((rest, resp)) => {
|
Ok((rest, resp)) => {
|
||||||
lines = rest;
|
lines = rest;
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ pub fn parse_capabilities(
|
||||||
let f = |mut lines| {
|
let f = |mut lines| {
|
||||||
let mut caps = HashSet::new();
|
let mut caps = HashSet::new();
|
||||||
loop {
|
loop {
|
||||||
match imap_proto::parse_response(lines) {
|
match imap_proto::parser::parse_response(lines) {
|
||||||
Ok((rest, Response::Capabilities(c))) => {
|
Ok((rest, Response::Capabilities(c))) => {
|
||||||
lines = rest;
|
lines = rest;
|
||||||
caps.extend(c);
|
caps.extend(c);
|
||||||
|
|
@ -179,7 +179,7 @@ pub fn parse_noop(
|
||||||
break Ok(());
|
break Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match imap_proto::parse_response(lines) {
|
match imap_proto::parser::parse_response(lines) {
|
||||||
Ok((rest, data)) => {
|
Ok((rest, data)) => {
|
||||||
lines = rest;
|
lines = rest;
|
||||||
if let Some(resp) = handle_unilateral(data, unsolicited) {
|
if let Some(resp) = handle_unilateral(data, unsolicited) {
|
||||||
|
|
@ -200,7 +200,7 @@ pub fn parse_mailbox(
|
||||||
let mut mailbox = Mailbox::default();
|
let mut mailbox = Mailbox::default();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match imap_proto::parse_response(lines) {
|
match imap_proto::parser::parse_response(lines) {
|
||||||
Ok((rest, Response::Data { status, code, .. })) => {
|
Ok((rest, Response::Data { status, code, .. })) => {
|
||||||
lines = rest;
|
lines = rest;
|
||||||
|
|
||||||
|
|
@ -212,6 +212,9 @@ pub fn parse_mailbox(
|
||||||
|
|
||||||
use imap_proto::ResponseCode;
|
use imap_proto::ResponseCode;
|
||||||
match code {
|
match code {
|
||||||
|
Some(ResponseCode::HighestModSeq(seq)) => {
|
||||||
|
mailbox.highest_mod_seq = Some(seq);
|
||||||
|
}
|
||||||
Some(ResponseCode::UidValidity(uid)) => {
|
Some(ResponseCode::UidValidity(uid)) => {
|
||||||
mailbox.uid_validity = Some(uid);
|
mailbox.uid_validity = Some(uid);
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +257,8 @@ pub fn parse_mailbox(
|
||||||
}
|
}
|
||||||
MailboxDatum::List { .. }
|
MailboxDatum::List { .. }
|
||||||
| MailboxDatum::MetadataSolicited { .. }
|
| MailboxDatum::MetadataSolicited { .. }
|
||||||
| MailboxDatum::MetadataUnsolicited { .. } => {}
|
| MailboxDatum::MetadataUnsolicited { .. }
|
||||||
|
| MailboxDatum::Search { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((rest, Response::Expunge(n))) => {
|
Ok((rest, Response::Expunge(n))) => {
|
||||||
|
|
@ -286,8 +290,8 @@ pub fn parse_ids(
|
||||||
break Ok(ids);
|
break Ok(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
match imap_proto::parse_response(lines) {
|
match imap_proto::parser::parse_response(lines) {
|
||||||
Ok((rest, Response::IDs(c))) => {
|
Ok((rest, Response::MailboxData(MailboxDatum::Search(c)))) => {
|
||||||
lines = rest;
|
lines = rest;
|
||||||
ids.extend(c);
|
ids.extend(c);
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +335,11 @@ fn handle_unilateral<'a>(
|
||||||
Response::Expunge(n) => {
|
Response::Expunge(n) => {
|
||||||
unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap();
|
unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap();
|
||||||
}
|
}
|
||||||
|
Response::Vanished { earlier, uids } => {
|
||||||
|
unsolicited
|
||||||
|
.send(UnsolicitedResponse::Vanished { earlier, uids })
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
res => {
|
res => {
|
||||||
return Some(res);
|
return Some(res);
|
||||||
}
|
}
|
||||||
|
|
@ -547,4 +556,59 @@ mod tests {
|
||||||
let ids: HashSet<u32> = ids.iter().cloned().collect();
|
let ids: HashSet<u32> = ids.iter().cloned().collect();
|
||||||
assert_eq!(ids, HashSet::<u32>::new());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ pub struct Mailbox {
|
||||||
/// The unique identifier validity value. See [`Uid`] for more details. If this is missing,
|
/// The unique identifier validity value. See [`Uid`] for more details. If this is missing,
|
||||||
/// the server does not support unique identifiers.
|
/// the server does not support unique identifiers.
|
||||||
pub uid_validity: Option<u32>,
|
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 {
|
impl Default for Mailbox {
|
||||||
|
|
@ -47,6 +51,7 @@ impl Default for Mailbox {
|
||||||
permanent_flags: Vec::new(),
|
permanent_flags: Vec::new(),
|
||||||
uid_next: None,
|
uid_next: None,
|
||||||
uid_validity: None,
|
uid_validity: None,
|
||||||
|
highest_mod_seq: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,14 +61,15 @@ impl fmt::Display for Mailbox {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\
|
"flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\
|
||||||
uid_next: {:?}, uid_validity: {:?}",
|
uid_next: {:?}, uid_validity: {:?}, highest_mod_seq: {:?}",
|
||||||
self.flags,
|
self.flags,
|
||||||
self.exists,
|
self.exists,
|
||||||
self.recent,
|
self.recent,
|
||||||
self.unseen,
|
self.unseen,
|
||||||
self.permanent_flags,
|
self.permanent_flags,
|
||||||
self.uid_next,
|
self.uid_next,
|
||||||
self.uid_validity
|
self.uid_validity,
|
||||||
|
self.highest_mod_seq,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,32 @@ pub enum UnsolicitedResponse {
|
||||||
/// sequence numbers 9, 8, 7, 6, and 5.
|
/// 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?
|
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
|
||||||
Expunge(Seq),
|
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,
|
/// This type wraps an input stream and a type that was constructed by parsing that input stream,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue