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.
This commit is contained in:
parent
d381723deb
commit
a9788ad1e0
4 changed files with 234 additions and 27 deletions
|
|
@ -687,7 +687,7 @@ impl<T: Read + Write> Session<T> {
|
||||||
/// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently
|
/// 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.
|
/// 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<Vec<Seq>> {
|
pub fn expunge(&mut self) -> Result<Deleted> {
|
||||||
self.run_command_and_read_response("EXPUNGE")
|
self.run_command_and_read_response("EXPUNGE")
|
||||||
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
|
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
|
||||||
}
|
}
|
||||||
|
|
@ -714,7 +714,7 @@ 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<Vec<Uid>> {
|
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_and_read_response(&format!("UID EXPUNGE {}", uid_set.as_ref()))
|
||||||
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
|
.and_then(|lines| parse_expunge(lines, &mut self.unsolicited_responses_tx))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
src/parse.rs
73
src/parse.rs
|
|
@ -127,13 +127,42 @@ pub fn parse_fetches(
|
||||||
pub fn parse_expunge(
|
pub fn parse_expunge(
|
||||||
lines: Vec<u8>,
|
lines: Vec<u8>,
|
||||||
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
) -> Result<Vec<u32>> {
|
) -> Result<Deleted> {
|
||||||
let f = |resp| match resp {
|
let mut lines: &[u8] = &lines;
|
||||||
Response::Expunge(id) => Ok(MapOrNot::Map(id)),
|
let mut expunged = Vec::new();
|
||||||
resp => Ok(MapOrNot::Not(resp)),
|
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(
|
pub fn parse_capabilities(
|
||||||
|
|
@ -561,31 +590,25 @@ mod tests {
|
||||||
fn parse_vanished_test() {
|
fn parse_vanished_test() {
|
||||||
// VANISHED can appear if the user has enabled QRESYNC (RFC 7162), in response to
|
// 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
|
// 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
|
// two cases the VANISHED response will be a different type than expected
|
||||||
// and so goes into the unsolicited respones channel. In the last case, VANISHED is
|
// and so goes into the unsolicited responses channel.
|
||||||
// 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 lines = b"* VANISHED 3\r\n";
|
||||||
let (mut send, recv) = mpsc::channel();
|
let (mut send, recv) = mpsc::channel();
|
||||||
let resp = parse_expunge(lines.to_vec(), &mut send).unwrap();
|
let resp = parse_expunge(lines.to_vec(), &mut send).unwrap();
|
||||||
assert!(resp.is_empty());
|
|
||||||
|
|
||||||
match recv.try_recv().unwrap() {
|
// Should be not empty, and have no seqs
|
||||||
UnsolicitedResponse::Vanished { earlier, uids } => {
|
assert!(!resp.is_empty());
|
||||||
assert!(!earlier);
|
assert_eq!(None, resp.seqs().next());
|
||||||
assert_eq!(uids.len(), 1);
|
|
||||||
assert_eq!(*uids[0].start(), 3);
|
// Should have one UID response
|
||||||
assert_eq!(*uids[0].end(), 3);
|
let mut uids = resp.uids();
|
||||||
}
|
assert_eq!(Some(3), uids.next());
|
||||||
what => panic!("Unexpected response in unsolicited responses: {:?}", what),
|
assert_eq!(None, uids.next());
|
||||||
}
|
|
||||||
|
// Should be nothing in the unsolicited responses channel
|
||||||
assert!(recv.try_recv().is_err());
|
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\
|
let lines = b"* VANISHED (EARLIER) 3:8,12,50:60\r\n\
|
||||||
* 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n";
|
* 49 FETCH (UID 117 FLAGS (\\Seen \\Answered) MODSEQ (90060115194045001))\r\n";
|
||||||
|
|
||||||
|
|
|
||||||
180
src/types/deleted.rs
Normal file
180
src/types/deleted.rs
Normal file
|
|
@ -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<Seq>),
|
||||||
|
/// Message UIDs given in a `VANISHED` response.
|
||||||
|
Vanished(Vec<RangeInclusive<Uid>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u32>) -> 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<RangeInclusive<u32>>) -> 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<Item = Seq> + '_ {
|
||||||
|
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<Item = Uid> + '_ {
|
||||||
|
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<dyn Iterator<Item = u32> + '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -216,6 +216,9 @@ pub use self::name::{Name, NameAttribute};
|
||||||
mod capabilities;
|
mod capabilities;
|
||||||
pub use self::capabilities::Capabilities;
|
pub use self::capabilities::Capabilities;
|
||||||
|
|
||||||
|
mod deleted;
|
||||||
|
pub use self::deleted::Deleted;
|
||||||
|
|
||||||
/// re-exported from imap_proto;
|
/// re-exported from imap_proto;
|
||||||
pub use imap_proto::StatusAttribute;
|
pub use imap_proto::StatusAttribute;
|
||||||
|
|
||||||
|
|
@ -350,6 +353,7 @@ impl<D> ZeroCopy<D> {
|
||||||
///
|
///
|
||||||
/// Only safe if `D` contains no references into the underlying input stream (i.e., the `owned`
|
/// Only safe if `D` contains no references into the underlying input stream (i.e., the `owned`
|
||||||
/// passed to `ZeroCopy::new`).
|
/// passed to `ZeroCopy::new`).
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) unsafe fn take(self) -> D {
|
pub(crate) unsafe fn take(self) -> D {
|
||||||
self.derived
|
self.derived
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue