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.
This commit is contained in:
Conrad Hoffmann 2022-07-14 11:59:44 +02:00
parent db29117463
commit 81ed9ff1cf
3 changed files with 61 additions and 34 deletions

View file

@ -784,8 +784,9 @@ impl<T: Read + Write> Session<T> {
/// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox. /// 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. /// The message sequence number of each message that is removed is returned.
pub fn expunge(&mut self) -> Result<Deleted> { pub fn expunge(&mut self) -> Result<Deleted> {
self.run_command_and_read_response("EXPUNGE") self.run_command("EXPUNGE")?;
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) 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 /// The [`UID EXPUNGE` command](https://tools.ietf.org/html/rfc4315#section-2.1) permanently
@ -811,8 +812,9 @@ impl<T: Read + Write> Session<T> {
/// Alternatively, the client may fall back to using just [`Session::expunge`], risking the /// Alternatively, the client may fall back to using just [`Session::expunge`], risking the
/// unintended removal of some messages. /// unintended removal of some messages.
pub fn uid_expunge<S: AsRef<str>>(&mut self, uid_set: S) -> Result<Deleted> { pub fn uid_expunge<S: AsRef<str>>(&mut self, uid_set: S) -> Result<Deleted> {
self.run_command_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref())) self.run_command(&format!("UID EXPUNGE {}", uid_set.as_ref()))?;
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx)) 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 /// The [`CHECK` command](https://tools.ietf.org/html/rfc3501#section-6.4.1) requests a

View file

@ -78,6 +78,7 @@ pub fn parse_expunge(
let mut lines: &[u8] = &lines; let mut lines: &[u8] = &lines;
let mut expunged = Vec::new(); let mut expunged = Vec::new();
let mut vanished = Vec::new(); let mut vanished = Vec::new();
let mut mod_seq: Option<u64> = None;
loop { loop {
if lines.is_empty() { if lines.is_empty() {
@ -85,6 +86,13 @@ pub fn parse_expunge(
} }
match imap_proto::parser::parse_response(lines) { 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))) => { Ok((rest, Response::Expunge(seq))) => {
lines = rest; lines = rest;
expunged.push(seq); expunged.push(seq);
@ -110,9 +118,9 @@ pub fn parse_expunge(
// always one or the other. // always one or the other.
// https://tools.ietf.org/html/rfc7162#section-3.2.10 // https://tools.ietf.org/html/rfc7162#section-3.2.10
if !vanished.is_empty() { if !vanished.is_empty() {
Ok(Deleted::from_vanished(vanished)) Ok(Deleted::from_vanished(vanished, mod_seq))
} else { } else {
Ok(Deleted::from_expunged(expunged)) Ok(Deleted::from_expunged(expunged, mod_seq))
} }
} }

View file

@ -1,13 +1,15 @@
use super::{Seq, Uid}; use super::{Seq, Uid};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// An enum representing message sequence numbers or UID sequence sets returned /// A struct containing message sequence numbers or UID sequence sets and a mod
/// in response to a `EXPUNGE` command. /// sequence returned in response to a `EXPUNGE` command.
/// ///
/// The `EXPUNGE` command may return several `EXPUNGE` responses referencing /// The `EXPUNGE` command may return several `EXPUNGE` responses referencing
/// message sequence numbers, or it may return a `VANISHED` response referencing /// message sequence numbers, or it may return a `VANISHED` response referencing
/// multiple UID values in a sequence set if the client has enabled /// 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 /// `Deleted` implements some iterators to make it easy to use. If the caller
/// knows that they should be receiving an `EXPUNGE` or `VANISHED` response, /// knows that they should be receiving an `EXPUNGE` or `VANISHED` response,
@ -39,7 +41,16 @@ use std::ops::RangeInclusive;
/// # } /// # }
/// ``` /// ```
#[derive(Debug, Clone)] #[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<u64>,
}
#[derive(Debug, Clone)]
pub enum DeletedMessages {
/// Message sequence numbers given in an `EXPUNGE` response. /// Message sequence numbers given in an `EXPUNGE` response.
Expunged(Vec<Seq>), Expunged(Vec<Seq>),
/// Message UIDs given in a `VANISHED` response. /// Message UIDs given in a `VANISHED` response.
@ -49,14 +60,20 @@ pub enum Deleted {
impl Deleted { impl Deleted {
/// Construct a new `Deleted` value from a vector of message sequence /// Construct a new `Deleted` value from a vector of message sequence
/// numbers returned in one or more `EXPUNGE` responses. /// numbers returned in one or more `EXPUNGE` responses.
pub fn from_expunged(v: Vec<u32>) -> Self { pub fn from_expunged(v: Vec<u32>, mod_seq: Option<u64>) -> Self {
Deleted::Expunged(v) Self {
messages: DeletedMessages::Expunged(v),
mod_seq: mod_seq,
}
} }
/// Construct a new `Deleted` value from a sequence-set of UIDs /// Construct a new `Deleted` value from a sequence-set of UIDs
/// returned in a `VANISHED` response /// returned in a `VANISHED` response
pub fn from_vanished(v: Vec<RangeInclusive<u32>>) -> Self { pub fn from_vanished(v: Vec<RangeInclusive<u32>>, mod_seq: Option<u64>) -> Self {
Deleted::Vanished(v) Self {
messages: DeletedMessages::Vanished(v),
mod_seq: mod_seq,
}
} }
/// Return an iterator over message sequence numbers from an `EXPUNGE` /// 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` /// can be used to ensure only sequence numbers returned in an `EXPUNGE`
/// response are processed. /// response are processed.
pub fn seqs(&self) -> impl Iterator<Item = Seq> + '_ { pub fn seqs(&self) -> impl Iterator<Item = Seq> + '_ {
match self { match &self.messages {
Deleted::Expunged(s) => s.iter(), DeletedMessages::Expunged(s) => s.iter(),
Deleted::Vanished(_) => [].iter(), DeletedMessages::Vanished(_) => [].iter(),
} }
.copied() .copied()
} }
@ -75,18 +92,18 @@ impl Deleted {
/// If the client is expecting UIDs this function can be used to ensure /// If the client is expecting UIDs this function can be used to ensure
/// only UIDs are processed. /// only UIDs are processed.
pub fn uids(&self) -> impl Iterator<Item = Uid> + '_ { pub fn uids(&self) -> impl Iterator<Item = Uid> + '_ {
match self { match &self.messages {
Deleted::Expunged(_) => [].iter(), DeletedMessages::Expunged(_) => [].iter(),
Deleted::Vanished(s) => s.iter(), DeletedMessages::Vanished(s) => s.iter(),
} }
.flat_map(|range| range.clone()) .flat_map(|range| range.clone())
} }
/// Return if the set is empty /// Return if the set is empty
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
match self { match &self.messages {
Deleted::Expunged(v) => v.is_empty(), DeletedMessages::Expunged(v) => v.is_empty(),
Deleted::Vanished(v) => v.is_empty(), DeletedMessages::Vanished(v) => v.is_empty(),
} }
} }
} }
@ -96,9 +113,9 @@ impl<'a> IntoIterator for &'a Deleted {
type IntoIter = Box<dyn Iterator<Item = u32> + 'a>; type IntoIter = Box<dyn Iterator<Item = u32> + 'a>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
match self { match &self.messages {
Deleted::Expunged(_) => Box::new(self.seqs()), DeletedMessages::Expunged(_) => Box::new(self.seqs()),
Deleted::Vanished(_) => Box::new(self.uids()), DeletedMessages::Vanished(_) => Box::new(self.uids()),
} }
} }
} }
@ -109,7 +126,7 @@ mod test {
#[test] #[test]
fn seq() { 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(); let mut i = seqs.into_iter();
assert_eq!(Some(3), i.next()); assert_eq!(Some(3), i.next());
assert_eq!(Some(6), i.next()); assert_eq!(Some(6), i.next());
@ -117,14 +134,14 @@ mod test {
assert_eq!(Some(12), i.next()); assert_eq!(Some(12), i.next());
assert_eq!(None, 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(); let mut i = seqs.into_iter();
assert_eq!(None, i.next()); assert_eq!(None, i.next());
} }
#[test] #[test]
fn seq_set() { 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(); let mut i = uids.into_iter();
assert_eq!(Some(1), i.next()); assert_eq!(Some(1), i.next());
assert_eq!(Some(3), i.next()); assert_eq!(Some(3), i.next());
@ -135,13 +152,13 @@ mod test {
assert_eq!(Some(12), i.next()); assert_eq!(Some(12), i.next());
assert_eq!(None, 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()); assert_eq!(None, uids.into_iter().next());
} }
#[test] #[test]
fn seqs() { 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; let mut count: u32 = 0;
for seq in seqs.seqs() { for seq in seqs.seqs() {
count += 3; count += 3;
@ -152,7 +169,7 @@ mod test {
#[test] #[test]
fn uids() { 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; let mut count: u32 = 0;
for uid in uids.uids() { for uid in uids.uids() {
count += 1; count += 1;
@ -163,7 +180,7 @@ mod test {
#[test] #[test]
fn generic_iteration() { 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; let mut count: u32 = 0;
for seq in &seqs { for seq in &seqs {
count += 3; count += 3;
@ -171,7 +188,7 @@ mod test {
} }
assert_eq!(count, 12); 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; let mut count: u32 = 0;
for uid in &uids { for uid in &uids {
count += 1; count += 1;