diff --git a/Cargo.toml b/Cargo.toml index a0b23cb2..518acb41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ imap-proto = "0.7" nom = "4.0" base64 = "0.10" chrono = "0.4" +enumset = "0.3.18" [dev-dependencies] lettre = "0.9" diff --git a/src/client.rs b/src/client.rs index 5540f4e3..c702d55a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,6 @@ use base64; use bufstream::BufStream; +use enumset::EnumSet; use native_tls::{TlsConnector, TlsStream}; use nom; use std::collections::HashSet; @@ -13,6 +14,7 @@ use super::error::{Error, ParseError, Result, ValidateError}; use super::extensions; use super::parse::*; use super::types::*; +use super::unsolicited_responses::UnsolicitedResponseSender; static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 0; @@ -48,7 +50,7 @@ fn validate_str(value: &str) -> Result { #[derive(Debug)] pub struct Session { conn: Connection, - unsolicited_responses_tx: mpsc::Sender, + unsolicited_responses_tx: UnsolicitedResponseSender, /// Server responses that are not related to the current command. See also the note on /// [unilateral server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). @@ -376,10 +378,37 @@ impl Session { Session { conn, unsolicited_responses: rx, - unsolicited_responses_tx: tx, + unsolicited_responses_tx: UnsolicitedResponseSender::new(tx), } } + /// Tells which unsolicited responses are required. Defaults to none. + /// + /// The server *is* allowed to unilaterally send things to the client for messages in + /// a selected mailbox whose status has changed. See the note on [unilateral server responses + /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This function tells + /// which events you want to hear about. + /// + /// If you request unsolicited responses, you have to regularly check the + /// `unsolicited_responses` channel of the [`Session`](struct.Session.html) for new responses. + pub fn filter_unsolicited_responses(&mut self, mask: EnumSet) { + self.unsolicited_responses_tx + .request(&self.unsolicited_responses, mask); + } + + /// Request all unsolicited responses from the server. Convenience method. + /// + /// The server *is* allowed to unilaterally send things to the client for messages in + /// a selected mailbox whose status has changed. See the note on [unilateral server responses + /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This function tells + /// which events you want to hear about. + /// + /// If you request unsolicited responses, you have to regularly check the + /// `unsolicited_responses` channel of the [`Session`](struct.Session.html) for new responses. + pub fn request_all_unsolicited_responses(&mut self) { + self.filter_unsolicited_responses(EnumSet::all()) + } + /// Selects a mailbox /// /// The `SELECT` command selects a mailbox so that messages in the mailbox can be accessed. @@ -995,7 +1024,8 @@ impl Session { /// /// See [`extensions::idle::Handle`] for details. pub fn idle(&mut self) -> Result> { - extensions::idle::Handle::make(self) + let sender = self.unsolicited_responses_tx.clone(); + extensions::idle::Handle::make(self, sender) } /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) appends @@ -1228,7 +1258,7 @@ impl Connection { // Remove CRLF let len = into.len(); let line = &into[(len - read)..(len - 2)]; - eprint!("S: {}\n", String::from_utf8_lossy(line)); + eprintln!("S: {}", String::from_utf8_lossy(line)); } Ok(read) @@ -1244,7 +1274,7 @@ impl Connection { self.stream.write_all(&[CR, LF])?; self.stream.flush()?; if self.debug { - eprint!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap()); + eprintln!("C: {}", String::from_utf8(buf.to_vec()).unwrap()); } Ok(()) } diff --git a/src/extensions/idle.rs b/src/extensions/idle.rs index 32ea0a02..30b52296 100644 --- a/src/extensions/idle.rs +++ b/src/extensions/idle.rs @@ -4,9 +4,11 @@ use client::Session; use error::{Error, Result}; use native_tls::TlsStream; +use parse; use std::io::{self, Read, Write}; use std::net::TcpStream; use std::time::Duration; +use unsolicited_responses::UnsolicitedResponseSender; /// `Handle` allows a client to block waiting for changes to the remote mailbox. /// @@ -26,7 +28,8 @@ use std::time::Duration; #[derive(Debug)] pub struct Handle<'a, T: Read + Write + 'a> { session: &'a mut Session, - keepalive: Duration, + unsolicited_responses_tx: UnsolicitedResponseSender, + keepalive: Option, done: bool, } @@ -45,10 +48,14 @@ pub trait SetReadTimeout { } impl<'a, T: Read + Write + 'a> Handle<'a, T> { - pub(crate) fn make(session: &'a mut Session) -> Result { + pub(crate) fn make( + session: &'a mut Session, + unsolicited_responses_tx: UnsolicitedResponseSender, + ) -> Result { let mut h = Handle { session, - keepalive: Duration::from_secs(29 * 60), + unsolicited_responses_tx, + keepalive: None, done: false, }; h.init()?; @@ -77,31 +84,53 @@ impl<'a, T: Read + Write + 'a> Handle<'a, T> { unreachable!(); } - fn terminate(&mut self) -> Result<()> { + fn terminate(&mut self, consume_response: bool) -> Result<()> { if !self.done { self.done = true; self.session.write_line(b"DONE")?; - self.session.read_response().map(|_| ()) - } else { - Ok(()) + if consume_response { + return self.session.read_response().map(|_| ()); + } } + Ok(()) } /// 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) -> Result<()> { - let mut v = Vec::new(); - match self.session.readline(&mut v).map(|_| ()) { - Err(Error::Io(ref e)) - if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => - { - // we need to refresh the IDLE connection - self.terminate()?; - self.init()?; - self.wait_inner() + let mut buffer = Vec::new(); + loop { + match self.session.readline(&mut buffer).map(|_| ()) { + Err(Error::Io(ref e)) + if e.kind() == io::ErrorKind::TimedOut + || e.kind() == io::ErrorKind::WouldBlock => + { + if self.keepalive.is_some() { + // we need to refresh the IDLE connection + self.terminate(true)?; + self.init()?; + } + } + Err(err) => return Err(err), + Ok(_) => { + let _ = parse::parse_idle(&buffer, &mut self.unsolicited_responses_tx)?; + self.terminate(false)?; + buffer.truncate(0); + + // Unsolicited responses coming in from the server are not multi-line, + // therefore, we don't need to worry about the remaining bytes since + // they should always be terminated at a LF. + loop { + let _ = self.session.readline(&mut buffer)?; + let found_ok = + parse::parse_idle(&buffer, &mut self.unsolicited_responses_tx)?; + if found_ok { + return Ok(()); + } + } + } } - r => r, } } @@ -114,20 +143,20 @@ 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. /// - /// The interval defaults to 29 minutes as dictated by RFC 2177. + /// The interval defaults to 29 minutes as advised by RFC 2177. pub fn set_keepalive(&mut self, interval: Duration) { - self.keepalive = interval; + self.keepalive = Some(interval); } /// Block until the selected mailbox changes. /// /// 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 - /// set to 29 minutes by default, as dictated by RFC 2177, but can be changed using + /// set to 29 minutes by default, as advised 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(mut self) -> Result<()> { // 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 @@ -135,7 +164,10 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { // re-issue it at least every 29 minutes to avoid being logged off. // This still allows a client to receive immediate mailbox updates even // though it need only "poll" at half hour intervals. - let keepalive = self.keepalive; + let keepalive = self + .keepalive + .get_or_insert_with(|| Duration::from_secs(29 * 60)) + .clone(); self.wait_timeout(keepalive) } @@ -154,7 +186,8 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { impl<'a, T: Read + Write + 'a> Drop for Handle<'a, T> { fn drop(&mut self) { // we don't want to panic here if we can't terminate the Idle - let _ = self.terminate().is_ok(); + // If we sent done, then we should suck up the OK. + let _ = self.terminate(true).is_ok(); } } @@ -164,8 +197,8 @@ impl<'a> SetReadTimeout for TcpStream { } } -impl<'a> SetReadTimeout for TlsStream { +impl<'a, T: SetReadTimeout + Read + Write + 'a> SetReadTimeout for TlsStream { fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { - self.get_ref().set_read_timeout(timeout).map_err(Error::Io) + self.get_mut().set_read_timeout(timeout) } } diff --git a/src/lib.rs b/src/lib.rs index 5dbece91..b20881a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,8 +67,11 @@ extern crate imap_proto; extern crate native_tls; extern crate nom; extern crate regex; +#[macro_use] +extern crate enumset; mod parse; +mod unsolicited_responses; pub mod types; diff --git a/src/parse.rs b/src/parse.rs index 8b32c174..098bb75f 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,10 +1,10 @@ -use imap_proto::{self, MailboxDatum, Response}; +use imap_proto::{self, MailboxDatum, Response, Status}; use regex::Regex; use std::collections::HashSet; -use std::sync::mpsc; use super::error::{Error, ParseError, Result}; use super::types::*; +use super::unsolicited_responses::UnsolicitedResponseSender; pub fn parse_authenticate_response(line: String) -> Result { let authenticate_regex = Regex::new("^\\+ (.*)\r\n").unwrap(); @@ -27,7 +27,7 @@ enum MapOrNot { unsafe fn parse_many( lines: Vec, mut map: F, - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> ZeroCopyResult> where F: FnMut(Response<'static>) -> Result>, @@ -46,7 +46,7 @@ where match map(resp)? { MapOrNot::Map(t) => things.push(t), MapOrNot::Not(resp) => match handle_unilateral(resp, unsolicited) { - Some(Response::Fetch(..)) => continue, + // Some(Response::Fetch(..)) => continue, Some(resp) => break Err(resp.into()), None => {} }, @@ -65,7 +65,7 @@ where pub fn parse_names( lines: Vec, - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> ZeroCopyResult> { let f = |resp| match resp { // https://github.com/djc/imap-proto/issues/4 @@ -86,7 +86,7 @@ pub fn parse_names( pub fn parse_fetches( lines: Vec, - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> ZeroCopyResult> { let f = |resp| match resp { Response::Fetch(num, attrs) => { @@ -121,7 +121,7 @@ pub fn parse_fetches( pub fn parse_expunge( lines: Vec, - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> Result> { let f = |resp| match resp { Response::Expunge(id) => Ok(MapOrNot::Map(id)), @@ -133,7 +133,7 @@ pub fn parse_expunge( pub fn parse_capabilities( lines: Vec, - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> ZeroCopyResult { let f = |mut lines| { let mut caps = HashSet::new(); @@ -163,10 +163,7 @@ pub fn parse_capabilities( unsafe { ZeroCopy::make(lines, f) } } -pub fn parse_noop( - lines: Vec, - unsolicited: &mut mpsc::Sender, -) -> Result<()> { +pub fn parse_noop(lines: Vec, unsolicited: &mut UnsolicitedResponseSender) -> Result<()> { let mut lines: &[u8] = &lines; loop { @@ -190,7 +187,7 @@ pub fn parse_noop( pub fn parse_mailbox( mut lines: &[u8], - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> Result { let mut mailbox = Mailbox::default(); @@ -202,7 +199,7 @@ pub fn parse_mailbox( if let imap_proto::Status::Ok = status { } else { // how can this happen for a Response::Data? - unreachable!(); + unreachable!("Received something other than OK from mailbox data"); } use imap_proto::ResponseCode; @@ -229,12 +226,10 @@ pub fn parse_mailbox( match m { MailboxDatum::Status { mailbox, status } => { - unsolicited - .send(UnsolicitedResponse::Status { - mailbox: mailbox.into(), - attributes: status, - }) - .unwrap(); + unsolicited.send(UnsolicitedResponse::Status { + mailbox: mailbox.into(), + attributes: status, + }); } MailboxDatum::Exists(e) => { mailbox.exists = e; @@ -252,7 +247,7 @@ pub fn parse_mailbox( } Ok((rest, Response::Expunge(n))) => { lines = rest; - unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); + unsolicited.send(UnsolicitedResponse::Expunge(n)); } Ok((_, resp)) => { break Err(resp.into()); @@ -270,7 +265,7 @@ pub fn parse_mailbox( pub fn parse_ids( lines: &[u8], - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> Result> { let mut lines = &lines[..]; let mut ids = HashSet::new(); @@ -297,29 +292,124 @@ pub fn parse_ids( } } +pub fn parse_idle<'a>( + mut lines: &'a [u8], + unsolicited: &mut UnsolicitedResponseSender, +) -> Result { + while !lines.is_empty() { + let resp = imap_proto::parse_response(lines); + match resp { + Ok(( + _rest, + Response::Done { + status, + information, + .. + }, + )) => { + match status { + imap_proto::Status::Ok => return Ok(true), + imap_proto::Status::Bad => { + return Err(Error::Bad(information.unwrap_or("").to_owned())) + } + imap_proto::Status::No => { + return Err(Error::Bad(information.unwrap_or("").to_owned())) + } + _ => unreachable!(), + } + } + Ok((rest, data)) => { + lines = rest; + if let Some(resp) = handle_unilateral(data, unsolicited) { + return Err(resp.into()); + } + } + Err(nom::Err::Incomplete(_)) => break, + Err(_) => { + return Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } + } + Ok(false) +} + // check if this is simply a unilateral server response // (see Section 7 of RFC 3501): fn handle_unilateral<'a>( res: Response<'a>, - unsolicited: &mut mpsc::Sender, + unsolicited: &mut UnsolicitedResponseSender, ) -> Option> { match res { Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { - unsolicited - .send(UnsolicitedResponse::Status { - mailbox: mailbox.into(), - attributes: status, - }) - .unwrap(); + unsolicited.send(UnsolicitedResponse::Status { + mailbox: mailbox.into(), + attributes: status, + }); } Response::MailboxData(MailboxDatum::Recent(n)) => { - unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); + unsolicited.send(UnsolicitedResponse::Recent(n)); } Response::MailboxData(MailboxDatum::Exists(n)) => { - unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap(); + unsolicited.send(UnsolicitedResponse::Exists(n)); } Response::Expunge(n) => { - unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); + unsolicited.send(UnsolicitedResponse::Expunge(n)); + } + Response::Data { + status: Status::Ok, + code, + information, + } => { + unsolicited.send(UnsolicitedResponse::Ok { + code: code.map(|c| c.into()), + information: information.map(|s| s.to_string()), + }); + } + Response::Data { + status: Status::Bad, + code, + information, + } => { + unsolicited.send(UnsolicitedResponse::Bad { + code: code.map(|c| c.into()), + information: information.map(|s| s.to_string()), + }); + } + Response::Data { + status: Status::No, + code, + information, + } => { + unsolicited.send(UnsolicitedResponse::No { + code: code.map(|c| c.into()), + information: information.map(|s| s.to_string()), + }); + } + Response::Data { + status: Status::Bye, + code, + information, + } => { + unsolicited.send(UnsolicitedResponse::Bye { + code: code.map(|c| c.into()), + information: information.map(|s| s.to_string()), + }); + } + Response::Fetch(id, attributes) => { + unsolicited.send(UnsolicitedResponse::Fetch { + id, + attributes: attributes + .iter() + .map(|a| match a { + imap_proto::types::AttributeValue::Flags(v) => { + UnsolicitedFetchAttribute::Flags( + v.iter().map(|s| s.to_string()).collect(), + ) + } + _ => UnsolicitedFetchAttribute::Other, + }) + .collect(), + }); } res => { return Some(res); @@ -331,12 +421,23 @@ fn handle_unilateral<'a>( #[cfg(test)] mod tests { use super::*; + use std::sync::mpsc; + + fn promiscuous_unsolicited_channel() -> ( + UnsolicitedResponseSender, + mpsc::Receiver, + ) { + let (send, recv) = mpsc::channel(); + let mut send = UnsolicitedResponseSender::new(send); + send.request(&recv, EnumSet::all()); + (send, recv) + } #[test] fn parse_capability_test() { let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; let lines = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); // shouldn't be any unexpected responses parsed assert!(recv.try_recv().is_err()); @@ -349,7 +450,7 @@ mod tests { #[test] #[should_panic] fn parse_capability_invalid_test() { - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let lines = b"* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; parse_capabilities(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); @@ -358,7 +459,7 @@ mod tests { #[test] fn parse_names_test() { let lines = b"* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let names = parse_names(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); assert_eq!(names.len(), 1); @@ -373,7 +474,7 @@ mod tests { #[test] fn parse_fetches_empty() { let lines = b""; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); assert!(fetches.is_empty()); @@ -384,7 +485,7 @@ mod tests { let lines = b"\ * 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n\ * 25 FETCH (FLAGS (\\Seen))\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); assert!(recv.try_recv().is_err()); assert_eq!(fetches.len(), 2); @@ -406,7 +507,7 @@ mod tests { let lines = b"\ * 37 FETCH (UID 74)\r\n\ * 1 RECENT\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); assert_eq!(recv.try_recv(), Ok(UnsolicitedResponse::Recent(1))); assert_eq!(fetches.len(), 1); @@ -419,7 +520,7 @@ mod tests { let lines = b"\ * LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n\ * 4 EXPUNGE\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let names = parse_names(lines.to_vec(), &mut send).unwrap(); assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Expunge(4)); @@ -440,7 +541,7 @@ mod tests { * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ * STATUS dev.github (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n\ * 4 EXISTS\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); assert_eq!(capabilities.len(), 4); @@ -469,7 +570,7 @@ mod tests { * SEARCH 23 42 4711\r\n\ * 1 RECENT\r\n\ * STATUS INBOX (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let ids = parse_ids(lines, &mut send).unwrap(); assert_eq!(ids, [23, 42, 4711].iter().cloned().collect()); @@ -493,7 +594,7 @@ mod tests { fn parse_ids_test() { let lines = b"* SEARCH 1600 1698 1739 1781 1795 1885 1891 1892 1893 1898 1899 1901 1911 1926 1932 1933 1993 1994 2007 2032 2033 2041 2053 2062 2063 2065 2066 2072 2078 2079 2082 2084 2095 2100 2101 2102 2103 2104 2107 2116 2120 2135 2138 2154 2163 2168 2172 2189 2193 2198 2199 2205 2212 2213 2221 2227 2267 2275 2276 2295 2300 2328 2330 2332 2333 2334\r\n\ * SEARCH 2335 2336 2337 2338 2339 2341 2342 2347 2349 2350 2358 2359 2362 2369 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2390 2392 2397 2400 2401 2403 2405 2409 2411 2414 2417 2419 2420 2424 2426 2428 2439 2454 2456 2467 2468 2469 2490 2515 2519 2520 2521\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let ids = parse_ids(lines, &mut send).unwrap(); assert!(recv.try_recv().is_err()); let ids: HashSet = ids.iter().cloned().collect(); @@ -516,7 +617,7 @@ mod tests { ); let lines = b"* SEARCH\r\n"; - let (mut send, recv) = mpsc::channel(); + let (mut send, recv) = promiscuous_unsolicited_channel(); let ids = parse_ids(lines, &mut send).unwrap(); assert!(recv.try_recv().is_err()); let ids: HashSet = ids.iter().cloned().collect(); diff --git a/src/types/mod.rs b/src/types/mod.rs index 09fc44d5..f0298765 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,6 @@ //! This module contains types used throughout the IMAP protocol. +pub use enumset::EnumSet; use std::borrow::Cow; /// From section [2.3.1.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1). @@ -204,6 +205,60 @@ pub use self::capabilities::Capabilities; /// re-exported from imap_proto; pub use imap_proto::StatusAttribute; +// We need a ResponseCode that is not tied to a lifetime, to be used in UnsolicitedResponse. +/// Response code that may be sent with OK/NO/BAD/BYE responses. +/// See [RFC 3501](https://tools.ietf.org/html/rfc3501#section-3.1). +#[derive(Debug, Eq, PartialEq)] +pub enum ResponseCode { + //Alert: not parsed by imap-proto yet. + //BadCharset: not parsed by imap-proto yet. + //Capability: not parsed by imap-proto yet. + //Parse: not parsed by imap-proto yet. + /// See [RFC 4551](https://tools.ietf.org/html/rfc4551#section-3.1.1). + HighestModSeq(u64), + /// Flags that can be changed permanently. + PermanentFlags(Vec), + /// The mailbox status has changed to read-only. + ReadOnly, + /// The mailbox status has changed to read-write. + ReadWrite, + /// Indicates that the mailbox must be created first. + TryCreate, + /// Next unique identifier value. + UidNext(u32), + /// The unique identifier validity value. + UidValidity(u32), + /// First message without the \Seen flag set. + Unseen(u32), +} + +impl<'a> From> for ResponseCode { + fn from(r: imap_proto::types::ResponseCode<'a>) -> Self { + match r { + imap_proto::types::ResponseCode::HighestModSeq(n) => ResponseCode::HighestModSeq(n), + imap_proto::types::ResponseCode::PermanentFlags(v) => { + ResponseCode::PermanentFlags(v.iter().map(|x| (*x).into()).collect()) + } + imap_proto::types::ResponseCode::ReadOnly => ResponseCode::ReadOnly, + imap_proto::types::ResponseCode::ReadWrite => ResponseCode::ReadWrite, + imap_proto::types::ResponseCode::TryCreate => ResponseCode::TryCreate, + imap_proto::types::ResponseCode::UidNext(n) => ResponseCode::UidNext(n), + imap_proto::types::ResponseCode::UidValidity(n) => ResponseCode::UidValidity(n), + imap_proto::types::ResponseCode::Unseen(n) => ResponseCode::Unseen(n), + } + } +} + +/// An attribute of the message refered to by a FETCH unsolicited response. +#[derive(Debug, Eq, PartialEq)] +pub enum UnsolicitedFetchAttribute { + /// The set of flags of this message. + Flags(Vec), + /// Some other attribute not handled yet. + // I don't know which attributes besides FLAGS make sense to be sent unsolicited. + Other, +} + /// 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. @@ -213,6 +268,8 @@ pub use imap_proto::StatusAttribute; #[derive(Debug, PartialEq, Eq)] pub enum UnsolicitedResponse { /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4). + /// + /// It can only happen during a [`Session::status`] command. Status { /// The mailbox that this status response is for. mailbox: String, @@ -260,8 +317,89 @@ pub enum UnsolicitedResponse { /// 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(u32), + + /// An unsolicited [`OK` response](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 [`NO` response](https://tools.ietf.org/html/rfc3501#section-7.1.2). + No { + /// Optional response code. + code: Option, + /// Information text that may be presented to the user. + information: Option, + }, + + /// An unsolicited [`BAD` response](https://tools.ietf.org/html/rfc3501#section-7.1.3). + Bad { + /// Optional response code. + code: Option, + /// Information text that may be presented to the user. + information: Option, + }, + + /// An unsolicited [`BYE` response](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](https://tools.ietf.org/html/rfc3501#section-7.4.2). + Fetch { + /// Message identifier. + id: u32, + /// Attribute values for this message. + attributes: Vec, + }, +} + +enum_set_type! { + /// Unsolicited responses categories, to be used by the + /// [`Session::request_unsolicited_responses`] method. + pub enum UnsolicitedResponseCategory { + /// Asks for `RECENT` responses. + Recent, + /// Asks for `EXISTS` responses. + Exists, + /// Asks for `EXPUNGE` responses. + Expunge, + /// Asks for `OK` responses. + Ok, + /// Asks for `NO` responses. + No, + /// Asks for `BAD` responses. + Bad, + /// Asks for the `BYE` response. + Bye, + /// Asks for `STATUS` responses. + Status, + /// Asks for `FETCH` responses. + Fetch, + } +} + +impl UnsolicitedResponse { + /// Category corresponding to a response + pub(crate) fn category(&self) -> UnsolicitedResponseCategory { + match self { + UnsolicitedResponse::Status { .. } => UnsolicitedResponseCategory::Status, + UnsolicitedResponse::Recent(_) => UnsolicitedResponseCategory::Recent, + UnsolicitedResponse::Exists(_) => UnsolicitedResponseCategory::Exists, + UnsolicitedResponse::Expunge(_) => UnsolicitedResponseCategory::Expunge, + UnsolicitedResponse::Ok { .. } => UnsolicitedResponseCategory::Ok, + UnsolicitedResponse::No { .. } => UnsolicitedResponseCategory::No, + UnsolicitedResponse::Bad { .. } => UnsolicitedResponseCategory::Bad, + UnsolicitedResponse::Bye { .. } => UnsolicitedResponseCategory::Bye, + UnsolicitedResponse::Fetch { .. } => UnsolicitedResponseCategory::Fetch, + } + } } /// This type wraps an input stream and a type that was constructed by parsing that input stream, diff --git a/src/unsolicited_responses.rs b/src/unsolicited_responses.rs new file mode 100644 index 00000000..55f1b118 --- /dev/null +++ b/src/unsolicited_responses.rs @@ -0,0 +1,37 @@ +use enumset::EnumSet; +use std::sync::mpsc; + +use super::types::{UnsolicitedResponse, UnsolicitedResponseCategory}; + +#[derive(Debug, Clone)] +pub struct UnsolicitedResponseSender { + sender: mpsc::Sender, + allow: EnumSet, +} + +impl UnsolicitedResponseSender { + pub(crate) fn new(sender: mpsc::Sender) -> UnsolicitedResponseSender { + UnsolicitedResponseSender { + sender, + allow: EnumSet::empty(), + } + } + + // Set the new filter mask, and remove unwanted responses from the current queue. + pub(crate) fn request( + &mut self, + receiver: &mpsc::Receiver, + mask: EnumSet, + ) { + self.allow = mask; + for message in receiver.try_iter() { + self.send(message); + } + } + + pub(crate) fn send(&mut self, message: UnsolicitedResponse) { + if self.allow.contains(message.category()) { + self.sender.send(message).unwrap(); + } + } +}