diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 451a222..de9ca8b 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); - } - Ok(WaitOutcome::TimedOut) + break Ok(WaitOutcome::TimedOut); } - Ok(()) => Ok(WaitOutcome::MailboxChanged), - Err(r) => Err(r), - }?; + 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, + } + } + 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 69f2a90..b4871ca 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 b527566..e673e1f 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 0000000..6ca6872 --- /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) + } +}