Merge pull request #172 from mordak/mod_seq_vanished
Support HIGHESTMODSEQ and VANISHED
This commit is contained in:
commit
17055d02a5
9 changed files with 344 additions and 36 deletions
|
|
@ -6,8 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- VANISHED support in EXPUNGE responses and unsolicited responses (#172).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- MSRV increased to 1.43 for nom6 and bitvec
|
||||||
|
- `expunge` and `uid_expunge` return `Result<Deleted>` instead of `Result<Vec<u32>>`.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,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.13"
|
base64 = "0.13"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,12 @@ jobs:
|
||||||
- job: msrv
|
- job: msrv
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-latest
|
vmImage: ubuntu-latest
|
||||||
displayName: "Minimum supported Rust version: 1.40.0"
|
displayName: "Minimum supported Rust version: 1.43.0"
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
steps:
|
steps:
|
||||||
- template: install-rust.yml@templates
|
- template: install-rust.yml@templates
|
||||||
parameters:
|
parameters:
|
||||||
rust: 1.40.0 # static-assertions (1.37+) and base64 (1.40+)
|
rust: 1.43.0 # nom6 depends on bitvec (1.43+)
|
||||||
- script: cargo check
|
- script: cargo check
|
||||||
displayName: cargo check
|
displayName: cargo check
|
||||||
- job: integration
|
- job: integration
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
use base64;
|
|
||||||
use bufstream::BufStream;
|
use bufstream::BufStream;
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
use native_tls::{TlsConnector, TlsStream};
|
use native_tls::{TlsConnector, TlsStream};
|
||||||
use nom;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{TcpStream, ToSocketAddrs};
|
use std::net::{TcpStream, ToSocketAddrs};
|
||||||
|
|
@ -838,7 +836,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))
|
||||||
}
|
}
|
||||||
|
|
@ -865,7 +863,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))
|
||||||
}
|
}
|
||||||
|
|
@ -1369,10 +1367,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 {
|
||||||
|
|
@ -1723,6 +1721,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));
|
||||||
|
|
@ -1769,6 +1768,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));
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
/// A set of errors that can occur in the IMAP client
|
/// A set of errors that can occur in the IMAP client
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||||
Io(IoError),
|
Io(IoError),
|
||||||
|
|
@ -43,8 +44,6 @@ pub enum Error {
|
||||||
Validate(ValidateError),
|
Validate(ValidateError),
|
||||||
/// Error appending an e-mail.
|
/// Error appending an e-mail.
|
||||||
Append,
|
Append,
|
||||||
#[doc(hidden)]
|
|
||||||
__Nonexhaustive,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IoError> for Error {
|
impl From<IoError> for Error {
|
||||||
|
|
@ -99,7 +98,6 @@ impl fmt::Display for Error {
|
||||||
Error::Bad(ref data) => write!(f, "Bad Response: {}", data),
|
Error::Bad(ref data) => write!(f, "Bad Response: {}", data),
|
||||||
Error::ConnectionLost => f.write_str("Connection Lost"),
|
Error::ConnectionLost => f.write_str("Connection Lost"),
|
||||||
Error::Append => f.write_str("Could not append mail to mailbox"),
|
Error::Append => f.write_str("Could not append mail to mailbox"),
|
||||||
Error::__Nonexhaustive => f.write_str("Unknown"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +117,6 @@ impl StdError for Error {
|
||||||
Error::No(_) => "No Response",
|
Error::No(_) => "No Response",
|
||||||
Error::ConnectionLost => "Connection lost",
|
Error::ConnectionLost => "Connection lost",
|
||||||
Error::Append => "Could not append mail to mailbox",
|
Error::Append => "Could not append mail to mailbox",
|
||||||
Error::__Nonexhaustive => "Unknown",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
117
src/parse.rs
117
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;
|
||||||
|
|
||||||
|
|
@ -127,13 +127,46 @@ 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 the server sends a VANISHED response then they must only send VANISHED
|
||||||
|
// in lieu of EXPUNGE responses for the rest of this connection, so it is
|
||||||
|
// always one or the other.
|
||||||
|
// https://tools.ietf.org/html/rfc7162#section-3.2.10
|
||||||
|
if !vanished.is_empty() {
|
||||||
|
Ok(Deleted::from_vanished(vanished))
|
||||||
|
} else {
|
||||||
|
Ok(Deleted::from_expunged(expunged))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_capabilities(
|
pub fn parse_capabilities(
|
||||||
|
|
@ -143,7 +176,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 +212,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 +233,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 +245,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 +290,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 +323,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);
|
||||||
}
|
}
|
||||||
|
|
@ -338,6 +375,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);
|
||||||
}
|
}
|
||||||
|
|
@ -554,4 +596,53 @@ 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 response will be a different type than expected
|
||||||
|
// and so goes into the unsolicited responses channel.
|
||||||
|
let lines = b"* VANISHED 3\r\n";
|
||||||
|
let (mut send, recv) = mpsc::channel();
|
||||||
|
let resp = parse_expunge(lines.to_vec(), &mut send).unwrap();
|
||||||
|
|
||||||
|
// Should be not empty, and have no seqs
|
||||||
|
assert!(!resp.is_empty());
|
||||||
|
assert_eq!(None, resp.seqs().next());
|
||||||
|
|
||||||
|
// Should have one UID response
|
||||||
|
let mut uids = resp.uids();
|
||||||
|
assert_eq!(Some(3), uids.next());
|
||||||
|
assert_eq!(None, uids.next());
|
||||||
|
|
||||||
|
// Should be nothing in the unsolicited responses channel
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ use std::fmt;
|
||||||
/// Meta-information about an IMAP mailbox, as returned by
|
/// Meta-information about an IMAP mailbox, as returned by
|
||||||
/// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends.
|
/// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Mailbox {
|
pub struct Mailbox {
|
||||||
/// Defined flags in the mailbox. See the description of the [FLAGS
|
/// Defined flags in the mailbox. See the description of the [FLAGS
|
||||||
/// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail.
|
/// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail.
|
||||||
|
|
@ -35,6 +36,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 mailbox. 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 +52,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 +62,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,13 +172,13 @@ impl Flag<'static> {
|
||||||
impl<'a> fmt::Display for Flag<'a> {
|
impl<'a> fmt::Display for Flag<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Flag::Seen => write!(f, "{}", "\\Seen"),
|
Flag::Seen => write!(f, "\\Seen"),
|
||||||
Flag::Answered => write!(f, "{}", "\\Answered"),
|
Flag::Answered => write!(f, "\\Answered"),
|
||||||
Flag::Flagged => write!(f, "{}", "\\Flagged"),
|
Flag::Flagged => write!(f, "\\Flagged"),
|
||||||
Flag::Deleted => write!(f, "{}", "\\Deleted"),
|
Flag::Deleted => write!(f, "\\Deleted"),
|
||||||
Flag::Draft => write!(f, "{}", "\\Draft"),
|
Flag::Draft => write!(f, "\\Draft"),
|
||||||
Flag::Recent => write!(f, "{}", "\\Recent"),
|
Flag::Recent => write!(f, "\\Recent"),
|
||||||
Flag::MayCreate => write!(f, "{}", "\\*"),
|
Flag::MayCreate => write!(f, "\\*"),
|
||||||
Flag::Custom(ref s) => write!(f, "{}", s),
|
Flag::Custom(ref s) => write!(f, "{}", s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -279,6 +282,32 @@ pub enum UnsolicitedResponse {
|
||||||
// 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>>,
|
||||||
|
},
|
||||||
|
|
||||||
/// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that
|
/// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that
|
||||||
/// identifies the flags (at a minimum, the system-defined flags) that are applicable in the
|
/// identifies the flags (at a minimum, the system-defined flags) that are applicable in the
|
||||||
/// mailbox. Flags other than the system flags can also exist, depending on server
|
/// mailbox. Flags other than the system flags can also exist, depending on server
|
||||||
|
|
@ -334,6 +363,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