diff --git a/Cargo.toml b/Cargo.toml index 848e8e7d..a7df73c8 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 = { version = "6.0", default-features = false } base64 = "0.13" chrono = { version = "0.4", default-features = false } @@ -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..b008b7c8 --- /dev/null +++ b/examples/idle.rs @@ -0,0 +1,84 @@ +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"); + + // 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"); + + // 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_while(|response| { + num_responses += 1; + println!("IDLE response #{}: {:?}", num_responses, response); + if num_responses >= max_responses { + // Stop IDLE + false + } else { + // Continue IDLE + true + } + }); + + match idle_result { + Ok(()) => println!("IDLE finished normally"), + Err(e) => println!("IDLE finished with error {:?}", e), + } + + imap.logout().expect("Could not log out"); +} 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", } diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 451a222e..fa081423 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,13 +15,36 @@ 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 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. +/// +/// ```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_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. /// @@ -40,11 +65,16 @@ pub enum WaitOutcome { MailboxChanged, } +/// A convenience function to always cause the IDLE handler to exit on any change. +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 /// 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. /// @@ -99,58 +129,102 @@ 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 { + /// 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, + { let mut v = Vec::new(); - loop { - let result = match self.session.readline(&mut v).map(|_| ()) { + let result = loop { + 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) { + // 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); + } + + // 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 { + let used = v.len() - rest.len(); + v.drain(0..used); + } + } + // Incomplete parse - do nothing and read more. + (_rest, None) => {} } - Ok(WaitOutcome::TimedOut) } - Ok(()) => Ok(WaitOutcome::MailboxChanged), - Err(r) => Err(r), - }?; - - // Handle Dovecot's imap_idle_notify_interval message - if v.eq_ignore_ascii_case(b"* OK Still here\r\n") { - v.clear(); - } else { - break Ok(result); + Err(r) => break Err(r), + }; + }; + + // 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 `false`, or until a response + /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. + pub fn wait_while(mut self, callback: F) -> Result<()> + where + F: FnMut(UnsolicitedResponse) -> bool, + { + self.wait_inner(true, callback).map(|_| ()) } } 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) { self.keepalive = interval; } - /// Block until the selected mailbox changes. + /// 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) -> Result<()> { + pub fn wait_keepalive_while(self, callback: F) -> Result<()> + where + 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 // the client off implicitly at the end of its timeout period. Because @@ -159,26 +233,33 @@ 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(|_| ()) - } - - /// Block until the selected mailbox changes, or until the given amount of time has expired. - #[deprecated(note = "use wait_with_timeout instead")] - pub fn wait_timeout(self, timeout: Duration) -> Result<()> { - self.wait_with_timeout(timeout).map(|_| ()) + self.timed_wait(keepalive, true, 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 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_while(self, timeout: Duration, callback: F) -> Result + where + F: FnMut(UnsolicitedResponse) -> bool, + { + 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) -> bool, + { 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 39916731..b28acecb 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}; @@ -105,12 +106,9 @@ 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(flags.iter().map(|f| Flag::from(f.to_string()))); + fetch.flags.extend(Flag::from_strs(flags)); } AttributeValue::Uid(uid) => fetch.uid = Some(*uid), AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), @@ -270,9 +268,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_strs(flags)); } _ => {} } @@ -296,9 +292,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_strs(flags)); } _ => {} } @@ -350,6 +344,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 +366,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..d175f439 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -168,6 +168,13 @@ impl Flag<'static> { _ => None, } } + + /// 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())) + } } impl<'a> fmt::Display for Flag<'a> { @@ -220,113 +227,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, 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..67a8c365 --- /dev/null +++ b/src/types/unsolicited_response.rs @@ -0,0 +1,210 @@ +use std::convert::TryFrom; + +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::{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 +/// 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. +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum UnsolicitedResponse { + /// 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 [`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 `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 { + /// Mailbox name for which annotations were changed. + mailbox: String, + /// List of annotations that were changed. + 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. + /// + /// 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>, + }, +} + +/// 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::Data { + status: Status::Bye, + code, + information, + } => Ok(UnsolicitedResponse::Bye { + code: code.map(|c| c.into_owned()), + information: information.map(|s| s.to_string()), + }), + Response::Data { + status: Status::Ok, + code, + information, + } => 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), + } + } +}