Implement a method to pass unilateral responses while IDLE.
While IDLE, the server sends unilateral responses to notify the client of changes to the mailbox as they happen. Instead of always exiting the IDLE on any change, allow the caller to pass a callback function which receives the messages and returns an action to either Continue IDLE or Stop and exit. For clients wishing to use the previous behaviour, a callback_stop convenience function is provided that terminates the IDLE on any change to the mailbox.
This commit is contained in:
parent
39a78fdea4
commit
529401a36d
4 changed files with 452 additions and 183 deletions
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
use crate::client::Session;
|
use crate::client::Session;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::parse::parse_idle;
|
||||||
|
use crate::types::UnsolicitedResponse;
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
use native_tls::TlsStream;
|
use native_tls::TlsStream;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
|
@ -13,8 +15,31 @@ use std::time::Duration;
|
||||||
///
|
///
|
||||||
/// The handle blocks using the [`IDLE` command](https://tools.ietf.org/html/rfc2177#section-3)
|
/// The handle blocks using the [`IDLE` command](https://tools.ietf.org/html/rfc2177#section-3)
|
||||||
/// specificed in [RFC 2177](https://tools.ietf.org/html/rfc2177) until the underlying server state
|
/// specificed in [RFC 2177](https://tools.ietf.org/html/rfc2177) until the underlying server state
|
||||||
/// changes in some way. While idling does inform the client what changes happened on the server,
|
/// changes in some way.
|
||||||
/// this implementation will currently just block until _anything_ changes, and then notify the
|
///
|
||||||
|
/// Each of the `wait` functions takes a callback function which receives any responses
|
||||||
|
/// that arrive on the channel while IDLE. The callback function implements whatever
|
||||||
|
/// logic is needed to handle the IDLE response, and then returns a [`CallbackAction`]
|
||||||
|
/// to `Continue` or `Stop` listening on the channel.
|
||||||
|
/// For users that want the IDLE to exit on any change (the behavior proior to version 3.0),
|
||||||
|
/// a convenience callback function `callback_stop` is provided.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use native_tls::TlsConnector;
|
||||||
|
/// use imap::extensions::idle;
|
||||||
|
/// let ssl_conn = TlsConnector::builder().build().unwrap();
|
||||||
|
/// let client = imap::connect(("example.com", 993), "example.com", &ssl_conn)
|
||||||
|
/// .expect("Could not connect to imap server");
|
||||||
|
/// let mut imap = client.login("user@example.com", "password")
|
||||||
|
/// .expect("Could not authenticate");
|
||||||
|
/// imap.select("INBOX")
|
||||||
|
/// .expect("Could not select mailbox");
|
||||||
|
///
|
||||||
|
/// let idle = imap.idle().expect("Could not IDLE");
|
||||||
|
///
|
||||||
|
/// // Exit on any mailbox change
|
||||||
|
/// let result = idle.wait_keepalive(idle::callback_stop);
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// Note that the server MAY consider a client inactive if it has an IDLE command running, and if
|
/// Note that the server MAY consider a client inactive if it has an IDLE command running, and if
|
||||||
/// such a server has an inactivity timeout it MAY log the client off implicitly at the end of its
|
/// such a server has an inactivity timeout it MAY log the client off implicitly at the end of its
|
||||||
|
|
@ -40,6 +65,21 @@ pub enum WaitOutcome {
|
||||||
MailboxChanged,
|
MailboxChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return type for IDLE response callbacks. Tells the IDLE connection
|
||||||
|
/// if it should continue monitoring the connection or not.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum CallbackAction {
|
||||||
|
/// Continue receiving responses from the IDLE connection.
|
||||||
|
Continue,
|
||||||
|
/// Stop receiving responses, and exit the IDLE wait.
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience function to always cause the IDLE handler to exit on any change.
|
||||||
|
pub fn callback_stop(_response: UnsolicitedResponse) -> CallbackAction {
|
||||||
|
CallbackAction::Stop
|
||||||
|
}
|
||||||
|
|
||||||
/// Must be implemented for a transport in order for a `Session` using that transport to support
|
/// Must be implemented for a transport in order for a `Session` using that transport to support
|
||||||
/// operations with timeouts.
|
/// operations with timeouts.
|
||||||
///
|
///
|
||||||
|
|
@ -100,37 +140,65 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> {
|
||||||
/// Internal helper that doesn't consume self.
|
/// Internal helper that doesn't consume self.
|
||||||
///
|
///
|
||||||
/// This is necessary so that we can keep using the inner `Session` in `wait_keepalive`.
|
/// This is necessary so that we can keep using the inner `Session` in `wait_keepalive`.
|
||||||
fn wait_inner(&mut self, reconnect: bool) -> Result<WaitOutcome> {
|
fn wait_inner<F>(&mut self, reconnect: bool, mut callback: F) -> Result<WaitOutcome>
|
||||||
|
where
|
||||||
|
F: FnMut(UnsolicitedResponse) -> CallbackAction,
|
||||||
|
{
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
loop {
|
let result = loop {
|
||||||
let result = match self.session.readline(&mut v).map(|_| ()) {
|
let rest = match self.session.readline(&mut v) {
|
||||||
Err(Error::Io(ref e))
|
Err(Error::Io(ref e))
|
||||||
if e.kind() == io::ErrorKind::TimedOut
|
if e.kind() == io::ErrorKind::TimedOut
|
||||||
|| e.kind() == io::ErrorKind::WouldBlock =>
|
|| e.kind() == io::ErrorKind::WouldBlock =>
|
||||||
{
|
{
|
||||||
if reconnect {
|
break Ok(WaitOutcome::TimedOut);
|
||||||
self.terminate()?;
|
|
||||||
self.init()?;
|
|
||||||
return self.wait_inner(reconnect);
|
|
||||||
}
|
}
|
||||||
Ok(WaitOutcome::TimedOut)
|
Ok(_len) => {
|
||||||
}
|
|
||||||
Ok(()) => Ok(WaitOutcome::MailboxChanged),
|
|
||||||
Err(r) => Err(r),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
// Handle Dovecot's imap_idle_notify_interval message
|
// Handle Dovecot's imap_idle_notify_interval message
|
||||||
if v.eq_ignore_ascii_case(b"* OK Still here\r\n") {
|
if v.eq_ignore_ascii_case(b"* OK Still here\r\n") {
|
||||||
v.clear();
|
v.clear();
|
||||||
} else {
|
continue;
|
||||||
break Ok(result);
|
|
||||||
}
|
}
|
||||||
|
match parse_idle(&v) {
|
||||||
|
(_rest, Some(Err(r))) => break Err(r),
|
||||||
|
(rest, Some(Ok(response))) => {
|
||||||
|
if let CallbackAction::Stop = callback(response) {
|
||||||
|
break Ok(WaitOutcome::MailboxChanged);
|
||||||
|
}
|
||||||
|
rest
|
||||||
|
}
|
||||||
|
(rest, None) => rest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(r) => break Err(r),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update remaining data with unparsed data if needed.
|
||||||
|
if rest.is_empty() {
|
||||||
|
v.clear();
|
||||||
|
} else if rest.len() != v.len() {
|
||||||
|
v = rest.into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reconnect on timeout if needed
|
||||||
|
match (reconnect, result) {
|
||||||
|
(true, Ok(WaitOutcome::TimedOut)) => {
|
||||||
|
self.terminate()?;
|
||||||
|
self.init()?;
|
||||||
|
self.wait_inner(reconnect, callback)
|
||||||
|
}
|
||||||
|
(_, result) => result,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the selected mailbox changes.
|
/// Block until the given callback returns `Stop`, or until an unhandled
|
||||||
pub fn wait(mut self) -> Result<()> {
|
/// response arrives on the IDLE channel.
|
||||||
self.wait_inner(true).map(|_| ())
|
pub fn wait<F>(mut self, callback: F) -> Result<()>
|
||||||
|
where
|
||||||
|
F: FnMut(UnsolicitedResponse) -> CallbackAction,
|
||||||
|
{
|
||||||
|
self.wait_inner(true, callback).map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +210,8 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> {
|
||||||
self.keepalive = interval;
|
self.keepalive = interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the selected mailbox changes.
|
/// Block until the given callback returns `Stop`, or until an unhandled
|
||||||
|
/// response arrives on the IDLE channel.
|
||||||
///
|
///
|
||||||
/// This method differs from [`Handle::wait`] in that it will periodically refresh the IDLE
|
/// This method differs from [`Handle::wait`] in that it will periodically refresh the IDLE
|
||||||
/// connection, to prevent the server from timing out our connection. The keepalive interval is
|
/// connection, to prevent the server from timing out our connection. The keepalive interval is
|
||||||
|
|
@ -150,7 +219,10 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> {
|
||||||
/// [`Handle::set_keepalive`].
|
/// [`Handle::set_keepalive`].
|
||||||
///
|
///
|
||||||
/// This is the recommended method to use for waiting.
|
/// This is the recommended method to use for waiting.
|
||||||
pub fn wait_keepalive(self) -> Result<()> {
|
pub fn wait_keepalive<F>(self, callback: F) -> Result<()>
|
||||||
|
where
|
||||||
|
F: FnMut(UnsolicitedResponse) -> CallbackAction,
|
||||||
|
{
|
||||||
// The server MAY consider a client inactive if it has an IDLE command
|
// The server MAY consider a client inactive if it has an IDLE command
|
||||||
// running, and if such a server has an inactivity timeout it MAY log
|
// running, and if such a server has an inactivity timeout it MAY log
|
||||||
// the client off implicitly at the end of its timeout period. Because
|
// the client off implicitly at the end of its timeout period. Because
|
||||||
|
|
@ -159,26 +231,42 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> {
|
||||||
// This still allows a client to receive immediate mailbox updates even
|
// This still allows a client to receive immediate mailbox updates even
|
||||||
// though it need only "poll" at half hour intervals.
|
// though it need only "poll" at half hour intervals.
|
||||||
let keepalive = self.keepalive;
|
let keepalive = self.keepalive;
|
||||||
self.timed_wait(keepalive, true).map(|_| ())
|
self.timed_wait(keepalive, true, callback).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the selected mailbox changes, or until the given amount of time has expired.
|
/// Block until the given amount of time has elapsed, or the given callback
|
||||||
|
/// returns `Stop`, or until an unhandled response arrives on the IDLE channel.
|
||||||
#[deprecated(note = "use wait_with_timeout instead")]
|
#[deprecated(note = "use wait_with_timeout instead")]
|
||||||
pub fn wait_timeout(self, timeout: Duration) -> Result<()> {
|
pub fn wait_timeout<F>(self, timeout: Duration, callback: F) -> Result<()>
|
||||||
self.wait_with_timeout(timeout).map(|_| ())
|
where
|
||||||
|
F: FnMut(UnsolicitedResponse) -> CallbackAction,
|
||||||
|
{
|
||||||
|
self.wait_with_timeout(timeout, callback).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until the selected mailbox changes, or until the given amount of time has expired.
|
/// Block until the given amount of time has elapsed, or the given callback
|
||||||
pub fn wait_with_timeout(self, timeout: Duration) -> Result<WaitOutcome> {
|
/// returns `Stop`, or until an unhandled response arrives on the IDLE channel.
|
||||||
self.timed_wait(timeout, false)
|
pub fn wait_with_timeout<F>(self, timeout: Duration, callback: F) -> Result<WaitOutcome>
|
||||||
|
where
|
||||||
|
F: FnMut(UnsolicitedResponse) -> CallbackAction,
|
||||||
|
{
|
||||||
|
self.timed_wait(timeout, false, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timed_wait(mut self, timeout: Duration, reconnect: bool) -> Result<WaitOutcome> {
|
fn timed_wait<F>(
|
||||||
|
mut self,
|
||||||
|
timeout: Duration,
|
||||||
|
reconnect: bool,
|
||||||
|
callback: F,
|
||||||
|
) -> Result<WaitOutcome>
|
||||||
|
where
|
||||||
|
F: FnMut(UnsolicitedResponse) -> CallbackAction,
|
||||||
|
{
|
||||||
self.session
|
self.session
|
||||||
.stream
|
.stream
|
||||||
.get_mut()
|
.get_mut()
|
||||||
.set_read_timeout(Some(timeout))?;
|
.set_read_timeout(Some(timeout))?;
|
||||||
let res = self.wait_inner(reconnect);
|
let res = self.wait_inner(reconnect, callback);
|
||||||
let _ = self.session.stream.get_mut().set_read_timeout(None).is_ok();
|
let _ = self.session.stream.get_mut().set_read_timeout(None).is_ok();
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
src/parse.rs
67
src/parse.rs
|
|
@ -2,6 +2,7 @@ use imap_proto::{MailboxDatum, Response, ResponseCode};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use super::error::{Error, ParseError, Result};
|
use super::error::{Error, ParseError, Result};
|
||||||
|
|
@ -350,6 +351,21 @@ pub fn parse_ids(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a single unsolicited response from IDLE responses.
|
||||||
|
pub fn parse_idle(lines: &[u8]) -> (&[u8], Option<Result<UnsolicitedResponse>>) {
|
||||||
|
match imap_proto::parser::parse_response(lines) {
|
||||||
|
Ok((rest, response)) => match UnsolicitedResponse::try_from(response) {
|
||||||
|
Ok(unsolicited) => (rest, Some(Ok(unsolicited))),
|
||||||
|
Err(res) => (rest, Some(Err(res.into()))),
|
||||||
|
},
|
||||||
|
Err(nom::Err::Incomplete(_)) => (lines, None),
|
||||||
|
Err(_) => (
|
||||||
|
lines,
|
||||||
|
Some(Err(Error::Parse(ParseError::Invalid(lines.to_vec())))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is simply a unilateral server response (see Section 7 of RFC 3501).
|
// Check if this is simply a unilateral server response (see Section 7 of RFC 3501).
|
||||||
//
|
//
|
||||||
// Returns `None` if the response was handled, `Some(res)` if not.
|
// Returns `None` if the response was handled, `Some(res)` if not.
|
||||||
|
|
@ -357,52 +373,13 @@ pub(crate) fn try_handle_unilateral<'a>(
|
||||||
res: Response<'a>,
|
res: Response<'a>,
|
||||||
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
|
||||||
) -> Option<Response<'a>> {
|
) -> Option<Response<'a>> {
|
||||||
match res {
|
match UnsolicitedResponse::try_from(res) {
|
||||||
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
|
Ok(response) => {
|
||||||
unsolicited
|
unsolicited.send(response).ok();
|
||||||
.send(UnsolicitedResponse::Status {
|
|
||||||
mailbox: mailbox.into(),
|
|
||||||
attributes: status,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
Response::MailboxData(MailboxDatum::Recent(n)) => {
|
|
||||||
unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap();
|
|
||||||
}
|
|
||||||
Response::MailboxData(MailboxDatum::Flags(flags)) => {
|
|
||||||
unsolicited
|
|
||||||
.send(UnsolicitedResponse::Flags(
|
|
||||||
flags
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| Flag::from(s.to_string()))
|
|
||||||
.collect(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
Response::MailboxData(MailboxDatum::Exists(n)) => {
|
|
||||||
unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap();
|
|
||||||
}
|
|
||||||
Response::Expunge(n) => {
|
|
||||||
unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap();
|
|
||||||
}
|
|
||||||
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => {
|
|
||||||
unsolicited
|
|
||||||
.send(UnsolicitedResponse::Metadata {
|
|
||||||
mailbox: mailbox.to_string(),
|
|
||||||
metadata_entries: values.iter().map(|s| s.to_string()).collect(),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
Response::Vanished { earlier, uids } => {
|
|
||||||
unsolicited
|
|
||||||
.send(UnsolicitedResponse::Vanished { earlier, uids })
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
res => {
|
|
||||||
return Some(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
None
|
||||||
|
}
|
||||||
|
Err(unhandled) => Some(unhandled),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
109
src/types/mod.rs
109
src/types/mod.rs
|
|
@ -220,113 +220,8 @@ pub use self::capabilities::Capabilities;
|
||||||
mod deleted;
|
mod deleted;
|
||||||
pub use self::deleted::Deleted;
|
pub use self::deleted::Deleted;
|
||||||
|
|
||||||
/// re-exported from imap_proto;
|
mod unsolicited_response;
|
||||||
pub use imap_proto::StatusAttribute;
|
pub use self::unsolicited_response::{AttributeValue, ResponseCode, UnsolicitedResponse};
|
||||||
|
|
||||||
/// Responses that the server sends that are not related to the current command.
|
|
||||||
/// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able
|
|
||||||
/// to accept any response at any time. These are the ones we've encountered in the wild.
|
|
||||||
///
|
|
||||||
/// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder,
|
|
||||||
/// so the user must take care when interpreting these.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum UnsolicitedResponse {
|
|
||||||
/// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4).
|
|
||||||
Status {
|
|
||||||
/// The mailbox that this status response is for.
|
|
||||||
mailbox: String,
|
|
||||||
/// The attributes of this mailbox.
|
|
||||||
attributes: Vec<StatusAttribute>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2)
|
|
||||||
/// indicating the number of messages with the `\Recent` flag set. This response occurs if the
|
|
||||||
/// size of the mailbox changes (e.g., new messages arrive).
|
|
||||||
///
|
|
||||||
/// > Note: It is not guaranteed that the message sequence
|
|
||||||
/// > numbers of recent messages will be a contiguous range of
|
|
||||||
/// > the highest n messages in the mailbox (where n is the
|
|
||||||
/// > value reported by the `RECENT` response). Examples of
|
|
||||||
/// > situations in which this is not the case are: multiple
|
|
||||||
/// > clients having the same mailbox open (the first session
|
|
||||||
/// > to be notified will see it as recent, others will
|
|
||||||
/// > probably see it as non-recent), and when the mailbox is
|
|
||||||
/// > re-ordered by a non-IMAP agent.
|
|
||||||
/// >
|
|
||||||
/// > The only reliable way to identify recent messages is to
|
|
||||||
/// > look at message flags to see which have the `\Recent` flag
|
|
||||||
/// > set, or to do a `SEARCH RECENT`.
|
|
||||||
Recent(u32),
|
|
||||||
|
|
||||||
/// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that
|
|
||||||
/// reports the number of messages in the mailbox. This response occurs if the size of the
|
|
||||||
/// mailbox changes (e.g., new messages arrive).
|
|
||||||
Exists(u32),
|
|
||||||
|
|
||||||
/// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that
|
|
||||||
/// reports that the specified message sequence number has been permanently removed from the
|
|
||||||
/// mailbox. The message sequence number for each successive message in the mailbox is
|
|
||||||
/// immediately decremented by 1, and this decrement is reflected in message sequence numbers
|
|
||||||
/// in subsequent responses (including other untagged `EXPUNGE` responses).
|
|
||||||
///
|
|
||||||
/// The EXPUNGE response also decrements the number of messages in the mailbox; it is not
|
|
||||||
/// necessary to send an `EXISTS` response with the new value.
|
|
||||||
///
|
|
||||||
/// As a result of the immediate decrement rule, message sequence numbers that appear in a set
|
|
||||||
/// of successive `EXPUNGE` responses depend upon whether the messages are removed starting
|
|
||||||
/// from lower numbers to higher numbers, or from higher numbers to lower numbers. For
|
|
||||||
/// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher"
|
|
||||||
/// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a
|
|
||||||
/// "higher to lower server" will send successive untagged `EXPUNGE` responses for message
|
|
||||||
/// 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 METADATA response (https://tools.ietf.org/html/rfc5464#section-4.4.2)
|
|
||||||
/// that reports a change in a server or mailbox annotation.
|
|
||||||
Metadata {
|
|
||||||
/// Mailbox name for which annotations were changed.
|
|
||||||
mailbox: String,
|
|
||||||
/// List of annotations that were changed.
|
|
||||||
metadata_entries: Vec<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// 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
|
|
||||||
/// implementation.
|
|
||||||
///
|
|
||||||
/// See [`Flag`] for details.
|
|
||||||
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
|
|
||||||
Flags(Vec<Flag<'static>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
||||||
/// which allows the parsed type to refer to data in the underlying stream instead of copying it.
|
/// which allows the parsed type to refer to data in the underlying stream instead of copying it.
|
||||||
|
|
|
||||||
309
src/types/unsolicited_response.rs
Normal file
309
src/types/unsolicited_response.rs
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use super::{Flag, Seq, Uid};
|
||||||
|
use crate::error::ParseError;
|
||||||
|
|
||||||
|
/// re-exported from imap_proto;
|
||||||
|
pub use imap_proto::StatusAttribute;
|
||||||
|
use imap_proto::{
|
||||||
|
AttributeValue as ImapProtoAttributeValue, MailboxDatum, Response,
|
||||||
|
ResponseCode as ImapProtoResponseCode, Status,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Responses that the server sends that are not related to the current command.
|
||||||
|
/// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able
|
||||||
|
/// to accept any response at any time. These are the ones we've encountered in the wild.
|
||||||
|
///
|
||||||
|
/// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder,
|
||||||
|
/// so the user must take care when interpreting these.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum UnsolicitedResponse {
|
||||||
|
/// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4).
|
||||||
|
Status {
|
||||||
|
/// The mailbox that this status response is for.
|
||||||
|
mailbox: String,
|
||||||
|
/// The attributes of this mailbox.
|
||||||
|
attributes: Vec<StatusAttribute>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2)
|
||||||
|
/// indicating the number of messages with the `\Recent` flag set. This response occurs if the
|
||||||
|
/// size of the mailbox changes (e.g., new messages arrive).
|
||||||
|
///
|
||||||
|
/// > Note: It is not guaranteed that the message sequence
|
||||||
|
/// > numbers of recent messages will be a contiguous range of
|
||||||
|
/// > the highest n messages in the mailbox (where n is the
|
||||||
|
/// > value reported by the `RECENT` response). Examples of
|
||||||
|
/// > situations in which this is not the case are: multiple
|
||||||
|
/// > clients having the same mailbox open (the first session
|
||||||
|
/// > to be notified will see it as recent, others will
|
||||||
|
/// > probably see it as non-recent), and when the mailbox is
|
||||||
|
/// > re-ordered by a non-IMAP agent.
|
||||||
|
/// >
|
||||||
|
/// > The only reliable way to identify recent messages is to
|
||||||
|
/// > look at message flags to see which have the `\Recent` flag
|
||||||
|
/// > set, or to do a `SEARCH RECENT`.
|
||||||
|
Recent(u32),
|
||||||
|
|
||||||
|
/// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that
|
||||||
|
/// reports the number of messages in the mailbox. This response occurs if the size of the
|
||||||
|
/// mailbox changes (e.g., new messages arrive).
|
||||||
|
Exists(u32),
|
||||||
|
|
||||||
|
/// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that
|
||||||
|
/// reports that the specified message sequence number has been permanently removed from the
|
||||||
|
/// mailbox. The message sequence number for each successive message in the mailbox is
|
||||||
|
/// immediately decremented by 1, and this decrement is reflected in message sequence numbers
|
||||||
|
/// in subsequent responses (including other untagged `EXPUNGE` responses).
|
||||||
|
///
|
||||||
|
/// The EXPUNGE response also decrements the number of messages in the mailbox; it is not
|
||||||
|
/// necessary to send an `EXISTS` response with the new value.
|
||||||
|
///
|
||||||
|
/// As a result of the immediate decrement rule, message sequence numbers that appear in a set
|
||||||
|
/// of successive `EXPUNGE` responses depend upon whether the messages are removed starting
|
||||||
|
/// from lower numbers to higher numbers, or from higher numbers to lower numbers. For
|
||||||
|
/// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher"
|
||||||
|
/// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a
|
||||||
|
/// "higher to lower server" will send successive untagged `EXPUNGE` responses for message
|
||||||
|
/// 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 METADATA response (https://tools.ietf.org/html/rfc5464#section-4.4.2)
|
||||||
|
/// that reports a change in a server or mailbox annotation.
|
||||||
|
Metadata {
|
||||||
|
/// Mailbox name for which annotations were changed.
|
||||||
|
mailbox: String,
|
||||||
|
/// List of annotations that were changed.
|
||||||
|
metadata_entries: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// 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
|
||||||
|
/// implementation.
|
||||||
|
///
|
||||||
|
/// See [`Flag`] for details.
|
||||||
|
// TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
|
||||||
|
Flags(Vec<Flag<'static>>),
|
||||||
|
|
||||||
|
/// An unsolicited `OK` response.
|
||||||
|
///
|
||||||
|
/// The `OK` response may have an optional `ResponseCode` that provides additional
|
||||||
|
/// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.1).
|
||||||
|
Ok {
|
||||||
|
/// Optional response code.
|
||||||
|
code: Option<ResponseCode>,
|
||||||
|
/// Information text that may be presented to the user.
|
||||||
|
information: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An unsolicited `BYE` response.
|
||||||
|
///
|
||||||
|
/// The `BYE` response may have an optional `ResponseCode` that provides additional
|
||||||
|
/// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.5).
|
||||||
|
Bye {
|
||||||
|
/// Optional response code.
|
||||||
|
code: Option<ResponseCode>,
|
||||||
|
/// Information text that may be presented to the user.
|
||||||
|
information: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An unsolicited `FETCH` response.
|
||||||
|
///
|
||||||
|
/// The server may unilaterally send `FETCH` responses, as described in
|
||||||
|
/// [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.4.2).
|
||||||
|
Fetch {
|
||||||
|
/// Message identifier.
|
||||||
|
id: u32,
|
||||||
|
/// Attribute values for this message.
|
||||||
|
attributes: Vec<AttributeValue>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to convert from a `imap_proto::Response`.
|
||||||
|
///
|
||||||
|
/// Not all `Response` variants are supported - only those which
|
||||||
|
/// are known or likely to be sent by a server as a unilateral response
|
||||||
|
/// during normal operations or during an IDLE session are implented.
|
||||||
|
///
|
||||||
|
/// If the conversion fails, the input `Reponse` is returned.
|
||||||
|
impl<'a> TryFrom<Response<'a>> for UnsolicitedResponse {
|
||||||
|
type Error = Response<'a>;
|
||||||
|
|
||||||
|
fn try_from(response: Response<'a>) -> Result<Self, Self::Error> {
|
||||||
|
match response {
|
||||||
|
Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
|
||||||
|
Ok(UnsolicitedResponse::Status {
|
||||||
|
mailbox: mailbox.into(),
|
||||||
|
attributes: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Response::MailboxData(MailboxDatum::Recent(n)) => Ok(UnsolicitedResponse::Recent(n)),
|
||||||
|
Response::MailboxData(MailboxDatum::Flags(flags)) => Ok(UnsolicitedResponse::Flags(
|
||||||
|
flags
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| Flag::from(s.to_string()))
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
|
Response::MailboxData(MailboxDatum::Exists(n)) => Ok(UnsolicitedResponse::Exists(n)),
|
||||||
|
Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => {
|
||||||
|
Ok(UnsolicitedResponse::Metadata {
|
||||||
|
mailbox: mailbox.to_string(),
|
||||||
|
metadata_entries: values.iter().map(|s| s.to_string()).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Response::Expunge(n) => Ok(UnsolicitedResponse::Expunge(n)),
|
||||||
|
Response::Vanished { earlier, uids } => {
|
||||||
|
Ok(UnsolicitedResponse::Vanished { earlier, uids })
|
||||||
|
}
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Ok,
|
||||||
|
ref code,
|
||||||
|
ref information,
|
||||||
|
} => {
|
||||||
|
let info = information.as_ref().map(|s| s.to_string());
|
||||||
|
if let Some(code) = code {
|
||||||
|
match ResponseCode::try_from(code) {
|
||||||
|
Ok(owncode) => Ok(UnsolicitedResponse::Ok {
|
||||||
|
code: Some(owncode),
|
||||||
|
information: info,
|
||||||
|
}),
|
||||||
|
_ => Err(response),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(UnsolicitedResponse::Ok {
|
||||||
|
code: None,
|
||||||
|
information: info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::Data {
|
||||||
|
status: Status::Bye,
|
||||||
|
ref code,
|
||||||
|
ref information,
|
||||||
|
} => {
|
||||||
|
let info = information.as_ref().map(|s| s.to_string());
|
||||||
|
if let Some(code) = code {
|
||||||
|
match ResponseCode::try_from(code) {
|
||||||
|
Ok(owncode) => Ok(UnsolicitedResponse::Bye {
|
||||||
|
code: Some(owncode),
|
||||||
|
information: info,
|
||||||
|
}),
|
||||||
|
_ => Err(response),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(UnsolicitedResponse::Bye {
|
||||||
|
code: None,
|
||||||
|
information: info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::Fetch(id, ref attributes) => {
|
||||||
|
match AttributeValue::try_from_imap_proto_vec(attributes) {
|
||||||
|
Ok(attrs) => Ok(UnsolicitedResponse::Fetch {
|
||||||
|
id,
|
||||||
|
attributes: attrs,
|
||||||
|
}),
|
||||||
|
_ => Err(response),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(response),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owned version of ResponseCode that wraps a subset of [`imap_proto::ResponseCode`]
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ResponseCode {
|
||||||
|
/// Highest ModSeq in the mailbox, [RFC4551](https://tools.ietf.org/html/rfc4551#section-3.1.1)
|
||||||
|
HighestModSeq(u64),
|
||||||
|
/// Next UID in the mailbox, [RFC3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1)
|
||||||
|
UidNext(Uid),
|
||||||
|
/// Mailbox UIDVALIDITY, [RFC3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1)
|
||||||
|
UidValidity(u32),
|
||||||
|
/// Sequence number of first message without the `\\Seen` flag
|
||||||
|
Unseen(Seq),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&ImapProtoResponseCode<'a>> for ResponseCode {
|
||||||
|
type Error = ParseError;
|
||||||
|
|
||||||
|
fn try_from(val: &ImapProtoResponseCode<'a>) -> Result<Self, Self::Error> {
|
||||||
|
match val {
|
||||||
|
ImapProtoResponseCode::HighestModSeq(seq) => Ok(ResponseCode::HighestModSeq(*seq)),
|
||||||
|
ImapProtoResponseCode::UidNext(uid) => Ok(ResponseCode::UidNext(*uid)),
|
||||||
|
ImapProtoResponseCode::UidValidity(uid) => Ok(ResponseCode::UidValidity(*uid)),
|
||||||
|
ImapProtoResponseCode::Unseen(seq) => Ok(ResponseCode::Unseen(*seq)),
|
||||||
|
unhandled => Err(ParseError::Unexpected(format!("{:?}", unhandled))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owned version of AttributeValue that wraps a subset of [`imap_proto::AttributeValue`].
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum AttributeValue {
|
||||||
|
/// Message Flags
|
||||||
|
Flags(Vec<Flag<'static>>),
|
||||||
|
/// Message ModSequence, [RFC4551](https://tools.ietf.org/html/rfc4551#section-3.3.2)
|
||||||
|
ModSeq(u64),
|
||||||
|
/// Message UID, [RFC3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1)
|
||||||
|
Uid(Uid),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&ImapProtoAttributeValue<'a>> for AttributeValue {
|
||||||
|
type Error = ParseError;
|
||||||
|
|
||||||
|
fn try_from(val: &ImapProtoAttributeValue<'a>) -> Result<Self, Self::Error> {
|
||||||
|
match val {
|
||||||
|
ImapProtoAttributeValue::Flags(flags) => {
|
||||||
|
let v = flags.iter().map(|v| Flag::from(v.to_string())).collect();
|
||||||
|
Ok(AttributeValue::Flags(v))
|
||||||
|
}
|
||||||
|
ImapProtoAttributeValue::ModSeq(seq) => Ok(AttributeValue::ModSeq(*seq)),
|
||||||
|
ImapProtoAttributeValue::Uid(uid) => Ok(AttributeValue::Uid(*uid)),
|
||||||
|
unhandled => Err(ParseError::Unexpected(format!("{:?}", unhandled))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AttributeValue {
|
||||||
|
fn try_from_imap_proto_vec(
|
||||||
|
vals: &[ImapProtoAttributeValue<'a>],
|
||||||
|
) -> Result<Vec<AttributeValue>, ParseError> {
|
||||||
|
let mut res = Vec::with_capacity(vals.len());
|
||||||
|
for attr in vals {
|
||||||
|
res.push(AttributeValue::try_from(attr)?);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue