diff --git a/src/client.rs b/src/client.rs index 52895f4..19d9df6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; use std::io::{self, Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; use std::ops::{Deref, DerefMut}; +use std::sync::mpsc; use std::time::Duration; use super::authenticator::Authenticator; @@ -44,6 +45,10 @@ fn validate_str(value: &str) -> Result { #[derive(Debug)] pub struct Session { conn: Connection, + /// 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). + pub unsolicited_responses: mpsc::Receiver, + unsolicited_responses_tx: mpsc::Sender, } /// An (unauthenticated) handle to talk to an IMAP server. This is what you get when first @@ -396,7 +401,7 @@ impl Client { ); } else { ok_or_unauth_client_err!(self.read_response_onto(&mut line), self); - return Ok(Session { conn: self.conn }); + return Ok(Session::new(self.conn)); } } } @@ -443,27 +448,34 @@ impl Client { self ); - Ok(Session { conn: self.conn }) + Ok(Session::new(self.conn)) } } impl Session { + // not public, just to avoid duplicating the channel creation code + fn new(conn: Connection) -> Self { + let (tx, rx) = mpsc::channel(); + Session { conn, unsolicited_responses: rx, unsolicited_responses_tx: tx } + } + /// Selects a mailbox /// /// Note that 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 means that if you use /// [`Connection::run_command_and_read_response`], you *may* see additional untagged `RECENT`, - /// `EXISTS`, `FETCH`, and `EXPUNGE` responses! + /// `EXISTS`, `FETCH`, and `EXPUNGE` responses. You can get them from the + /// `unsolicited_responses` channel of the [`Session`](struct.Session.html). pub fn select(&mut self, mailbox_name: &str) -> Result { self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) - .and_then(|lines| parse_mailbox(&lines[..])) + .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) } /// Examine is identical to Select, but the selected mailbox is identified as read-only pub fn examine(&mut self, mailbox_name: &str) -> Result { self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) - .and_then(|lines| parse_mailbox(&lines[..])) + .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) } /// Fetch retreives data associated with a set of messages in the mailbox. @@ -473,7 +485,7 @@ impl Session { /// server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). pub fn fetch(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult> { self.run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query)) - .and_then(parse_fetches) + .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) } /// Fetch retreives data associated with a set of messages by UID in the mailbox. @@ -483,7 +495,7 @@ impl Session { /// server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). pub fn uid_fetch(&mut self, uid_set: &str, query: &str) -> ZeroCopyResult> { self.run_command_and_read_response(&format!("UID FETCH {} {}", uid_set, query)) - .and_then(parse_fetches) + .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) } /// Noop always succeeds, and it does nothing. @@ -530,7 +542,7 @@ impl Session { /// Capability requests a listing of capabilities that the server supports. pub fn capabilities(&mut self) -> ZeroCopyResult { self.run_command_and_read_response("CAPABILITY") - .and_then(parse_capabilities) + .and_then(|lines| parse_capabilities(lines, &mut self.unsolicited_responses_tx)) } /// Expunge permanently removes all messages that have the \Deleted flag set from the currently @@ -560,12 +572,12 @@ impl Session { /// Store alters data associated with a message in the mailbox. pub fn store(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult> { self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query)) - .and_then(parse_fetches) + .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) } pub fn uid_store(&mut self, uid_set: &str, query: &str) -> ZeroCopyResult> { self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query)) - .and_then(parse_fetches) + .and_then(|lines| parse_fetches(lines, &mut self.unsolicited_responses_tx)) } /// Copy copies the specified message to the end of the specified destination mailbox. @@ -612,7 +624,7 @@ impl Session { quote!(reference_name), mailbox_search_pattern )) - .and_then(parse_names) + .and_then(|lines| parse_names(lines, &mut self.unsolicited_responses_tx)) } /// The LSUB command returns a subset of names from the set of names @@ -627,7 +639,7 @@ impl Session { quote!(reference_name), mailbox_search_pattern )) - .and_then(parse_names) + .and_then(|lines| parse_names(lines, &mut self.unsolicited_responses_tx)) } /// The STATUS command requests the status of the indicated mailbox. @@ -637,7 +649,7 @@ impl Session { validate_str(mailbox_name)?, status_data_items )) - .and_then(|lines| parse_mailbox(&lines[..])) + .and_then(|lines| parse_mailbox(&lines[..], &mut self.unsolicited_responses_tx)) } /// Returns a handle that can be used to block until the state of the currently selected @@ -664,14 +676,14 @@ impl Session { /// the list of message sequence numbers of those messages. pub fn search(&mut self, query: &str) -> Result> { self.run_command_and_read_response(&format!("SEARCH {}", query)) - .and_then(parse_ids) + .and_then(|lines| parse_ids(lines, &mut self.unsolicited_responses_tx)) } /// Searches the mailbox for messages that match the given criteria and returns /// the list of unique identifier numbers of those messages. pub fn uid_search(&mut self, query: &str) -> Result> { self.run_command_and_read_response(&format!("UID SEARCH {}", query)) - .and_then(parse_ids) + .and_then(|lines| parse_ids(lines, &mut self.unsolicited_responses_tx)) } // these are only here because they are public interface, the rest is in `Connection` @@ -836,9 +848,7 @@ mod tests { macro_rules! mock_session { ($s:expr) => { - Session { - conn: Client::new($s).conn, - } + Session::new(Client::new($s).conn) }; } diff --git a/src/parse.rs b/src/parse.rs index 8c1c0ba..194b02a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,7 @@ use imap_proto::{self, MailboxDatum, Response}; use regex::Regex; use std::collections::HashSet; +use std::sync::mpsc; use super::error::{Error, ParseError, Result}; use super::types::*; @@ -23,7 +24,7 @@ enum MapOrNot { Ignore, } -unsafe fn parse_many(lines: Vec, mut map: F) -> ZeroCopyResult> +unsafe fn parse_many(lines: Vec, mut map: F, unsolicited: &mut mpsc::Sender) -> ZeroCopyResult> where F: FnMut(Response<'static>) -> MapOrNot, { @@ -44,15 +45,28 @@ where // check if this is simply a unilateral server response // (see Section 7 of RFC 3501): match resp { - Response::MailboxData(MailboxDatum::Recent { .. }) - | Response::MailboxData(MailboxDatum::Exists { .. }) - | Response::Fetch(..) - | Response::Expunge(..) => { + Response::MailboxData(MailboxDatum::Recent(n)) => { + unsolicited.send(UnsolicitedResponse::Recent(n)) + .unwrap(); + } + Response::MailboxData(MailboxDatum::Exists(n)) => { + unsolicited.send(UnsolicitedResponse::Exists(n)) + .unwrap(); + } + Response::Expunge(id) => { + unsolicited.send(UnsolicitedResponse::Expunge(id)) + .unwrap(); + } + Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { + unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)) + .unwrap(); + } + Response::Fetch(..) => { continue; } resp => break Err(resp.into()), } - } + }, MapOrNot::Ignore => continue, } } @@ -66,7 +80,7 @@ where ZeroCopy::new(lines, f) } -pub fn parse_names(lines: Vec) -> ZeroCopyResult> { +pub fn parse_names(lines: Vec, unsolicited: &mut mpsc::Sender) -> ZeroCopyResult> { use imap_proto::MailboxDatum; let f = |resp| match resp { // https://github.com/djc/imap-proto/issues/4 @@ -87,10 +101,10 @@ pub fn parse_names(lines: Vec) -> ZeroCopyResult> { resp => MapOrNot::Not(resp), }; - unsafe { parse_many(lines, f) } + unsafe { parse_many(lines, f, unsolicited) } } -pub fn parse_fetches(lines: Vec) -> ZeroCopyResult> { +pub fn parse_fetches(lines: Vec, unsolicited: &mut mpsc::Sender) -> ZeroCopyResult> { let f = |resp| match resp { Response::Fetch(num, attrs) => { let mut fetch = Fetch { @@ -121,10 +135,10 @@ pub fn parse_fetches(lines: Vec) -> ZeroCopyResult> { resp => MapOrNot::Not(resp), }; - unsafe { parse_many(lines, f) } + unsafe { parse_many(lines, f, unsolicited) } } -pub fn parse_capabilities(lines: Vec) -> ZeroCopyResult { +pub fn parse_capabilities(lines: Vec, unsolicited: &mut mpsc::Sender) -> ZeroCopyResult { let f = |mut lines| { let mut caps = HashSet::new(); loop { @@ -132,25 +146,42 @@ pub fn parse_capabilities(lines: Vec) -> ZeroCopyResult { Ok((rest, Response::Capabilities(c))) => { lines = rest; caps.extend(c); - - if lines.is_empty() { - break Ok(Capabilities(caps)); - } } - Ok((_, resp)) => { - break Err(resp.into()); + Ok((rest, data)) => { + lines = rest; + match data { + Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { + unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)).unwrap(); + } + Response::MailboxData(MailboxDatum::Recent(n)) => { + unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); + } + Response::MailboxData(MailboxDatum::Exists(n)) => { + unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap(); + } + Response::Expunge(n) => { + unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); + } + resp => { + break Err(resp.into()); + } + } } _ => { break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); } } + + if lines.is_empty() { + break Ok(Capabilities(caps)); + } } }; unsafe { ZeroCopy::new(lines, f) } } -pub fn parse_mailbox(mut lines: &[u8]) -> Result { +pub fn parse_mailbox(mut lines: &[u8], unsolicited: &mut mpsc::Sender) -> Result { let mut mailbox = Mailbox::default(); loop { @@ -188,8 +219,8 @@ pub fn parse_mailbox(mut lines: &[u8]) -> Result { use imap_proto::MailboxDatum; match m { - MailboxDatum::Status { .. } => { - // TODO: we probably want to expose statuses too + MailboxDatum::Status { mailbox, status } => { + unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)).unwrap(); } MailboxDatum::Exists(e) => { mailbox.exists = e; @@ -205,6 +236,10 @@ pub fn parse_mailbox(mut lines: &[u8]) -> Result { MailboxDatum::SubList { .. } | MailboxDatum::List { .. } => {} } } + Ok((rest, Response::Expunge(n))) => { + lines = rest; + unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); + } Ok((_, resp)) => { break Err(resp.into()); } @@ -219,21 +254,38 @@ pub fn parse_mailbox(mut lines: &[u8]) -> Result { } } -pub fn parse_ids(lines: Vec) -> Result> { +pub fn parse_ids(lines: Vec, unsolicited: &mut mpsc::Sender) -> Result> { let mut lines = &lines[..]; let mut ids = HashSet::new(); loop { + if lines.is_empty() { + break Ok(ids); + } + match imap_proto::parse_response(lines) { Ok((rest, Response::IDs(c))) => { lines = rest; ids.extend(c); - - if lines.is_empty() { - break Ok(ids); - } } - Ok((_, resp)) => { - break Err(resp.into()); + Ok((rest, data)) => { + lines = rest; + match data { + Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { + unsolicited.send(UnsolicitedResponse::Status(mailbox.into(), status)).unwrap(); + } + Response::MailboxData(MailboxDatum::Recent(n)) => { + unsolicited.send(UnsolicitedResponse::Recent(n)).unwrap(); + } + Response::MailboxData(MailboxDatum::Exists(n)) => { + unsolicited.send(UnsolicitedResponse::Exists(n)).unwrap(); + } + Response::Expunge(n) => { + unsolicited.send(UnsolicitedResponse::Expunge(n)).unwrap(); + } + resp => { + break Err(resp.into()); + } + } } _ => { break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); @@ -250,7 +302,10 @@ mod tests { 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 capabilities = parse_capabilities(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::channel(); + let capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); + // shouldn't be any unexpected responses parsed + assert!(recv.try_recv().is_err()); assert_eq!(capabilities.len(), 4); for e in expected_capabilities { assert!(capabilities.has(e)); @@ -260,14 +315,18 @@ mod tests { #[test] #[should_panic] fn parse_capability_invalid_test() { + let (mut send, recv) = mpsc::channel(); let lines = b"* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; - parse_capabilities(lines.to_vec()).unwrap(); + parse_capabilities(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); } #[test] fn parse_names_test() { let lines = b"* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"; - let names = parse_names(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::channel(); + let names = parse_names(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); assert_eq!(names.len(), 1); assert_eq!(names[0].attributes(), &["\\HasNoChildren"]); assert_eq!(names[0].delimiter(), "."); @@ -277,7 +336,9 @@ mod tests { #[test] fn parse_fetches_empty() { let lines = b""; - let fetches = parse_fetches(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::channel(); + let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); assert!(fetches.is_empty()); } @@ -286,7 +347,9 @@ mod tests { let lines = b"\ * 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n\ * 25 FETCH (FLAGS (\\Seen))\r\n"; - let fetches = parse_fetches(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::channel(); + let fetches = parse_fetches(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); assert_eq!(fetches.len(), 2); assert_eq!(fetches[0].message, 24); assert_eq!(fetches[0].flags(), &["\\Seen"]); @@ -304,17 +367,79 @@ mod tests { let lines = b"\ * 37 FETCH (UID 74)\r\n\ * 1 RECENT\r\n"; - let fetches = parse_fetches(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::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); assert_eq!(fetches[0].message, 37); assert_eq!(fetches[0].uid, Some(74)); } + #[test] + fn parse_names_w_unilateral() { + let lines = b"\ + * LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n\ + * 4 EXPUNGE\r\n"; + let (mut send, recv) = mpsc::channel(); + let names = parse_names(lines.to_vec(), &mut send).unwrap(); + + assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Expunge(4)); + + assert_eq!(names.len(), 1); + assert_eq!(names[0].attributes(), &["\\HasNoChildren"]); + assert_eq!(names[0].delimiter(), "."); + assert_eq!(names[0].name(), "INBOX"); + + } + + #[test] + fn parse_capabilities_w_unilateral() { + use types::StatusAttribute::*; + + let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; + let lines = b"\ + * 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 capabilities = parse_capabilities(lines.to_vec(), &mut send).unwrap(); + + assert_eq!(capabilities.len(), 4); + for e in expected_capabilities { + assert!(capabilities.has(e)); + } + + assert_eq!(recv.try_recv().unwrap(), + UnsolicitedResponse::Status("dev.github".to_string(), vec![Messages(10), UidNext(11), UidValidity(1408806928), Unseen(0)])); + assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Exists(4)); + } + + #[test] + fn parse_ids_w_unilateral() { + use types::StatusAttribute::*; + + let lines = b"\ + * 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 ids = parse_ids(lines.to_vec(), &mut send).unwrap(); + + assert_eq!(ids, [23, 42, 4711].iter().cloned().collect()); + + assert_eq!(recv.try_recv().unwrap(), UnsolicitedResponse::Recent(1)); + assert_eq!(recv.try_recv().unwrap(), + UnsolicitedResponse::Status("INBOX".to_string(), vec![Messages(10), UidNext(11), UidValidity(1408806928), Unseen(0)])); + } + + #[test] 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 ids = parse_ids(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::channel(); + let ids = parse_ids(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); let ids: HashSet = ids.iter().cloned().collect(); assert_eq!( ids, @@ -335,7 +460,9 @@ mod tests { ); let lines = b"* SEARCH\r\n"; - let ids = parse_ids(lines.to_vec()).unwrap(); + let (mut send, recv) = mpsc::channel(); + let ids = parse_ids(lines.to_vec(), &mut send).unwrap(); + assert!(recv.try_recv().is_err()); let ids: HashSet = ids.iter().cloned().collect(); assert_eq!(ids, HashSet::::new()); } diff --git a/src/types/mod.rs b/src/types/mod.rs index 9809472..cf1ba54 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,6 +10,25 @@ pub use self::name::Name; mod capabilities; pub use self::capabilities::Capabilities; + +/// 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)] +pub enum UnsolicitedResponse { + Status(String, Vec), + Recent(u32), + Exists(u32), + Expunge(u32), +} + + pub struct ZeroCopy { _owned: Box<[u8]>, derived: D,