From 820d1eccf7f2a84ef9a78c6a74925c075515e91c Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Sat, 11 Aug 2018 12:38:50 +0200 Subject: [PATCH 1/8] client: separate out UnauthenticatedClient, InnerClient fixes #80 (client separation) --- src/client.rs | 471 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 282 insertions(+), 189 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7008735d..277ac2f0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -37,6 +37,18 @@ fn validate_str(value: &str) -> Result { /// Stream to interface with the IMAP server. This interface is only for the command stream. #[derive(Debug)] pub struct Client { + inner: InnerClient, +} + +// TODO: desc +#[derive(Debug)] +pub struct UnauthenticatedClient { + inner: InnerClient, +} + +// TODO: docs +#[derive(Debug)] +struct InnerClient { stream: BufStream, tag: u32, pub debug: bool, @@ -91,13 +103,13 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { // a) if there's an error, or // b) *after* we send DONE let mut v = Vec::new(); - self.client.readline(&mut v)?; + self.client.inner.readline(&mut v)?; if v.starts_with(b"+") { self.done = false; return Ok(()); } - self.client.read_response_onto(&mut v)?; + self.client.inner.read_response_onto(&mut v)?; // We should *only* get a continuation on an error (i.e., it gives BAD or NO). unreachable!(); } @@ -105,8 +117,8 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { fn terminate(&mut self) -> Result<()> { if !self.done { self.done = true; - self.client.write_line(b"DONE")?; - self.client.read_response().map(|_| ()) + self.client.inner.write_line(b"DONE")?; + self.client.inner.read_response().map(|_| ()) } else { Ok(()) } @@ -117,7 +129,7 @@ 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<()> { let mut v = Vec::new(); - match self.client.readline(&mut v).map(|_| ()) { + match self.client.inner.readline(&mut v).map(|_| ()) { Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => { @@ -167,11 +179,12 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> IdleHandle<'a, T> { /// Block until the selected mailbox changes, or until the given amount of time has expired. pub fn wait_timeout(mut self, timeout: Duration) -> Result<()> { self.client + .inner .stream .get_mut() .set_read_timeout(Some(timeout))?; let res = self.wait_inner(); - self.client.stream.get_mut().set_read_timeout(None).is_ok(); + self.client.inner.stream.get_mut().set_read_timeout(None).is_ok(); res } } @@ -197,14 +210,14 @@ impl<'a> SetReadTimeout for TlsStream { } } -impl Client { +impl UnauthenticatedClient { /// Creates a new client. - pub fn connect(addr: A) -> Result> { + pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = Client::new(stream); + let mut socket = UnauthenticatedClient::new(stream); - socket.read_greeting()?; + socket.inner.read_greeting()?; Ok(socket) } Err(e) => Err(Error::Io(e)), @@ -218,31 +231,31 @@ impl Client { mut self, domain: &str, ssl_connector: &TlsConnector, - ) -> Result>> { + ) -> Result>> { // TODO This needs to be tested - self.run_command_and_check_ok("STARTTLS")?; - TlsConnector::connect(ssl_connector, domain, self.stream.into_inner()?) - .map(Client::new) + self.inner.run_command_and_check_ok("STARTTLS")?; + TlsConnector::connect(ssl_connector, domain, self.inner.stream.into_inner()?) + .map(UnauthenticatedClient::new) .map_err(Error::TlsHandshake) } } -impl Client> { +impl UnauthenticatedClient> { /// Creates a client with an SSL wrapper. pub fn secure_connect( addr: A, domain: &str, ssl_connector: &TlsConnector, - ) -> Result>> { + ) -> Result>> { match TcpStream::connect(addr) { Ok(stream) => { let ssl_stream = match TlsConnector::connect(ssl_connector, domain, stream) { Ok(s) => s, Err(e) => return Err(Error::TlsHandshake(e)), }; - let mut socket = Client::new(ssl_stream); + let mut socket = UnauthenticatedClient::new(ssl_stream); - socket.read_greeting()?; + socket.inner.read_greeting()?; Ok(socket) } Err(e) => Err(Error::Io(e)), @@ -250,53 +263,228 @@ impl Client> { } } -impl Client { +impl UnauthenticatedClient { /// Creates a new client with the underlying stream. - pub fn new(stream: T) -> Client { - Client { - stream: BufStream::new(stream), - tag: INITIAL_TAG, - debug: false, + pub fn new(stream: T) -> UnauthenticatedClient { + UnauthenticatedClient { + inner: InnerClient { + stream: BufStream::new(stream), + tag: INITIAL_TAG, + debug: false, + }, } } /// Authenticate will authenticate with the server, using the authenticator given. pub fn authenticate( - &mut self, + mut self, auth_type: &str, authenticator: A, - ) -> Result<()> { - self.run_command(&format!("AUTHENTICATE {}", auth_type))?; - self.do_auth_handshake(authenticator) + ) -> ::std::result::Result, (Error, UnauthenticatedClient)> { + // explicit match block neccessary to convert error to tuple and not bind self too early + // (see also comment on `login`) + match self.inner.run_command(&format!("AUTHENTICATE {}", auth_type)) { + Ok(_) => self.do_auth_handshake(authenticator), + Err(e) => Err((e, self)), + } } /// This func does the handshake process once the authenticate command is made. - fn do_auth_handshake(&mut self, authenticator: A) -> Result<()> { + fn do_auth_handshake( + mut self, + authenticator: A + ) -> ::std::result::Result, (Error, UnauthenticatedClient)> { // TODO Clean up this code loop { let mut line = Vec::new(); - self.readline(&mut line)?; + // explicit match blocks neccessary to convert error to tuple and not bind self too + // early (see also comment on `login`) + if let Err(e) = self.inner.readline(&mut line) { + return Err((e, self)); + } if line.starts_with(b"+") { - let data = parse_authenticate_response(String::from_utf8(line).unwrap())?; + let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { + Ok(l) => l, + Err(e) => return Err((e, self)), + }; let auth_response = authenticator.process(data); - self.write_line(auth_response.into_bytes().as_slice())? + if let Err(e) = self.inner.write_line(auth_response.into_bytes().as_slice()) { + return Err((e, self)); + } } else { - return self.read_response_onto(&mut line).map(|_| ()); + return match self.inner.read_response_onto(&mut line) { + Ok(()) => Ok(Client { inner: self.inner }), + Err(e) => Err((e, self)), + } } } } /// Log in to the IMAP server. - pub fn login(&mut self, username: &str, password: &str) -> Result<()> { - self.run_command_and_check_ok(&format!( - "LOGIN {} {}", - validate_str(username)?, - validate_str(password)? - )) + pub fn login( + mut self, + username: &str, + password: &str + ) -> ::std::result::Result, (Error, UnauthenticatedClient)> { + // note that we need the explicit match blocks here for two reasons: + // 1. we need to convert the validate_str error type to our tuple of + // (Error, UnauthenticatedClient) + // 2. we can't use `.map_err(|e| (e, self))` because that would capture self into the + // closure. this way borowck sees that self is only bound in the error case where we + // return anyways. + let u = match validate_str(username) { + Ok(u) => u, + Err(e) => return Err((e, self)), + }; + let p = match validate_str(password) { + Ok(p) => p, + Err(e) => return Err((e, self)), + }; + + match self.inner.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)) { + Ok(()) => Ok(Client { inner: self.inner }), + Err(e) => Err((e, self)), + } + } +} + +impl InnerClient { + fn read_greeting(&mut self) -> Result<()> { + let mut v = Vec::new(); + self.readline(&mut v)?; + Ok(()) + } + + fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { + self.run_command_and_read_response(command).map(|_| ()) + } + + fn run_command(&mut self, untagged_command: &str) -> Result<()> { + let command = self.create_command(untagged_command.to_string()); + self.write_line(command.into_bytes().as_slice()) + } + + fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { + self.run_command(untagged_command)?; + self.read_response() + } + + fn read_response(&mut self) -> Result> { + let mut v = Vec::new(); + self.read_response_onto(&mut v)?; + Ok(v) + } + + 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(); + 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 => {} + } + } + } + + fn readline(&mut self, into: &mut Vec) -> Result { + use std::io::BufRead; + let read = self.stream.read_until(LF, into)?; + if read == 0 { + return Err(Error::ConnectionLost); + } + + if self.debug { + // Remove CRLF + let len = into.len(); + let line = &into[(len - read)..(len - 2)]; + print!("S: {}\n", String::from_utf8_lossy(line)); + } + + Ok(read) } + fn create_command(&mut self, command: String) -> String { + self.tag += 1; + let command = format!("{}{} {}", TAG_PREFIX, self.tag, command); + return command; + } + + fn write_line(&mut self, buf: &[u8]) -> Result<()> { + self.stream.write_all(buf)?; + self.stream.write_all(&[CR, LF])?; + self.stream.flush()?; + if self.debug { + print!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap()); + } + Ok(()) + } +} + + +impl Client { /// Selects a mailbox /// /// Note that the server *is* allowed to unilaterally send things to the client for messages in @@ -305,13 +493,15 @@ impl Client { /// [`Client::run_command_and_read_response`], you *may* see additional untagged `RECENT`, /// `EXISTS`, `FETCH`, and `EXPUNGE` responses! pub fn select(&mut self, mailbox_name: &str) -> Result { - self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) + self.inner + .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 { - self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) + self.inner + .run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) .and_then(|lines| parse_mailbox(&lines[..])) } @@ -321,7 +511,8 @@ impl Client { /// messages in the selected mailbox whose status has changed. See the note on [unilateral /// 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)) + self.inner + .run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query)) .and_then(|lines| parse_fetches(lines)) } @@ -331,33 +522,39 @@ impl Client { /// messages in the selected mailbox whose status has changed. See the note on [unilateral /// 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)) + self.inner + .run_command_and_read_response(&format!("UID FETCH {} {}", uid_set, query)) .and_then(|lines| parse_fetches(lines)) } /// Noop always succeeds, and it does nothing. pub fn noop(&mut self) -> Result<()> { - self.run_command_and_check_ok("NOOP") + self.inner + .run_command_and_check_ok("NOOP") } /// Logout informs the server that the client is done with the connection. pub fn logout(&mut self) -> Result<()> { - self.run_command_and_check_ok("LOGOUT") + self.inner + .run_command_and_check_ok("LOGOUT") } /// Create creates a mailbox with the given name. pub fn create(&mut self, mailbox_name: &str) -> Result<()> { - self.run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name)?)) + self.inner + .run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name)?)) } /// Delete permanently removes the mailbox with the given name. pub fn delete(&mut self, mailbox_name: &str) -> Result<()> { - self.run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name)?)) + self.inner + .run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name)?)) } /// Rename changes the name of a mailbox. pub fn rename(&mut self, current_mailbox_name: &str, new_mailbox_name: &str) -> Result<()> { - self.run_command_and_check_ok(&format!( + self.inner + .run_command_and_check_ok(&format!( "RENAME {} {}", quote!(current_mailbox_name), quote!(new_mailbox_name) @@ -367,56 +564,66 @@ impl Client { /// Subscribe adds the specified mailbox name to the server's set of "active" or "subscribed" /// mailboxes as returned by the LSUB command. pub fn subscribe(&mut self, mailbox: &str) -> Result<()> { - self.run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox))) + self.inner + .run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox))) } /// Unsubscribe removes the specified mailbox name from the server's set of /// "active" or "subscribed mailboxes as returned by the LSUB command. pub fn unsubscribe(&mut self, mailbox: &str) -> Result<()> { - self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox))) + self.inner + .run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox))) } /// Capability requests a listing of capabilities that the server supports. pub fn capabilities(&mut self) -> ZeroCopyResult { - self.run_command_and_read_response(&format!("CAPABILITY")) + self.inner + .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 /// selected mailbox. pub fn expunge(&mut self) -> Result<()> { - self.run_command_and_check_ok("EXPUNGE") + self.inner + .run_command_and_check_ok("EXPUNGE") } /// Check requests a checkpoint of the currently selected mailbox. pub fn check(&mut self) -> Result<()> { - self.run_command_and_check_ok("CHECK") + self.inner + .run_command_and_check_ok("CHECK") } /// Close permanently removes all messages that have the \Deleted flag set from the currently /// selected mailbox, and returns to the authenticated state from the selected state. pub fn close(&mut self) -> Result<()> { - self.run_command_and_check_ok("CLOSE") + self.inner + .run_command_and_check_ok("CLOSE") } /// 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)) + self.inner + .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) -> ZeroCopyResult> { - self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query)) + self.inner + .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. pub fn copy(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> { - self.run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name)) + self.inner + .run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name)) } pub fn uid_copy(&mut self, uid_set: &str, mailbox_name: &str) -> Result<()> { - self.run_command_and_check_ok(&format!("UID COPY {} {}", uid_set, mailbox_name)) + self.inner + .run_command_and_check_ok(&format!("UID COPY {} {}", uid_set, mailbox_name)) } /// The LIST command returns a subset of names from the complete set @@ -426,7 +633,8 @@ impl Client { reference_name: &str, mailbox_search_pattern: &str, ) -> ZeroCopyResult> { - self.run_command_and_read_response(&format!( + self.inner + .run_command_and_read_response(&format!( "LIST {} {}", quote!(reference_name), mailbox_search_pattern @@ -440,7 +648,8 @@ impl Client { reference_name: &str, mailbox_search_pattern: &str, ) -> ZeroCopyResult> { - self.run_command_and_read_response(&format!( + self.inner + .run_command_and_read_response(&format!( "LSUB {} {}", quote!(reference_name), mailbox_search_pattern @@ -449,7 +658,8 @@ impl Client { /// 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_read_response(&format!( + self.inner + .run_command_and_read_response(&format!( "STATUS {} {}", validate_str(mailbox_name)?, status_data_items @@ -464,27 +674,28 @@ impl Client { /// The APPEND command adds a mail to a mailbox. pub fn append(&mut self, folder: &str, content: &[u8]) -> Result<()> { - self.run_command(&format!("APPEND \"{}\" {{{}}}", folder, content.len()))?; + self.inner + .run_command(&format!("APPEND \"{}\" {{{}}}", folder, content.len()))?; let mut v = Vec::new(); - self.readline(&mut v)?; + self.inner.readline(&mut v)?; if !v.starts_with(b"+") { return Err(Error::Append); } - self.stream.write_all(content)?; - self.stream.write_all(b"\r\n")?; - self.stream.flush()?; - self.read_response().map(|_| ()) + self.inner.stream.write_all(content)?; + self.inner.stream.write_all(b"\r\n")?; + self.inner.stream.flush()?; + self.inner.read_response().map(|_| ()) } + // these are only here because they are public interface, the rest is in InnerClient /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { - self.run_command_and_read_response(command).map(|_| ()) + self.inner.run_command_and_read_response(command).map(|_| (())) } /// Runs any command passed to it. pub fn run_command(&mut self, untagged_command: &str) -> Result<()> { - let command = self.create_command(untagged_command.to_string()); - self.write_line(command.into_bytes().as_slice()) + self.inner.run_command(untagged_command) } /// Run a raw IMAP command and read back its response. @@ -494,125 +705,7 @@ impl Client { /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that you *may* see /// additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses! pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { - self.run_command(untagged_command)?; - self.read_response() - } - - fn read_response(&mut self) -> Result> { - let mut v = Vec::new(); - self.read_response_onto(&mut v)?; - Ok(v) - } - - 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(); - 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 => {} - } - } - } - - fn read_greeting(&mut self) -> Result<()> { - let mut v = Vec::new(); - self.readline(&mut v)?; - Ok(()) - } - - fn readline(&mut self, into: &mut Vec) -> Result { - use std::io::BufRead; - let read = self.stream.read_until(LF, into)?; - if read == 0 { - return Err(Error::ConnectionLost); - } - - if self.debug { - // Remove CRLF - let len = into.len(); - let line = &into[(len - read)..(len - 2)]; - print!("S: {}\n", String::from_utf8_lossy(line)); - } - - Ok(read) - } - - fn create_command(&mut self, command: String) -> String { - self.tag += 1; - let command = format!("{}{} {}", TAG_PREFIX, self.tag, command); - return command; - } - - fn write_line(&mut self, buf: &[u8]) -> Result<()> { - self.stream.write_all(buf)?; - self.stream.write_all(&[CR, LF])?; - self.stream.flush()?; - if self.debug { - print!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap()); - } - Ok(()) + self.inner.run_command_and_read_response(untagged_command) } } From 3d6dd58f19faa39f5831a971fc24c625039e4f0e Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Sat, 11 Aug 2018 13:41:48 +0200 Subject: [PATCH 2/8] adjust examples to Client/UnauthenticatedClient splitup --- examples/basic.rs | 12 +++++++++--- examples/gmail_oauth2.rs | 14 ++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index 4f28dfdf..fc643a35 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,7 @@ extern crate imap; extern crate native_tls; -use imap::client::Client; +use imap::client::UnauthenticatedClient; use native_tls::TlsConnector; // To connect to the gmail IMAP server with this you will need to allow unsecure apps access. @@ -12,9 +12,15 @@ fn main() { let port = 993; let socket_addr = (domain, port); let ssl_connector = TlsConnector::builder().build().unwrap(); - let mut imap_socket = Client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); + let unauth_client = UnauthenticatedClient::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); - imap_socket.login("username", "password").unwrap(); + let mut imap_socket = match unauth_client.login("username", "password") { + Ok(c) => c, + Err((e, _unauth_client)) => { + println!("failed to login: {}", e); + return; + }, + }; match imap_socket.capabilities() { Ok(capabilities) => for capability in capabilities.iter() { diff --git a/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs index 06ccae6a..d334cf75 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -4,7 +4,7 @@ extern crate native_tls; use base64::encode; use imap::authenticator::Authenticator; -use imap::client::Client; +use imap::client::UnauthenticatedClient; use native_tls::TlsConnector; struct GmailOAuth2 { @@ -33,9 +33,15 @@ fn main() { let port = 993; let socket_addr = (domain, port); let ssl_connector = TlsConnector::builder().build().unwrap(); - let mut imap_socket = Client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); - - imap_socket.authenticate("XOAUTH2", gmail_auth).unwrap(); + let unauth_client = UnauthenticatedClient::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); + + let mut imap_socket = match unauth_client.authenticate("XOAUTH2", gmail_auth) { + Ok(c) => c, + Err((e, _unauth_client)) => { + println!("error authenticating: {}", e); + return; + } + }; match imap_socket.select("INBOX") { Ok(mailbox) => println!("{}", mailbox), From 71f8e6bcd29ac3ff0f9199fb4ba587d9f3ee365c Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Wed, 29 Aug 2018 14:40:02 +0200 Subject: [PATCH 3/8] rename types as per suggestion in PR #84 --- src/client.rs | 268 ++++++++++++++++++++++++++------------------------ 1 file changed, 139 insertions(+), 129 deletions(-) diff --git a/src/client.rs b/src/client.rs index 277ac2f0..6a353f21 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,6 +4,7 @@ use nom::IResult; use std::io::{self, Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; use std::time::Duration; +use std::ops::{Deref,DerefMut}; use super::authenticator::Authenticator; use super::error::{Error, ParseError, Result, ValidateError}; @@ -36,24 +37,55 @@ fn validate_str(value: &str) -> Result { /// Stream to interface with the IMAP server. This interface is only for the command stream. #[derive(Debug)] -pub struct Client { - inner: InnerClient, +pub struct Session { + conn: Connection, } // TODO: desc #[derive(Debug)] -pub struct UnauthenticatedClient { - inner: InnerClient, +pub struct Client { + conn: Connection, } // TODO: docs #[derive(Debug)] -struct InnerClient { +pub struct Connection { stream: BufStream, tag: u32, pub debug: bool, } +// these instances are so we can make the self.inner stuff a little prettier +// TODO(dario): docs +impl Deref for Client { + type Target = Connection; + + fn deref(&self) -> &Connection { + &self.conn + } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Connection { + &mut self.conn + } +} + +impl Deref for Session { + type Target = Connection; + + fn deref(&self) -> &Connection { + &self.conn + } +} + +impl DerefMut for Session { + fn deref_mut(&mut self) -> &mut Connection { + &mut self.conn + } +} + + /// `IdleHandle` allows a client to block waiting for changes to the remote mailbox. /// /// The handle blocks using the IMAP IDLE command specificed in [RFC @@ -62,12 +94,12 @@ struct InnerClient { /// As long a the handle is active, the mailbox cannot be otherwise accessed. #[derive(Debug)] pub struct IdleHandle<'a, T: Read + Write + 'a> { - client: &'a mut Client, + session: &'a mut Session, keepalive: Duration, done: bool, } -/// Must be implemented for a transport in order for a `Client` using that transport to support +/// Must be implemented for a transport in order for a `Session` using that transport to support /// operations with timeouts. /// /// Examples of where this is useful is for `IdleHandle::wait_keepalive` and @@ -82,9 +114,9 @@ pub trait SetReadTimeout { } impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { - fn new(client: &'a mut Client) -> Result { + fn new(session: &'a mut Session) -> Result { let mut h = IdleHandle { - client: client, + session: session, keepalive: Duration::from_secs(29 * 60), done: false, }; @@ -96,20 +128,20 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { // https://tools.ietf.org/html/rfc2177 // // The IDLE command takes no arguments. - self.client.run_command("IDLE")?; + self.session.run_command("IDLE")?; // A tagged response will be sent either // // a) if there's an error, or // b) *after* we send DONE let mut v = Vec::new(); - self.client.inner.readline(&mut v)?; + self.session.readline(&mut v)?; if v.starts_with(b"+") { self.done = false; return Ok(()); } - self.client.inner.read_response_onto(&mut v)?; + self.session.read_response_onto(&mut v)?; // We should *only* get a continuation on an error (i.e., it gives BAD or NO). unreachable!(); } @@ -117,8 +149,8 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { fn terminate(&mut self) -> Result<()> { if !self.done { self.done = true; - self.client.inner.write_line(b"DONE")?; - self.client.inner.read_response().map(|_| ()) + self.session.write_line(b"DONE")?; + self.session.read_response().map(|_| ()) } else { Ok(()) } @@ -126,10 +158,10 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> { /// Internal helper that doesn't consume self. /// - /// This is necessary so that we can keep using the inner `Client` in `wait_keepalive`. + /// 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.client.inner.readline(&mut v).map(|_| ()) { + match self.session.readline(&mut v).map(|_| ()) { Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => { @@ -178,13 +210,12 @@ impl<'a, T: SetReadTimeout + Read + Write + 'a> IdleHandle<'a, T> { /// Block until the selected mailbox changes, or until the given amount of time has expired. pub fn wait_timeout(mut self, timeout: Duration) -> Result<()> { - self.client - .inner + self.session .stream .get_mut() .set_read_timeout(Some(timeout))?; let res = self.wait_inner(); - self.client.inner.stream.get_mut().set_read_timeout(None).is_ok(); + self.session.stream.get_mut().set_read_timeout(None).is_ok(); res } } @@ -210,20 +241,42 @@ impl<'a> SetReadTimeout for TlsStream { } } -impl UnauthenticatedClient { - /// Creates a new client. - pub fn connect(addr: A) -> Result> { - match TcpStream::connect(addr) { - Ok(stream) => { - let mut socket = UnauthenticatedClient::new(stream); +/// Creates a new client. +pub fn connect(addr: A) -> Result> { + match TcpStream::connect(addr) { + Ok(stream) => { + let mut socket = Client::new(stream); - socket.inner.read_greeting()?; - Ok(socket) - } - Err(e) => Err(Error::Io(e)), + socket.read_greeting()?; + Ok(socket) } + Err(e) => Err(Error::Io(e)), } +} +/// Creates a client with an SSL wrapper. +pub fn secure_connect( + addr: A, + domain: &str, + ssl_connector: &TlsConnector, +) -> Result>> { + match TcpStream::connect(addr) { + Ok(stream) => { + let ssl_stream = match TlsConnector::connect(ssl_connector, domain, stream) { + Ok(s) => s, + Err(e) => return Err(Error::TlsHandshake(e)), + }; + let mut socket = Client::new(ssl_stream); + + socket.read_greeting()?; + Ok(socket) + } + Err(e) => Err(Error::Io(e)), + } +} + + +impl Client { /// This will upgrade a regular TCP connection to use SSL. /// /// Use the domain parameter for openssl's SNI and hostname verification. @@ -231,43 +284,21 @@ impl UnauthenticatedClient { mut self, domain: &str, ssl_connector: &TlsConnector, - ) -> Result>> { + ) -> Result>> { // TODO This needs to be tested - self.inner.run_command_and_check_ok("STARTTLS")?; - TlsConnector::connect(ssl_connector, domain, self.inner.stream.into_inner()?) - .map(UnauthenticatedClient::new) + // TODO(dario): adjust + self.run_command_and_check_ok("STARTTLS")?; + TlsConnector::connect(ssl_connector, domain, self.conn.stream.into_inner()?) + .map(Client::new) .map_err(Error::TlsHandshake) } } -impl UnauthenticatedClient> { - /// Creates a client with an SSL wrapper. - pub fn secure_connect( - addr: A, - domain: &str, - ssl_connector: &TlsConnector, - ) -> Result>> { - match TcpStream::connect(addr) { - Ok(stream) => { - let ssl_stream = match TlsConnector::connect(ssl_connector, domain, stream) { - Ok(s) => s, - Err(e) => return Err(Error::TlsHandshake(e)), - }; - let mut socket = UnauthenticatedClient::new(ssl_stream); - - socket.inner.read_greeting()?; - Ok(socket) - } - Err(e) => Err(Error::Io(e)), - } - } -} - -impl UnauthenticatedClient { +impl Client { /// Creates a new client with the underlying stream. - pub fn new(stream: T) -> UnauthenticatedClient { - UnauthenticatedClient { - inner: InnerClient { + pub fn new(stream: T) -> Client { + Client { + conn: Connection { stream: BufStream::new(stream), tag: INITIAL_TAG, debug: false, @@ -280,10 +311,11 @@ impl UnauthenticatedClient { mut self, auth_type: &str, authenticator: A, - ) -> ::std::result::Result, (Error, UnauthenticatedClient)> { + ) -> ::std::result::Result, (Error, Client)> { // explicit match block neccessary to convert error to tuple and not bind self too early // (see also comment on `login`) - match self.inner.run_command(&format!("AUTHENTICATE {}", auth_type)) { + // TODO(dario): macro as suggested in pull/84 + match self.run_command(&format!("AUTHENTICATE {}", auth_type)) { Ok(_) => self.do_auth_handshake(authenticator), Err(e) => Err((e, self)), } @@ -293,13 +325,13 @@ impl UnauthenticatedClient { fn do_auth_handshake( mut self, authenticator: A - ) -> ::std::result::Result, (Error, UnauthenticatedClient)> { + ) -> ::std::result::Result, (Error, Client)> { // TODO Clean up this code loop { let mut line = Vec::new(); // explicit match blocks neccessary to convert error to tuple and not bind self too // early (see also comment on `login`) - if let Err(e) = self.inner.readline(&mut line) { + if let Err(e) = self.readline(&mut line) { return Err((e, self)); } @@ -310,12 +342,12 @@ impl UnauthenticatedClient { }; let auth_response = authenticator.process(data); - if let Err(e) = self.inner.write_line(auth_response.into_bytes().as_slice()) { + if let Err(e) = self.write_line(auth_response.into_bytes().as_slice()) { return Err((e, self)); } } else { - return match self.inner.read_response_onto(&mut line) { - Ok(()) => Ok(Client { inner: self.inner }), + return match self.read_response_onto(&mut line) { + Ok(()) => Ok(Session { conn: self.conn }), Err(e) => Err((e, self)), } } @@ -327,13 +359,14 @@ impl UnauthenticatedClient { mut self, username: &str, password: &str - ) -> ::std::result::Result, (Error, UnauthenticatedClient)> { + ) -> ::std::result::Result, (Error, Client)> { // note that we need the explicit match blocks here for two reasons: // 1. we need to convert the validate_str error type to our tuple of - // (Error, UnauthenticatedClient) + // (Error, Client) // 2. we can't use `.map_err(|e| (e, self))` because that would capture self into the // closure. this way borowck sees that self is only bound in the error case where we // return anyways. + // TODO(dario): macro? let u = match validate_str(username) { Ok(u) => u, Err(e) => return Err((e, self)), @@ -343,14 +376,14 @@ impl UnauthenticatedClient { Err(e) => return Err((e, self)), }; - match self.inner.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)) { - Ok(()) => Ok(Client { inner: self.inner }), + match self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)) { + Ok(()) => Ok(Session { conn: self.conn }), Err(e) => Err((e, self)), } } } -impl InnerClient { +impl Connection { fn read_greeting(&mut self) -> Result<()> { let mut v = Vec::new(); self.readline(&mut v)?; @@ -484,24 +517,22 @@ impl InnerClient { } -impl Client { +impl Session { /// 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 - /// [`Client::run_command_and_read_response`], you *may* see additional untagged `RECENT`, + /// [`Connection::run_command_and_read_response`], you *may* see additional untagged `RECENT`, /// `EXISTS`, `FETCH`, and `EXPUNGE` responses! pub fn select(&mut self, mailbox_name: &str) -> Result { - self.inner - .run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) + 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 { - self.inner - .run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) + self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) .and_then(|lines| parse_mailbox(&lines[..])) } @@ -511,8 +542,7 @@ impl Client { /// messages in the selected mailbox whose status has changed. See the note on [unilateral /// 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.inner - .run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query)) + self.run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query)) .and_then(|lines| parse_fetches(lines)) } @@ -522,39 +552,33 @@ impl Client { /// messages in the selected mailbox whose status has changed. See the note on [unilateral /// 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.inner - .run_command_and_read_response(&format!("UID FETCH {} {}", uid_set, query)) + 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. pub fn noop(&mut self) -> Result<()> { - self.inner - .run_command_and_check_ok("NOOP") + self.run_command_and_check_ok("NOOP") } /// Logout informs the server that the client is done with the connection. pub fn logout(&mut self) -> Result<()> { - self.inner - .run_command_and_check_ok("LOGOUT") + self.run_command_and_check_ok("LOGOUT") } /// Create creates a mailbox with the given name. pub fn create(&mut self, mailbox_name: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name)?)) + self.run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name)?)) } /// Delete permanently removes the mailbox with the given name. pub fn delete(&mut self, mailbox_name: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name)?)) + self.run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name)?)) } /// Rename changes the name of a mailbox. pub fn rename(&mut self, current_mailbox_name: &str, new_mailbox_name: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!( + self.run_command_and_check_ok(&format!( "RENAME {} {}", quote!(current_mailbox_name), quote!(new_mailbox_name) @@ -564,66 +588,56 @@ impl Client { /// Subscribe adds the specified mailbox name to the server's set of "active" or "subscribed" /// mailboxes as returned by the LSUB command. pub fn subscribe(&mut self, mailbox: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox))) + self.run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox))) } /// Unsubscribe removes the specified mailbox name from the server's set of /// "active" or "subscribed mailboxes as returned by the LSUB command. pub fn unsubscribe(&mut self, mailbox: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox))) + self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox))) } /// Capability requests a listing of capabilities that the server supports. pub fn capabilities(&mut self) -> ZeroCopyResult { - self.inner - .run_command_and_read_response(&format!("CAPABILITY")) + 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 /// selected mailbox. pub fn expunge(&mut self) -> Result<()> { - self.inner - .run_command_and_check_ok("EXPUNGE") + self.run_command_and_check_ok("EXPUNGE") } /// Check requests a checkpoint of the currently selected mailbox. pub fn check(&mut self) -> Result<()> { - self.inner - .run_command_and_check_ok("CHECK") + self.run_command_and_check_ok("CHECK") } /// Close permanently removes all messages that have the \Deleted flag set from the currently /// selected mailbox, and returns to the authenticated state from the selected state. pub fn close(&mut self) -> Result<()> { - self.inner - .run_command_and_check_ok("CLOSE") + self.run_command_and_check_ok("CLOSE") } /// Store alters data associated with a message in the mailbox. pub fn store(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult> { - self.inner - .run_command_and_read_response(&format!("STORE {} {}", sequence_set, query)) + 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) -> ZeroCopyResult> { - self.inner - .run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query)) + 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. pub fn copy(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name)) + self.run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name)) } pub fn uid_copy(&mut self, uid_set: &str, mailbox_name: &str) -> Result<()> { - self.inner - .run_command_and_check_ok(&format!("UID COPY {} {}", uid_set, mailbox_name)) + self.run_command_and_check_ok(&format!("UID COPY {} {}", uid_set, mailbox_name)) } /// The LIST command returns a subset of names from the complete set @@ -633,8 +647,7 @@ impl Client { reference_name: &str, mailbox_search_pattern: &str, ) -> ZeroCopyResult> { - self.inner - .run_command_and_read_response(&format!( + self.run_command_and_read_response(&format!( "LIST {} {}", quote!(reference_name), mailbox_search_pattern @@ -648,8 +661,7 @@ impl Client { reference_name: &str, mailbox_search_pattern: &str, ) -> ZeroCopyResult> { - self.inner - .run_command_and_read_response(&format!( + self.run_command_and_read_response(&format!( "LSUB {} {}", quote!(reference_name), mailbox_search_pattern @@ -658,8 +670,7 @@ impl Client { /// The STATUS command requests the status of the indicated mailbox. pub fn status(&mut self, mailbox_name: &str, status_data_items: &str) -> Result { - self.inner - .run_command_and_read_response(&format!( + self.run_command_and_read_response(&format!( "STATUS {} {}", validate_str(mailbox_name)?, status_data_items @@ -674,28 +685,27 @@ impl Client { /// The APPEND command adds a mail to a mailbox. pub fn append(&mut self, folder: &str, content: &[u8]) -> Result<()> { - self.inner - .run_command(&format!("APPEND \"{}\" {{{}}}", folder, content.len()))?; + self.run_command(&format!("APPEND \"{}\" {{{}}}", folder, content.len()))?; let mut v = Vec::new(); - self.inner.readline(&mut v)?; + self.readline(&mut v)?; if !v.starts_with(b"+") { return Err(Error::Append); } - self.inner.stream.write_all(content)?; - self.inner.stream.write_all(b"\r\n")?; - self.inner.stream.flush()?; - self.inner.read_response().map(|_| ()) + self.stream.write_all(content)?; + self.stream.write_all(b"\r\n")?; + self.stream.flush()?; + self.read_response().map(|_| ()) } - // these are only here because they are public interface, the rest is in InnerClient + // these are only here because they are public interface, the rest is in `Connection` /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { - self.inner.run_command_and_read_response(command).map(|_| (())) + self.run_command_and_read_response(command).map(|_| (())) } /// Runs any command passed to it. pub fn run_command(&mut self, untagged_command: &str) -> Result<()> { - self.inner.run_command(untagged_command) + self.conn.run_command(untagged_command) } /// Run a raw IMAP command and read back its response. @@ -705,7 +715,7 @@ impl Client { /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that you *may* see /// additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses! pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { - self.inner.run_command_and_read_response(untagged_command) + self.conn.run_command_and_read_response(untagged_command) } } From da2ac87ca74e904173d44d9018e31feba2c637d0 Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Wed, 29 Aug 2018 15:55:43 +0200 Subject: [PATCH 4/8] client: move matching in login functions to macro as suggested in PR #84 comments --- src/client.rs | 60 ++++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6a353f21..85de9cb6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -286,7 +286,6 @@ impl Client { ssl_connector: &TlsConnector, ) -> Result>> { // TODO This needs to be tested - // TODO(dario): adjust self.run_command_and_check_ok("STARTTLS")?; TlsConnector::connect(ssl_connector, domain, self.conn.stream.into_inner()?) .map(Client::new) @@ -294,6 +293,19 @@ impl Client { } } +// as the pattern of returning the unauthenticated `Client` back with a login error is relatively +// common, it's abstacted away into a macro here. Note that in theory we wouldn't need the second +// parameter, and could just use the identifier `self` from the surrounding function, but being +// explicit here seems a lot clearer. +macro_rules! ok_or_unauth_client_err { + ($r:expr, $self:expr) => { + match $r { + Ok(o) => o, + Err(e) => return Err((e, $self)) + } + } +} + impl Client { /// Creates a new client with the underlying stream. pub fn new(stream: T) -> Client { @@ -307,18 +319,15 @@ impl Client { } /// Authenticate will authenticate with the server, using the authenticator given. - pub fn authenticate( + pub fn authenticate ( mut self, auth_type: &str, authenticator: A, ) -> ::std::result::Result, (Error, Client)> { // explicit match block neccessary to convert error to tuple and not bind self too early // (see also comment on `login`) - // TODO(dario): macro as suggested in pull/84 - match self.run_command(&format!("AUTHENTICATE {}", auth_type)) { - Ok(_) => self.do_auth_handshake(authenticator), - Err(e) => Err((e, self)), - } + ok_or_unauth_client_err!(self.run_command(&format!("AUTHENTICATE {}", auth_type)), self); + self.do_auth_handshake(authenticator) } /// This func does the handshake process once the authenticate command is made. @@ -331,25 +340,17 @@ impl Client { let mut line = Vec::new(); // explicit match blocks neccessary to convert error to tuple and not bind self too // early (see also comment on `login`) - if let Err(e) = self.readline(&mut line) { - return Err((e, self)); - } + ok_or_unauth_client_err!(self.readline(&mut line), self); if line.starts_with(b"+") { - let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { - Ok(l) => l, - Err(e) => return Err((e, self)), - }; + let data = ok_or_unauth_client_err!( + parse_authenticate_response(String::from_utf8(line).unwrap()), self); let auth_response = authenticator.process(data); - if let Err(e) = self.write_line(auth_response.into_bytes().as_slice()) { - return Err((e, self)); - } + ok_or_unauth_client_err!(self.write_line(auth_response.into_bytes().as_slice()), self); } else { - return match self.read_response_onto(&mut line) { - Ok(()) => Ok(Session { conn: self.conn }), - Err(e) => Err((e, self)), - } + ok_or_unauth_client_err!(self.read_response_onto(&mut line), self); + return Ok(Session { conn: self.conn }); } } } @@ -366,20 +367,11 @@ impl Client { // 2. we can't use `.map_err(|e| (e, self))` because that would capture self into the // closure. this way borowck sees that self is only bound in the error case where we // return anyways. - // TODO(dario): macro? - let u = match validate_str(username) { - Ok(u) => u, - Err(e) => return Err((e, self)), - }; - let p = match validate_str(password) { - Ok(p) => p, - Err(e) => return Err((e, self)), - }; + let u = ok_or_unauth_client_err!(validate_str(username), self); + let p = ok_or_unauth_client_err!(validate_str(password), self); + ok_or_unauth_client_err!(self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)), self); - match self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p)) { - Ok(()) => Ok(Session { conn: self.conn }), - Err(e) => Err((e, self)), - } + Ok(Session { conn: self.conn }) } } From abad1f41712b5dced9ebbd11fae5abec50058392 Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Wed, 29 Aug 2018 16:08:53 +0200 Subject: [PATCH 5/8] adjust examples to the latest changes --- examples/basic.rs | 13 ++++++------- examples/gmail_oauth2.rs | 11 +++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index fc643a35..df74eb91 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,6 @@ extern crate imap; extern crate native_tls; -use imap::client::UnauthenticatedClient; use native_tls::TlsConnector; // To connect to the gmail IMAP server with this you will need to allow unsecure apps access. @@ -12,9 +11,9 @@ fn main() { let port = 993; let socket_addr = (domain, port); let ssl_connector = TlsConnector::builder().build().unwrap(); - let unauth_client = UnauthenticatedClient::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); + let client = imap::client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); - let mut imap_socket = match unauth_client.login("username", "password") { + let mut imap_session = match client.login("username", "password") { Ok(c) => c, Err((e, _unauth_client)) => { println!("failed to login: {}", e); @@ -22,26 +21,26 @@ fn main() { }, }; - match imap_socket.capabilities() { + match imap_session.capabilities() { Ok(capabilities) => for capability in capabilities.iter() { println!("{}", capability); }, Err(e) => println!("Error parsing capability: {}", e), }; - match imap_socket.select("INBOX") { + match imap_session.select("INBOX") { Ok(mailbox) => { println!("{}", mailbox); } Err(e) => println!("Error selecting INBOX: {}", e), }; - match imap_socket.fetch("2", "body[text]") { + match imap_session.fetch("2", "body[text]") { Ok(msgs) => for msg in &msgs { print!("{:?}", msg); }, Err(e) => println!("Error Fetching email 2: {}", e), }; - imap_socket.logout().unwrap(); + imap_session.logout().unwrap(); } diff --git a/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs index d334cf75..051c7e24 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -4,7 +4,6 @@ extern crate native_tls; use base64::encode; use imap::authenticator::Authenticator; -use imap::client::UnauthenticatedClient; use native_tls::TlsConnector; struct GmailOAuth2 { @@ -33,9 +32,9 @@ fn main() { let port = 993; let socket_addr = (domain, port); let ssl_connector = TlsConnector::builder().build().unwrap(); - let unauth_client = UnauthenticatedClient::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); + let client = imap::client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); - let mut imap_socket = match unauth_client.authenticate("XOAUTH2", gmail_auth) { + let mut imap_session = match client.authenticate("XOAUTH2", gmail_auth) { Ok(c) => c, Err((e, _unauth_client)) => { println!("error authenticating: {}", e); @@ -43,17 +42,17 @@ fn main() { } }; - match imap_socket.select("INBOX") { + match imap_session.select("INBOX") { Ok(mailbox) => println!("{}", mailbox), Err(e) => println!("Error selecting INBOX: {}", e), }; - match imap_socket.fetch("2", "body[text]") { + match imap_session.fetch("2", "body[text]") { Ok(msgs) => for msg in &msgs { print!("{:?}", msg); }, Err(e) => println!("Error Fetching email 2: {}", e), }; - imap_socket.logout().unwrap(); + imap_session.logout().unwrap(); } From 515d5747425257c5b8384b94eb15ba524802a19a Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Wed, 29 Aug 2018 17:37:57 +0200 Subject: [PATCH 6/8] src/lib.rs: sync code in doc-comment with examples/basic.rs is there some way to include the code directly from examples/basic.rs? --- examples/basic.rs | 4 ++-- src/lib.rs | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index df74eb91..53e74577 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -16,9 +16,9 @@ fn main() { let mut imap_session = match client.login("username", "password") { Ok(c) => c, Err((e, _unauth_client)) => { - println!("failed to login: {}", e); + eprintln!("failed to login: {}", e); return; - }, + } }; match imap_session.capabilities() { diff --git a/src/lib.rs b/src/lib.rs index a5ec0fba..553671b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,8 @@ //! See the `examples/` directory for more examples. //! //! ```no_run -//! # extern crate imap; +//! extern crate imap; //! extern crate native_tls; -//! # use imap::client::Client; //! //! // To connect to the gmail IMAP server with this you will need to allow unsecure apps access. //! // See: https://support.google.com/accounts/answer/6010255?hl=en @@ -18,11 +17,17 @@ //! let port = 993; //! let socket_addr = (domain, port); //! let ssl_connector = native_tls::TlsConnector::builder().build().unwrap(); -//! let mut imap_socket = Client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); +//! let client = imap::client::secure_connect(socket_addr, domain, &ssl_connector).unwrap(); //! -//! imap_socket.login("username", "password").unwrap(); +//! let mut imap_session = match client.login("username", "password") { +//! Ok(c) => c, +//! Err((e, _unauth_client)) => { +//! eprintln!("failed to login: {}", e); +//! return; +//! } +//! }; //! -//! match imap_socket.capabilities() { +//! match imap_session.capabilities() { //! Ok(capabilities) => { //! for capability in capabilities.iter() { //! println!("{}", capability); @@ -31,14 +36,14 @@ //! Err(e) => println!("Error parsing capabilities: {}", e), //! }; //! -//! match imap_socket.select("INBOX") { +//! match imap_session.select("INBOX") { //! Ok(mailbox) => { //! println!("{}", mailbox); //! } //! Err(e) => println!("Error selecting INBOX: {}", e), //! }; //! -//! match imap_socket.fetch("2", "body[text]") { +//! match imap_session.fetch("2", "body[text]") { //! Ok(messages) => { //! for message in messages.iter() { //! print!("{:?}", message); @@ -47,7 +52,7 @@ //! Err(e) => println!("Error Fetching email 2: {}", e), //! }; //! -//! imap_socket.logout().unwrap(); +//! imap_session.logout().unwrap(); //! } //! ``` From 1708db5ae254060376f28ba06b3a57527cdccb25 Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Wed, 29 Aug 2018 17:46:39 +0200 Subject: [PATCH 7/8] adjust tests to the change in structs --- src/client.rs | 110 ++++++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/src/client.rs b/src/client.rs index 85de9cb6..e8422180 100644 --- a/src/client.rs +++ b/src/client.rs @@ -717,6 +717,12 @@ mod tests { use super::super::mock_stream::MockStream; use super::*; + macro_rules! mock_session { + ($s:expr) => { + Session { conn: Client::new($s).conn } + } + } + #[test] fn read_response() { let response = "a0 OK Logged in.\r\n"; @@ -732,9 +738,9 @@ mod tests { * 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(); + let mut session = mock_session!(mock_stream); + session.read_response().unwrap(); + session.read_response().unwrap(); } #[test] @@ -808,10 +814,10 @@ mod tests { let password = "password"; let command = format!("a1 LOGIN {} {}\r\n", quote!(username), quote!(password)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.login(username, password).unwrap(); + let client = Client::new(mock_stream); + let session = client.login(username, password).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid login command" ); } @@ -821,10 +827,10 @@ mod tests { let response = b"a1 OK Logout completed.\r\n".to_vec(); let command = format!("a1 LOGOUT\r\n"); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.logout().unwrap(); + let mut session = mock_session!(mock_stream); + session.logout().unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid logout command" ); } @@ -840,12 +846,12 @@ mod tests { quote!(new_mailbox_name) ); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client + let mut session = mock_session!(mock_stream); + session .rename(current_mailbox_name, new_mailbox_name) .unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid rename command" ); } @@ -856,10 +862,10 @@ mod tests { let mailbox = "INBOX"; let command = format!("a1 SUBSCRIBE {}\r\n", quote!(mailbox)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.subscribe(mailbox).unwrap(); + let mut session = mock_session!(mock_stream); + session.subscribe(mailbox).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid subscribe command" ); } @@ -870,10 +876,10 @@ mod tests { let mailbox = "INBOX"; let command = format!("a1 UNSUBSCRIBE {}\r\n", quote!(mailbox)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.unsubscribe(mailbox).unwrap(); + let mut session = mock_session!(mock_stream); + session.unsubscribe(mailbox).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid unsubscribe command" ); } @@ -882,10 +888,10 @@ mod tests { fn expunge() { let response = b"a1 OK EXPUNGE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.expunge().unwrap(); + let mut session = mock_session!(mock_stream); + session.expunge().unwrap(); assert!( - client.stream.get_ref().written_buf == b"a1 EXPUNGE\r\n".to_vec(), + session.stream.get_ref().written_buf == b"a1 EXPUNGE\r\n".to_vec(), "Invalid expunge command" ); } @@ -894,10 +900,10 @@ mod tests { fn check() { let response = b"a1 OK CHECK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.check().unwrap(); + let mut session = mock_session!(mock_stream); + session.check().unwrap(); assert!( - client.stream.get_ref().written_buf == b"a1 CHECK\r\n".to_vec(), + session.stream.get_ref().written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command" ); } @@ -931,10 +937,10 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 EXAMINE {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - let mailbox = client.examine(mailbox_name).unwrap(); + let mut session = mock_session!(mock_stream); + let mailbox = session.examine(mailbox_name).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid examine command" ); assert_eq!(mailbox, expected_mailbox); @@ -977,10 +983,10 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 SELECT {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - let mailbox = client.select(mailbox_name).unwrap(); + let mut session = mock_session!(mock_stream); + let mailbox = session.select(mailbox_name).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid select command" ); assert_eq!(mailbox, expected_mailbox); @@ -993,10 +999,10 @@ mod tests { .to_vec(); 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.capabilities().unwrap(); + let mut session = mock_session!(mock_stream); + let capabilities = session.capabilities().unwrap(); assert!( - client.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(), + session.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(), "Invalid capability command" ); assert_eq!(capabilities.len(), 4); @@ -1011,10 +1017,10 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 CREATE {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.create(mailbox_name).unwrap(); + let mut session = mock_session!(mock_stream); + session.create(mailbox_name).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid create command" ); } @@ -1025,10 +1031,10 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 DELETE {}\r\n", quote!(mailbox_name)); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.delete(mailbox_name).unwrap(); + let mut session = mock_session!(mock_stream); + session.delete(mailbox_name).unwrap(); assert!( - client.stream.get_ref().written_buf == command.as_bytes().to_vec(), + session.stream.get_ref().written_buf == command.as_bytes().to_vec(), "Invalid delete command" ); } @@ -1037,10 +1043,10 @@ mod tests { fn noop() { let response = b"a1 OK NOOP completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.noop().unwrap(); + let mut session = mock_session!(mock_stream); + session.noop().unwrap(); assert!( - client.stream.get_ref().written_buf == b"a1 NOOP\r\n".to_vec(), + session.stream.get_ref().written_buf == b"a1 NOOP\r\n".to_vec(), "Invalid noop command" ); } @@ -1049,10 +1055,10 @@ mod tests { fn close() { let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = Client::new(mock_stream); - client.close().unwrap(); + let mut session = mock_session!(mock_stream); + session.close().unwrap(); assert!( - client.stream.get_ref().written_buf == b"a1 CLOSE\r\n".to_vec(), + session.stream.get_ref().written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command" ); } @@ -1069,7 +1075,7 @@ mod tests { fn generic_store(prefix: &str, op: F) where - F: FnOnce(&mut Client, &str, &str) -> Result, + F: FnOnce(&mut Session, &str, &str) -> Result, { let res = "* 2 FETCH (FLAGS (\\Deleted \\Seen))\r\n\ * 3 FETCH (FLAGS (\\Deleted))\r\n\ @@ -1091,7 +1097,7 @@ mod tests { fn generic_copy(prefix: &str, op: F) where - F: FnOnce(&mut Client, &str, &str) -> Result, + F: FnOnce(&mut Session, &str, &str) -> Result, { generic_with_uid( "OK COPY completed\r\n", @@ -1115,21 +1121,21 @@ mod tests { fn generic_fetch(prefix: &str, op: F) where - F: FnOnce(&mut Client, &str, &str) -> Result, + F: FnOnce(&mut Session, &str, &str) -> Result, { generic_with_uid("OK FETCH completed\r\n", "FETCH", "1", "BODY[]", prefix, op); } fn generic_with_uid(res: &str, cmd: &str, seq: &str, query: &str, prefix: &str, op: F) where - F: FnOnce(&mut Client, &str, &str) -> Result, + F: FnOnce(&mut Session, &str, &str) -> Result, { let resp = format!("a1 {}\r\n", res).as_bytes().to_vec(); let line = format!("a1{}{} {} {}\r\n", prefix, cmd, seq, query); - let mut client = Client::new(MockStream::new(resp)); - let _ = op(&mut client, seq, query); + let mut session = mock_session!(MockStream::new(resp)); + let _ = op(&mut session, seq, query); assert!( - client.stream.get_ref().written_buf == line.as_bytes().to_vec(), + session.stream.get_ref().written_buf == line.as_bytes().to_vec(), "Invalid command" ); } From 73efb038826091b5642f2f0006423c3268c443c8 Mon Sep 17 00:00:00 2001 From: Johannes Schilling Date: Thu, 30 Aug 2018 19:13:13 +0200 Subject: [PATCH 8/8] client: more docs, move Connection impl block as discussed/suggested at https://github.com/mattnenterprise/rust-imap/pull/84#pullrequestreview-150636568 --- src/client.rs | 365 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 218 insertions(+), 147 deletions(-) diff --git a/src/client.rs b/src/client.rs index e8422180..f9b023e3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -35,19 +35,29 @@ fn validate_str(value: &str) -> Result { Ok(quoted) } -/// Stream to interface with the IMAP server. This interface is only for the command stream. +/// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from +/// a succesful login attempt. +/// +/// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying +/// primitives type. #[derive(Debug)] pub struct Session { conn: Connection, } -// TODO: desc +/// An (unauthenticated) handle to talk to an IMAP server. This is what you get when first +/// connecting. A succesfull call to [`login`](struct.Client.html#method.login) will return a +/// [`Session`](struct.Session.html) instance, providing the usual IMAP methods. +/// +/// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying +/// primitives type. #[derive(Debug)] pub struct Client { conn: Connection, } -// TODO: docs +/// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful +/// login) use a `Connection` internally for the TCP stream primitives. #[derive(Debug)] pub struct Connection { stream: BufStream, @@ -55,8 +65,8 @@ pub struct Connection { pub debug: bool, } -// these instances are so we can make the self.inner stuff a little prettier -// TODO(dario): docs +// `Deref` instances are so we can make use of the same underlying primitives in `Client` and +// `Session` impl Deref for Client { type Target = Connection; @@ -241,7 +251,22 @@ impl<'a> SetReadTimeout for TlsStream { } } -/// Creates a new client. +/// Creates a new client. The usual IMAP commands are part of the [`Session`](struct.Session.html) +/// type, returned from a succesful call to [`Client::login`](struct.Client.html#method.login). +/// ```rust,no_run +/// # extern crate native_tls; +/// # extern crate imap; +/// # use std::io; +/// # use native_tls::TlsConnector; +/// # fn main() { +/// // a plain, unencrypted TCP connection +/// let client = imap::client::connect(("imap.example.org", 143)).unwrap(); +/// +/// // upgrade to SSL +/// let ssl_connector = TlsConnector::builder().build().unwrap(); +/// let ssl_client = client.secure("imap.example.org", &ssl_connector); +/// # } +/// ``` pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { @@ -254,7 +279,22 @@ pub fn connect(addr: A) -> Result> { } } -/// Creates a client with an SSL wrapper. +/// Creates a `Client` with an SSL wrapper. The usual IMAP commands are part of the +/// [`Session`](struct.Session.html) type, returned from a succesful call to +/// [`Client::login`](struct.Client.html#method.login). +/// ```rust,no_run +/// # extern crate native_tls; +/// # extern crate imap; +/// # use std::io; +/// # use native_tls::TlsConnector; +/// # fn main() { +/// let ssl_connector = TlsConnector::builder().build().unwrap(); +/// let ssl_client = imap::client::secure_connect( +/// ("imap.example.org", 993), +/// "imap.example.org", +/// &ssl_connector).unwrap(); +/// # } +/// ``` pub fn secure_connect( addr: A, domain: &str, @@ -293,10 +333,13 @@ impl Client { } } -// as the pattern of returning the unauthenticated `Client` back with a login error is relatively -// common, it's abstacted away into a macro here. Note that in theory we wouldn't need the second -// parameter, and could just use the identifier `self` from the surrounding function, but being -// explicit here seems a lot clearer. +// As the pattern of returning the unauthenticated `Client` (a.k.a. `self`) back with a login error +// is relatively common, it's abstacted away into a macro here. +// +// Note: 1) using `.map_err(|e| (e, self))` or similar here makes the closure own self, so we can't +// do that. +// 2) in theory we wouldn't need the second parameter, and could just use the identifier +// `self` from the surrounding function, but being explicit here seems a lot cleaner. macro_rules! ok_or_unauth_client_err { ($r:expr, $self:expr) => { match $r { @@ -324,8 +367,6 @@ impl Client { auth_type: &str, authenticator: A, ) -> ::std::result::Result, (Error, Client)> { - // explicit match block neccessary to convert error to tuple and not bind self too early - // (see also comment on `login`) ok_or_unauth_client_err!(self.run_command(&format!("AUTHENTICATE {}", auth_type)), self); self.do_auth_handshake(authenticator) } @@ -355,7 +396,36 @@ impl Client { } } - /// Log in to the IMAP server. + /// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is + /// returned; on error the original `Client` instance is returned in addition to the error. + /// This is because `login` takes ownership of `self`, so in order to try again (e.g. after + /// prompting the user for credetials), ownership of the original `Client` needs to be + /// transferred back to the caller. + /// + /// ```rust,no_run + /// # extern crate imap; + /// # extern crate native_tls; + /// # use std::io; + /// # use native_tls::TlsConnector; + /// # fn main() { + /// # let ssl_connector = TlsConnector::builder().build().unwrap(); + /// let ssl_client = imap::client::secure_connect( + /// ("imap.example.org", 993), + /// "imap.example.org", + /// &ssl_connector).unwrap(); + /// + /// // try to login + /// let session = match ssl_client.login("user", "pass") { + /// Ok(s) => s, + /// Err((e, orig_client)) => { + /// eprintln!("error logging in: {}", e); + /// // prompt user and try again with orig_client here + /// return; + /// } + /// }; + /// + /// // use session for IMAP commands + /// # } pub fn login( mut self, username: &str, @@ -375,139 +445,6 @@ impl Client { } } -impl Connection { - fn read_greeting(&mut self) -> Result<()> { - let mut v = Vec::new(); - self.readline(&mut v)?; - Ok(()) - } - - fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { - self.run_command_and_read_response(command).map(|_| ()) - } - - fn run_command(&mut self, untagged_command: &str) -> Result<()> { - let command = self.create_command(untagged_command.to_string()); - self.write_line(command.into_bytes().as_slice()) - } - - fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { - self.run_command(untagged_command)?; - self.read_response() - } - - fn read_response(&mut self) -> Result> { - let mut v = Vec::new(); - self.read_response_onto(&mut v)?; - Ok(v) - } - - 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(); - 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 => {} - } - } - } - - fn readline(&mut self, into: &mut Vec) -> Result { - use std::io::BufRead; - let read = self.stream.read_until(LF, into)?; - if read == 0 { - return Err(Error::ConnectionLost); - } - - if self.debug { - // Remove CRLF - let len = into.len(); - let line = &into[(len - read)..(len - 2)]; - print!("S: {}\n", String::from_utf8_lossy(line)); - } - - Ok(read) - } - - fn create_command(&mut self, command: String) -> String { - self.tag += 1; - let command = format!("{}{} {}", TAG_PREFIX, self.tag, command); - return command; - } - - fn write_line(&mut self, buf: &[u8]) -> Result<()> { - self.stream.write_all(buf)?; - self.stream.write_all(&[CR, LF])?; - self.stream.flush()?; - if self.debug { - print!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap()); - } - Ok(()) - } -} - impl Session { /// Selects a mailbox @@ -711,6 +648,140 @@ impl Session { } } +impl Connection { + fn read_greeting(&mut self) -> Result<()> { + let mut v = Vec::new(); + self.readline(&mut v)?; + Ok(()) + } + + fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { + self.run_command_and_read_response(command).map(|_| ()) + } + + fn run_command(&mut self, untagged_command: &str) -> Result<()> { + let command = self.create_command(untagged_command.to_string()); + self.write_line(command.into_bytes().as_slice()) + } + + fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { + self.run_command(untagged_command)?; + self.read_response() + } + + fn read_response(&mut self) -> Result> { + let mut v = Vec::new(); + self.read_response_onto(&mut v)?; + Ok(v) + } + + 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(); + 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 => {} + } + } + } + + fn readline(&mut self, into: &mut Vec) -> Result { + use std::io::BufRead; + let read = self.stream.read_until(LF, into)?; + if read == 0 { + return Err(Error::ConnectionLost); + } + + if self.debug { + // Remove CRLF + let len = into.len(); + let line = &into[(len - read)..(len - 2)]; + print!("S: {}\n", String::from_utf8_lossy(line)); + } + + Ok(read) + } + + fn create_command(&mut self, command: String) -> String { + self.tag += 1; + let command = format!("{}{} {}", TAG_PREFIX, self.tag, command); + return command; + } + + fn write_line(&mut self, buf: &[u8]) -> Result<()> { + self.stream.write_all(buf)?; + self.stream.write_all(&[CR, LF])?; + self.stream.flush()?; + if self.debug { + print!("C: {}\n", String::from_utf8(buf.to_vec()).unwrap()); + } + Ok(()) + } +} + + #[cfg(test)] mod tests { use super::super::error::Result;