From 8981a038708db7827a4337ee49a066716511a69b Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 12:29:16 -0400 Subject: [PATCH 01/65] Initial step of making client testable with generics --- example.rs | 2 +- src/client.rs | 90 +++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/example.rs b/example.rs index 894af51..2a55948 100644 --- a/example.rs +++ b/example.rs @@ -6,7 +6,7 @@ use imap::client::IMAPStream; use imap::client::IMAPMailbox; fn main() { - let mut imap_socket = match IMAPStream::connect(("imap.gmail.com", 993), Some(SslContext::new(SslMethod::Sslv23).unwrap())) { + let mut imap_socket = match IMAPStream::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()) { Ok(s) => s, Err(e) => panic!("{}", e) }; diff --git a/src/client.rs b/src/client.rs index 27e45dd..e3e8040 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,16 +3,13 @@ use openssl::ssl::{SslContext, SslStream}; use std::io::{Error, ErrorKind, Read, Result, Write}; use regex::Regex; -enum IMAPStreamTypes { - Basic(TcpStream), - Ssl(SslStream) -} +static TAG_PREFIX: &'static str = "a"; +const INITIAL_TAG: i32 = 1; /// Stream to interface with the IMAP server. This interface is only for the command stream. -pub struct IMAPStream { - stream: IMAPStreamTypes, - tag: u32, - tag_prefix: &'static str +pub struct IMAPStream { + stream: T, + tag: u32 } pub struct IMAPMailbox { @@ -25,15 +22,12 @@ pub struct IMAPMailbox { pub uid_validity: Option } -impl IMAPStream { - /// Creates an IMAP Stream. - pub fn connect(addr: A, ssl_context: Option) -> Result { +impl IMAPStream { + /// + pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = match ssl_context { - Some(context) => IMAPStream { stream: IMAPStreamTypes::Ssl(SslStream::connect(&context, stream).unwrap()), tag: 1, tag_prefix: "a"}, - None => IMAPStream { stream: IMAPStreamTypes::Basic(stream), tag: 1, tag_prefix: "a"}, - }; + let mut socket = IMAPStream { stream: stream, tag: INITIAL_TAG}; try!(socket.read_greeting()); Ok(socket) @@ -41,6 +35,24 @@ impl IMAPStream { Err(e) => Err(e) } } +} + +impl IMAPStream> { + /// + pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { + match TcpStream::connect(addr) { + Ok(stream) => { + let mut socket = IMAPStream { stream: SslStream::connect(&ssl_context, stream).unwrap(), tag: INITIAL_TAG}; + + try!(socket.read_greeting()); + Ok(socket) + }, + Err(e) => Err(e) + } + } +} + +impl IMAPStream { /// Log in to the IMAP server. pub fn login(&mut self, username: & str, password: & str) -> Result<()> { @@ -50,12 +62,12 @@ impl IMAPStream { /// Selects a mailbox pub fn select(&mut self, mailbox_name: &str) -> Result { match self.run_command(&format!("SELECT {}", mailbox_name).to_string()) { - Ok(lines) => IMAPStream::parse_select_or_examine(lines), + Ok(lines) => self.parse_select_or_examine(lines), Err(e) => Err(e) } } - fn parse_select_or_examine(lines: Vec) -> Result { + fn parse_select_or_examine(&mut self, lines: Vec) -> Result { let exists_regex = match Regex::new(r"^\* (\d+) EXISTS\r\n") { Ok(re) => re, Err(err) => panic!("{}", err), @@ -92,7 +104,7 @@ impl IMAPStream { }; //Check Ok - match IMAPStream::parse_response_ok(lines.clone()) { + match self.parse_response_ok(lines.clone()) { Ok(_) => (), Err(e) => return Err(e) }; @@ -138,7 +150,7 @@ impl IMAPStream { /// Examine is identical to Select, but the selected mailbox is identified as read-only pub fn examine(&mut self, mailbox_name: &str) -> Result { match self.run_command(&format!("EXAMINE {}", mailbox_name).to_string()) { - Ok(lines) => IMAPStream::parse_select_or_examine(lines), + Ok(lines) => self.parse_select_or_examine(lines), Err(e) => Err(e) } } @@ -188,19 +200,19 @@ impl IMAPStream { /// Capability requests a listing of capabilities that the server supports. pub fn capability(&mut self) -> Result> { match self.run_command(&format!("CAPABILITY").to_string()) { - Ok(lines) => IMAPStream::parse_capability(lines), + Ok(lines) => self.parse_capability(lines), Err(e) => Err(e) } } - fn parse_capability(lines: Vec) -> Result> { + fn parse_capability(&mut self, lines: Vec) -> Result> { let capability_regex = match Regex::new(r"^\* CAPABILITY (.*)\r\n") { Ok(re) => re, Err(err) => panic!("{}", err), }; //Check Ok - match IMAPStream::parse_response_ok(lines.clone()) { + match self.parse_response_ok(lines.clone()) { Ok(_) => (), Err(e) => return Err(e) }; @@ -240,7 +252,7 @@ impl IMAPStream { pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { match self.run_command(command) { - Ok(lines) => IMAPStream::parse_response_ok(lines), + Ok(lines) => self.parse_response_ok(lines), Err(e) => Err(e) } } @@ -248,7 +260,7 @@ impl IMAPStream { pub fn run_command(&mut self, untagged_command: &str) -> Result> { let command = self.create_command(untagged_command.to_string()); - match self.write_str(&*command) { + match self.stream.write_fmt(format_args!("{}", &*command)) { Ok(_) => (), Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to write")), }; @@ -263,7 +275,7 @@ impl IMAPStream { return ret; } - fn parse_response_ok(lines: Vec) -> Result<()> { + fn parse_response_ok(&mut self, lines: Vec) -> Result<()> { let ok_regex = match Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)") { Ok(re) => re, Err(err) => panic!("{}", err), @@ -280,34 +292,20 @@ impl IMAPStream { return Err(Error::new(ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string())); } - fn write_str(&mut self, s: &str) -> Result<()> { - match self.stream { - IMAPStreamTypes::Ssl(ref mut stream) => stream.write_fmt(format_args!("{}", s)), - IMAPStreamTypes::Basic(ref mut stream) => stream.write_fmt(format_args!("{}", s)), - } - } - - fn read(&mut self, buf: &mut [u8]) -> Result { - match self.stream { - IMAPStreamTypes::Ssl(ref mut stream) => stream.read(buf), - IMAPStreamTypes::Basic(ref mut stream) => stream.read(buf), - } - } - fn read_response(&mut self) -> Result> { //Carriage return let cr = 0x0d; //Line Feed let lf = 0x0a; let mut found_tag_line = false; - let start_str = format!("a{} ", self.tag); + let start_str = format!("{}{} ", TAG_PREFIX, self.tag); let mut lines: Vec = Vec::new(); while !found_tag_line { let mut line_buffer: Vec = Vec::new(); while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) { let byte_buffer: &mut [u8] = &mut [0]; - match self.read(byte_buffer) { + match self.stream.read(byte_buffer) { Ok(_) => {}, Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")), } @@ -335,7 +333,7 @@ impl IMAPStream { let mut line_buffer: Vec = Vec::new(); while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) { let byte_buffer: &mut [u8] = &mut [0]; - match self.read(byte_buffer) { + match self.stream.read(byte_buffer) { Ok(_) => {}, Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")), } @@ -346,13 +344,7 @@ impl IMAPStream { } fn create_command(&mut self, command: String) -> String { - let command = format!("{}{} {}\r\n", self.tag_prefix, self.tag, command); + let command = format!("{}{} {}\r\n", TAG_PREFIX, self.tag, command); return command; } } - -#[test] -fn connect() { - let imap = IMAPStream::connect(("this-is-not-an-imap-server", 143), None); - assert!(imap.is_err()); -} From 78f22be6228867a4ad780d1981d31d5158e2b8f2 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 13:32:13 -0400 Subject: [PATCH 02/65] Fix initial tag tyype and remove regex creation checks --- src/client.rs | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/client.rs b/src/client.rs index e3e8040..19b6791 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use std::io::{Error, ErrorKind, Read, Result, Write}; use regex::Regex; static TAG_PREFIX: &'static str = "a"; -const INITIAL_TAG: i32 = 1; +const INITIAL_TAG: u32 = 1; /// Stream to interface with the IMAP server. This interface is only for the command stream. pub struct IMAPStream { @@ -68,40 +68,19 @@ impl IMAPStream { } fn parse_select_or_examine(&mut self, lines: Vec) -> Result { - let exists_regex = match Regex::new(r"^\* (\d+) EXISTS\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let exists_regex = Regex::new(r"^\* (\d+) EXISTS\r\n").unwrap(); - let recent_regex = match Regex::new(r"^\* (\d+) RECENT\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let recent_regex = Regex::new(r"^\* (\d+) RECENT\r\n").unwrap(); - let flags_regex = match Regex::new(r"^\* FLAGS (.+)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let flags_regex = Regex::new(r"^\* FLAGS (.+)\r\n").unwrap(); - let unseen_regex = match Regex::new(r"^OK \[UNSEEN (\d+)\](.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let unseen_regex = Regex::new(r"^OK \[UNSEEN (\d+)\](.*)\r\n").unwrap(); - let uid_validity_regex = match Regex::new(r"^OK \[UIDVALIDITY (\d+)\](.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let uid_validity_regex = Regex::new(r"^OK \[UIDVALIDITY (\d+)\](.*)\r\n").unwrap(); - let uid_next_regex = match Regex::new(r"^OK \[UIDNEXT (\d+)\](.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let uid_next_regex = Regex::new(r"^OK \[UIDNEXT (\d+)\](.*)\r\n").unwrap(); - let permanent_flags_regex = match Regex::new(r"^OK \[PERMANENTFLAGS (.+)\]\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let permanent_flags_regex = Regex::new(r"^OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); //Check Ok match self.parse_response_ok(lines.clone()) { From d949de3d0bf43c64d1fe043d7150c7dd8bc38fcc Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 13:34:39 -0400 Subject: [PATCH 03/65] Adding comments for connection functions --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 19b6791..23fbdc3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,7 +23,7 @@ pub struct IMAPMailbox { } impl IMAPStream { - /// + /// Creates an IMAP Stream. pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { @@ -38,7 +38,7 @@ impl IMAPStream { } impl IMAPStream> { - /// + /// Creates an IMAP Stream with an Ssl wrapper. pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { match TcpStream::connect(addr) { Ok(stream) => { From f4954de642bbcdfe9ac169257dfac93e7243d588 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 13:52:20 -0400 Subject: [PATCH 04/65] Renaming TCPStream to Client --- example.rs | 4 ++-- src/client.rs | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/example.rs b/example.rs index 2a55948..a204e7d 100644 --- a/example.rs +++ b/example.rs @@ -2,11 +2,11 @@ extern crate imap; extern crate openssl; use openssl::ssl::{SslContext, SslMethod}; -use imap::client::IMAPStream; +use imap::client::Client; use imap::client::IMAPMailbox; fn main() { - let mut imap_socket = match IMAPStream::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()) { + let mut imap_socket = match Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()) { Ok(s) => s, Err(e) => panic!("{}", e) }; diff --git a/src/client.rs b/src/client.rs index 23fbdc3..00906ee 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 1; /// Stream to interface with the IMAP server. This interface is only for the command stream. -pub struct IMAPStream { +pub struct Client { stream: T, tag: u32 } @@ -22,12 +22,12 @@ pub struct IMAPMailbox { pub uid_validity: Option } -impl IMAPStream { - /// Creates an IMAP Stream. - pub fn connect(addr: A) -> Result> { +impl Client { + /// Creates a new client. + pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = IMAPStream { stream: stream, tag: INITIAL_TAG}; + let mut socket = Client { stream: stream, tag: INITIAL_TAG}; try!(socket.read_greeting()); Ok(socket) @@ -37,12 +37,12 @@ impl IMAPStream { } } -impl IMAPStream> { - /// Creates an IMAP Stream with an Ssl wrapper. - pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { +impl Client> { + /// Creates a client with an SSL wrapper. + pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = IMAPStream { stream: SslStream::connect(&ssl_context, stream).unwrap(), tag: INITIAL_TAG}; + let mut socket = Client { stream: SslStream::connect(&ssl_context, stream).unwrap(), tag: INITIAL_TAG}; try!(socket.read_greeting()); Ok(socket) @@ -52,7 +52,7 @@ impl IMAPStream> { } } -impl IMAPStream { +impl Client { /// Log in to the IMAP server. pub fn login(&mut self, username: & str, password: & str) -> Result<()> { From b25a2c4215c41d714bc6d92aebe8772eaf6074c8 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 14:15:16 -0400 Subject: [PATCH 05/65] Adding base testing work --- src/client.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/client.rs b/src/client.rs index 00906ee..0585e8d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -327,3 +327,80 @@ impl Client { return command; } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Read, Result, Write, Error, ErrorKind}; + + struct MockStream { + read_buf: Vec, + read_pos: usize, + written_buf: Vec + } + + impl MockStream { + fn new(read_buf: Vec) -> MockStream { + MockStream{ + read_buf: read_buf, + read_pos: 0, + written_buf: Vec::new() + } + } + } + + impl Read for MockStream { + fn read(&mut self, buf: &mut[u8]) -> Result { + if self.read_pos >= self.read_buf.len() { + return Err(Error::new(ErrorKind::UnexpectedEof, "EOF")) + } + let write_len = min(buf.len(), self.read_buf.len() - self.read_pos); + let max_pos = self.read_pos + write_len; + for x in self.read_pos..max_pos { + buf[x - self.read_pos] = self.read_buf[x]; + } + self.read_pos += write_len; + Ok(write_len) + } + } + + impl Write for MockStream { + fn write(&mut self, buf: &[u8]) -> Result { + self.written_buf.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } + } + + fn min(a: usize, b: usize) -> usize { + if a < b { + a + } else if b < a { + b + } else { + a + } + } + + fn create_client_with_mock_stream(mock_stream: MockStream) -> Client { + Client { + stream: mock_stream, + tag: 1 + } + } + + #[test] + fn close() { + let response = b"a1 OK CLOSE completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut imap_stream = create_client_with_mock_stream(mock_stream); + match imap_stream.close() { + Err(err) => panic!("Error reading response: {}", err), + _ => {}, + } + assert!(imap_stream.stream.written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command"); + } +} From 59bee5bd42ce59192f1f82d6e48343f7d78f048a Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 14:51:08 -0400 Subject: [PATCH 06/65] Adding mock stream to its own file --- src/client.rs | 54 +--------------------------------------------- src/lib.rs | 3 +++ src/mock_stream.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 src/mock_stream.rs diff --git a/src/client.rs b/src/client.rs index 0585e8d..576947f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -331,59 +331,7 @@ impl Client { #[cfg(test)] mod tests { use super::*; - use std::io::{Read, Result, Write, Error, ErrorKind}; - - struct MockStream { - read_buf: Vec, - read_pos: usize, - written_buf: Vec - } - - impl MockStream { - fn new(read_buf: Vec) -> MockStream { - MockStream{ - read_buf: read_buf, - read_pos: 0, - written_buf: Vec::new() - } - } - } - - impl Read for MockStream { - fn read(&mut self, buf: &mut[u8]) -> Result { - if self.read_pos >= self.read_buf.len() { - return Err(Error::new(ErrorKind::UnexpectedEof, "EOF")) - } - let write_len = min(buf.len(), self.read_buf.len() - self.read_pos); - let max_pos = self.read_pos + write_len; - for x in self.read_pos..max_pos { - buf[x - self.read_pos] = self.read_buf[x]; - } - self.read_pos += write_len; - Ok(write_len) - } - } - - impl Write for MockStream { - fn write(&mut self, buf: &[u8]) -> Result { - self.written_buf.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> Result<()> { - Ok(()) - } - } - - fn min(a: usize, b: usize) -> usize { - if a < b { - a - } else if b < a { - b - } else { - a - } - } + use super::super::mock_stream::MockStream; fn create_client_with_mock_stream(mock_stream: MockStream) -> Client { Client { diff --git a/src/lib.rs b/src/lib.rs index 372205b..a49f77a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,6 @@ extern crate openssl; extern crate regex; pub mod client; + +#[cfg(test)] +mod mock_stream; diff --git a/src/mock_stream.rs b/src/mock_stream.rs new file mode 100644 index 0000000..da2af04 --- /dev/null +++ b/src/mock_stream.rs @@ -0,0 +1,53 @@ +use std::io::{Read, Result, Write, Error, ErrorKind}; + +pub struct MockStream { + read_buf: Vec, + read_pos: usize, + pub written_buf: Vec +} + +impl MockStream { + pub fn new(read_buf: Vec) -> MockStream { + MockStream{ + read_buf: read_buf, + read_pos: 0, + written_buf: Vec::new() + } + } +} + +impl Read for MockStream { + fn read(&mut self, buf: &mut[u8]) -> Result { + if self.read_pos >= self.read_buf.len() { + return Err(Error::new(ErrorKind::UnexpectedEof, "EOF")) + } + let write_len = min(buf.len(), self.read_buf.len() - self.read_pos); + let max_pos = self.read_pos + write_len; + for x in self.read_pos..max_pos { + buf[x - self.read_pos] = self.read_buf[x]; + } + self.read_pos += write_len; + Ok(write_len) + } +} + +impl Write for MockStream { + fn write(&mut self, buf: &[u8]) -> Result { + self.written_buf.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +fn min(a: usize, b: usize) -> usize { + if a < b { + a + } else if b < a { + b + } else { + a + } +} From 2e05110ba2b9a004eb80ad462bdbd604c5a1e058 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 15:20:59 -0400 Subject: [PATCH 07/65] Adding INITIAL_TAG to test code --- src/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 576947f..5cafe52 100644 --- a/src/client.rs +++ b/src/client.rs @@ -331,12 +331,13 @@ impl Client { #[cfg(test)] mod tests { use super::*; + use super::INITIAL_TAG; use super::super::mock_stream::MockStream; fn create_client_with_mock_stream(mock_stream: MockStream) -> Client { Client { stream: mock_stream, - tag: 1 + tag: INITIAL_TAG } } From 7f92a1429e4e31b86974aa26c3cfb01dae7628fd Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 15:41:46 -0400 Subject: [PATCH 08/65] Fixing some formatting of code --- src/client.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5cafe52..9ef62c8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -27,7 +27,10 @@ impl Client { pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = Client { stream: stream, tag: INITIAL_TAG}; + let mut socket = Client { + stream: stream, + tag: INITIAL_TAG + }; try!(socket.read_greeting()); Ok(socket) @@ -42,7 +45,10 @@ impl Client> { pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = Client { stream: SslStream::connect(&ssl_context, stream).unwrap(), tag: INITIAL_TAG}; + let mut socket = Client { + stream: SslStream::connect(&ssl_context, stream).unwrap(), + tag: INITIAL_TAG + }; try!(socket.read_greeting()); Ok(socket) From fb89e4a50ae85b1e988c49a2a2d51bdab87a04f0 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 15:46:43 -0400 Subject: [PATCH 09/65] Changing IMAPMailbox to Mailbox --- example.rs | 5 ++--- src/client.rs | 32 +++++++++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/example.rs b/example.rs index a204e7d..0f4161a 100644 --- a/example.rs +++ b/example.rs @@ -2,8 +2,7 @@ extern crate imap; extern crate openssl; use openssl::ssl::{SslContext, SslMethod}; -use imap::client::Client; -use imap::client::IMAPMailbox; +use imap::client::{Client, Mailbox}; fn main() { let mut imap_socket = match Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()) { @@ -25,7 +24,7 @@ fn main() { }; match imap_socket.select("INBOX") { - Ok(IMAPMailbox{flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity}) => { + Ok(Mailbox{flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity}) => { println!("flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?}, uid_next: {:?}, uid_validity: {:?}", flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity); }, Err(_) => println!("Error selecting INBOX") diff --git a/src/client.rs b/src/client.rs index 9ef62c8..0f24add 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ pub struct Client { tag: u32 } -pub struct IMAPMailbox { +pub struct Mailbox { pub flags: String, pub exists: u32, pub recent: u32, @@ -22,6 +22,20 @@ pub struct IMAPMailbox { pub uid_validity: Option } +impl Default for Mailbox { + fn default() -> Mailbox { + Mailbox { + flags: "".to_string(), + exists: 0, + recent: 0, + unseen: None, + permanent_flags: None, + uid_next: None, + uid_validity: None + } + } +} + impl Client { /// Creates a new client. pub fn connect(addr: A) -> Result> { @@ -66,14 +80,14 @@ impl Client { } /// Selects a mailbox - pub fn select(&mut self, mailbox_name: &str) -> Result { + pub fn select(&mut self, mailbox_name: &str) -> Result { match self.run_command(&format!("SELECT {}", mailbox_name).to_string()) { Ok(lines) => self.parse_select_or_examine(lines), Err(e) => Err(e) } } - fn parse_select_or_examine(&mut self, lines: Vec) -> Result { + fn parse_select_or_examine(&mut self, 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(); @@ -94,15 +108,7 @@ impl Client { Err(e) => return Err(e) }; - let mut mailbox = IMAPMailbox{ - flags: "".to_string(), - exists: 0, - recent: 0, - unseen: None, - permanent_flags: None, - uid_next: None, - uid_validity: None - }; + let mut mailbox = Mailbox::default(); for line in lines.iter() { if exists_regex.is_match(line) { @@ -133,7 +139,7 @@ impl Client { } /// Examine is identical to Select, but the selected mailbox is identified as read-only - pub fn examine(&mut self, mailbox_name: &str) -> Result { + pub fn examine(&mut self, mailbox_name: &str) -> Result { match self.run_command(&format!("EXAMINE {}", mailbox_name).to_string()) { Ok(lines) => self.parse_select_or_examine(lines), Err(e) => Err(e) From b243f1ab5a1c02e71d9a426a01adac7fee039bc9 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 15:48:14 -0400 Subject: [PATCH 10/65] Removing uneeded return statement --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 0f24add..b6d3a32 100644 --- a/src/client.rs +++ b/src/client.rs @@ -135,7 +135,7 @@ impl Client { } } - return Ok(mailbox); + Ok(mailbox) } /// Examine is identical to Select, but the selected mailbox is identified as read-only From b8002d1fb8a9f2bf295adc6ef47fc61ac2973d6e Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 15:49:28 -0400 Subject: [PATCH 11/65] Adding TODO for make sure the client can read the response correctly --- src/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.rs b/src/client.rs index b6d3a32..c241e39 100644 --- a/src/client.rs +++ b/src/client.rs @@ -355,6 +355,7 @@ mod tests { #[test] fn close() { + // TODO Make sure the response was read correctly let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut imap_stream = create_client_with_mock_stream(mock_stream); From 1cf02a409cff8f930a799f8fe217ae84c2dc4ee2 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 16:11:36 -0400 Subject: [PATCH 12/65] Adding more tests --- src/client.rs | 49 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index c241e39..21d36fb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use std::io::{Error, ErrorKind, Read, Result, Write}; use regex::Regex; static TAG_PREFIX: &'static str = "a"; -const INITIAL_TAG: u32 = 1; +const INITIAL_TAG: u32 = 0; /// Stream to interface with the IMAP server. This interface is only for the command stream. pub struct Client { @@ -261,8 +261,6 @@ impl Client { Err(_) => Err(Error::new(ErrorKind::Other, "Failed to read")), }; - self.tag += 1; - return ret; } @@ -335,6 +333,7 @@ impl Client { } fn create_command(&mut self, command: String) -> String { + self.tag += 1; let command = format!("{}{} {}\r\n", TAG_PREFIX, self.tag, command); return command; } @@ -353,16 +352,54 @@ mod tests { } } + #[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 = create_client_with_mock_stream(mock_stream); + match client.read_response() { + Ok(r) => assert!(expected_response == r, "expected response doesn't equal actual"), + Err(err) => panic!("Error reading response: {}", err), + } + } + + #[test] + fn read_greeting() { + let greeting = "* OK Dovecot ready.\r\n"; + let mock_stream = MockStream::new(greeting.as_bytes().to_vec()); + let mut client = create_client_with_mock_stream(mock_stream); + match client.read_greeting() { + Err(err) => panic!("Error reading response: {}", err), + _ => {}, + } + } + + #[test] + fn create_command() { + let base_command = "CHECK"; + let mock_stream = MockStream::new(Vec::new()); + let mut imap_stream = create_client_with_mock_stream(mock_stream); + + let expected_command = format!("a1 {}\r\n", base_command); + let command = imap_stream.create_command(String::from(base_command)); + assert!(command == expected_command, "expected command doesn't equal actual command"); + + let expected_command2 = format!("a2 {}\r\n", base_command); + let command2 = imap_stream.create_command(String::from(base_command)); + assert!(command2 == expected_command2, "expected command doesn't equal actual command"); + } + #[test] fn close() { // TODO Make sure the response was read correctly let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut imap_stream = create_client_with_mock_stream(mock_stream); - match imap_stream.close() { + let mut client = create_client_with_mock_stream(mock_stream); + match client.close() { Err(err) => panic!("Error reading response: {}", err), _ => {}, } - assert!(imap_stream.stream.written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command"); + assert!(client.stream.written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command"); } } From 91bcdf1a4e5d4096d9b3d16d5a57246fb58667f9 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 16:12:43 -0400 Subject: [PATCH 13/65] Removing uneeded wrapper around read_response in run_command --- src/client.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 21d36fb..42077f9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -256,12 +256,7 @@ impl Client { Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to write")), }; - let ret = match self.read_response() { - Ok(lines) => Ok(lines), - Err(_) => Err(Error::new(ErrorKind::Other, "Failed to read")), - }; - - return ret; + self.read_response() } fn parse_response_ok(&mut self, lines: Vec) -> Result<()> { From 350329b5a45b7e1b20c093e4fd17a7f49b81ca74 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 16:13:56 -0400 Subject: [PATCH 14/65] Fixing expunge command to actually send EXPUNGE --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 42077f9..8459013 100644 --- a/src/client.rs +++ b/src/client.rs @@ -222,7 +222,7 @@ impl Client { /// 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("CHECK") + self.run_command_and_check_ok("EXPUNGE") } /// Check requests a checkpoint of the currently selected mailbox. From 44b92ec7aca947adbbc3fa07fa495823460f4539 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 16:16:13 -0400 Subject: [PATCH 15/65] Adding test for check --- src/client.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client.rs b/src/client.rs index 8459013..82620cb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -385,6 +385,19 @@ mod tests { assert!(command2 == expected_command2, "expected command doesn't equal actual command"); } + #[test] + fn check() { + // TODO Make sure the response was read correctly + let response = b"a1 OK CHECK completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + match client.check() { + Err(err) => panic!("Error reading response: {}", err), + _ => {}, + } + assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid close command"); + } + #[test] fn close() { // TODO Make sure the response was read correctly From ff2fa37efaf70c54be69a5c108ae29e80191b1ea Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 16:32:40 -0400 Subject: [PATCH 16/65] Removing uneeded matching in the test statements --- src/client.rs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/client.rs b/src/client.rs index 82620cb..249e8e6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -353,10 +353,8 @@ mod tests { let expected_response: Vec = vec![response.to_string()]; let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mut client = create_client_with_mock_stream(mock_stream); - match client.read_response() { - Ok(r) => assert!(expected_response == r, "expected response doesn't equal actual"), - Err(err) => panic!("Error reading response: {}", err), - } + let actual_response = client.read_response().unwrap(); + assert!(expected_response == actual_response, "expected response doesn't equal actual"); } #[test] @@ -364,10 +362,7 @@ mod tests { let greeting = "* OK Dovecot ready.\r\n"; let mock_stream = MockStream::new(greeting.as_bytes().to_vec()); let mut client = create_client_with_mock_stream(mock_stream); - match client.read_greeting() { - Err(err) => panic!("Error reading response: {}", err), - _ => {}, - } + client.read_greeting().unwrap(); } #[test] @@ -391,10 +386,7 @@ mod tests { let response = b"a1 OK CHECK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); - match client.check() { - Err(err) => panic!("Error reading response: {}", err), - _ => {}, - } + client.check().unwrap(); assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid close command"); } @@ -404,10 +396,7 @@ mod tests { let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); - match client.close() { - Err(err) => panic!("Error reading response: {}", err), - _ => {}, - } + client.close().unwrap(); assert!(client.stream.written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command"); } } From bfe882b2a71ffcc6ec1fe6220dfe0f241ae78dd1 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 16:33:53 -0400 Subject: [PATCH 17/65] Adding proper eror message for invalid check command in test --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 249e8e6..d3e80c4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -387,7 +387,7 @@ mod tests { let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); client.check().unwrap(); - assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid close command"); + assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command"); } #[test] From f41b7916efad3cf09faa412f517fa2a21d80f518 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 17:41:51 -0400 Subject: [PATCH 18/65] Adding more testing for EXPUNGE, SUBSCRIBE, and UNSUBSCRIBE --- src/client.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/client.rs b/src/client.rs index d3e80c4..0c804ef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -380,6 +380,40 @@ mod tests { assert!(command2 == expected_command2, "expected command doesn't equal actual command"); } + #[test] + fn subscribe() { + // TODO Make sure the response was read correctly + let response = b"a1 OK SUBSCRIBE completed\r\n".to_vec(); + let mailbox = "INBOX"; + let command = format!("a1 SUBSCRIBE {}\r\n", mailbox); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.subscribe(mailbox).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid subscribe command"); + } + + #[test] + fn unsubscribe() { + // TODO Make sure the response was read correctly + let response = b"a1 OK UNSUBSCRIBE completed\r\n".to_vec(); + let mailbox = "INBOX"; + let command = format!("a1 UNSUBSCRIBE {}\r\n", mailbox); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.unsubscribe(mailbox).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid unsubscribe command"); + } + + #[test] + fn expunge() { + // TODO Make sure the response was read correctly + let response = b"a1 OK EXPUNGE completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.expunge().unwrap(); + assert!(client.stream.written_buf == b"a1 EXPUNGE\r\n".to_vec(), "Invalid expunge command"); + } + #[test] fn check() { // TODO Make sure the response was read correctly From 735ba9562a565a7fcd94ae281df84914356c134d Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 20:06:37 -0400 Subject: [PATCH 19/65] Adding test for RENAME command --- src/client.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client.rs b/src/client.rs index 0c804ef..19d325a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -380,6 +380,19 @@ mod tests { assert!(command2 == expected_command2, "expected command doesn't equal actual command"); } + #[test] + fn rename() { + // TODO Make sure the response was read correctly + let response = b"a1 OK RENAME completed\r\n".to_vec(); + let current_mailbox_name = "INBOX"; + let new_mailbox_name = "NEWINBOX"; + let command = format!("a1 RENAME {} {}\r\n", current_mailbox_name, new_mailbox_name); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.rename(current_mailbox_name, new_mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid rename command"); + } + #[test] fn subscribe() { // TODO Make sure the response was read correctly From 2d2c0ba96116c94e5b09603f2c3481bc5bf7139a Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 20:11:41 -0400 Subject: [PATCH 20/65] Adding login and logout test --- src/client.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/client.rs b/src/client.rs index 19d325a..c0c0904 100644 --- a/src/client.rs +++ b/src/client.rs @@ -380,6 +380,30 @@ mod tests { assert!(command2 == expected_command2, "expected command doesn't equal actual command"); } + #[test] + fn login() { + // TODO Make sure the response was read correctly + let response = b"a1 OK Logged in\r\n".to_vec(); + let username = "username"; + let password = "password"; + let command = format!("a1 LOGIN {} {}\r\n", username, password); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.login(username, password).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid login command"); + } + + #[test] + fn logout() { + // TODO Make sure the response was read correctly + 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 = create_client_with_mock_stream(mock_stream); + client.logout().unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid logout command"); + } + #[test] fn rename() { // TODO Make sure the response was read correctly From de06ae79601a7368789949d3b38f6be202f86d62 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 20:13:24 -0400 Subject: [PATCH 21/65] Adding NOOP test --- src/client.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/client.rs b/src/client.rs index c0c0904..eb8e281 100644 --- a/src/client.rs +++ b/src/client.rs @@ -461,6 +461,16 @@ mod tests { assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command"); } + #[test] + fn noop() { + // TODO Make sure the response was read correctly + let response = b"a1 OK NOOP completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.noop().unwrap(); + assert!(client.stream.written_buf == b"a1 NOOP\r\n".to_vec(), "Invalid noop command"); + } + #[test] fn close() { // TODO Make sure the response was read correctly From 6dd23c4647d373cec528827c1d13982216eb252e Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 20:19:31 -0400 Subject: [PATCH 22/65] Adding tests for create and delete --- src/client.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/client.rs b/src/client.rs index eb8e281..17f51fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -461,6 +461,30 @@ mod tests { assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command"); } + #[test] + fn create() { + // TODO Make sure the response was read correctly + let response = b"a1 OK CREATE completed\r\n".to_vec(); + let mailbox_name = "INBOX"; + let command = format!("a1 CREATE {}\r\n", mailbox_name); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.create(mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid create command"); + } + + #[test] + fn delete() { + // TODO Make sure the response was read correctly + let response = b"a1 OK DELETE completed\r\n".to_vec(); + let mailbox_name = "INBOX"; + let command = format!("a1 DELETE {}\r\n", mailbox_name); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.delete(mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid delete command"); + } + #[test] fn noop() { // TODO Make sure the response was read correctly From 50a55d59c4e349ea175e677fdbe0950c7708c1be Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 20:28:59 -0400 Subject: [PATCH 23/65] Adding FETCH test --- src/client.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client.rs b/src/client.rs index 17f51fd..fde7e01 100644 --- a/src/client.rs +++ b/src/client.rs @@ -417,6 +417,19 @@ mod tests { assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid rename command"); } + #[test] + fn fetch() { + // TODO Make sure the response was read correctly + let response = b"a1 OK FETCH completed\r\n".to_vec(); + let sequence_set = "1"; + let query = "BODY[]"; + let command = format!("a1 FETCH {} {}\r\n", sequence_set, query); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + client.fetch(sequence_set, query).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid fetch command"); + } + #[test] fn subscribe() { // TODO Make sure the response was read correctly From 625fb09c8b426296ead488943f993a0f5b592204 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 20:34:19 -0400 Subject: [PATCH 24/65] Move mailbox to its own file --- example.rs | 3 ++- src/client.rs | 26 ++------------------------ src/lib.rs | 1 + src/mailbox.rs | 23 +++++++++++++++++++++++ 4 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 src/mailbox.rs diff --git a/example.rs b/example.rs index 0f4161a..0f166cd 100644 --- a/example.rs +++ b/example.rs @@ -2,7 +2,8 @@ extern crate imap; extern crate openssl; use openssl::ssl::{SslContext, SslMethod}; -use imap::client::{Client, Mailbox}; +use imap::client::Client; +use imap::mailbox::Mailbox; fn main() { let mut imap_socket = match Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()) { diff --git a/src/client.rs b/src/client.rs index fde7e01..4345341 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,6 +3,8 @@ use openssl::ssl::{SslContext, SslStream}; use std::io::{Error, ErrorKind, Read, Result, Write}; use regex::Regex; +use super::mailbox::Mailbox; + static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 0; @@ -12,30 +14,6 @@ pub struct Client { tag: u32 } -pub struct Mailbox { - pub flags: String, - pub exists: u32, - pub recent: u32, - pub unseen: Option, - pub permanent_flags: Option, - pub uid_next: Option, - pub uid_validity: Option -} - -impl Default for Mailbox { - fn default() -> Mailbox { - Mailbox { - flags: "".to_string(), - exists: 0, - recent: 0, - unseen: None, - permanent_flags: None, - uid_next: None, - uid_validity: None - } - } -} - impl Client { /// Creates a new client. pub fn connect(addr: A) -> Result> { diff --git a/src/lib.rs b/src/lib.rs index a49f77a..9727a66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ extern crate openssl; extern crate regex; pub mod client; +pub mod mailbox; #[cfg(test)] mod mock_stream; diff --git a/src/mailbox.rs b/src/mailbox.rs new file mode 100644 index 0000000..f41491d --- /dev/null +++ b/src/mailbox.rs @@ -0,0 +1,23 @@ +pub struct Mailbox { + pub flags: String, + pub exists: u32, + pub recent: u32, + pub unseen: Option, + pub permanent_flags: Option, + pub uid_next: Option, + pub uid_validity: Option +} + +impl Default for Mailbox { + fn default() -> Mailbox { + Mailbox { + flags: "".to_string(), + exists: 0, + recent: 0, + unseen: None, + permanent_flags: None, + uid_next: None, + uid_validity: None + } + } +} From eeffe7420fafe8a55a2efd5ebfe6caa5cf80410a Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 21:31:17 -0400 Subject: [PATCH 25/65] Moving line reading into its own function --- src/client.rs | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4345341..a76c007 100644 --- a/src/client.rs +++ b/src/client.rs @@ -255,31 +255,20 @@ impl Client { } fn read_response(&mut self) -> Result> { - //Carriage return - let cr = 0x0d; - //Line Feed - let lf = 0x0a; let mut found_tag_line = false; let start_str = format!("{}{} ", TAG_PREFIX, self.tag); let mut lines: Vec = Vec::new(); while !found_tag_line { - let mut line_buffer: Vec = Vec::new(); - while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) { - let byte_buffer: &mut [u8] = &mut [0]; - match self.stream.read(byte_buffer) { - Ok(_) => {}, - Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")), + match self.readline() { + Ok(raw_data) => { + let line = String::from_utf8(raw_data).unwrap(); + lines.push(line.clone()); + if (&*line).starts_with(&*start_str) { + found_tag_line = true; } - line_buffer.push(byte_buffer[0]); - } - - let line = String::from_utf8(line_buffer).unwrap(); - - lines.push(line.clone()); - - if (&*line).starts_with(&*start_str) { - found_tag_line = true; + }, + Err(err) => return Err(err) } } @@ -287,6 +276,13 @@ impl Client { } fn read_greeting(&mut self) -> Result<()> { + match self.readline() { + Ok(_) => Ok(()), + Err(err) => Err(err) + } + } + + fn readline(&mut self) -> Result> { //Carriage return let cr = 0x0d; //Line Feed @@ -301,8 +297,7 @@ impl Client { } line_buffer.push(byte_buffer[0]); } - - Ok(()) + Ok(line_buffer) } fn create_command(&mut self, command: String) -> String { From d5e8877eaf6a6ad57b93de5360985856f4e76506 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 21:55:04 -0400 Subject: [PATCH 26/65] Changing error message for readline --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index a76c007..e8fc85d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -293,7 +293,7 @@ impl Client { let byte_buffer: &mut [u8] = &mut [0]; match self.stream.read(byte_buffer) { Ok(_) => {}, - Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")), + Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read line")), } line_buffer.push(byte_buffer[0]); } From bebcfab52c00b3fa2d25b413129ee2f28b4fd39f Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 22:02:24 -0400 Subject: [PATCH 27/65] Adding test for readline failing on err --- src/client.rs | 9 +++++++++ src/mock_stream.rs | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index e8fc85d..0367536 100644 --- a/src/client.rs +++ b/src/client.rs @@ -338,6 +338,15 @@ mod tests { client.read_greeting().unwrap(); } + #[test] + #[should_panic] + fn readline_err() { + // TODO Check the error test + let mock_stream = MockStream::new_err(); + let mut client = create_client_with_mock_stream(mock_stream); + client.readline().unwrap(); + } + #[test] fn create_command() { let base_command = "CHECK"; diff --git a/src/mock_stream.rs b/src/mock_stream.rs index da2af04..94c28e2 100644 --- a/src/mock_stream.rs +++ b/src/mock_stream.rs @@ -3,7 +3,8 @@ use std::io::{Read, Result, Write, Error, ErrorKind}; pub struct MockStream { read_buf: Vec, read_pos: usize, - pub written_buf: Vec + pub written_buf: Vec, + err_on_read: bool } impl MockStream { @@ -11,13 +12,26 @@ impl MockStream { MockStream{ read_buf: read_buf, read_pos: 0, - written_buf: Vec::new() + written_buf: Vec::new(), + err_on_read: false + } + } + + pub fn new_err() -> MockStream { + MockStream{ + read_buf: Vec::new(), + read_pos: 0, + written_buf: Vec::new(), + err_on_read: true } } } impl Read for MockStream { fn read(&mut self, buf: &mut[u8]) -> Result { + if self.err_on_read { + return Err(Error::new(ErrorKind::Other, "MockStream Error")) + } if self.read_pos >= self.read_buf.len() { return Err(Error::new(ErrorKind::UnexpectedEof, "EOF")) } From 8e99f80dca98c7d398541f37123141f65fed8df4 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 23:05:22 -0400 Subject: [PATCH 28/65] Add testing for the examine command --- src/client.rs | 37 +++++++++++++++++++++++++++++++++---- src/mailbox.rs | 9 +++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0367536..8c37878 100644 --- a/src/client.rs +++ b/src/client.rs @@ -72,13 +72,13 @@ impl Client { let flags_regex = Regex::new(r"^\* FLAGS (.+)\r\n").unwrap(); - let unseen_regex = Regex::new(r"^OK \[UNSEEN (\d+)\](.*)\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_validity_regex = Regex::new(r"^\* OK \[UIDVALIDITY (\d+)\](.*)\r\n").unwrap(); - let uid_next_regex = Regex::new(r"^OK \[UIDNEXT (\d+)\](.*)\r\n").unwrap(); + let uid_next_regex = Regex::new(r"^\* OK \[UIDNEXT (\d+)\](.*)\r\n").unwrap(); - let permanent_flags_regex = Regex::new(r"^OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); + let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); //Check Ok match self.parse_response_ok(lines.clone()) { @@ -312,6 +312,7 @@ mod tests { use super::*; use super::INITIAL_TAG; use super::super::mock_stream::MockStream; + use super::super::mailbox::Mailbox; fn create_client_with_mock_stream(mock_stream: MockStream) -> Client { Client { @@ -456,6 +457,34 @@ mod tests { assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command"); } + #[test] + fn examine() { + let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\ + * OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\ + * 1 EXISTS\r\n\ + * 1 RECENT\r\n\ + * OK [UNSEEN 1] First unseen.\r\n\ + * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\ + * OK [UIDNEXT 2] Predicted next UID\r\n\ + a1 OK [READ-ONLY] Select completed.\r\n".to_vec(); + let expected_mailbox = Mailbox { + flags: String::from("(\\Answered \\Flagged \\Deleted \\Seen \\Draft)"), + exists: 1, + recent: 1, + unseen: Some(1), + permanent_flags: None, + uid_next: Some(2), + uid_validity: Some(1257842737) + }; + let mailbox_name = "INBOX"; + let command = format!("a1 EXAMINE {}\r\n", mailbox_name); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + let mailbox = client.examine(mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid create command"); + assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); + } + #[test] fn create() { // TODO Make sure the response was read correctly diff --git a/src/mailbox.rs b/src/mailbox.rs index f41491d..876bf85 100644 --- a/src/mailbox.rs +++ b/src/mailbox.rs @@ -1,3 +1,6 @@ +use std::fmt; + +#[derive(Eq,PartialEq)] pub struct Mailbox { pub flags: String, pub exists: u32, @@ -21,3 +24,9 @@ impl Default for Mailbox { } } } + +impl fmt::Display for Mailbox { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?}, uid_next: {:?}, uid_validity: {:?}", self.flags, self.exists, self.recent, self.unseen, self.permanent_flags, self.uid_next, self.uid_validity) + } +} From 2aa66be25dda8c87d6e921eb0322a459d8281aba Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 23:16:03 -0400 Subject: [PATCH 29/65] Adding capability tests --- src/client.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/client.rs b/src/client.rs index 8c37878..f03d7ed 100644 --- a/src/client.rs +++ b/src/client.rs @@ -485,6 +485,18 @@ mod tests { assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); } + #[test] + fn capability() { + let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\ + a1 OK CAPABILITY completed\r\n".to_vec(); + let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + let capabilities = client.capability().unwrap(); + assert!(client.stream.written_buf == b"a1 CAPABILITY\r\n".to_vec(), "Invalid capability command"); + assert!(capabilities == expected_capabilities, "Unexpected capabilities response"); + } + #[test] fn create() { // TODO Make sure the response was read correctly From eca26018e8c5d87026ddb53612e0e34501174af9 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 23:22:36 -0400 Subject: [PATCH 30/65] Removing error check for invalid regex --- src/client.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index f03d7ed..2f0e313 100644 --- a/src/client.rs +++ b/src/client.rs @@ -238,10 +238,7 @@ impl Client { } fn parse_response_ok(&mut self, lines: Vec) -> Result<()> { - let ok_regex = match Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)"); let last_line = lines.last().unwrap(); for cap in ok_regex.captures_iter(last_line) { From c08385e06185f2d5f006ae0b89215ad768a82e7c Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 23:26:13 -0400 Subject: [PATCH 31/65] Removing more uneeded regex checks --- src/client.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2f0e313..649012e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -175,10 +175,7 @@ impl Client { } fn parse_capability(&mut self, lines: Vec) -> Result> { - let capability_regex = match Regex::new(r"^\* CAPABILITY (.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; + let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); //Check Ok match self.parse_response_ok(lines.clone()) { @@ -238,7 +235,7 @@ impl Client { } fn parse_response_ok(&mut self, lines: Vec) -> Result<()> { - let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)"); + let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)").unwrap(); let last_line = lines.last().unwrap(); for cap in ok_regex.captures_iter(last_line) { From 080bcebfb8bf78a88c2ec590dbb8c777a1410783 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 23:31:31 -0400 Subject: [PATCH 32/65] Moving basic example into examples directory --- example.rs => examples/basic.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example.rs => examples/basic.rs (100%) diff --git a/example.rs b/examples/basic.rs similarity index 100% rename from example.rs rename to examples/basic.rs From 668f71d5e9ba84249a06daa3ed5263502830dd5f Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 21 Jun 2016 23:34:47 -0400 Subject: [PATCH 33/65] Removing bin specification in Cargo.toml --- Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1883590..3a2a0f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,3 @@ path = "src/lib.rs" [dependencies] openssl = "0.7.13" regex = "0.1.71" - -[[bin]] -name = "example" -path = "example.rs" From 829b7de5424182eff42331520d290176fd9cf0b5 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 22 Jun 2016 19:17:18 -0400 Subject: [PATCH 34/65] Moving parse response ok to its own file --- src/client.rs | 21 ++++----------------- src/lib.rs | 2 ++ src/parse.rs | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 src/parse.rs diff --git a/src/client.rs b/src/client.rs index 649012e..d705ed3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,6 +4,7 @@ use std::io::{Error, ErrorKind, Read, Result, Write}; use regex::Regex; use super::mailbox::Mailbox; +use super::parse::parse_response_ok; static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 0; @@ -81,7 +82,7 @@ impl Client { let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); //Check Ok - match self.parse_response_ok(lines.clone()) { + match parse_response_ok(lines.clone()) { Ok(_) => (), Err(e) => return Err(e) }; @@ -178,7 +179,7 @@ impl Client { let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); //Check Ok - match self.parse_response_ok(lines.clone()) { + match parse_response_ok(lines.clone()) { Ok(_) => (), Err(e) => return Err(e) }; @@ -218,7 +219,7 @@ impl Client { pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { match self.run_command(command) { - Ok(lines) => self.parse_response_ok(lines), + Ok(lines) => parse_response_ok(lines), Err(e) => Err(e) } } @@ -234,20 +235,6 @@ impl Client { self.read_response() } - fn parse_response_ok(&mut self, lines: Vec) -> Result<()> { - let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)").unwrap(); - let last_line = lines.last().unwrap(); - - for cap in ok_regex.captures_iter(last_line) { - let response_type = cap.at(2).unwrap_or(""); - if response_type == "OK" { - return Ok(()); - } - } - - return Err(Error::new(ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string())); - } - fn read_response(&mut self) -> Result> { let mut found_tag_line = false; let start_str = format!("{}{} ", TAG_PREFIX, self.tag); diff --git a/src/lib.rs b/src/lib.rs index 9727a66..b0afb0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,5 +9,7 @@ extern crate regex; pub mod client; pub mod mailbox; +mod parse; + #[cfg(test)] mod mock_stream; diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..4e2f71e --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,16 @@ +use std::io::{Error, ErrorKind, Result}; +use regex::Regex; + +pub fn parse_response_ok(lines: Vec) -> Result<()> { + let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)").unwrap(); + let last_line = lines.last().unwrap(); + + for cap in ok_regex.captures_iter(last_line) { + let response_type = cap.at(2).unwrap_or(""); + if response_type == "OK" { + return Ok(()); + } + } + + return Err(Error::new(ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string())); +} From 5d94a5d7d75d4241519eb5bad1a2b41ec620aebe Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 22 Jun 2016 19:19:56 -0400 Subject: [PATCH 35/65] Moving parse capability into parse file --- src/client.rs | 24 ++---------------------- src/parse.rs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/client.rs b/src/client.rs index d705ed3..aeed4ef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use std::io::{Error, ErrorKind, Read, Result, Write}; use regex::Regex; use super::mailbox::Mailbox; -use super::parse::parse_response_ok; +use super::parse::{parse_response_ok, parse_capability}; static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 0; @@ -170,31 +170,11 @@ impl Client { /// Capability requests a listing of capabilities that the server supports. pub fn capability(&mut self) -> Result> { match self.run_command(&format!("CAPABILITY").to_string()) { - Ok(lines) => self.parse_capability(lines), + Ok(lines) => parse_capability(lines), Err(e) => Err(e) } } - fn parse_capability(&mut self, 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) - }; - - for line in lines.iter() { - if capability_regex.is_match(line) { - let cap = capability_regex.captures(line).unwrap(); - let capabilities_str = cap.at(1).unwrap(); - return Ok(capabilities_str.split(' ').map(|x| x.to_string()).collect()); - } - } - - Err(Error::new(ErrorKind::Other, "Error parsing capabilities response")) - } - /// Expunge permanently removes all messages that have the \Deleted flag set from the currently /// selected mailbox. pub fn expunge(&mut self) -> Result<()> { diff --git a/src/parse.rs b/src/parse.rs index 4e2f71e..3b2e88e 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,26 @@ use std::io::{Error, ErrorKind, Result}; use regex::Regex; +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) + }; + + for line in lines.iter() { + if capability_regex.is_match(line) { + let cap = capability_regex.captures(line).unwrap(); + let capabilities_str = cap.at(1).unwrap(); + return Ok(capabilities_str.split(' ').map(|x| x.to_string()).collect()); + } + } + + Err(Error::new(ErrorKind::Other, "Error parsing capabilities response")) +} + pub fn parse_response_ok(lines: Vec) -> Result<()> { let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)").unwrap(); let last_line = lines.last().unwrap(); From e344438e60099d47f73a8ddb88c66f294f529b91 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 22 Jun 2016 19:32:52 -0400 Subject: [PATCH 36/65] Moving pars select or examine to parse --- src/client.rs | 58 +++------------------------------------------------ src/parse.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/src/client.rs b/src/client.rs index aeed4ef..0635b49 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,9 @@ use std::net::{TcpStream, ToSocketAddrs}; use openssl::ssl::{SslContext, SslStream}; use std::io::{Error, ErrorKind, Read, Result, Write}; -use regex::Regex; use super::mailbox::Mailbox; -use super::parse::{parse_response_ok, parse_capability}; +use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine}; static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 0; @@ -61,66 +60,15 @@ impl Client { /// Selects a mailbox pub fn select(&mut self, mailbox_name: &str) -> Result { match self.run_command(&format!("SELECT {}", mailbox_name).to_string()) { - Ok(lines) => self.parse_select_or_examine(lines), + Ok(lines) => parse_select_or_examine(lines), Err(e) => Err(e) } } - fn parse_select_or_examine(&mut self, 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(); - - let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); - - //Check Ok - match parse_response_ok(lines.clone()) { - Ok(_) => (), - Err(e) => return Err(e) - }; - - 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.at(1).unwrap().parse::().unwrap(); - } else if recent_regex.is_match(line) { - let cap = recent_regex.captures(line).unwrap(); - mailbox.recent = cap.at(1).unwrap().parse::().unwrap(); - } else if flags_regex.is_match(line) { - let cap = flags_regex.captures(line).unwrap(); - mailbox.flags = cap.at(1).unwrap().to_string(); - } else if unseen_regex.is_match(line) { - let cap = unseen_regex.captures(line).unwrap(); - mailbox.unseen = Some(cap.at(1).unwrap().parse::().unwrap()); - } else if uid_validity_regex.is_match(line) { - let cap = uid_validity_regex.captures(line).unwrap(); - mailbox.uid_validity = Some(cap.at(1).unwrap().parse::().unwrap()); - } else if uid_next_regex.is_match(line) { - let cap = uid_next_regex.captures(line).unwrap(); - mailbox.uid_next = Some(cap.at(1).unwrap().parse::().unwrap()); - } else if permanent_flags_regex.is_match(line) { - let cap = permanent_flags_regex.captures(line).unwrap(); - mailbox.permanent_flags = Some(cap.at(1).unwrap().to_string()); - } - } - - Ok(mailbox) - } - /// Examine is identical to Select, but the selected mailbox is identified as read-only pub fn examine(&mut self, mailbox_name: &str) -> Result { match self.run_command(&format!("EXAMINE {}", mailbox_name).to_string()) { - Ok(lines) => self.parse_select_or_examine(lines), + Ok(lines) => parse_select_or_examine(lines), Err(e) => Err(e) } } diff --git a/src/parse.rs b/src/parse.rs index 3b2e88e..704b728 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,8 @@ use std::io::{Error, ErrorKind, Result}; use regex::Regex; +use super::mailbox::Mailbox; + pub fn parse_capability(lines: Vec) -> Result> { let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); @@ -34,3 +36,54 @@ pub fn parse_response_ok(lines: Vec) -> Result<()> { return Err(Error::new(ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string())); } + +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(); + + let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); + + //Check Ok + match parse_response_ok(lines.clone()) { + Ok(_) => (), + Err(e) => return Err(e) + }; + + 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.at(1).unwrap().parse::().unwrap(); + } else if recent_regex.is_match(line) { + let cap = recent_regex.captures(line).unwrap(); + mailbox.recent = cap.at(1).unwrap().parse::().unwrap(); + } else if flags_regex.is_match(line) { + let cap = flags_regex.captures(line).unwrap(); + mailbox.flags = cap.at(1).unwrap().to_string(); + } else if unseen_regex.is_match(line) { + let cap = unseen_regex.captures(line).unwrap(); + mailbox.unseen = Some(cap.at(1).unwrap().parse::().unwrap()); + } else if uid_validity_regex.is_match(line) { + let cap = uid_validity_regex.captures(line).unwrap(); + mailbox.uid_validity = Some(cap.at(1).unwrap().parse::().unwrap()); + } else if uid_next_regex.is_match(line) { + let cap = uid_next_regex.captures(line).unwrap(); + mailbox.uid_next = Some(cap.at(1).unwrap().parse::().unwrap()); + } else if permanent_flags_regex.is_match(line) { + let cap = permanent_flags_regex.captures(line).unwrap(); + mailbox.permanent_flags = Some(cap.at(1).unwrap().to_string()); + } + } + + Ok(mailbox) +} From 2011d3a92a85a748fadcef3cbc1f87f88458e7d6 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 22 Jun 2016 21:14:24 -0400 Subject: [PATCH 37/65] Adding docs for pub functions that run commands --- src/client.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client.rs b/src/client.rs index 0635b49..2d23145 100644 --- a/src/client.rs +++ b/src/client.rs @@ -145,6 +145,7 @@ impl Client { self.run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name).to_string()) } + /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { match self.run_command(command) { Ok(lines) => parse_response_ok(lines), @@ -152,6 +153,7 @@ impl Client { } } + /// 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()); From 808640e00de591e25e52c44fd4118331f37bf6e5 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 22 Jun 2016 21:16:39 -0400 Subject: [PATCH 38/65] Adding test for select --- src/client.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 2d23145..cedfdd2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -392,7 +392,35 @@ mod tests { let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); let mailbox = client.examine(mailbox_name).unwrap(); - assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid create command"); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid examine command"); + assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); + } + + #[test] + fn select() { + let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\ + * OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\ + * 1 EXISTS\r\n\ + * 1 RECENT\r\n\ + * OK [UNSEEN 1] First unseen.\r\n\ + * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\ + * OK [UIDNEXT 2] Predicted next UID\r\n\ + a1 OK [READ-ONLY] Select completed.\r\n".to_vec(); + let expected_mailbox = Mailbox { + flags: String::from("(\\Answered \\Flagged \\Deleted \\Seen \\Draft)"), + exists: 1, + recent: 1, + unseen: Some(1), + permanent_flags: None, + uid_next: Some(2), + uid_validity: Some(1257842737) + }; + let mailbox_name = "INBOX"; + let command = format!("a1 SELECT {}\r\n", mailbox_name); + let mock_stream = MockStream::new(response); + let mut client = create_client_with_mock_stream(mock_stream); + let mailbox = client.select(mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid select command"); assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); } From 923339e5f0f22ec1e52d58d93b34b345eb179f8e Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 22 Jun 2016 21:25:48 -0400 Subject: [PATCH 39/65] More through testing of select and examine --- src/client.rs | 6 +++--- src/parse.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index cedfdd2..51f85ab 100644 --- a/src/client.rs +++ b/src/client.rs @@ -383,7 +383,7 @@ mod tests { exists: 1, recent: 1, unseen: Some(1), - permanent_flags: None, + permanent_flags: Some(String::from("()")), uid_next: Some(2), uid_validity: Some(1257842737) }; @@ -399,7 +399,7 @@ mod tests { #[test] fn select() { let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\ - * OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\ + * OK [PERMANENTFLAGS (\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)] Read-only mailbox.\r\n\ * 1 EXISTS\r\n\ * 1 RECENT\r\n\ * OK [UNSEEN 1] First unseen.\r\n\ @@ -411,7 +411,7 @@ mod tests { exists: 1, recent: 1, unseen: Some(1), - permanent_flags: None, + permanent_flags: Some(String::from("(\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)")), uid_next: Some(2), uid_validity: Some(1257842737) }; diff --git a/src/parse.rs b/src/parse.rs index 704b728..0fbbbef 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -50,7 +50,7 @@ pub fn parse_select_or_examine(lines: Vec) -> Result { let uid_next_regex = Regex::new(r"^\* OK \[UIDNEXT (\d+)\](.*)\r\n").unwrap(); - let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\]\r\n").unwrap(); + let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\](.*)\r\n").unwrap(); //Check Ok match parse_response_ok(lines.clone()) { From 6c826625fd0af7039ddd85b85dd9ed3923262ff6 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Thu, 23 Jun 2016 22:44:12 -0400 Subject: [PATCH 40/65] Initial work for adding errors --- src/client.rs | 12 ++++++----- src/error.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/parse.rs | 7 +++--- 4 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 src/error.rs diff --git a/src/client.rs b/src/client.rs index 51f85ab..7bd8107 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,11 @@ use std::net::{TcpStream, ToSocketAddrs}; use openssl::ssl::{SslContext, SslStream}; -use std::io::{Error, ErrorKind, Read, Result, Write}; +use std::io::{ErrorKind, Read, Write}; +use std::io::{self}; use super::mailbox::Mailbox; use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine}; +use super::error::{Error, Result}; static TAG_PREFIX: &'static str = "a"; const INITIAL_TAG: u32 = 0; @@ -27,7 +29,7 @@ impl Client { try!(socket.read_greeting()); Ok(socket) }, - Err(e) => Err(e) + Err(e) => Err(Error::Io(e)) } } } @@ -45,7 +47,7 @@ impl Client> { try!(socket.read_greeting()); Ok(socket) }, - Err(e) => Err(e) + Err(e) => Err(Error::Io(e)) } } } @@ -159,7 +161,7 @@ impl Client { match self.stream.write_fmt(format_args!("{}", &*command)) { Ok(_) => (), - Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to write")), + Err(_) => return Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to write"))), }; self.read_response() @@ -204,7 +206,7 @@ impl Client { let byte_buffer: &mut [u8] = &mut [0]; match self.stream.read(byte_buffer) { Ok(_) => {}, - Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read line")), + Err(_) => return Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to read line"))), } line_buffer.push(byte_buffer[0]); } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8084b46 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,60 @@ +extern crate openssl; + +use std::io::{self}; +use std::result; +use std::fmt::{self}; +use std::error::Error as StdError; + +use openssl::ssl::error::SslError; + +pub type Result = result::Result; + +/// A set of errors that can occur in the IMAP client +#[derive(Debug)] +pub enum Error { + Io(io::Error), + Ssl(SslError), + BadResponse(Vec), + NoResponse(Vec), +} + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: SslError) -> Error { + Error::Ssl(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Io(ref e) => fmt::Display::fmt(e, f), + Error::Ssl(ref e) => fmt::Display::fmt(e, f), + ref e => f.write_str(e.description()), + } + } +} + +impl StdError for Error { + fn description(&self) -> &str { + match *self { + Error::Io(ref e) => e.description(), + Error::Ssl(ref e) => e.description(), + Error::BadResponse(_) => "Bad Response", + Error::NoResponse(_) => "No Response" + } + } + + fn cause(&self) -> Option<&StdError> { + match *self { + Error::Io(ref e) => Some(e), + Error::Ssl(ref e) => Some(e), + _ => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b0afb0b..affa530 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ extern crate openssl; extern crate regex; pub mod client; +pub mod error; pub mod mailbox; mod parse; diff --git a/src/parse.rs b/src/parse.rs index 0fbbbef..d8b522f 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,7 +1,8 @@ -use std::io::{Error, ErrorKind, Result}; +use std::io::{self}; use regex::Regex; use super::mailbox::Mailbox; +use super::error::{Error, Result}; pub fn parse_capability(lines: Vec) -> Result> { let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); @@ -20,7 +21,7 @@ pub fn parse_capability(lines: Vec) -> Result> { } } - Err(Error::new(ErrorKind::Other, "Error parsing capabilities response")) + Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Error parsing capabilities response"))) } pub fn parse_response_ok(lines: Vec) -> Result<()> { @@ -34,7 +35,7 @@ pub fn parse_response_ok(lines: Vec) -> Result<()> { } } - return Err(Error::new(ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string())); + Err(Error::Io(io::Error::new(io::ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string()))) } pub fn parse_select_or_examine(lines: Vec) -> Result { From aa989ce0e54d5705b9769df73453000dbfb1bcbd Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Thu, 23 Jun 2016 22:56:22 -0400 Subject: [PATCH 41/65] Fixing some extra imports --- src/client.rs | 2 +- src/error.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7bd8107..8bb7e87 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,6 @@ use std::net::{TcpStream, ToSocketAddrs}; use openssl::ssl::{SslContext, SslStream}; -use std::io::{ErrorKind, Read, Write}; +use std::io::{Read, Write}; use std::io::{self}; use super::mailbox::Mailbox; diff --git a/src/error.rs b/src/error.rs index 8084b46..b03045c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,3 @@ -extern crate openssl; - use std::io::{self}; use std::result; use std::fmt::{self}; From 56edd6c46babac3dc975354f565231e4bb63d5f7 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 09:54:03 -0400 Subject: [PATCH 42/65] Adding some comments and cleaning up imports --- src/client.rs | 3 +-- src/error.rs | 14 +++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8bb7e87..f93728c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,6 @@ use std::net::{TcpStream, ToSocketAddrs}; use openssl::ssl::{SslContext, SslStream}; -use std::io::{Read, Write}; -use std::io::{self}; +use std::io::{self, Read, Write}; use super::mailbox::Mailbox; use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine}; diff --git a/src/error.rs b/src/error.rs index b03045c..361444c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ -use std::io::{self}; +use std::io::Error as IoError; use std::result; -use std::fmt::{self}; +use std::fmt; use std::error::Error as StdError; use openssl::ssl::error::SslError; @@ -10,14 +10,18 @@ pub type Result = result::Result; /// A set of errors that can occur in the IMAP client #[derive(Debug)] pub enum Error { - Io(io::Error), + /// An `io::Error` that occurred while trying to read or write to a network stream. + Io(IoError), + /// An error from the `openssl` library. Ssl(SslError), + /// A BAD response from the IMAP server. BadResponse(Vec), + /// A NO response from the IMAP server. NoResponse(Vec), } -impl From for Error { - fn from(err: io::Error) -> Error { +impl From for Error { + fn from(err: IoError) -> Error { Error::Io(err) } } From 016149d1c09fccda200cc9b1f62cf234ab5d073a Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 16:07:52 -0400 Subject: [PATCH 43/65] Adding travis cargo exclude pattern --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3611d5c..f07b338 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,4 @@ addons: - libdw-dev after_success: - travis-cargo --only stable doc-upload -- travis-cargo coveralls --no-sudo +- travis-cargo coveralls --exclude-pattern="/.cargo,/target" --no-sudo From 69926159218c55d11fd8934d2939d680ec8e8036 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 16:13:17 -0400 Subject: [PATCH 44/65] Removing uneeded /.cargo exclude because it is included by default --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f07b338..7227473 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,4 @@ addons: - libdw-dev after_success: - travis-cargo --only stable doc-upload -- travis-cargo coveralls --exclude-pattern="/.cargo,/target" --no-sudo +- travis-cargo coveralls --exclude-pattern="/target" --no-sudo From 378b4bbb96231e1a49ceac7986079beaf897642e Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 19:38:04 -0400 Subject: [PATCH 45/65] Adding errors for parsing problems --- src/error.rs | 36 +++++++++++++++++++++++++++++++++++- src/parse.rs | 7 +++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/error.rs b/src/error.rs index 361444c..4d2cd89 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,8 @@ pub enum Error { BadResponse(Vec), /// A NO response from the IMAP server. NoResponse(Vec), + // Error parsing a server response. + Parse(ParseError) } impl From for Error { @@ -47,8 +49,9 @@ impl StdError for Error { match *self { Error::Io(ref e) => e.description(), Error::Ssl(ref e) => e.description(), + Error::Parse(ref e) => e.description(), Error::BadResponse(_) => "Bad Response", - Error::NoResponse(_) => "No Response" + Error::NoResponse(_) => "No Response", } } @@ -60,3 +63,34 @@ 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) +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ref e => f.write_str(e.description()), + } + } +} + +impl StdError for ParseError { + fn description(&self) -> &str { + match *self { + ParseError::StatusResponse(_) => "Unable to parse status response", + ParseError::Capability(_) => "Unable to parse capability response" + } + } + + fn cause(&self) -> Option<&StdError> { + match *self { + _ => None + } + } +} diff --git a/src/parse.rs b/src/parse.rs index d8b522f..ef398c3 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,8 +1,7 @@ -use std::io::{self}; use regex::Regex; use super::mailbox::Mailbox; -use super::error::{Error, Result}; +use super::error::{Error, ParseError, Result}; pub fn parse_capability(lines: Vec) -> Result> { let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); @@ -21,7 +20,7 @@ pub fn parse_capability(lines: Vec) -> Result> { } } - Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Error parsing capabilities response"))) + Err(Error::Parse(ParseError::Capability(lines))) } pub fn parse_response_ok(lines: Vec) -> Result<()> { @@ -35,7 +34,7 @@ pub fn parse_response_ok(lines: Vec) -> Result<()> { } } - Err(Error::Io(io::Error::new(io::ErrorKind::Other, format!("Invalid Response: {}", last_line).to_string()))) + Err(Error::Parse(ParseError::StatusResponse(lines.clone()))) } pub fn parse_select_or_examine(lines: Vec) -> Result { From 69179ebdac84a86bed5643fa97944341914f9e41 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 19:49:41 -0400 Subject: [PATCH 46/65] Add testing for capability parsing --- src/parse.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/parse.rs b/src/parse.rs index ef398c3..d813466 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -87,3 +87,23 @@ pub fn parse_select_or_examine(lines: Vec) -> Result { Ok(mailbox) } + +#[cfg(test)] +mod tests { + use super::*; + + #[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"); + } + + #[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(); + } +} From a87c58bf91d8006278e728cd3b8c4988ae792f33 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 20:10:38 -0400 Subject: [PATCH 47/65] Adding more tests for the parser --- src/parse.rs | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index d813466..0fa7651 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -24,13 +24,26 @@ pub fn parse_capability(lines: Vec) -> Result> { } pub fn parse_response_ok(lines: Vec) -> Result<()> { - let ok_regex = Regex::new(r"^([a-zA-Z0-9]+) ([a-zA-Z0-9]+)(.*)").unwrap(); - let last_line = lines.last().unwrap(); + match parse_response(lines) { + Ok(_) => Ok(()), + Err(e) => return Err(e) + } +} - for cap in ok_regex.captures_iter(last_line) { +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()))) + }; + + for cap in regex.captures_iter(last_line) { let response_type = cap.at(2).unwrap_or(""); - if response_type == "OK" { - return Ok(()); + match response_type { + "OK" => return Ok(lines.clone()), + "BAD" => return Err(Error::BadResponse(lines.clone())), + "NO" => return Err(Error::NoResponse(lines.clone())), + _ => {} } } @@ -106,4 +119,26 @@ mod tests { 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(); } + + #[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"); + } + + #[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(); + } + + #[test] + #[should_panic] + fn parse_response_invalid2_test() { + let lines = vec![String::from("* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"), String::from("a2 broken.\r\n")]; + parse_response(lines).unwrap(); + } } From 964d7c99ef7d676533377874a35bd9e15f7efff6 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 20:40:56 -0400 Subject: [PATCH 48/65] Making the basic example more readable --- examples/basic.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index 0f166cd..e0e9b75 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -6,14 +6,9 @@ use imap::client::Client; use imap::mailbox::Mailbox; fn main() { - let mut imap_socket = match Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()) { - Ok(s) => s, - Err(e) => panic!("{}", e) - }; + let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap(); - if let Err(e) = imap_socket.login("username", "password") { - println!("Error: {}", e) - }; + imap_socket.login("username", "password").unwrap(); match imap_socket.capability() { Ok(capabilities) => { @@ -40,7 +35,5 @@ fn main() { Err(_) => println!("Error Fetching email 2") }; - if let Err(e) = imap_socket.logout() { - println!("Error: {}", e) - }; + imap_socket.logout().unwrap(); } From 5dec8b79ace71981a0dffb9adca49507d221cd36 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 20:42:40 -0400 Subject: [PATCH 49/65] Cleaning up more of the basic example --- examples/basic.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index e0e9b75..bc33a7e 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -16,14 +16,14 @@ fn main() { println!("{}", capability); } }, - Err(_) => println!("Error retreiving capabilities") + Err(e) => println!("Error parsing capability: {}", e) }; match imap_socket.select("INBOX") { Ok(Mailbox{flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity}) => { println!("flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?}, uid_next: {:?}, uid_validity: {:?}", flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity); }, - Err(_) => println!("Error selecting INBOX") + Err(e) => println!("Error selecting INBOX: {}", e) }; match imap_socket.fetch("2", "body[text]") { @@ -32,7 +32,7 @@ fn main() { print!("{}", line); } }, - Err(_) => println!("Error Fetching email 2") + Err(e) => println!("Error Fetching email 2: {}", e) }; imap_socket.logout().unwrap(); From bf5031e8c4b892d6cf42e0b991b78f9cbe54600f Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 20:51:42 -0400 Subject: [PATCH 50/65] Removing complicated printing of mailbox --- examples/basic.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index bc33a7e..0f7f60f 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -3,7 +3,6 @@ extern crate openssl; use openssl::ssl::{SslContext, SslMethod}; use imap::client::Client; -use imap::mailbox::Mailbox; fn main() { let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap(); @@ -20,8 +19,8 @@ fn main() { }; match imap_socket.select("INBOX") { - Ok(Mailbox{flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity}) => { - println!("flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?}, uid_next: {:?}, uid_validity: {:?}", flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity); + Ok(mailbox) => { + println!("{}", mailbox); }, Err(e) => println!("Error selecting INBOX: {}", e) }; From 8418983253568bc9ce6fe1a463c4784ca676e14d Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 20:56:11 -0400 Subject: [PATCH 51/65] Adding README.md to examples --- examples/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..afa9711 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +Examples +======== + +This directory examples of working with the IMAP client. + +Current Examples: + * basic - This is a very basic example of using the client. + +Planned Examples: + * error_handling - This will show usage of handling errors that the client can return. + * gmail_oauth2 - This will be an example using oauth2 for logging into gmail as a secure appplication. From 92341ad1258066f40e4af4cc17d323a42012c062 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 21:17:18 -0400 Subject: [PATCH 52/65] Adding list command --- src/client.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index f93728c..c80fb12 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,7 @@ use openssl::ssl::{SslContext, SslStream}; use std::io::{self, Read, Write}; use super::mailbox::Mailbox; -use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine}; +use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine, parse_response}; use super::error::{Error, Result}; static TAG_PREFIX: &'static str = "a"; @@ -146,6 +146,15 @@ impl Client { self.run_command_and_check_ok(&format!("COPY {} {}", sequence_set, mailbox_name).to_string()) } + /// The LIST command returns a subset of names from the complete set + /// of all names available to the client. + pub fn list(&mut self, reference_name: &str, mailbox_search_pattern: &str) -> Result> { + match self.run_command(&format!("LIST {} {}", reference_name, mailbox_search_pattern)) { + Ok(lines) => parse_response(lines), + Err(e) => Err(e) + } + } + /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { match self.run_command(command) { From 472f77de55029c5699b25731dfd9d8e10acc8e1e Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 21:20:50 -0400 Subject: [PATCH 53/65] Adding LSUB command --- src/client.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client.rs b/src/client.rs index c80fb12..cf2ed01 100644 --- a/src/client.rs +++ b/src/client.rs @@ -155,6 +155,15 @@ impl Client { } } + /// The LSUB command returns a subset of names from the set of names + /// that the user has declared as being "active" or "subscribed". + pub fn lsub(&mut self, reference_name: &str, mailbox_search_pattern: &str) -> Result> { + match self.run_command(&format!("LSUB {} {}", reference_name, mailbox_search_pattern)) { + Ok(lines) => parse_response(lines), + Err(e) => Err(e) + } + } + /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { match self.run_command(command) { From d2a3482f1f321be569fdf91e07e5fc55282fc42a Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 21:27:36 -0400 Subject: [PATCH 54/65] Adding status command and putting parsing response into method --- src/client.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index cf2ed01..e8f0228 100644 --- a/src/client.rs +++ b/src/client.rs @@ -149,19 +149,18 @@ impl Client { /// The LIST command returns a subset of names from the complete set /// of all names available to the client. pub fn list(&mut self, reference_name: &str, mailbox_search_pattern: &str) -> Result> { - match self.run_command(&format!("LIST {} {}", reference_name, mailbox_search_pattern)) { - Ok(lines) => parse_response(lines), - Err(e) => Err(e) - } + self.run_command_and_parse(&format!("LIST {} {}", reference_name, mailbox_search_pattern)) } /// The LSUB command returns a subset of names from the set of names /// that the user has declared as being "active" or "subscribed". pub fn lsub(&mut self, reference_name: &str, mailbox_search_pattern: &str) -> Result> { - match self.run_command(&format!("LSUB {} {}", reference_name, mailbox_search_pattern)) { - Ok(lines) => parse_response(lines), - Err(e) => Err(e) - } + self.run_command_and_parse(&format!("LSUB {} {}", reference_name, mailbox_search_pattern)) + } + + /// 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)) } /// Runs a command and checks if it returns OK. @@ -172,6 +171,14 @@ impl Client { } } + // Run a command and parse the status response. + pub fn run_command_and_parse(&mut self, command: &str) -> Result> { + match self.run_command(command) { + Ok(lines) => parse_response(lines), + Err(e) => Err(e) + } + } + /// 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()); From 318b14bc621178ae0b4a4286d4e680b1bb111410 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 21:30:37 -0400 Subject: [PATCH 55/65] removing TODOs for making sure test response was read correctly --- src/client.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/client.rs b/src/client.rs index e8f0228..8956215 100644 --- a/src/client.rs +++ b/src/client.rs @@ -302,7 +302,6 @@ mod tests { #[test] fn login() { - // TODO Make sure the response was read correctly let response = b"a1 OK Logged in\r\n".to_vec(); let username = "username"; let password = "password"; @@ -315,7 +314,6 @@ mod tests { #[test] fn logout() { - // TODO Make sure the response was read correctly let response = b"a1 OK Logout completed.\r\n".to_vec(); let command = format!("a1 LOGOUT\r\n"); let mock_stream = MockStream::new(response); @@ -326,7 +324,6 @@ mod tests { #[test] fn rename() { - // TODO Make sure the response was read correctly let response = b"a1 OK RENAME completed\r\n".to_vec(); let current_mailbox_name = "INBOX"; let new_mailbox_name = "NEWINBOX"; @@ -339,7 +336,6 @@ mod tests { #[test] fn fetch() { - // TODO Make sure the response was read correctly let response = b"a1 OK FETCH completed\r\n".to_vec(); let sequence_set = "1"; let query = "BODY[]"; @@ -352,7 +348,6 @@ mod tests { #[test] fn subscribe() { - // TODO Make sure the response was read correctly let response = b"a1 OK SUBSCRIBE completed\r\n".to_vec(); let mailbox = "INBOX"; let command = format!("a1 SUBSCRIBE {}\r\n", mailbox); @@ -364,7 +359,6 @@ mod tests { #[test] fn unsubscribe() { - // TODO Make sure the response was read correctly let response = b"a1 OK UNSUBSCRIBE completed\r\n".to_vec(); let mailbox = "INBOX"; let command = format!("a1 UNSUBSCRIBE {}\r\n", mailbox); @@ -376,7 +370,6 @@ mod tests { #[test] fn expunge() { - // TODO Make sure the response was read correctly let response = b"a1 OK EXPUNGE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); @@ -386,7 +379,6 @@ mod tests { #[test] fn check() { - // TODO Make sure the response was read correctly let response = b"a1 OK CHECK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); @@ -464,7 +456,6 @@ mod tests { #[test] fn create() { - // TODO Make sure the response was read correctly let response = b"a1 OK CREATE completed\r\n".to_vec(); let mailbox_name = "INBOX"; let command = format!("a1 CREATE {}\r\n", mailbox_name); @@ -476,7 +467,6 @@ mod tests { #[test] fn delete() { - // TODO Make sure the response was read correctly let response = b"a1 OK DELETE completed\r\n".to_vec(); let mailbox_name = "INBOX"; let command = format!("a1 DELETE {}\r\n", mailbox_name); @@ -488,7 +478,6 @@ mod tests { #[test] fn noop() { - // TODO Make sure the response was read correctly let response = b"a1 OK NOOP completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); @@ -498,7 +487,6 @@ mod tests { #[test] fn close() { - // TODO Make sure the response was read correctly let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); let mut client = create_client_with_mock_stream(mock_stream); From a9d59209e31c8c3dd24e3f77195693522b5d6d32 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Fri, 24 Jun 2016 22:36:46 -0400 Subject: [PATCH 56/65] Adding new function to create client with an underlying stream --- src/client.rs | 66 +++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8956215..6eaa98b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,10 +20,7 @@ impl Client { pub fn connect(addr: A) -> Result> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = Client { - stream: stream, - tag: INITIAL_TAG - }; + let mut socket = Client::new(stream); try!(socket.read_greeting()); Ok(socket) @@ -38,10 +35,11 @@ impl Client> { pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { match TcpStream::connect(addr) { Ok(stream) => { - let mut socket = Client { - stream: SslStream::connect(&ssl_context, stream).unwrap(), - tag: INITIAL_TAG + let ssl_stream = match SslStream::connect(&ssl_context, stream) { + Ok(s) => s, + Err(e) => return Err(Error::Ssl(e)) }; + let mut socket = Client::new(ssl_stream); try!(socket.read_greeting()); Ok(socket) @@ -53,6 +51,14 @@ impl Client> { impl Client { + /// Creates a new client with the underlying stream. + pub fn new(stream: T) -> Client { + Client{ + stream: stream, + tag: INITIAL_TAG + } + } + /// Log in to the IMAP server. pub fn login(&mut self, username: & str, password: & str) -> Result<()> { self.run_command_and_check_ok(&format!("LOGIN {} {}", username, password).to_string()) @@ -247,23 +253,15 @@ impl Client { #[cfg(test)] mod tests { use super::*; - use super::INITIAL_TAG; use super::super::mock_stream::MockStream; use super::super::mailbox::Mailbox; - fn create_client_with_mock_stream(mock_stream: MockStream) -> Client { - Client { - stream: mock_stream, - tag: INITIAL_TAG - } - } - #[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 = create_client_with_mock_stream(mock_stream); + 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"); } @@ -272,7 +270,7 @@ mod tests { fn read_greeting() { let greeting = "* OK Dovecot ready.\r\n"; let mock_stream = MockStream::new(greeting.as_bytes().to_vec()); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.read_greeting().unwrap(); } @@ -281,7 +279,7 @@ mod tests { fn readline_err() { // TODO Check the error test let mock_stream = MockStream::new_err(); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.readline().unwrap(); } @@ -289,7 +287,7 @@ mod tests { fn create_command() { let base_command = "CHECK"; let mock_stream = MockStream::new(Vec::new()); - let mut imap_stream = create_client_with_mock_stream(mock_stream); + let mut imap_stream = Client::new(mock_stream); let expected_command = format!("a1 {}\r\n", base_command); let command = imap_stream.create_command(String::from(base_command)); @@ -307,7 +305,7 @@ mod tests { let password = "password"; let command = format!("a1 LOGIN {} {}\r\n", username, password); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.login(username, password).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid login command"); } @@ -317,7 +315,7 @@ 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 = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.logout().unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid logout command"); } @@ -329,7 +327,7 @@ mod tests { let new_mailbox_name = "NEWINBOX"; let command = format!("a1 RENAME {} {}\r\n", current_mailbox_name, new_mailbox_name); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.rename(current_mailbox_name, new_mailbox_name).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid rename command"); } @@ -341,7 +339,7 @@ mod tests { let query = "BODY[]"; let command = format!("a1 FETCH {} {}\r\n", sequence_set, query); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.fetch(sequence_set, query).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid fetch command"); } @@ -352,7 +350,7 @@ mod tests { let mailbox = "INBOX"; let command = format!("a1 SUBSCRIBE {}\r\n", mailbox); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.subscribe(mailbox).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid subscribe command"); } @@ -363,7 +361,7 @@ mod tests { let mailbox = "INBOX"; let command = format!("a1 UNSUBSCRIBE {}\r\n", mailbox); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.unsubscribe(mailbox).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid unsubscribe command"); } @@ -372,7 +370,7 @@ mod tests { fn expunge() { let response = b"a1 OK EXPUNGE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.expunge().unwrap(); assert!(client.stream.written_buf == b"a1 EXPUNGE\r\n".to_vec(), "Invalid expunge command"); } @@ -381,7 +379,7 @@ mod tests { fn check() { let response = b"a1 OK CHECK completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.check().unwrap(); assert!(client.stream.written_buf == b"a1 CHECK\r\n".to_vec(), "Invalid check command"); } @@ -408,7 +406,7 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 EXAMINE {}\r\n", mailbox_name); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); let mailbox = client.examine(mailbox_name).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid examine command"); assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); @@ -436,7 +434,7 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 SELECT {}\r\n", mailbox_name); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); let mailbox = client.select(mailbox_name).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid select command"); assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); @@ -448,7 +446,7 @@ mod tests { a1 OK CAPABILITY completed\r\n".to_vec(); let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); let capabilities = client.capability().unwrap(); assert!(client.stream.written_buf == b"a1 CAPABILITY\r\n".to_vec(), "Invalid capability command"); assert!(capabilities == expected_capabilities, "Unexpected capabilities response"); @@ -460,7 +458,7 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 CREATE {}\r\n", mailbox_name); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.create(mailbox_name).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid create command"); } @@ -471,7 +469,7 @@ mod tests { let mailbox_name = "INBOX"; let command = format!("a1 DELETE {}\r\n", mailbox_name); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.delete(mailbox_name).unwrap(); assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid delete command"); } @@ -480,7 +478,7 @@ mod tests { fn noop() { let response = b"a1 OK NOOP completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.noop().unwrap(); assert!(client.stream.written_buf == b"a1 NOOP\r\n".to_vec(), "Invalid noop command"); } @@ -489,7 +487,7 @@ mod tests { fn close() { let response = b"a1 OK CLOSE completed\r\n".to_vec(); let mock_stream = MockStream::new(response); - let mut client = create_client_with_mock_stream(mock_stream); + let mut client = Client::new(mock_stream); client.close().unwrap(); assert!(client.stream.written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command"); } From 8653a02b8732c20225ecbc87c7a9a27711bca1a7 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Sun, 26 Jun 2016 22:40:15 -0400 Subject: [PATCH 57/65] Adding initial work for authentication --- src/authenticator.rs | 3 +++ src/client.rs | 29 ++++++++++++++++++++++++++++- src/error.rs | 7 +++++-- src/lib.rs | 1 + src/parse.rs | 15 +++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/authenticator.rs diff --git a/src/authenticator.rs b/src/authenticator.rs new file mode 100644 index 0000000..16004a1 --- /dev/null +++ b/src/authenticator.rs @@ -0,0 +1,3 @@ +pub trait Authenticator { + fn process(&self, String) -> String; +} diff --git a/src/client.rs b/src/client.rs index 6eaa98b..0f5e98d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,8 @@ use openssl::ssl::{SslContext, SslStream}; use std::io::{self, Read, Write}; use super::mailbox::Mailbox; -use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine, parse_response}; +use super::authenticator::Authenticator; +use super::parse::{parse_response_ok, parse_capability, parse_select_or_examine, parse_response, parse_authenticate_response}; use super::error::{Error, Result}; static TAG_PREFIX: &'static str = "a"; @@ -59,6 +60,32 @@ impl Client { } } + pub fn authenticate(&mut self, auth_type: &str, authenticator: A) -> Result<()> { + match self.run_command(&format!("AUTHENTICATE {}\r\n", auth_type).to_string()) { + Ok(lines) => { + // TODO test this for the many authentication use cases + let data = match parse_authenticate_response(lines) { + Ok(d) => d, + Err(e) => return Err(e) + }; + let auth_response = authenticator.process(data); + match self.stream.write_all(auth_response.into_bytes().as_slice()) { + Err(e) => return Err(Error::Io(e)), + _ => {} + }; + match self.stream.write(vec![0x0d, 0x0a].as_slice()) { + Err(e) => return Err(Error::Io(e)), + _ => {} + }; + match self.read_response() { + Ok(_) => Ok(()), + Err(e) => return Err(e) + } + }, + Err(e) => Err(e) + } + } + /// Log in to the IMAP server. pub fn login(&mut self, username: & str, password: & str) -> Result<()> { self.run_command_and_check_ok(&format!("LOGIN {} {}", username, password).to_string()) diff --git a/src/error.rs b/src/error.rs index 4d2cd89..40a75a4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -69,7 +69,9 @@ 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) + Capability(Vec), + // Authentication errors. + Authentication(Vec) } impl fmt::Display for ParseError { @@ -84,7 +86,8 @@ 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::Capability(_) => "Unable to parse capability response", + ParseError::Authentication(_) => "Unable to parse authentication response" } } diff --git a/src/lib.rs b/src/lib.rs index affa530..18ef667 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ extern crate openssl; extern crate regex; +pub mod authenticator; pub mod client; pub mod error; pub mod mailbox; diff --git a/src/parse.rs b/src/parse.rs index 0fa7651..38e2d34 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,6 +3,21 @@ use regex::Regex; use super::mailbox::Mailbox; use super::error::{Error, ParseError, Result}; +pub fn parse_authenticate_response(lines: Vec) -> Result { + let authenticate_regex = Regex::new("^+ (.*)\r\n").unwrap(); + let last_line = match lines.last() { + Some(l) => l, + None => return Err(Error::Parse(ParseError::Authentication(lines.clone()))) + }; + + for cap in authenticate_regex.captures_iter(last_line) { + let data = cap.at(1).unwrap_or(""); + return Ok(String::from(data)); + } + + Err(Error::Parse(ParseError::Authentication(lines.clone()))) +} + pub fn parse_capability(lines: Vec) -> Result> { let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); From 8a1162ada4cddd43c9c8a223459f4c5469ef442c Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 28 Jun 2016 21:48:25 -0400 Subject: [PATCH 58/65] Initial testing for authentication --- Cargo.toml | 3 +++ examples/gmail_oauth2.rs | 47 ++++++++++++++++++++++++++++++++++++++++ src/client.rs | 43 +++++++++++++++++++++++------------- src/error.rs | 2 +- src/parse.rs | 12 ++++------ 5 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 examples/gmail_oauth2.rs diff --git a/Cargo.toml b/Cargo.toml index 3a2a0f0..75584b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,6 @@ path = "src/lib.rs" [dependencies] openssl = "0.7.13" regex = "0.1.71" + +[dev-dependencies] +base64 = "0.2.0" diff --git a/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs new file mode 100644 index 0000000..381b4f0 --- /dev/null +++ b/examples/gmail_oauth2.rs @@ -0,0 +1,47 @@ +extern crate imap; +extern crate openssl; +extern crate base64; + +use openssl::ssl::{SslContext, SslMethod}; +use base64::{encode, decode}; +use imap::client::Client; +use imap::authenticator::Authenticator; + +struct GmailOAuth2 { + user: String, + access_token: String +} + +impl Authenticator for GmailOAuth2 { + fn process(&self, data: String) -> String { + String::from("dXNlcj1tYXR0bWNjb3kxMTBAZ21haWwuY29tAWF1dGg9QmVhcmVyIHlhMjkuQ2k4UUEzQ1Y5SW1OQ0Z1NDNpbkZRcngtSUR0cjVFSkZHNXdEM1IySzBXdTdiM1dzVG1Md") + } +} + +fn main() { + let mut gmail_auth = GmailOAuth2{ + user: String::from("email@gmail.com"), + access_token: String::from("") + }; + let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap(); + + imap_socket.authenticate("XOAUTH2", gmail_auth).unwrap(); + + match imap_socket.select("INBOX") { + Ok(mailbox) => { + println!("{}", mailbox); + }, + Err(e) => println!("Error selecting INBOX: {}", e) + }; + + match imap_socket.fetch("2", "body[text]") { + Ok(lines) => { + for line in lines.iter() { + print!("{}", line); + } + }, + Err(e) => println!("Error Fetching email 2: {}", e) + }; + + imap_socket.logout().unwrap(); +} diff --git a/src/client.rs b/src/client.rs index 0f5e98d..4954172 100644 --- a/src/client.rs +++ b/src/client.rs @@ -61,14 +61,20 @@ impl Client { } pub fn authenticate(&mut self, auth_type: &str, authenticator: A) -> Result<()> { - match self.run_command(&format!("AUTHENTICATE {}\r\n", auth_type).to_string()) { - Ok(lines) => { + match self.run_command(&format!("AUTHENTICATE {}", auth_type).to_string()) { + Ok(_) => { + let line = match self.readline() { + Ok(l) => l, + Err(e) => return Err(e) + }; // TODO test this for the many authentication use cases - let data = match parse_authenticate_response(lines) { + let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { Ok(d) => d, Err(e) => return Err(e) }; + println!("Done parsing authenticating response"); let auth_response = authenticator.process(data); + println!("Writing: {}", auth_response.clone()); match self.stream.write_all(auth_response.into_bytes().as_slice()) { Err(e) => return Err(Error::Io(e)), _ => {} @@ -79,7 +85,7 @@ impl Client { }; match self.read_response() { Ok(_) => Ok(()), - Err(e) => return Err(e) + Err(e) => Err(e) } }, Err(e) => Err(e) @@ -93,7 +99,7 @@ impl Client { /// Selects a mailbox pub fn select(&mut self, mailbox_name: &str) -> Result { - match self.run_command(&format!("SELECT {}", mailbox_name).to_string()) { + match self.run_command_and_read_response(&format!("SELECT {}", mailbox_name).to_string()) { Ok(lines) => parse_select_or_examine(lines), Err(e) => Err(e) } @@ -101,7 +107,7 @@ impl Client { /// Examine is identical to Select, but the selected mailbox is identified as read-only pub fn examine(&mut self, mailbox_name: &str) -> Result { - match self.run_command(&format!("EXAMINE {}", mailbox_name).to_string()) { + match self.run_command_and_read_response(&format!("EXAMINE {}", mailbox_name).to_string()) { Ok(lines) => parse_select_or_examine(lines), Err(e) => Err(e) } @@ -109,7 +115,7 @@ impl Client { /// Fetch retreives data associated with a message in the mailbox. pub fn fetch(&mut self, sequence_set: &str, query: &str) -> Result> { - self.run_command(&format!("FETCH {} {}", sequence_set, query).to_string()) + self.run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query).to_string()) } /// Noop always succeeds, and it does nothing. @@ -151,7 +157,7 @@ impl Client { /// Capability requests a listing of capabilities that the server supports. pub fn capability(&mut self) -> Result> { - match self.run_command(&format!("CAPABILITY").to_string()) { + match self.run_command_and_read_response(&format!("CAPABILITY").to_string()) { Ok(lines) => parse_capability(lines), Err(e) => Err(e) } @@ -198,7 +204,7 @@ impl Client { /// Runs a command and checks if it returns OK. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { - match self.run_command(command) { + match self.run_command_and_read_response(command) { Ok(lines) => parse_response_ok(lines), Err(e) => Err(e) } @@ -206,22 +212,27 @@ impl Client { // Run a command and parse the status response. pub fn run_command_and_parse(&mut self, command: &str) -> Result> { - match self.run_command(command) { + match self.run_command_and_read_response(command) { Ok(lines) => parse_response(lines), Err(e) => Err(e) } } /// Runs any command passed to it. - pub fn run_command(&mut self, untagged_command: &str) -> Result> { + pub fn run_command(&mut self, untagged_command: &str) -> Result<()> { let command = self.create_command(untagged_command.to_string()); match self.stream.write_fmt(format_args!("{}", &*command)) { - Ok(_) => (), - Err(_) => return Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to write"))), - }; + Ok(_) => Ok(()), + Err(_) => Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to write"))), + } + } - self.read_response() + pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result> { + match self.run_command(untagged_command) { + Ok(_) => self.read_response(), + Err(e) => Err(e) + } } fn read_response(&mut self) -> Result> { @@ -265,8 +276,10 @@ impl Client { Ok(_) => {}, Err(_) => return Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to read line"))), } + print!("{}", String::from_utf8_lossy(byte_buffer)); line_buffer.push(byte_buffer[0]); } + println!("{}", String::from_utf8(line_buffer.clone()).unwrap()); Ok(line_buffer) } diff --git a/src/error.rs b/src/error.rs index 40a75a4..d9e8e84 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,7 +71,7 @@ pub enum ParseError { // Error parsing the cabability response. Capability(Vec), // Authentication errors. - Authentication(Vec) + Authentication(String) } impl fmt::Display for ParseError { diff --git a/src/parse.rs b/src/parse.rs index 38e2d34..b421e36 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,19 +3,15 @@ use regex::Regex; use super::mailbox::Mailbox; use super::error::{Error, ParseError, Result}; -pub fn parse_authenticate_response(lines: Vec) -> Result { - let authenticate_regex = Regex::new("^+ (.*)\r\n").unwrap(); - let last_line = match lines.last() { - Some(l) => l, - None => return Err(Error::Parse(ParseError::Authentication(lines.clone()))) - }; +pub fn parse_authenticate_response(line: String) -> Result { + let authenticate_regex = Regex::new("^+(.*)\r\n").unwrap(); - for cap in authenticate_regex.captures_iter(last_line) { + for cap in authenticate_regex.captures_iter(line.as_str()) { let data = cap.at(1).unwrap_or(""); return Ok(String::from(data)); } - Err(Error::Parse(ParseError::Authentication(lines.clone()))) + Err(Error::Parse(ParseError::Authentication(line))) } pub fn parse_capability(lines: Vec) -> Result> { From 8d39bfd343685ca8a3e7a3c2dd26f89b4f8675eb Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Tue, 28 Jun 2016 22:29:55 -0400 Subject: [PATCH 59/65] Hardening the oauth2 example for gmail --- examples/gmail_oauth2.rs | 11 ++++---- src/authenticator.rs | 1 + src/client.rs | 59 ++++++++++++++++++++++++---------------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs index 381b4f0..5814e58 100644 --- a/examples/gmail_oauth2.rs +++ b/examples/gmail_oauth2.rs @@ -3,7 +3,7 @@ extern crate openssl; extern crate base64; use openssl::ssl::{SslContext, SslMethod}; -use base64::{encode, decode}; +use base64::{encode}; use imap::client::Client; use imap::authenticator::Authenticator; @@ -13,15 +13,16 @@ struct GmailOAuth2 { } impl Authenticator for GmailOAuth2 { + #[allow(unused_variables)] fn process(&self, data: String) -> String { - String::from("dXNlcj1tYXR0bWNjb3kxMTBAZ21haWwuY29tAWF1dGg9QmVhcmVyIHlhMjkuQ2k4UUEzQ1Y5SW1OQ0Z1NDNpbkZRcngtSUR0cjVFSkZHNXdEM1IySzBXdTdiM1dzVG1Md") + encode(format!("user={}\x01auth=Bearer {}\x01\x01", self.user, self.access_token).as_bytes()) } } fn main() { - let mut gmail_auth = GmailOAuth2{ - user: String::from("email@gmail.com"), - access_token: String::from("") + let gmail_auth = GmailOAuth2{ + user: String::from("sombody@gmail.com"), + access_token: String::from("") }; let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap(); diff --git a/src/authenticator.rs b/src/authenticator.rs index 16004a1..825ec9a 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,3 +1,4 @@ +/// This will allow plugable authentication mechanisms. pub trait Authenticator { fn process(&self, String) -> String; } diff --git a/src/client.rs b/src/client.rs index 4954172..9a8f8e0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -63,29 +63,41 @@ impl Client { pub fn authenticate(&mut self, auth_type: &str, authenticator: A) -> Result<()> { match self.run_command(&format!("AUTHENTICATE {}", auth_type).to_string()) { Ok(_) => { - let line = match self.readline() { - Ok(l) => l, - Err(e) => return Err(e) - }; - // TODO test this for the many authentication use cases - let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { - Ok(d) => d, - Err(e) => return Err(e) - }; - println!("Done parsing authenticating response"); - let auth_response = authenticator.process(data); - println!("Writing: {}", auth_response.clone()); - match self.stream.write_all(auth_response.into_bytes().as_slice()) { - Err(e) => return Err(Error::Io(e)), - _ => {} - }; - match self.stream.write(vec![0x0d, 0x0a].as_slice()) { - Err(e) => return Err(Error::Io(e)), - _ => {} - }; - match self.read_response() { - Ok(_) => Ok(()), - Err(e) => Err(e) + loop { + let line = match self.readline() { + Ok(l) => l, + Err(e) => return Err(e) + }; + if line.starts_with(b"+") { + let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { + Ok(d) => d, + Err(e) => return Err(e) + }; + let auth_response = authenticator.process(data); + match self.stream.write_all(auth_response.into_bytes().as_slice()) { + Err(e) => return Err(Error::Io(e)), + _ => {} + }; + match self.stream.write(vec![0x0d, 0x0a].as_slice()) { + Err(e) => return Err(Error::Io(e)), + _ => {} + }; + } else if line.starts_with(format!("{}{} ", TAG_PREFIX, self.tag).as_bytes()) { + match parse_response(vec![String::from_utf8(line).unwrap()]) { + Ok(_) => return Ok(()), + Err(e) => return Err(e) + }; + } else { + let mut lines = match self.read_response() { + Ok(l) => l, + Err(e) => return Err(e) + }; + lines.insert(0, String::from_utf8(line).unwrap()); + match parse_response(lines.clone()) { + Ok(_) => return Ok(()), + Err(e) => return Err(e) + }; + } } }, Err(e) => Err(e) @@ -279,7 +291,6 @@ impl Client { print!("{}", String::from_utf8_lossy(byte_buffer)); line_buffer.push(byte_buffer[0]); } - println!("{}", String::from_utf8(line_buffer.clone()).unwrap()); Ok(line_buffer) } From 43f4737b85c3f07d33dfad55bbf137da9d9df8e6 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 29 Jun 2016 16:45:36 -0400 Subject: [PATCH 60/65] Cleaning up some of the authenticator code --- src/client.rs | 81 +++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/src/client.rs b/src/client.rs index 9a8f8e0..63858b9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -60,50 +60,55 @@ impl Client { } } + /// Authenticate will authenticate with the server, using the authenticator given. pub fn authenticate(&mut self, auth_type: &str, authenticator: A) -> Result<()> { match self.run_command(&format!("AUTHENTICATE {}", auth_type).to_string()) { - Ok(_) => { - loop { - let line = match self.readline() { - Ok(l) => l, - Err(e) => return Err(e) - }; - if line.starts_with(b"+") { - let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { - Ok(d) => d, - Err(e) => return Err(e) - }; - let auth_response = authenticator.process(data); - match self.stream.write_all(auth_response.into_bytes().as_slice()) { - Err(e) => return Err(Error::Io(e)), - _ => {} - }; - match self.stream.write(vec![0x0d, 0x0a].as_slice()) { - Err(e) => return Err(Error::Io(e)), - _ => {} - }; - } else if line.starts_with(format!("{}{} ", TAG_PREFIX, self.tag).as_bytes()) { - match parse_response(vec![String::from_utf8(line).unwrap()]) { - Ok(_) => return Ok(()), - Err(e) => return Err(e) - }; - } else { - let mut lines = match self.read_response() { - Ok(l) => l, - Err(e) => return Err(e) - }; - lines.insert(0, String::from_utf8(line).unwrap()); - match parse_response(lines.clone()) { - Ok(_) => return Ok(()), - Err(e) => return Err(e) - }; - } - } - }, + Ok(_) => self.do_auth_handshake(authenticator), Err(e) => Err(e) } } + /// This func does the handshake process once the authenticate command is made. + fn do_auth_handshake(&mut self, authenticator: A) -> Result<()> { + // TODO Clean up this code + loop { + let line = match self.readline() { + Ok(l) => l, + Err(e) => return Err(e) + }; + if line.starts_with(b"+") { + let data = match parse_authenticate_response(String::from_utf8(line).unwrap()) { + Ok(d) => d, + Err(e) => return Err(e) + }; + let auth_response = authenticator.process(data); + match self.stream.write_all(auth_response.into_bytes().as_slice()) { + Err(e) => return Err(Error::Io(e)), + _ => {} + }; + match self.stream.write(vec![0x0d, 0x0a].as_slice()) { + Err(e) => return Err(Error::Io(e)), + _ => {} + }; + } else if line.starts_with(format!("{}{} ", TAG_PREFIX, self.tag).as_bytes()) { + match parse_response(vec![String::from_utf8(line).unwrap()]) { + Ok(_) => return Ok(()), + Err(e) => return Err(e) + }; + } else { + let mut lines = match self.read_response() { + Ok(l) => l, + Err(e) => return Err(e) + }; + lines.insert(0, String::from_utf8(line).unwrap()); + match parse_response(lines.clone()) { + Ok(_) => return Ok(()), + Err(e) => return Err(e) + }; + } + } + } + /// Log in to the IMAP server. pub fn login(&mut self, username: & str, password: & str) -> Result<()> { self.run_command_and_check_ok(&format!("LOGIN {} {}", username, password).to_string()) From 25633dee98ecf89db9c41d84832e8c0cea512a81 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 29 Jun 2016 17:01:48 -0400 Subject: [PATCH 61/65] Adding option to upgrade tcp connection to ssl connection --- src/client.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client.rs b/src/client.rs index 63858b9..24e857e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -29,6 +29,19 @@ impl Client { Err(e) => Err(Error::Io(e)) } } + + /// This will upgrade a regular TCP connection to use SSL. + pub fn secure(mut self, ssl_context: SslContext) -> Result>> { + // TODO This needs to be tested + match self.run_command_and_check_ok("STARTTLS") { + Err(e) => return Err(e), + _ => {} + }; + match SslStream::connect(&ssl_context, self.stream) { + Ok(s) => Ok(Client::new(s)), + Err(e) => Err(Error::Ssl(e)) + } + } } impl Client> { From 4b8a2946894e621e8d2027900eb2ae76084f4214 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 29 Jun 2016 18:48:50 -0400 Subject: [PATCH 62/65] Fixing examples README.md --- examples/README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/README.md b/examples/README.md index afa9711..cf1ce7b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,9 +3,6 @@ Examples This directory examples of working with the IMAP client. -Current Examples: +Examples: * basic - This is a very basic example of using the client. - -Planned Examples: - * error_handling - This will show usage of handling errors that the client can return. - * gmail_oauth2 - This will be an example using oauth2 for logging into gmail as a secure appplication. + * gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication. From b593d943ef377f927d9196fc4c426544dc2f355e Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 29 Jun 2016 18:53:23 -0400 Subject: [PATCH 63/65] Updating README.md and the basic example --- README.md | 75 ++++++++++++++++++----------------------------- examples/basic.rs | 2 ++ 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index aeab44f..168b306 100644 --- a/README.md +++ b/README.md @@ -2,70 +2,53 @@ rust-imap ================ IMAP Client for Rust -This client has SSL support. SSL is configured using an SSLContext that is passed into the connect method of a IMAPStream. If no SSL -support is wanted just pass in None. The library rust-openssl is used to support SSL for this project. - - [![Build Status](https://travis-ci.org/mattnenterprise/rust-imap.svg)](https://travis-ci.org/mattnenterprise/rust-imap) [![crates.io](http://meritbadge.herokuapp.com/imap)](https://crates.io/crates/imap) [Documentation](http://mattnenterprise.github.io/rust-imap) -### Installation - -Add imap via your `Cargo.toml`: -```toml -[dependencies] -imap = "*" -``` - ### Usage +Here is a basic example of using the client. See the examples directory for more examples. ```rust extern crate imap; extern crate openssl; use openssl::ssl::{SslContext, SslMethod}; -use imap::client::IMAPStream; -use imap::client::IMAPMailbox; +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 fn main() { - let mut imap_socket = match IMAPStream::connect("imap.gmail.com", 993, Some(SslContext::new(SslMethod::Sslv23).unwrap())) { - Ok(s) => s, - Err(e) => panic!("{}", e) - }; + let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap(); - if let Err(e) = imap_socket.login("username", "password") { - println!("Error: {}", e) - }; + imap_socket.login("username", "password").unwrap(); - match imap_socket.capability() { - Ok(capabilities) => { - for capability in capabilities.iter() { - println!("{}", capability); - } - }, - Err(_) => println!("Error retreiving capabilities") - }; + match imap_socket.capability() { + Ok(capabilities) => { + for capability in capabilities.iter() { + println!("{}", capability); + } + }, + Err(e) => println!("Error parsing capability: {}", e) + }; - match imap_socket.select("INBOX") { - Ok(IMAPMailbox{flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity}) => { - println!("flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?}, uid_next: {:?}, uid_validity: {:?}", flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity); - }, - Err(_) => println!("Error selecting INBOX") - }; + match imap_socket.select("INBOX") { + Ok(mailbox) => { + println!("{}", mailbox); + }, + Err(e) => println!("Error selecting INBOX: {}", e) + }; - match imap_socket.fetch("2", "body[text]") { - Ok(lines) => { - for line in lines.iter() { - print!("{}", line); - } - }, - Err(_) => println!("Error Fetching email 2") - }; + match imap_socket.fetch("2", "body[text]") { + Ok(lines) => { + for line in lines.iter() { + print!("{}", line); + } + }, + Err(e) => println!("Error Fetching email 2: {}", e) + }; - if let Err(e) = imap_socket.logout() { - println!("Error: {}", e) - }; + imap_socket.logout().unwrap(); } ``` diff --git a/examples/basic.rs b/examples/basic.rs index 0f7f60f..97474b1 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -4,6 +4,8 @@ extern crate openssl; use openssl::ssl::{SslContext, SslMethod}; 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 fn main() { let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap(); From 2311c19435bafb33e5d8518d1d9e2b66a7a02d90 Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 29 Jun 2016 18:56:13 -0400 Subject: [PATCH 64/65] Adding coverage status badge to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 168b306..adb8214 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ IMAP Client for Rust [![Build Status](https://travis-ci.org/mattnenterprise/rust-imap.svg)](https://travis-ci.org/mattnenterprise/rust-imap) [![crates.io](http://meritbadge.herokuapp.com/imap)](https://crates.io/crates/imap) +[![Coverage Status](https://coveralls.io/repos/github/mattnenterprise/rust-imap/badge.svg?branch=master)](https://coveralls.io/github/mattnenterprise/rust-imap?branch=master) [Documentation](http://mattnenterprise.github.io/rust-imap) From c8f4437ee00215065fcec148f328df19b2e3354a Mon Sep 17 00:00:00 2001 From: Matt McCoy Date: Wed, 29 Jun 2016 18:57:46 -0400 Subject: [PATCH 65/65] Fixing the example README.md --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index cf1ce7b..7de0c82 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,7 @@ Examples ======== -This directory examples of working with the IMAP client. +This directory contains examples of working with the IMAP client. Examples: * basic - This is a very basic example of using the client.