diff --git a/Cargo.toml b/Cargo.toml index 6af16483..3a00f05c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ path = "src/lib.rs" native-tls = "0.1" regex = "0.2" bufstream = "0.1" +imap-proto = "0.3" +nom = "3.2.1" [dev-dependencies] base64 = "0.7" diff --git a/examples/basic.rs b/examples/basic.rs index 50567e64..c699ea24 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -16,7 +16,7 @@ fn main() { imap_socket.login("username", "password").unwrap(); - match imap_socket.capability() { + match imap_socket.capabilities() { Ok(capabilities) => for capability in capabilities.iter() { println!("{}", capability); }, @@ -31,8 +31,8 @@ fn main() { }; match imap_socket.fetch("2", "body[text]") { - Ok(lines) => for line in lines.iter() { - print!("{}", line); + Ok(msgs) => for msg in &msgs { + print!("{:?}", msg); }, Err(e) => println!("Error Fetching email 2: {}", e), }; diff --git a/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs index e2cba0bb..a92bfaae 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -44,8 +44,8 @@ fn main() { }; match imap_socket.fetch("2", "body[text]") { - Ok(lines) => for line in lines.iter() { - print!("{}", line); + Ok(msgs) => for msg in &msgs { + print!("{:?}", msg); }, Err(e) => println!("Error Fetching email 2: {}", e), }; diff --git a/src/client.rs b/src/client.rs index 9d296a9e..b17a048d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,11 +3,12 @@ use native_tls::{TlsConnector, TlsStream}; use std::io::{self, Read, Write}; use std::time::Duration; use bufstream::BufStream; +use nom::IResult; -use super::mailbox::Mailbox; +use super::types::*; use super::authenticator::Authenticator; -use super::parse::{parse_authenticate_response, parse_capability, parse_response, - parse_response_ok, parse_select_or_examine}; +use super::parse::{parse_authenticate_response, parse_capabilities, parse_fetches, parse_mailbox, + parse_names}; use super::error::{Error, ParseError, Result, ValidateError}; static TAG_PREFIX: &'static str = "a"; @@ -88,27 +89,23 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { // // a) if there's an error, or // b) *after* we send DONE - let tag = format!("{}{} ", TAG_PREFIX, self.client.tag); - let raw_data = try!(self.client.readline()); - let line = String::from_utf8(raw_data).unwrap(); - if line.starts_with(&tag) { - try!(parse_response(vec![line])); - // We should *only* get a continuation on an error (i.e., it gives BAD or NO). - unreachable!(); - } else if !line.starts_with("+") { - return Err(Error::BadResponse(vec![line])); + let mut v = Vec::new(); + try!(self.client.readline(&mut v)); + if v.starts_with(b"+") { + self.done = false; + return Ok(()); } - self.done = false; - Ok(()) + self.client.read_response_onto(&mut v)?; + // We should *only* get a continuation on an error (i.e., it gives BAD or NO). + unreachable!(); } fn terminate(&mut self) -> Result<()> { if !self.done { self.done = true; try!(self.client.write_line(b"DONE")); - let lines = try!(self.client.read_response()); - parse_response_ok(lines) + self.client.read_response().map(|_| ()) } else { Ok(()) } @@ -118,7 +115,8 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { /// /// This is necessary so that we can keep using the inner `Client` in `wait_keepalive`. fn wait_inner(&mut self) -> Result<()> { - match self.client.readline().map(|_| ()) { + let mut v = Vec::new(); + match self.client.readline(&mut v).map(|_| ()) { Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => { @@ -272,7 +270,8 @@ impl Client { fn do_auth_handshake(&mut self, authenticator: A) -> Result<()> { // TODO Clean up this code loop { - let line = try!(self.readline()); + let mut line = Vec::new(); + try!(self.readline(&mut line)); if line.starts_with(b"+") { let data = try!(parse_authenticate_response( @@ -281,14 +280,8 @@ impl Client { let auth_response = authenticator.process(data); try!(self.write_line(auth_response.into_bytes().as_slice())) - } else if line.starts_with(format!("{}{} ", TAG_PREFIX, self.tag).as_bytes()) { - try!(parse_response(vec![String::from_utf8(line).unwrap()])); - return Ok(()); } else { - let mut lines = try!(self.read_response()); - lines.insert(0, String::from_utf8(line).unwrap()); - try!(parse_response(lines.clone())); - return Ok(()); + return self.read_response_onto(&mut line).map(|_| ()); } } } @@ -304,27 +297,25 @@ impl Client { /// Selects a mailbox pub fn select(&mut self, mailbox_name: &str) -> Result { - let lines = try!( - self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) - ); - parse_select_or_examine(lines) + self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) + .and_then(|lines| parse_mailbox(&lines[..])) } /// Examine is identical to Select, but the selected mailbox is identified as read-only pub fn examine(&mut self, mailbox_name: &str) -> Result { - let lines = try!( - self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) - ); - parse_select_or_examine(lines) + self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) + .and_then(|lines| parse_mailbox(&lines[..])) } /// Fetch retreives data associated with a message in the mailbox. - pub fn fetch(&mut self, sequence_set: &str, query: &str) -> Result> { + pub fn fetch(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult> { self.run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query)) + .and_then(|lines| parse_fetches(lines)) } - pub fn uid_fetch(&mut self, uid_set: &str, query: &str) -> Result> { + 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(|lines| parse_fetches(lines)) } /// Noop always succeeds, and it does nothing. @@ -369,9 +360,9 @@ impl Client { } /// Capability requests a listing of capabilities that the server supports. - pub fn capability(&mut self) -> Result> { - let lines = try!(self.run_command_and_read_response(&format!("CAPABILITY"))); - parse_capability(lines) + pub fn capabilities(&mut self) -> ZeroCopyResult { + self.run_command_and_read_response(&format!("CAPABILITY")) + .and_then(|lines| parse_capabilities(lines)) } /// Expunge permanently removes all messages that have the \Deleted flag set from the currently @@ -392,12 +383,14 @@ impl Client { } /// Store alters data associated with a message in the mailbox. - pub fn store(&mut self, sequence_set: &str, query: &str) -> Result> { + pub fn store(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult> { self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query)) + .and_then(|lines| parse_fetches(lines)) } - pub fn uid_store(&mut self, uid_set: &str, query: &str) -> Result> { + 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(|lines| parse_fetches(lines)) } /// Copy copies the specified message to the end of the specified destination mailbox. @@ -415,12 +408,12 @@ impl Client { &mut self, reference_name: &str, mailbox_search_pattern: &str, - ) -> Result> { - self.run_command_and_parse(&format!( + ) -> ZeroCopyResult> { + self.run_command_and_read_response(&format!( "LIST {} {}", quote!(reference_name), mailbox_search_pattern - )) + )).and_then(|lines| parse_names(lines)) } /// The LSUB command returns a subset of names from the set of names @@ -429,17 +422,21 @@ impl Client { &mut self, reference_name: &str, mailbox_search_pattern: &str, - ) -> Result> { - self.run_command_and_parse(&format!( + ) -> ZeroCopyResult> { + self.run_command_and_read_response(&format!( "LSUB {} {}", quote!(reference_name), mailbox_search_pattern - )) + )).and_then(|lines| parse_names(lines)) } /// The STATUS command requests the status of the indicated mailbox. - pub fn status(&mut self, mailbox_name: &str, status_data_items: &str) -> Result> { - self.run_command_and_parse(&format!("STATUS {} {}", mailbox_name, status_data_items)) + pub fn status(&mut self, mailbox_name: &str, status_data_items: &str) -> Result { + self.run_command_and_read_response(&format!( + "STATUS {} {}", + validate_str(mailbox_name)?, + status_data_items + )).and_then(|lines| parse_mailbox(&lines[..])) } /// Returns a handle that can be used to block until the state of the currently selected @@ -449,28 +446,24 @@ impl Client { } /// The APPEND command adds a mail to a mailbox. - pub fn append(&mut self, folder: &str, content: &[u8]) -> Result> { - try!(self.run_command(&format!("APPEND \"{}\" {{{}}}", folder, content.len()))); - let line = try!(self.readline()); - if !line.starts_with(b"+") { + pub fn append(&mut self, folder: &str, content: &[u8]) -> Result<()> { + try!(self.run_command( + &format!("APPEND \"{}\" {{{}}}", folder, content.len()) + )); + let mut v = Vec::new(); + try!(self.readline(&mut v)); + if !v.starts_with(b"+") { return Err(Error::Append); } try!(self.stream.write_all(content)); try!(self.stream.write_all(b"\r\n")); try!(self.stream.flush()); - self.read_response() + self.read_response().map(|_| ()) } /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { - let lines = try!(self.run_command_and_read_response(command)); - parse_response_ok(lines) - } - - // Run a command and parse the status response. - pub fn run_command_and_parse(&mut self, command: &str) -> Result> { - let lines = try!(self.run_command_and_read_response(command)); - parse_response(lines) + self.run_command_and_read_response(command).map(|_| ()) } /// Runs any command passed to it. @@ -479,49 +472,110 @@ impl Client { self.write_line(command.into_bytes().as_slice()) } - pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { + pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { try!(self.run_command(untagged_command)); self.read_response() } - fn read_response(&mut self) -> Result> { - let mut found_tag_line = false; - let start_str = format!("{}{} ", TAG_PREFIX, self.tag); - let mut lines: Vec = Vec::new(); + fn read_response(&mut self) -> Result> { + let mut v = Vec::new(); + self.read_response_onto(&mut v)?; + Ok(v) + } - while !found_tag_line { - let raw_data = try!(self.readline()); - let line = String::from_utf8(raw_data) - .map_err(|err| Error::Parse(ParseError::DataNotUtf8(err)))?; - lines.push(line.clone()); - if (&*line).starts_with(&*start_str) { - found_tag_line = true; + fn read_response_onto(&mut self, data: &mut Vec) -> Result<()> { + let mut continue_from = None; + let mut try_first = !data.is_empty(); + let match_tag = format!("{}{}", TAG_PREFIX, self.tag); + loop { + let line_start = if try_first { + try_first = false; + 0 + } else { + let start_new = data.len(); + try!(self.readline(data)); + continue_from.take().unwrap_or(start_new) + }; + + let break_with = { + use imap_proto::{parse_response, Response, Status}; + let line = &data[line_start..]; + + match parse_response(line) { + IResult::Done( + _, + Response::Done { + tag, + status, + information, + .. + }, + ) => { + assert_eq!(tag.as_bytes(), match_tag.as_bytes()); + Some(match status { + Status::Bad | Status::No => { + Err((status, information.map(|s| s.to_string()))) + } + Status::Ok => Ok(()), + status => Err((status, None)), + }) + } + IResult::Done(..) => None, + IResult::Incomplete(..) => { + continue_from = Some(line_start); + None + } + _ => Some(Err((Status::Bye, None))), + } + }; + + match break_with { + Some(Ok(_)) => { + data.truncate(line_start); + break Ok(()); + } + Some(Err((status, expl))) => { + use imap_proto::Status; + match status { + Status::Bad => { + break Err(Error::BadResponse( + expl.unwrap_or("no explanation given".to_string()), + )) + } + Status::No => { + break Err(Error::NoResponse( + expl.unwrap_or("no explanation given".to_string()), + )) + } + _ => break Err(Error::Parse(ParseError::Invalid(data.split_off(0)))), + } + } + None => {} } } - - Ok(lines) } fn read_greeting(&mut self) -> Result<()> { - try!(self.readline()); + let mut v = Vec::new(); + try!(self.readline(&mut v)); Ok(()) } - fn readline(&mut self) -> Result> { + fn readline(&mut self, into: &mut Vec) -> Result { use std::io::BufRead; - let mut line_buffer: Vec = Vec::new(); - if try!(self.stream.read_until(LF, &mut line_buffer)) == 0 { + let read = try!(self.stream.read_until(LF, into)); + if read == 0 { return Err(Error::ConnectionLost); } if self.debug { // Remove CRLF - let len = line_buffer.len(); - let line = &line_buffer[..(len - 2)]; + let len = into.len(); + let line = &into[(len - read - 2)..(len - 2)]; print!("S: {}\n", String::from_utf8_lossy(line)); } - Ok(line_buffer) + Ok(read) } fn create_command(&mut self, command: String) -> String { @@ -545,20 +599,27 @@ impl Client { mod tests { use super::*; use super::super::mock_stream::MockStream; - use super::super::mailbox::Mailbox; use super::super::error::Result; #[test] fn read_response() { let response = "a0 OK Logged in.\r\n"; - let expected_response: Vec = vec![response.to_string()]; let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mut client = Client::new(mock_stream); let actual_response = client.read_response().unwrap(); - assert!( - expected_response == actual_response, - "expected response doesn't equal actual" - ); + assert_eq!(Vec::::new(), actual_response); + } + + + #[test] + fn fetch_body() { + let response = "a0 OK Logged in.\r\n\ + * 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n\ + a0 OK FETCH completed\r\n"; + let mock_stream = MockStream::new(response.as_bytes().to_vec()); + let mut client = Client::new(mock_stream); + client.read_response().unwrap(); + client.read_response().unwrap(); } @@ -578,7 +639,9 @@ mod tests { .with_buf(greeting.as_bytes().to_vec()) .with_delay(); let mut client = Client::new(mock_stream); - let actual_response = String::from_utf8(client.readline().unwrap()).unwrap(); + let mut v = Vec::new(); + client.readline(&mut v).unwrap(); + let actual_response = String::from_utf8(v).unwrap(); assert_eq!(expected_response, actual_response); } @@ -586,7 +649,8 @@ mod tests { fn readline_eof() { let mock_stream = MockStream::default().with_eof(); let mut client = Client::new(mock_stream); - if let Err(Error::ConnectionLost) = client.readline() { + let mut v = Vec::new(); + if let Err(Error::ConnectionLost) = client.readline(&mut v) { } else { unreachable!("EOF read did not return connection lost"); } @@ -598,7 +662,8 @@ mod tests { // TODO Check the error test let mock_stream = MockStream::default().with_err(); let mut client = Client::new(mock_stream); - client.readline().unwrap(); + let mut v = Vec::new(); + client.readline(&mut v).unwrap(); } #[test] @@ -735,11 +800,17 @@ mod tests { a1 OK [READ-ONLY] Select completed.\r\n" .to_vec(); let expected_mailbox = Mailbox { - flags: String::from("(\\Answered \\Flagged \\Deleted \\Seen \\Draft)"), + flags: vec![ + "\\Answered".to_string(), + "\\Flagged".to_string(), + "\\Deleted".to_string(), + "\\Seen".to_string(), + "\\Draft".to_string(), + ], exists: 1, recent: 1, unseen: Some(1), - permanent_flags: Some(String::from("()")), + permanent_flags: vec![], uid_next: Some(2), uid_validity: Some(1257842737), }; @@ -752,7 +823,7 @@ mod tests { client.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid examine command" ); - assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); + assert_eq!(mailbox, expected_mailbox); } #[test] @@ -768,13 +839,24 @@ mod tests { a1 OK [READ-ONLY] Select completed.\r\n" .to_vec(); let expected_mailbox = Mailbox { - flags: String::from("(\\Answered \\Flagged \\Deleted \\Seen \\Draft)"), + flags: vec![ + "\\Answered".to_string(), + "\\Flagged".to_string(), + "\\Deleted".to_string(), + "\\Seen".to_string(), + "\\Draft".to_string(), + ], exists: 1, recent: 1, unseen: Some(1), - permanent_flags: Some(String::from( - "(\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)", - )), + permanent_flags: vec![ + "\\*".to_string(), + "\\Answered".to_string(), + "\\Flagged".to_string(), + "\\Deleted".to_string(), + "\\Draft".to_string(), + "\\Seen".to_string(), + ], uid_next: Some(2), uid_validity: Some(1257842737), }; @@ -787,7 +869,7 @@ mod tests { client.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid select command" ); - assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); + assert_eq!(mailbox, expected_mailbox); } #[test] @@ -798,15 +880,15 @@ mod tests { let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; let mock_stream = MockStream::new(response); let mut client = Client::new(mock_stream); - let capabilities = client.capability().unwrap(); + let capabilities = client.capabilities().unwrap(); assert!( client.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(), "Invalid capability command" ); - assert!( - capabilities == expected_capabilities, - "Unexpected capabilities response" - ); + assert_eq!(capabilities.len(), 4); + for e in expected_capabilities { + assert!(capabilities.has(e)); + } } #[test] diff --git a/src/error.rs b/src/error.rs index 2e4f17cd..4420355a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,7 @@ use std::error::Error as StdError; use std::net::TcpStream; use std::string::FromUtf8Error; +use imap_proto::Response; use native_tls::HandshakeError as TlsHandshakeError; use native_tls::Error as TlsError; use bufstream::IntoInnerError as BufError; @@ -21,9 +22,9 @@ pub enum Error { /// An error from the `native_tls` library while managing the socket. Tls(TlsError), /// A BAD response from the IMAP server. - BadResponse(Vec), + BadResponse(String), /// A NO response from the IMAP server. - NoResponse(Vec), + NoResponse(String), /// The connection was terminated unexpectedly. ConnectionLost, // Error parsing a server response. @@ -58,6 +59,12 @@ impl From for Error { } } +impl<'a> From> for Error { + fn from(err: Response<'a>) -> Error { + Error::Parse(ParseError::Unexpected(format!("{:?}", err))) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -65,12 +72,9 @@ impl fmt::Display for Error { Error::Tls(ref e) => fmt::Display::fmt(e, f), Error::TlsHandshake(ref e) => fmt::Display::fmt(e, f), Error::Validate(ref e) => fmt::Display::fmt(e, f), - Error::BadResponse(ref data) => write!( - f, - "{}: {}", - &String::from(self.description()), - &data.join("\n") - ), + Error::NoResponse(ref data) | Error::BadResponse(ref data) => { + write!(f, "{}: {}", &String::from(self.description()), data) + } ref e => f.write_str(e.description()), } } @@ -105,9 +109,9 @@ impl StdError for Error { #[derive(Debug)] pub enum ParseError { // Indicates an error parsing the status response. Such as OK, NO, and BAD. - StatusResponse(Vec), - // Error parsing the cabability response. - Capability(Vec), + Invalid(Vec), + // An unexpected response was encountered. + Unexpected(String), // Authentication errors. Authentication(String), DataNotUtf8(FromUtf8Error), @@ -124,8 +128,8 @@ impl fmt::Display for ParseError { impl StdError for ParseError { fn description(&self) -> &str { match *self { - ParseError::StatusResponse(_) => "Unable to parse status response", - ParseError::Capability(_) => "Unable to parse capability response", + 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/lib.rs b/src/lib.rs index 9901de5e..46a66571 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,15 +4,19 @@ //! imap is a IMAP client for Rust. extern crate bufstream; +extern crate imap_proto; extern crate native_tls; +extern crate nom; extern crate regex; +mod types; +mod parse; + pub mod authenticator; pub mod client; pub mod error; -pub mod mailbox; -mod parse; +pub use types::*; #[cfg(test)] mod mock_stream; diff --git a/src/parse.rs b/src/parse.rs index 13ceca7d..d511fe67 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,8 @@ use regex::Regex; +use nom::IResult; +use imap_proto::{self, Response}; -use super::mailbox::Mailbox; +use super::types::*; use super::error::{Error, ParseError, Result}; pub fn parse_authenticate_response(line: String) -> Result { @@ -14,102 +16,186 @@ pub fn parse_authenticate_response(line: String) -> Result { Err(Error::Parse(ParseError::Authentication(line))) } -pub fn parse_capability(lines: Vec) -> Result> { - let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); - - //Check Ok - match parse_response_ok(lines.clone()) { - Ok(_) => (), - Err(e) => return Err(e), - }; +enum MapOrNot { + Map(T), + Not(Response<'static>), +} - for line in lines.iter() { - if capability_regex.is_match(line) { - let cap = capability_regex.captures(line).unwrap(); - let capabilities_str = cap.get(1).unwrap().as_str(); - return Ok(capabilities_str.split(' ').map(|x| x.to_string()).collect()); +unsafe fn parse_many(lines: Vec, mut map: F) -> ZeroCopyResult> +where + F: FnMut(Response<'static>) -> MapOrNot, +{ + let f = |mut lines| { + let mut things = Vec::new(); + loop { + match imap_proto::parse_response(lines) { + IResult::Done(rest, resp) => { + lines = rest; + + match map(resp) { + MapOrNot::Map(t) => things.push(t), + MapOrNot::Not(resp) => break Err(resp.into()), + } + + if lines.is_empty() { + break Ok(things); + } + } + _ => { + break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } } - } - - Err(Error::Parse(ParseError::Capability(lines))) -} + }; -pub fn parse_response_ok(lines: Vec) -> Result<()> { - match parse_response(lines) { - Ok(_) => Ok(()), - Err(e) => return Err(e), - } + ZeroCopy::new(lines, f) } -pub fn parse_response(lines: Vec) -> Result> { - let regex = Regex::new(r"^([a-zA-Z0-9]+) (OK|NO|BAD)(.*)").unwrap(); - let last_line = match lines.last() { - Some(l) => l, - None => return Err(Error::Parse(ParseError::StatusResponse(lines.clone()))), +pub fn parse_names(lines: Vec) -> ZeroCopyResult> { + use imap_proto::MailboxDatum; + let f = |resp| match resp { + // https://github.com/djc/imap-proto/issues/4 + Response::MailboxData(MailboxDatum::List { + flags, + delimiter, + name, + }) | + Response::MailboxData(MailboxDatum::SubList { + flags, + delimiter, + name, + }) => MapOrNot::Map(Name { + attributes: flags, + delimiter, + name, + }), + resp => MapOrNot::Not(resp), }; - for cap in regex.captures_iter(last_line) { - let response_type = cap.get(2).map(|x| x.as_str()).unwrap_or(""); - match response_type { - "OK" => return Ok(lines.clone()), - "BAD" => return Err(Error::BadResponse(lines.clone())), - "NO" => return Err(Error::NoResponse(lines.clone())), - _ => {} - } - } - - Err(Error::Parse(ParseError::StatusResponse(lines.clone()))) + unsafe { parse_many(lines, f) } } -pub fn parse_select_or_examine(lines: Vec) -> Result { - let exists_regex = Regex::new(r"^\* (\d+) EXISTS\r\n").unwrap(); - - let recent_regex = Regex::new(r"^\* (\d+) RECENT\r\n").unwrap(); - - let flags_regex = Regex::new(r"^\* FLAGS (.+)\r\n").unwrap(); - - let unseen_regex = Regex::new(r"^\* OK \[UNSEEN (\d+)\](.*)\r\n").unwrap(); - - let uid_validity_regex = Regex::new(r"^\* OK \[UIDVALIDITY (\d+)\](.*)\r\n").unwrap(); - - let uid_next_regex = Regex::new(r"^\* OK \[UIDNEXT (\d+)\](.*)\r\n").unwrap(); +pub fn parse_fetches(lines: Vec) -> ZeroCopyResult> { + let f = |resp| match resp { + Response::Fetch(num, attrs) => { + let mut fetch = Fetch { + message: num, + flags: vec![], + uid: None, + rfc822: None, + }; + + for attr in attrs { + use imap_proto::AttributeValue; + match attr { + AttributeValue::Flags(flags) => { + fetch.flags.extend(flags); + } + AttributeValue::Uid(uid) => fetch.uid = Some(uid), + AttributeValue::Rfc822(rfc) => fetch.rfc822 = rfc, + _ => {} + } + } + + MapOrNot::Map(fetch) + } + resp => MapOrNot::Not(resp), + }; - let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\](.*)\r\n").unwrap(); + unsafe { parse_many(lines, f) } +} - //Check Ok - match parse_response_ok(lines.clone()) { - Ok(_) => (), - Err(e) => return Err(e), +pub fn parse_capabilities(lines: Vec) -> ZeroCopyResult { + let f = |mut lines| { + use std::collections::HashSet; + let mut caps = HashSet::new(); + loop { + match imap_proto::parse_response(lines) { + IResult::Done(rest, Response::Capabilities(c)) => { + lines = rest; + caps.extend(c); + + if lines.is_empty() { + break Ok(Capabilities(caps)); + } + } + IResult::Done(_, resp) => { + break Err(resp.into()); + } + _ => { + break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } + } + } }; + unsafe { ZeroCopy::new(lines, f) } +} + +pub fn parse_mailbox(mut lines: &[u8]) -> Result { let mut mailbox = Mailbox::default(); - for line in lines.iter() { - if exists_regex.is_match(line) { - let cap = exists_regex.captures(line).unwrap(); - mailbox.exists = cap.get(1).unwrap().as_str().parse::().unwrap(); - } else if recent_regex.is_match(line) { - let cap = recent_regex.captures(line).unwrap(); - mailbox.recent = cap.get(1).unwrap().as_str().parse::().unwrap(); - } else if flags_regex.is_match(line) { - let cap = flags_regex.captures(line).unwrap(); - mailbox.flags = cap.get(1).unwrap().as_str().to_string(); - } else if unseen_regex.is_match(line) { - let cap = unseen_regex.captures(line).unwrap(); - mailbox.unseen = Some(cap.get(1).unwrap().as_str().parse::().unwrap()); - } else if uid_validity_regex.is_match(line) { - let cap = uid_validity_regex.captures(line).unwrap(); - mailbox.uid_validity = Some(cap.get(1).unwrap().as_str().parse::().unwrap()); - } else if uid_next_regex.is_match(line) { - let cap = uid_next_regex.captures(line).unwrap(); - mailbox.uid_next = Some(cap.get(1).unwrap().as_str().parse::().unwrap()); - } else if permanent_flags_regex.is_match(line) { - let cap = permanent_flags_regex.captures(line).unwrap(); - mailbox.permanent_flags = Some(cap.get(1).unwrap().as_str().to_string()); + loop { + match imap_proto::parse_response(lines) { + IResult::Done(rest, Response::Data { status, code, .. }) => { + lines = rest; + + if let imap_proto::Status::Ok = status { + } else { + // how can this happen for a Response::Data? + unreachable!(); + } + + use imap_proto::ResponseCode; + match code { + Some(ResponseCode::UidValidity(uid)) => { + mailbox.uid_validity = Some(uid); + } + Some(ResponseCode::UidNext(unext)) => { + mailbox.uid_next = Some(unext); + } + Some(ResponseCode::Unseen(n)) => { + mailbox.unseen = Some(n); + } + Some(ResponseCode::PermanentFlags(flags)) => { + mailbox + .permanent_flags + .extend(flags.into_iter().map(|s| s.to_string())); + } + _ => {} + } + } + IResult::Done(rest, Response::MailboxData(m)) => { + lines = rest; + + use imap_proto::MailboxDatum; + match m { + MailboxDatum::Exists(e) => { + mailbox.exists = e; + } + MailboxDatum::Recent(r) => { + mailbox.recent = r; + } + MailboxDatum::Flags(flags) => { + mailbox + .flags + .extend(flags.into_iter().map(|s| s.to_string())); + } + MailboxDatum::SubList { .. } | MailboxDatum::List { .. } => {} + } + } + IResult::Done(_, resp) => { + break Err(resp.into()); + } + _ => { + break Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); + } } - } - Ok(mailbox) + if lines.is_empty() { + break Ok(mailbox); + } + } } #[cfg(test)] @@ -118,51 +204,46 @@ mod tests { #[test] fn parse_capability_test() { - let expected_capabilities = vec![ - String::from("IMAP4rev1"), - String::from("STARTTLS"), - String::from("AUTH=GSSAPI"), - String::from("LOGINDISABLED"), - ]; - let lines = vec![ - String::from("* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"), - String::from("a1 OK CAPABILITY completed\r\n"), - ]; - let capabilities = parse_capability(lines).unwrap(); - assert!( - capabilities == expected_capabilities, - "Unexpected capabilities parse response" - ); + 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(); + assert_eq!(capabilities.len(), 4); + for e in expected_capabilities { + assert!(capabilities.has(e)); + } } #[test] #[should_panic] fn parse_capability_invalid_test() { - let lines = vec![ - String::from("* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"), - String::from("a1 OK CAPABILITY completed\r\n"), - ]; - parse_capability(lines).unwrap(); + let lines = b"* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; + parse_capabilities(lines.to_vec()).unwrap(); } #[test] - fn parse_response_test() { - let lines = vec![ - String::from("* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"), - String::from("a2 OK List completed.\r\n"), - ]; - let expected_lines = lines.clone(); - let actual_lines = parse_response(lines).unwrap(); - assert!(expected_lines == actual_lines, "Unexpected parse response"); + fn parse_names_test() { + let lines = b"* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"; + let names = parse_names(lines.to_vec()).unwrap(); + assert_eq!(names.len(), 1); + assert_eq!(names[0].attributes(), &["\\HasNoChildren"]); + assert_eq!(names[0].delimiter(), "."); + assert_eq!(names[0].name(), "INBOX"); } #[test] - #[should_panic] - fn parse_response_invalid_test() { - let lines = vec![ - String::from("* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"), - String::from("a2 BAD broken.\r\n"), - ]; - parse_response(lines).unwrap(); + fn parse_fetches_test() { + 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(); + assert_eq!(fetches.len(), 2); + assert_eq!(fetches[0].message, 24); + assert_eq!(fetches[0].flags(), &["\\Seen"]); + assert_eq!(fetches[0].uid, Some(4827943)); + assert_eq!(fetches[0].rfc822(), None); + assert_eq!(fetches[1].message, 25); + assert_eq!(fetches[1].flags(), &["\\Seen"]); + assert_eq!(fetches[1].uid, None); + assert_eq!(fetches[1].rfc822(), None); } } diff --git a/src/types/capabilities.rs b/src/types/capabilities.rs new file mode 100644 index 00000000..a42c7e75 --- /dev/null +++ b/src/types/capabilities.rs @@ -0,0 +1,29 @@ +// Note that none of these fields are *actually* 'static. +// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`. +use std::collections::HashSet; +use std::collections::hash_set::Iter; +pub struct Capabilities(pub(crate) HashSet<&'static str>); + +use std::hash::Hash; +use std::borrow::Borrow; +impl Capabilities { + pub fn has(&self, s: &S) -> bool + where + for<'a> &'a str: Borrow, + S: Hash + Eq, + { + self.0.contains(s) + } + + pub fn iter<'a>(&'a self) -> Iter<'a, &'a str> { + self.0.iter() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/src/types/fetch.rs b/src/types/fetch.rs new file mode 100644 index 00000000..23f900db --- /dev/null +++ b/src/types/fetch.rs @@ -0,0 +1,19 @@ +// Note that none of these fields are *actually* 'static. +// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`. +#[derive(Debug, Eq, PartialEq)] +pub struct Fetch { + pub message: u32, + pub(crate) flags: Vec<&'static str>, + pub uid: Option, + pub(crate) rfc822: Option<&'static [u8]>, +} + +impl Fetch { + pub fn flags<'a>(&'a self) -> &'a [&'a str] { + &self.flags[..] + } + + pub fn rfc822<'a>(&'a self) -> Option<&'a [u8]> { + self.rfc822 + } +} diff --git a/src/mailbox.rs b/src/types/mailbox.rs similarity index 78% rename from src/mailbox.rs rename to src/types/mailbox.rs index f6bb3757..1d3a59d6 100644 --- a/src/mailbox.rs +++ b/src/types/mailbox.rs @@ -2,11 +2,11 @@ use std::fmt; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Mailbox { - pub flags: String, + pub flags: Vec, pub exists: u32, pub recent: u32, pub unseen: Option, - pub permanent_flags: Option, + pub permanent_flags: Vec, pub uid_next: Option, pub uid_validity: Option, } @@ -14,11 +14,11 @@ pub struct Mailbox { impl Default for Mailbox { fn default() -> Mailbox { Mailbox { - flags: "".to_string(), + flags: Vec::new(), exists: 0, recent: 0, unseen: None, - permanent_flags: None, + permanent_flags: Vec::new(), uid_next: None, uid_validity: None, } @@ -29,7 +29,7 @@ impl fmt::Display for Mailbox { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ + "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ uid_next: {:?}, uid_validity: {:?}", self.flags, self.exists, diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 00000000..42f89600 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,127 @@ +mod mailbox; +pub use self::mailbox::Mailbox; + +mod fetch; +pub use self::fetch::Fetch; + +mod name; +pub use self::name::Name; + +mod capabilities; +pub use self::capabilities::Capabilities; + +pub struct ZeroCopy { + owned: Box<[u8]>, + derived: D, +} + +impl ZeroCopy { + /// Derive a new `ZeroCopy` view of the byte data stored in `owned`. + /// + /// # Safety + /// + /// The `derive` callback will be passed a `&'static [u8]`. However, this reference is not, in + /// fact `'static`. Instead, it is only valid for as long as the `ZeroCopy` lives. Therefore, + /// it is *only* safe to call this function if *every* accessor on `D` returns either a type + /// that does not contain any borrows, *or* where the return type is bound to the lifetime of + /// `&self`. + /// + /// It is *not* safe for the error type `E` to borrow from the passed reference. + pub unsafe fn new(owned: Vec, derive: F) -> Result + where + F: FnOnce(&'static [u8]) -> Result, + { + use std::mem; + + // the memory pointed to by `owned` now has a stable address (on the heap). + // even if we move the `Box` (i.e., into `ZeroCopy`), a slice to it will remain valid. + let owned = owned.into_boxed_slice(); + + // this is the unsafe part -- the implementor of `derive` must be aware that the reference + // they are passed is not *really* 'static, but rather the lifetime of `&self`. + let static_owned_ref: &'static [u8] = mem::transmute(&*owned); + + Ok(ZeroCopy { + owned, + derived: derive(static_owned_ref)?, + }) + } +} + +use super::error::Error; +pub type ZeroCopyResult = Result, Error>; + +use std::ops::Deref; +impl Deref for ZeroCopy { + type Target = D; + fn deref(&self) -> &Self::Target { + &self.derived + } +} + +// re-implement standard traits +// basically copied from Rc + +impl PartialEq for ZeroCopy { + fn eq(&self, other: &ZeroCopy) -> bool { + **self == **other + } + fn ne(&self, other: &ZeroCopy) -> bool { + **self != **other + } +} +impl Eq for ZeroCopy {} + +use std::cmp::Ordering; +impl PartialOrd for ZeroCopy { + fn partial_cmp(&self, other: &ZeroCopy) -> Option { + (**self).partial_cmp(&**other) + } + fn lt(&self, other: &ZeroCopy) -> bool { + **self < **other + } + fn le(&self, other: &ZeroCopy) -> bool { + **self <= **other + } + fn gt(&self, other: &ZeroCopy) -> bool { + **self > **other + } + fn ge(&self, other: &ZeroCopy) -> bool { + **self >= **other + } +} +impl Ord for ZeroCopy { + fn cmp(&self, other: &ZeroCopy) -> Ordering { + (**self).cmp(&**other) + } +} + +use std::hash::{Hash, Hasher}; +impl Hash for ZeroCopy { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +use std::fmt; +impl fmt::Display for ZeroCopy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} +impl fmt::Debug for ZeroCopy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl<'a, D> IntoIterator for &'a ZeroCopy +where + &'a D: IntoIterator, +{ + type Item = <&'a D as IntoIterator>::Item; + type IntoIter = <&'a D as IntoIterator>::IntoIter; + fn into_iter(self) -> Self::IntoIter { + (**self).into_iter() + } +} diff --git a/src/types/name.rs b/src/types/name.rs new file mode 100644 index 00000000..74c635f2 --- /dev/null +++ b/src/types/name.rs @@ -0,0 +1,22 @@ +// Note that none of these fields are *actually* 'static. +// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`. +#[derive(Debug, Eq, PartialEq)] +pub struct Name { + pub(crate) attributes: Vec<&'static str>, + pub(crate) delimiter: &'static str, + pub(crate) name: &'static str, +} + +impl Name { + pub fn attributes<'a>(&'a self) -> &'a [&'a str] { + &self.attributes[..] + } + + pub fn delimiter<'a>(&'a self) -> &'a str { + self.delimiter + } + + pub fn name<'a>(&'a self) -> &'a str { + self.name + } +}