From 529401a36d55e79b656ccf7ad2f5d48a6fe0ea08 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sun, 14 Mar 2021 19:51:00 -0400 Subject: [PATCH 01/20] 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. --- src/extensions/idle.rs | 150 ++++++++++++--- src/parse.rs | 65 ++----- src/types/mod.rs | 109 +---------- src/types/unsolicited_response.rs | 309 ++++++++++++++++++++++++++++++ 4 files changed, 451 insertions(+), 182 deletions(-) create mode 100644 src/types/unsolicited_response.rs diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 451a222e..de9ca8be 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -3,6 +3,8 @@ use crate::client::Session; use crate::error::{Error, Result}; +use crate::parse::parse_idle; +use crate::types::UnsolicitedResponse; #[cfg(feature = "tls")] use native_tls::TlsStream; 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) /// 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, -/// this implementation will currently just block until _anything_ changes, and then notify the +/// changes in some way. +/// +/// 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 /// 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, } +/// 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 /// operations with timeouts. /// @@ -100,37 +140,65 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { /// Internal helper that doesn't consume self. /// /// This is necessary so that we can keep using the inner `Session` in `wait_keepalive`. - fn wait_inner(&mut self, reconnect: bool) -> Result { + fn wait_inner(&mut self, reconnect: bool, mut callback: F) -> Result + where + F: FnMut(UnsolicitedResponse) -> CallbackAction, + { let mut v = Vec::new(); - loop { - let result = match self.session.readline(&mut v).map(|_| ()) { + let result = loop { + let rest = match self.session.readline(&mut v) { Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => { - if reconnect { - self.terminate()?; - self.init()?; - return self.wait_inner(reconnect); + break Ok(WaitOutcome::TimedOut); + } + Ok(_len) => { + // Handle Dovecot's imap_idle_notify_interval message + if v.eq_ignore_ascii_case(b"* OK Still here\r\n") { + v.clear(); + continue; + } + 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, } - Ok(WaitOutcome::TimedOut) } - Ok(()) => Ok(WaitOutcome::MailboxChanged), - Err(r) => Err(r), - }?; + Err(r) => break Err(r), + }; - // Handle Dovecot's imap_idle_notify_interval message - if v.eq_ignore_ascii_case(b"* OK Still here\r\n") { + // Update remaining data with unparsed data if needed. + if rest.is_empty() { v.clear(); - } else { - break Ok(result); + } 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. - pub fn wait(mut self) -> Result<()> { - self.wait_inner(true).map(|_| ()) + /// Block until the given callback returns `Stop`, or until an unhandled + /// response arrives on the IDLE channel. + pub fn wait(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; } - /// 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 /// 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`]. /// /// This is the recommended method to use for waiting. - pub fn wait_keepalive(self) -> Result<()> { + pub fn wait_keepalive(self, callback: F) -> Result<()> + where + F: FnMut(UnsolicitedResponse) -> CallbackAction, + { // 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 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 // though it need only "poll" at half hour intervals. 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")] - pub fn wait_timeout(self, timeout: Duration) -> Result<()> { - self.wait_with_timeout(timeout).map(|_| ()) + pub fn wait_timeout(self, timeout: Duration, callback: F) -> Result<()> + 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. - pub fn wait_with_timeout(self, timeout: Duration) -> Result { - self.timed_wait(timeout, false) + /// 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. + pub fn wait_with_timeout(self, timeout: Duration, callback: F) -> Result + where + F: FnMut(UnsolicitedResponse) -> CallbackAction, + { + self.timed_wait(timeout, false, callback) } - fn timed_wait(mut self, timeout: Duration, reconnect: bool) -> Result { + fn timed_wait( + mut self, + timeout: Duration, + reconnect: bool, + callback: F, + ) -> Result + where + F: FnMut(UnsolicitedResponse) -> CallbackAction, + { self.session .stream .get_mut() .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(); res } diff --git a/src/parse.rs b/src/parse.rs index 69f2a90b..b4871ca3 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -2,6 +2,7 @@ use imap_proto::{MailboxDatum, Response, ResponseCode}; use lazy_static::lazy_static; use regex::Regex; use std::collections::HashSet; +use std::convert::TryFrom; use std::sync::mpsc; 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>) { + 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). // // 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>, unsolicited: &mut mpsc::Sender, ) -> Option> { - match res { - Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { - unsolicited - .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); + match UnsolicitedResponse::try_from(res) { + Ok(response) => { + unsolicited.send(response).ok(); + None } + Err(unhandled) => Some(unhandled), } - None } #[cfg(test)] diff --git a/src/types/mod.rs b/src/types/mod.rs index b527566c..e673e1f9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -220,113 +220,8 @@ pub use self::capabilities::Capabilities; mod deleted; pub use self::deleted::Deleted; -/// re-exported from imap_proto; -pub use imap_proto::StatusAttribute; - -/// 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, - }, - - /// 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, - }, - - /// 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 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>, - }, - - /// 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>), -} +mod unsolicited_response; +pub use self::unsolicited_response::{AttributeValue, ResponseCode, UnsolicitedResponse}; /// 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. diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs new file mode 100644 index 00000000..6ca68722 --- /dev/null +++ b/src/types/unsolicited_response.rs @@ -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, + }, + + /// 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, + }, + + /// 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 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>, + }, + + /// 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>), + + /// 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, + /// Information text that may be presented to the user. + information: Option, + }, + + /// 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, + /// Information text that may be presented to the user. + information: Option, + }, + + /// 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, + }, +} + +/// 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> for UnsolicitedResponse { + type Error = Response<'a>; + + fn try_from(response: Response<'a>) -> Result { + 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 { + 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>), + /// 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 { + 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, ParseError> { + let mut res = Vec::with_capacity(vals.len()); + for attr in vals { + res.push(AttributeValue::try_from(attr)?); + } + Ok(res) + } +} From 2874bfd93387e7b14a1050830dc533b3823e8d52 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Sat, 27 Mar 2021 21:10:27 -0400 Subject: [PATCH 02/20] Add IDLE example. --- Cargo.toml | 5 ++++ examples/idle.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 examples/idle.rs diff --git a/Cargo.toml b/Cargo.toml index aeabd5ea..8c8af38e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ lazy_static = "1.4" lettre = "0.9" lettre_email = "0.9" rustls-connector = "0.13.0" +structopt = "0.3" [[example]] name = "basic" @@ -40,6 +41,10 @@ required-features = ["default"] name = "gmail_oauth2" required-features = ["default"] +[[example]] +name = "idle" +required-features = ["default"] + [[test]] name = "imap_integration" required-features = ["default"] diff --git a/examples/idle.rs b/examples/idle.rs new file mode 100644 index 00000000..a374388c --- /dev/null +++ b/examples/idle.rs @@ -0,0 +1,77 @@ +use imap::extensions::idle; +use native_tls::TlsConnector; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "idle")] +struct Opt { + // The server name to connect to + #[structopt(short, long)] + server: String, + + // The port to use + #[structopt(short, long, default_value = "993")] + port: u16, + + // The account username + #[structopt(short, long)] + username: String, + + // The account password. In a production system passwords + // would normally be in a config or fetched at runtime from + // a password manager or user prompt and not passed on the + // command line. + #[structopt(short = "w", long)] + password: String, + + // The mailbox to IDLE on + #[structopt(short, long, default_value = "INBOX")] + mailbox: String, + + #[structopt( + short = "x", + long, + help = "The number of responses to receive before exiting", + default_value = "5" + )] + max_responses: usize, +} + +fn main() { + let opt = Opt::from_args(); + + let ssl_conn = TlsConnector::builder().build().unwrap(); + let client = imap::connect((opt.server.clone(), opt.port), opt.server, &ssl_conn) + .expect("Could not connect to imap server"); + let mut imap = client + .login(opt.username, opt.password) + .expect("Could not authenticate"); + imap.debug = true; + imap.select(opt.mailbox).expect("Could not select mailbox"); + + let idle = imap.idle().expect("Could not IDLE"); + + // Implement a trivial counter that causes the IDLE callback to end the IDLE + // after a fixed number of responses. + // + // A threaded client could use channels or shared data to interact with the + // rest of the program and update mailbox state, decide to exit the IDLE, etc. + let mut num_responses = 0; + let max_responses = opt.max_responses; + let idle_result = idle.wait_keepalive(|response| { + num_responses += 1; + println!("IDLE response #{}: {:?}", num_responses, response); + if num_responses >= max_responses { + idle::CallbackAction::Stop + } else { + idle::CallbackAction::Continue + } + }); + + match idle_result { + Ok(()) => println!("IDLE finished normally"), + Err(e) => println!("IDLE finished with error {:?}", e), + } + + imap.logout().expect("Could not log out"); +} From c9b7c0a3e6aefa5f3d8490412b8bcf87ec878b64 Mon Sep 17 00:00:00 2001 From: mordak Date: Mon, 5 Apr 2021 07:03:06 -0500 Subject: [PATCH 03/20] Update src/extensions/idle.rs Co-authored-by: Jon Gjengset --- src/extensions/idle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index de9ca8be..a50b9055 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -22,7 +22,7 @@ use std::time::Duration; /// 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. +/// a convenience callback function [`callback_stop`] is provided. /// /// ```no_run /// # use native_tls::TlsConnector; From bbff7d45b8e39c92fc487376dce2752950f70bc7 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 5 Apr 2021 15:27:20 -0400 Subject: [PATCH 04/20] Remove deprecated wait_timeout() --- src/extensions/idle.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index de9ca8be..02edcd34 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -234,16 +234,6 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { self.timed_wait(keepalive, true, callback).map(|_| ()) } - /// 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")] - pub fn wait_timeout(self, timeout: Duration, callback: F) -> Result<()> - where - F: FnMut(UnsolicitedResponse) -> CallbackAction, - { - self.wait_with_timeout(timeout, callback).map(|_| ()) - } - /// 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. pub fn wait_with_timeout(self, timeout: Duration, callback: F) -> Result From bb38142ab3f7d73ac52ecdc5d93f42f379f9b2a0 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 5 Apr 2021 15:29:24 -0400 Subject: [PATCH 05/20] Change callback_stop to stop_on_any. --- src/extensions/idle.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 02edcd34..0c9ca352 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -22,7 +22,7 @@ use std::time::Duration; /// 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. +/// a convenience callback function `stop_on_any` is provided. /// /// ```no_run /// # use native_tls::TlsConnector; @@ -38,7 +38,7 @@ use std::time::Duration; /// let idle = imap.idle().expect("Could not IDLE"); /// /// // Exit on any mailbox change -/// let result = idle.wait_keepalive(idle::callback_stop); +/// let result = idle.wait_keepalive(idle::stop_on_any); /// ``` /// /// Note that the server MAY consider a client inactive if it has an IDLE command running, and if @@ -76,7 +76,7 @@ pub enum CallbackAction { } /// A convenience function to always cause the IDLE handler to exit on any change. -pub fn callback_stop(_response: UnsolicitedResponse) -> CallbackAction { +pub fn stop_on_any(_response: UnsolicitedResponse) -> CallbackAction { CallbackAction::Stop } From e8a7c918c080b2976d93c026a8cb5a91f4c2cf8f Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 5 Apr 2021 15:33:48 -0400 Subject: [PATCH 06/20] Comment example where we turn on debugging. --- examples/idle.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/idle.rs b/examples/idle.rs index a374388c..2cb7c405 100644 --- a/examples/idle.rs +++ b/examples/idle.rs @@ -46,7 +46,13 @@ fn main() { let mut imap = client .login(opt.username, opt.password) .expect("Could not authenticate"); + + // Turn on debug output so we can see the actual traffic coming + // from the server and how it is handled in our callback. + // This wouldn't be turned on in a production build, but is helpful + // in examples and for debugging. imap.debug = true; + imap.select(opt.mailbox).expect("Could not select mailbox"); let idle = imap.idle().expect("Could not IDLE"); From b8bd1e4cc7e82aa3ae3ef16c37b5064c4c138a58 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 5 Apr 2021 15:39:38 -0400 Subject: [PATCH 07/20] Reorder UnsolicitedResponse alphabetically so it is easier to follow. --- src/types/unsolicited_response.rs | 134 +++++++++++++++--------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index 6ca68722..c97c910e 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -19,32 +19,16 @@ use imap_proto::{ #[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, - }, - - /// 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). + /// An unsolicited `BYE` response. /// - /// > 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), + /// 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, + /// Information text that may be presented to the user. + information: Option, + }, /// 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 @@ -70,6 +54,26 @@ pub enum UnsolicitedResponse { // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? Expunge(Seq), + /// 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, + }, + + /// 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>), + /// 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 { @@ -79,6 +83,44 @@ pub enum UnsolicitedResponse { metadata_entries: Vec, }, + /// 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, + /// Information text that may be presented to the user. + information: Option, + }, + + /// 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 [`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, + }, + /// 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. /// @@ -104,48 +146,6 @@ pub enum UnsolicitedResponse { /// The list of `UID`s which have been removed uids: Vec>, }, - - /// 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>), - - /// 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, - /// Information text that may be presented to the user. - information: Option, - }, - - /// 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, - /// Information text that may be presented to the user. - information: Option, - }, - - /// 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, - }, } /// Try to convert from a `imap_proto::Response`. From e1db863691263ca946562e5aca28fa2e741c47f1 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 5 Apr 2021 15:59:12 -0400 Subject: [PATCH 08/20] Add helper function to transform a vec of flag strings into a vec of Flags. Also use it where we were previously iterating manually. --- src/parse.rs | 12 +++--------- src/types/mod.rs | 5 +++++ src/types/unsolicited_response.rs | 12 ++++-------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index 806fbab4..bc4a9b81 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -109,9 +109,7 @@ pub fn parse_fetches( use imap_proto::AttributeValue; match attr { AttributeValue::Flags(flags) => { - fetch - .flags - .extend(flags.iter().map(|f| Flag::from(f.to_string()))); + fetch.flags.extend(Flag::from_vec(flags)); } AttributeValue::Uid(uid) => fetch.uid = Some(*uid), AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), @@ -271,9 +269,7 @@ pub fn parse_mailbox( mailbox.unseen = Some(n); } Some(ResponseCode::PermanentFlags(flags)) => { - mailbox - .permanent_flags - .extend(flags.into_iter().map(String::from).map(Flag::from)); + mailbox.permanent_flags.extend(Flag::from_vec(&flags)); } _ => {} } @@ -297,9 +293,7 @@ pub fn parse_mailbox( mailbox.recent = r; } MailboxDatum::Flags(flags) => { - mailbox - .flags - .extend(flags.into_iter().map(String::from).map(Flag::from)); + mailbox.flags.extend(Flag::from_vec(&flags)); } _ => {} } diff --git a/src/types/mod.rs b/src/types/mod.rs index e673e1f9..78133aba 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -168,6 +168,11 @@ impl Flag<'static> { _ => None, } } + + /// Helper function to transform a [`Vec`] of flag strings into a [`Vec`] of [`Flag`]. + pub fn from_vec(v: &[S]) -> Vec { + v.iter().map(|s| Self::from(s.to_string())).collect() + } } impl<'a> fmt::Display for Flag<'a> { diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index c97c910e..56600899 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -167,12 +167,9 @@ impl<'a> TryFrom> for UnsolicitedResponse { }) } 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::Flags(flags)) => { + Ok(UnsolicitedResponse::Flags(Flag::from_vec(&flags))) + } Response::MailboxData(MailboxDatum::Exists(n)) => Ok(UnsolicitedResponse::Exists(n)), Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => { Ok(UnsolicitedResponse::Metadata { @@ -286,8 +283,7 @@ impl<'a> TryFrom<&ImapProtoAttributeValue<'a>> for AttributeValue { fn try_from(val: &ImapProtoAttributeValue<'a>) -> Result { match val { ImapProtoAttributeValue::Flags(flags) => { - let v = flags.iter().map(|v| Flag::from(v.to_string())).collect(); - Ok(AttributeValue::Flags(v)) + Ok(AttributeValue::Flags(Flag::from_vec(&flags))) } ImapProtoAttributeValue::ModSeq(seq) => Ok(AttributeValue::ModSeq(*seq)), ImapProtoAttributeValue::Uid(uid) => Ok(AttributeValue::Uid(*uid)), From 064c2e08dc6d0d7956374f64c8826acb514edb6f Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 5 Apr 2021 16:10:32 -0400 Subject: [PATCH 09/20] Use drain() instead of reallocating. --- src/extensions/idle.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 0c9ca352..7e38ce52 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -177,7 +177,8 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { if rest.is_empty() { v.clear(); } else if rest.len() != v.len() { - v = rest.into(); + let used = v.len() - rest.len(); + v.drain(0..used); } }; From 9126d3c15b3c557781bfd58f9ce46c53da579ad6 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Tue, 6 Apr 2021 21:54:52 -0400 Subject: [PATCH 10/20] Improve documentation around unhandled responses. UnsolicitedResponse is not exhaustive, and open an issue if you find one that isn't handled. --- src/extensions/idle.rs | 13 +++++++------ src/types/unsolicited_response.rs | 7 ++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 984859b3..5be5a075 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -193,8 +193,8 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { } } - /// Block until the given callback returns `Stop`, or until an unhandled - /// response arrives on the IDLE channel. + /// Block until the given callback returns `Stop`, or until a response + /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. pub fn wait(mut self, callback: F) -> Result<()> where F: FnMut(UnsolicitedResponse) -> CallbackAction, @@ -211,8 +211,8 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { self.keepalive = interval; } - /// Block until the given callback returns `Stop`, or until an unhandled - /// response arrives on the IDLE channel. + /// Block until the given callback returns `Stop`, or until a response + /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. /// /// 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 @@ -235,8 +235,9 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { self.timed_wait(keepalive, true, callback).map(|_| ()) } - /// 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. + /// Block until the given given amount of time has elapsed, the given callback + /// returns `Stop`, or until a response arrives that is not explicitly handled + /// by [`UnsolicitedResponse`]. pub fn wait_with_timeout(self, timeout: Duration, callback: F) -> Result where F: FnMut(UnsolicitedResponse) -> CallbackAction, diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index 56600899..f755ea22 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -12,7 +12,12 @@ use imap_proto::{ /// 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. +/// to accept any response at any time. +/// +/// Not all possible responses are explicitly enumerated here because in practice only +/// some types of responses are delivered as unsolicited responses. If you encounter an +/// unsolicited response in the wild that is not handled here, please +/// [open an issue](https://github.com/jonhoo/rust-imap/issues) and let us know! /// /// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder, /// so the user must take care when interpreting these. From 11adcfc97b67c199d6975f3b18f78d32b9c05510 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Tue, 6 Apr 2021 22:41:41 -0400 Subject: [PATCH 11/20] Tweak to how we handle incomplete parse. --- src/extensions/idle.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 5be5a075..c1b6ee79 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -176,7 +176,15 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { // Update remaining data with unparsed data if needed. if rest.is_empty() { v.clear(); - } else if rest.len() != v.len() { + } else { + // Assert on partial parse in debug builds - we expect to always parse all + // or none of the input buffer. On release builds, we still do the right thing. + debug_assert!( + rest.len() != v.len(), + "Unexpected partial parse: input: {:?}, output: {:?}", + v, + rest + ); let used = v.len() - rest.len(); v.drain(0..used); } From 5942553e7db201c861376325921efaae881a0b83 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 7 Apr 2021 18:09:46 -0400 Subject: [PATCH 12/20] Use iterators for Flag::from_strs() --- src/parse.rs | 6 +++--- src/types/mod.rs | 8 +++++--- src/types/unsolicited_response.rs | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index bc4a9b81..b871dc27 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -109,7 +109,7 @@ pub fn parse_fetches( use imap_proto::AttributeValue; match attr { AttributeValue::Flags(flags) => { - fetch.flags.extend(Flag::from_vec(flags)); + fetch.flags.extend(Flag::from_strs(flags)); } AttributeValue::Uid(uid) => fetch.uid = Some(*uid), AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), @@ -269,7 +269,7 @@ pub fn parse_mailbox( mailbox.unseen = Some(n); } Some(ResponseCode::PermanentFlags(flags)) => { - mailbox.permanent_flags.extend(Flag::from_vec(&flags)); + mailbox.permanent_flags.extend(Flag::from_strs(flags)); } _ => {} } @@ -293,7 +293,7 @@ pub fn parse_mailbox( mailbox.recent = r; } MailboxDatum::Flags(flags) => { - mailbox.flags.extend(Flag::from_vec(&flags)); + mailbox.flags.extend(Flag::from_strs(flags)); } _ => {} } diff --git a/src/types/mod.rs b/src/types/mod.rs index 78133aba..b1b1d2ad 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -169,9 +169,11 @@ impl Flag<'static> { } } - /// Helper function to transform a [`Vec`] of flag strings into a [`Vec`] of [`Flag`]. - pub fn from_vec(v: &[S]) -> Vec { - v.iter().map(|s| Self::from(s.to_string())).collect() + /// Helper function to transform Strings into owned Flags + pub fn from_strs( + v: impl IntoIterator, + ) -> impl Iterator> { + v.into_iter().map(|s| Flag::from(s.to_string())) } } diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index f755ea22..80aa46a6 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -173,7 +173,7 @@ impl<'a> TryFrom> for UnsolicitedResponse { } Response::MailboxData(MailboxDatum::Recent(n)) => Ok(UnsolicitedResponse::Recent(n)), Response::MailboxData(MailboxDatum::Flags(flags)) => { - Ok(UnsolicitedResponse::Flags(Flag::from_vec(&flags))) + Ok(UnsolicitedResponse::Flags(Flag::from_strs(flags).collect())) } Response::MailboxData(MailboxDatum::Exists(n)) => Ok(UnsolicitedResponse::Exists(n)), Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => { @@ -288,7 +288,7 @@ impl<'a> TryFrom<&ImapProtoAttributeValue<'a>> for AttributeValue { fn try_from(val: &ImapProtoAttributeValue<'a>) -> Result { match val { ImapProtoAttributeValue::Flags(flags) => { - Ok(AttributeValue::Flags(Flag::from_vec(&flags))) + Ok(AttributeValue::Flags(Flag::from_strs(flags).collect())) } ImapProtoAttributeValue::ModSeq(seq) => Ok(AttributeValue::ModSeq(*seq)), ImapProtoAttributeValue::Uid(uid) => Ok(AttributeValue::Uid(*uid)), From 7eb2cfde74206d6f62e75bc074ebcea6abaa9482 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 7 Apr 2021 19:45:04 -0400 Subject: [PATCH 13/20] Use bool instead of CallbackAction. --- examples/idle.rs | 7 ++++--- src/extensions/idle.rs | 36 +++++++++++++----------------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/examples/idle.rs b/examples/idle.rs index 2cb7c405..e1e14a42 100644 --- a/examples/idle.rs +++ b/examples/idle.rs @@ -1,4 +1,3 @@ -use imap::extensions::idle; use native_tls::TlsConnector; use structopt::StructOpt; @@ -68,9 +67,11 @@ fn main() { num_responses += 1; println!("IDLE response #{}: {:?}", num_responses, response); if num_responses >= max_responses { - idle::CallbackAction::Stop + // Stop IDLE + false } else { - idle::CallbackAction::Continue + // Continue IDLE + true } }); diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index c1b6ee79..7e9a2ea0 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -19,8 +19,8 @@ use std::time::Duration; /// /// 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. +/// logic is needed to handle the IDLE response, and then returns a boolean +/// to continue idling (`true`) or stop (`false`). /// For users that want the IDLE to exit on any change (the behavior proior to version 3.0), /// a convenience callback function [`stop_on_any`] is provided. /// @@ -65,19 +65,9 @@ pub enum WaitOutcome { 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 stop_on_any(_response: UnsolicitedResponse) -> CallbackAction { - CallbackAction::Stop +pub fn stop_on_any(_response: UnsolicitedResponse) -> bool { + false } /// Must be implemented for a transport in order for a `Session` using that transport to support @@ -142,7 +132,7 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { /// This is necessary so that we can keep using the inner `Session` in `wait_keepalive`. fn wait_inner(&mut self, reconnect: bool, mut callback: F) -> Result where - F: FnMut(UnsolicitedResponse) -> CallbackAction, + F: FnMut(UnsolicitedResponse) -> bool, { let mut v = Vec::new(); let result = loop { @@ -162,7 +152,7 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { match parse_idle(&v) { (_rest, Some(Err(r))) => break Err(r), (rest, Some(Ok(response))) => { - if let CallbackAction::Stop = callback(response) { + if !callback(response) { break Ok(WaitOutcome::MailboxChanged); } rest @@ -201,11 +191,11 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { } } - /// Block until the given callback returns `Stop`, or until a response + /// Block until the given callback returns `false`, or until a response /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. pub fn wait(mut self, callback: F) -> Result<()> where - F: FnMut(UnsolicitedResponse) -> CallbackAction, + F: FnMut(UnsolicitedResponse) -> bool, { self.wait_inner(true, callback).map(|_| ()) } @@ -219,7 +209,7 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { self.keepalive = interval; } - /// Block until the given callback returns `Stop`, or until a response + /// Block until the given callback returns `false`, or until a response /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. /// /// This method differs from [`Handle::wait`] in that it will periodically refresh the IDLE @@ -230,7 +220,7 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { /// This is the recommended method to use for waiting. pub fn wait_keepalive(self, callback: F) -> Result<()> where - F: FnMut(UnsolicitedResponse) -> CallbackAction, + F: FnMut(UnsolicitedResponse) -> bool, { // 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 @@ -244,11 +234,11 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { } /// Block until the given given amount of time has elapsed, the given callback - /// returns `Stop`, or until a response arrives that is not explicitly handled + /// returns `false`, or until a response arrives that is not explicitly handled /// by [`UnsolicitedResponse`]. pub fn wait_with_timeout(self, timeout: Duration, callback: F) -> Result where - F: FnMut(UnsolicitedResponse) -> CallbackAction, + F: FnMut(UnsolicitedResponse) -> bool, { self.timed_wait(timeout, false, callback) } @@ -260,7 +250,7 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { callback: F, ) -> Result where - F: FnMut(UnsolicitedResponse) -> CallbackAction, + F: FnMut(UnsolicitedResponse) -> bool, { self.session .stream From efa02f07ab2f5ef1f805636b6d5f7a34e68cf7bf Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 7 Apr 2021 20:02:34 -0400 Subject: [PATCH 14/20] Remove wrapper around ResponseCode. --- src/types/mod.rs | 2 +- src/types/unsolicited_response.rs | 88 ++++++------------------------- 2 files changed, 17 insertions(+), 73 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index b1b1d2ad..d175f439 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -228,7 +228,7 @@ mod deleted; pub use self::deleted::Deleted; mod unsolicited_response; -pub use self::unsolicited_response::{AttributeValue, ResponseCode, UnsolicitedResponse}; +pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse}; /// 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. diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index 80aa46a6..214bb51c 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -4,11 +4,9 @@ use super::{Flag, Seq, Uid}; use crate::error::ParseError; /// re-exported from imap_proto; +pub use imap_proto::ResponseCode; pub use imap_proto::StatusAttribute; -use imap_proto::{ - AttributeValue as ImapProtoAttributeValue, MailboxDatum, Response, - ResponseCode as ImapProtoResponseCode, Status, -}; +use imap_proto::{AttributeValue as ImapProtoAttributeValue, MailboxDatum, Response, 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 @@ -30,7 +28,7 @@ pub enum UnsolicitedResponse { /// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.5). Bye { /// Optional response code. - code: Option, + code: Option>, /// Information text that may be presented to the user. information: Option, }, @@ -94,7 +92,7 @@ pub enum UnsolicitedResponse { /// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.1). Ok { /// Optional response code. - code: Option, + code: Option>, /// Information text that may be presented to the user. information: Option, }, @@ -188,46 +186,20 @@ impl<'a> TryFrom> for UnsolicitedResponse { } 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, - }) - } - } + code, + information, + } => Ok(UnsolicitedResponse::Ok { + code: code.map(|c| c.into_owned()), + information: information.map(|s| s.to_string()), + }), 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, - }) - } - } + code, + information, + } => Ok(UnsolicitedResponse::Bye { + code: code.map(|c| c.into_owned()), + information: information.map(|s| s.to_string()), + }), Response::Fetch(id, ref attributes) => { match AttributeValue::try_from_imap_proto_vec(attributes) { Ok(attrs) => Ok(UnsolicitedResponse::Fetch { @@ -242,34 +214,6 @@ impl<'a> TryFrom> for UnsolicitedResponse { } } -/// 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 { - 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] From f2d7919f60825d47ef3e14d19d247d8c94280729 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 7 Apr 2021 20:10:33 -0400 Subject: [PATCH 15/20] Do not wrap AttributeValue. --- src/parse.rs | 1 - src/types/unsolicited_response.rs | 60 +++++-------------------------- 2 files changed, 8 insertions(+), 53 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index b871dc27..b28acecb 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -106,7 +106,6 @@ pub fn parse_fetches( // set some common fields eaglery for attr in &fetch.fetch { - use imap_proto::AttributeValue; match attr { AttributeValue::Flags(flags) => { fetch.flags.extend(Flag::from_strs(flags)); diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index 214bb51c..1bf75d60 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -1,12 +1,12 @@ use std::convert::TryFrom; -use super::{Flag, Seq, Uid}; -use crate::error::ParseError; +use super::{Flag, Seq}; /// re-exported from imap_proto; +pub use imap_proto::AttributeValue; pub use imap_proto::ResponseCode; pub use imap_proto::StatusAttribute; -use imap_proto::{AttributeValue as ImapProtoAttributeValue, MailboxDatum, Response, Status}; +use imap_proto::{MailboxDatum, Response, 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 @@ -65,7 +65,7 @@ pub enum UnsolicitedResponse { /// Message identifier. id: u32, /// Attribute values for this message. - attributes: Vec, + attributes: Vec>, }, /// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that @@ -200,55 +200,11 @@ impl<'a> TryFrom> for UnsolicitedResponse { code: code.map(|c| c.into_owned()), information: information.map(|s| s.to_string()), }), - Response::Fetch(id, ref attributes) => { - match AttributeValue::try_from_imap_proto_vec(attributes) { - Ok(attrs) => Ok(UnsolicitedResponse::Fetch { - id, - attributes: attrs, - }), - _ => Err(response), - } - } + Response::Fetch(id, attributes) => Ok(UnsolicitedResponse::Fetch { + id, + attributes: attributes.into_iter().map(|a| a.into_owned()).collect(), + }), _ => Err(response), } } } - -/// 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>), - /// 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 { - match val { - ImapProtoAttributeValue::Flags(flags) => { - Ok(AttributeValue::Flags(Flag::from_strs(flags).collect())) - } - 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, ParseError> { - let mut res = Vec::with_capacity(vals.len()); - for attr in vals { - res.push(AttributeValue::try_from(attr)?); - } - Ok(res) - } -} From 584c9542d8a94ce65343999b7b7d882dd9896d6d Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 7 Apr 2021 20:17:00 -0400 Subject: [PATCH 16/20] Reorder variants alphabetically in try_from. --- src/types/unsolicited_response.rs | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/types/unsolicited_response.rs b/src/types/unsolicited_response.rs index 1bf75d60..67a8c365 100644 --- a/src/types/unsolicited_response.rs +++ b/src/types/unsolicited_response.rs @@ -163,47 +163,47 @@ impl<'a> TryFrom> for UnsolicitedResponse { fn try_from(response: Response<'a>) -> Result { 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(Flag::from_strs(flags).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, + status: Status::Bye, code, information, - } => Ok(UnsolicitedResponse::Ok { + } => Ok(UnsolicitedResponse::Bye { code: code.map(|c| c.into_owned()), information: information.map(|s| s.to_string()), }), Response::Data { - status: Status::Bye, + status: Status::Ok, code, information, - } => Ok(UnsolicitedResponse::Bye { + } => Ok(UnsolicitedResponse::Ok { code: code.map(|c| c.into_owned()), information: information.map(|s| s.to_string()), }), + Response::Expunge(n) => Ok(UnsolicitedResponse::Expunge(n)), Response::Fetch(id, attributes) => Ok(UnsolicitedResponse::Fetch { id, attributes: attributes.into_iter().map(|a| a.into_owned()).collect(), }), + Response::MailboxData(MailboxDatum::Exists(n)) => Ok(UnsolicitedResponse::Exists(n)), + Response::MailboxData(MailboxDatum::Flags(flags)) => { + Ok(UnsolicitedResponse::Flags(Flag::from_strs(flags).collect())) + } + Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => { + Ok(UnsolicitedResponse::Metadata { + mailbox: mailbox.to_string(), + metadata_entries: values.iter().map(|s| s.to_string()).collect(), + }) + } + Response::MailboxData(MailboxDatum::Recent(n)) => Ok(UnsolicitedResponse::Recent(n)), + Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { + Ok(UnsolicitedResponse::Status { + mailbox: mailbox.into(), + attributes: status, + }) + } + Response::Vanished { earlier, uids } => { + Ok(UnsolicitedResponse::Vanished { earlier, uids }) + } _ => Err(response), } } From 692dcdd27e2398874116cd003e610a4d5667e100 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Wed, 7 Apr 2021 21:18:17 -0400 Subject: [PATCH 17/20] Move buffer management into parse match arms. --- src/extensions/idle.rs | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 7e9a2ea0..696f3c03 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -136,7 +136,7 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { { let mut v = Vec::new(); let result = loop { - let rest = match self.session.readline(&mut v) { + match self.session.readline(&mut v) { Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => @@ -150,34 +150,35 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { continue; } match parse_idle(&v) { + // Something went wrong parsing. (_rest, Some(Err(r))) => break Err(r), + // Complete response. We expect rest to be empty. (rest, Some(Ok(response))) => { if !callback(response) { break Ok(WaitOutcome::MailboxChanged); } - rest + if rest.is_empty() { + v.clear(); + } else { + // Assert on partial parse in debug builds - we expect + // to always parse all or none of the input buffer. + // On release builds, we still do the right thing. + debug_assert!( + rest.len() != v.len(), + "Unexpected partial parse: input: {:?}, output: {:?}", + v, + rest + ); + let used = v.len() - rest.len(); + v.drain(0..used); + } } - (rest, None) => rest, + // Incomplete parse - do nothing and read more. + (_rest, None) => (), } } Err(r) => break Err(r), }; - - // Update remaining data with unparsed data if needed. - if rest.is_empty() { - v.clear(); - } else { - // Assert on partial parse in debug builds - we expect to always parse all - // or none of the input buffer. On release builds, we still do the right thing. - debug_assert!( - rest.len() != v.len(), - "Unexpected partial parse: input: {:?}, output: {:?}", - v, - rest - ); - let used = v.len() - rest.len(); - v.drain(0..used); - } }; // Reconnect on timeout if needed From 4232c773b5555f05c70266ca566c9b658d7236ac Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Thu, 8 Apr 2021 19:50:20 -0400 Subject: [PATCH 18/20] wait to wait_while --- examples/idle.rs | 2 +- src/extensions/idle.rs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/idle.rs b/examples/idle.rs index e1e14a42..b008b7c8 100644 --- a/examples/idle.rs +++ b/examples/idle.rs @@ -63,7 +63,7 @@ fn main() { // rest of the program and update mailbox state, decide to exit the IDLE, etc. let mut num_responses = 0; let max_responses = opt.max_responses; - let idle_result = idle.wait_keepalive(|response| { + let idle_result = idle.wait_keepalive_while(|response| { num_responses += 1; println!("IDLE response #{}: {:?}", num_responses, response); if num_responses >= max_responses { diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 696f3c03..cf55151c 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -38,13 +38,13 @@ use std::time::Duration; /// let idle = imap.idle().expect("Could not IDLE"); /// /// // Exit on any mailbox change -/// let result = idle.wait_keepalive(idle::stop_on_any); +/// let result = idle.wait_keepalive_while(idle::stop_on_any); /// ``` /// /// 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 /// timeout period. Because of that, clients using IDLE are advised to terminate the IDLE and -/// re-issue it at least every 29 minutes to avoid being logged off. [`Handle::wait_keepalive`] +/// re-issue it at least every 29 minutes to avoid being logged off. [`Handle::wait_keepalive_while`] /// does this. This still allows a client to receive immediate mailbox updates even though it need /// only "poll" at half hour intervals. /// @@ -73,8 +73,8 @@ pub fn stop_on_any(_response: UnsolicitedResponse) -> bool { /// Must be implemented for a transport in order for a `Session` using that transport to support /// operations with timeouts. /// -/// Examples of where this is useful is for `Handle::wait_keepalive` and -/// `Handle::wait_timeout`. +/// Examples of where this is useful is for `Handle::wait_keepalive_while` and +/// `Handle::wait_timeout_while`. pub trait SetReadTimeout { /// Set the timeout for subsequent reads to the given one. /// @@ -129,7 +129,7 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { /// 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_while`. fn wait_inner(&mut self, reconnect: bool, mut callback: F) -> Result where F: FnMut(UnsolicitedResponse) -> bool, @@ -194,7 +194,7 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { /// Block until the given callback returns `false`, or until a response /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. - pub fn wait(mut self, callback: F) -> Result<()> + pub fn wait_while(mut self, callback: F) -> Result<()> where F: FnMut(UnsolicitedResponse) -> bool, { @@ -203,7 +203,7 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { } impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { - /// Set the keep-alive interval to use when `wait_keepalive` is called. + /// Set the keep-alive interval to use when `wait_keepalive_while` is called. /// /// The interval defaults to 29 minutes as dictated by RFC 2177. pub fn set_keepalive(&mut self, interval: Duration) { @@ -213,13 +213,13 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { /// Block until the given callback returns `false`, or until a response /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. /// - /// This method differs from [`Handle::wait`] in that it will periodically refresh the IDLE + /// This method differs from [`Handle::wait_while`] in that it will periodically refresh the IDLE /// connection, to prevent the server from timing out our connection. The keepalive interval is /// set to 29 minutes by default, as dictated by RFC 2177, but can be changed using /// [`Handle::set_keepalive`]. /// /// This is the recommended method to use for waiting. - pub fn wait_keepalive(self, callback: F) -> Result<()> + pub fn wait_keepalive_while(self, callback: F) -> Result<()> where F: FnMut(UnsolicitedResponse) -> bool, { @@ -237,7 +237,7 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { /// Block until the given given amount of time has elapsed, the given callback /// returns `false`, or until a response arrives that is not explicitly handled /// by [`UnsolicitedResponse`]. - pub fn wait_with_timeout(self, timeout: Duration, callback: F) -> Result + pub fn wait_with_timeout_while(self, timeout: Duration, callback: F) -> Result where F: FnMut(UnsolicitedResponse) -> bool, { From 08de3362b4f0766159ec4989e4002a70ab2c8b00 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Thu, 8 Apr 2021 20:08:51 -0400 Subject: [PATCH 19/20] Move debug assertion. --- src/extensions/idle.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index cf55151c..fa081423 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -157,24 +157,26 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { if !callback(response) { break Ok(WaitOutcome::MailboxChanged); } + + // Assert on partial parse in debug builds - we expect + // to always parse all or none of the input buffer. + // On release builds, we still do the right thing. + debug_assert!( + rest.is_empty(), + "Unexpected partial parse: input: {:?}, output: {:?}", + v, + rest, + ); + if rest.is_empty() { v.clear(); } else { - // Assert on partial parse in debug builds - we expect - // to always parse all or none of the input buffer. - // On release builds, we still do the right thing. - debug_assert!( - rest.len() != v.len(), - "Unexpected partial parse: input: {:?}, output: {:?}", - v, - rest - ); let used = v.len() - rest.len(); v.drain(0..used); } } // Incomplete parse - do nothing and read more. - (_rest, None) => (), + (_rest, None) => {} } } Err(r) => break Err(r), From 1cabb3bb56a315982e319a5f63af184ae97c4487 Mon Sep 17 00:00:00 2001 From: Todd Mortimer Date: Mon, 19 Apr 2021 20:39:59 -0400 Subject: [PATCH 20/20] Promote Unexpected error from ParseError to Error. --- Cargo.toml | 2 +- src/error.rs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c8af38e..274f4c6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ default = ["tls"] native-tls = { version = "0.2.2", optional = true } regex = "1.0" bufstream = "0.1" -imap-proto = "0.14.0" +imap-proto = "0.14.1" nom = "6.0" base64 = "0.13" chrono = "0.4" diff --git a/src/error.rs b/src/error.rs index 94d21f25..f3a84b9d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -76,6 +76,10 @@ pub enum Error { Validate(ValidateError), /// Error appending an e-mail. Append, + /// An unexpected response was received. This could be a response from a command, + /// or an unsolicited response that could not be converted into a local type in + /// [`UnsolicitedResponse`]. + Unexpected(Response<'static>), } impl From for Error { @@ -112,7 +116,7 @@ impl From for Error { impl<'a> From> for Error { fn from(err: Response<'a>) -> Error { - Error::Parse(ParseError::Unexpected(format!("{:?}", err))) + Error::Unexpected(err.into_owned()) } } @@ -130,6 +134,7 @@ impl fmt::Display for Error { Error::Bad(ref data) => write!(f, "Bad Response: {}", data), Error::ConnectionLost => f.write_str("Connection Lost"), Error::Append => f.write_str("Could not append mail to mailbox"), + Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r), } } } @@ -149,6 +154,7 @@ impl StdError for Error { Error::No(_) => "No Response", Error::ConnectionLost => "Connection lost", Error::Append => "Could not append mail to mailbox", + Error::Unexpected(_) => "Unexpected Response", } } @@ -170,8 +176,6 @@ impl StdError for Error { pub enum ParseError { /// Indicates an error parsing the status response. Such as OK, NO, and BAD. Invalid(Vec), - /// An unexpected response was encountered. - Unexpected(String), /// The client could not find or decode the server's authentication challenge. Authentication(String, Option), /// The client received data that was not UTF-8 encoded. @@ -182,7 +186,6 @@ impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { ParseError::Invalid(_) => f.write_str("Unable to parse status response"), - ParseError::Unexpected(_) => f.write_str("Encountered unexpected parse response"), ParseError::Authentication(_, _) => { f.write_str("Unable to parse authentication response") } @@ -195,7 +198,6 @@ impl StdError for ParseError { fn description(&self) -> &str { match *self { ParseError::Invalid(_) => "Unable to parse status response", - ParseError::Unexpected(_) => "Encountered unexpected parsed response", ParseError::Authentication(_, _) => "Unable to parse authentication response", ParseError::DataNotUtf8(_, _) => "Unable to parse data as UTF-8 text", }