diff --git a/.travis.yml b/.travis.yml index 3611d5c..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 --no-sudo +- travis-cargo coveralls --exclude-pattern="/target" --no-sudo diff --git a/Cargo.toml b/Cargo.toml index 1883590..75584b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,5 @@ path = "src/lib.rs" openssl = "0.7.13" regex = "0.1.71" -[[bin]] -name = "example" -path = "example.rs" +[dev-dependencies] +base64 = "0.2.0" diff --git a/README.md b/README.md index aeab44f..adb8214 100644 --- a/README.md +++ b/README.md @@ -2,70 +2,54 @@ 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) +[![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) -### 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/example.rs b/example.rs deleted file mode 100644 index 894af51..0000000 --- a/example.rs +++ /dev/null @@ -1,46 +0,0 @@ -extern crate imap; -extern crate openssl; - -use openssl::ssl::{SslContext, SslMethod}; -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())) { - Ok(s) => s, - Err(e) => panic!("{}", e) - }; - - if let Err(e) = imap_socket.login("username", "password") { - println!("Error: {}", e) - }; - - match imap_socket.capability() { - Ok(capabilities) => { - for capability in capabilities.iter() { - println!("{}", capability); - } - }, - Err(_) => println!("Error retreiving capabilities") - }; - - 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.fetch("2", "body[text]") { - Ok(lines) => { - for line in lines.iter() { - print!("{}", line); - } - }, - Err(_) => println!("Error Fetching email 2") - }; - - if let Err(e) = imap_socket.logout() { - println!("Error: {}", e) - }; -} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..7de0c82 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +Examples +======== + +This directory contains examples of working with the IMAP client. + +Examples: + * basic - This is a very basic example of using the client. + * gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication. diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..97474b1 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,40 @@ +extern crate imap; +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(); + + imap_socket.login("username", "password").unwrap(); + + 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(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/examples/gmail_oauth2.rs b/examples/gmail_oauth2.rs new file mode 100644 index 0000000..5814e58 --- /dev/null +++ b/examples/gmail_oauth2.rs @@ -0,0 +1,48 @@ +extern crate imap; +extern crate openssl; +extern crate base64; + +use openssl::ssl::{SslContext, SslMethod}; +use base64::{encode}; +use imap::client::Client; +use imap::authenticator::Authenticator; + +struct GmailOAuth2 { + user: String, + access_token: String +} + +impl Authenticator for GmailOAuth2 { + #[allow(unused_variables)] + fn process(&self, data: String) -> String { + encode(format!("user={}\x01auth=Bearer {}\x01\x01", self.user, self.access_token).as_bytes()) + } +} + +fn main() { + 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(); + + 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/authenticator.rs b/src/authenticator.rs new file mode 100644 index 0000000..825ec9a --- /dev/null +++ b/src/authenticator.rs @@ -0,0 +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 27e45dd..24e857e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,151 +1,151 @@ use std::net::{TcpStream, ToSocketAddrs}; use openssl::ssl::{SslContext, SslStream}; -use std::io::{Error, ErrorKind, Read, Result, Write}; -use regex::Regex; +use std::io::{self, Read, Write}; -enum IMAPStreamTypes { - Basic(TcpStream), - Ssl(SslStream) -} +use super::mailbox::Mailbox; +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"; +const INITIAL_TAG: u32 = 0; /// 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 Client { + stream: T, + tag: u32 } -pub struct IMAPMailbox { - 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 IMAPStream { - /// Creates an IMAP Stream. - pub fn connect(addr: A, ssl_context: Option) -> Result { +impl Client { + /// Creates a new client. + 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 = Client::new(stream); try!(socket.read_greeting()); Ok(socket) }, + 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> { + /// Creates a client with an SSL wrapper. + pub fn secure_connect(addr: A, ssl_context: SslContext) -> Result>> { + match TcpStream::connect(addr) { + Ok(stream) => { + 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) + }, + Err(e) => Err(Error::Io(e)) + } + } +} + +impl Client { + + /// Creates a new client with the underlying stream. + pub fn new(stream: T) -> Client { + Client{ + stream: stream, + tag: INITIAL_TAG + } + } + + /// 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(_) => 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()) } /// 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), + pub fn select(&mut self, mailbox_name: &str) -> Result { + match self.run_command_and_read_response(&format!("SELECT {}", mailbox_name).to_string()) { + Ok(lines) => parse_select_or_examine(lines), Err(e) => Err(e) } } - fn parse_select_or_examine(lines: Vec) -> Result { - let exists_regex = match Regex::new(r"^\* (\d+) EXISTS\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - let recent_regex = match Regex::new(r"^\* (\d+) RECENT\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - let flags_regex = match Regex::new(r"^\* FLAGS (.+)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - let unseen_regex = match Regex::new(r"^OK \[UNSEEN (\d+)\](.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - let uid_validity_regex = match Regex::new(r"^OK \[UIDVALIDITY (\d+)\](.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - let uid_next_regex = match Regex::new(r"^OK \[UIDNEXT (\d+)\](.*)\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - let permanent_flags_regex = match Regex::new(r"^OK \[PERMANENTFLAGS (.+)\]\r\n") { - Ok(re) => re, - Err(err) => panic!("{}", err), - }; - - //Check Ok - match IMAPStream::parse_response_ok(lines.clone()) { - Ok(_) => (), - 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 - }; - - 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()); - } - } - - return 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) => IMAPStream::parse_select_or_examine(lines), + pub fn examine(&mut self, mailbox_name: &str) -> Result { + match self.run_command_and_read_response(&format!("EXAMINE {}", mailbox_name).to_string()) { + Ok(lines) => parse_select_or_examine(lines), Err(e) => Err(e) } } /// 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. @@ -187,39 +187,16 @@ 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), + match self.run_command_and_read_response(&format!("CAPABILITY").to_string()) { + Ok(lines) => parse_capability(lines), Err(e) => Err(e) } } - fn parse_capability(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()) { - 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<()> { - self.run_command_and_check_ok("CHECK") + self.run_command_and_check_ok("EXPUNGE") } /// Check requests a checkpoint of the currently selected mailbox. @@ -238,88 +215,71 @@ impl IMAPStream { 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> { + 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> { + 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. pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { - match self.run_command(command) { - Ok(lines) => IMAPStream::parse_response_ok(lines), + match self.run_command_and_read_response(command) { + Ok(lines) => parse_response_ok(lines), Err(e) => Err(e) } } - pub fn run_command(&mut self, untagged_command: &str) -> Result> { + // Run a command and parse the status response. + pub fn run_command_and_parse(&mut self, command: &str) -> Result> { + 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<()> { let command = self.create_command(untagged_command.to_string()); - match self.write_str(&*command) { - Ok(_) => (), - 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")), - }; - - self.tag += 1; - - return ret; - } - - fn parse_response_ok(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 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 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)), + match self.stream.write_fmt(format_args!("{}", &*command)) { + Ok(_) => Ok(()), + Err(_) => Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to write"))), } } - 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), + 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> { - //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) { - 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) } } @@ -327,6 +287,13 @@ impl IMAPStream { } 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 @@ -335,24 +302,262 @@ 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")), + 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]); } - - Ok(()) + Ok(line_buffer) } fn create_command(&mut self, command: String) -> String { - let command = format!("{}{} {}\r\n", self.tag_prefix, self.tag, command); + self.tag += 1; + 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()); +#[cfg(test)] +mod tests { + use super::*; + use super::super::mock_stream::MockStream; + use super::super::mailbox::Mailbox; + + #[test] + fn read_response() { + let response = "a0 OK Logged in.\r\n"; + let expected_response: Vec = vec![response.to_string()]; + let mock_stream = MockStream::new(response.as_bytes().to_vec()); + let mut client = Client::new(mock_stream); + let actual_response = client.read_response().unwrap(); + assert!(expected_response == actual_response, "expected response doesn't equal actual"); + } + + #[test] + fn read_greeting() { + let greeting = "* OK Dovecot ready.\r\n"; + let mock_stream = MockStream::new(greeting.as_bytes().to_vec()); + let mut client = Client::new(mock_stream); + client.read_greeting().unwrap(); + } + + #[test] + #[should_panic] + fn readline_err() { + // TODO Check the error test + let mock_stream = MockStream::new_err(); + let mut client = Client::new(mock_stream); + client.readline().unwrap(); + } + + #[test] + fn create_command() { + let base_command = "CHECK"; + let mock_stream = MockStream::new(Vec::new()); + 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)); + 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 login() { + 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 = Client::new(mock_stream); + client.login(username, password).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid login command"); + } + + #[test] + fn logout() { + let response = b"a1 OK Logout completed.\r\n".to_vec(); + let command = format!("a1 LOGOUT\r\n"); + let mock_stream = MockStream::new(response); + let mut client = Client::new(mock_stream); + client.logout().unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid logout command"); + } + + #[test] + fn rename() { + 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 = 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"); + } + + #[test] + fn fetch() { + 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 = Client::new(mock_stream); + client.fetch(sequence_set, query).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid fetch command"); + } + + #[test] + fn subscribe() { + 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 = Client::new(mock_stream); + client.subscribe(mailbox).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid subscribe command"); + } + + #[test] + fn unsubscribe() { + 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 = Client::new(mock_stream); + client.unsubscribe(mailbox).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid unsubscribe command"); + } + + #[test] + fn expunge() { + let response = b"a1 OK EXPUNGE completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = Client::new(mock_stream); + client.expunge().unwrap(); + assert!(client.stream.written_buf == b"a1 EXPUNGE\r\n".to_vec(), "Invalid expunge command"); + } + + #[test] + fn check() { + let response = b"a1 OK CHECK completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = Client::new(mock_stream); + client.check().unwrap(); + 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: Some(String::from("()")), + 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 = 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"); + } + + #[test] + fn select() { + let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\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\ + * 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: Some(String::from("(\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)")), + 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 = 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"); + } + + #[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 = 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"); + } + + #[test] + fn create() { + 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 = Client::new(mock_stream); + client.create(mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid create command"); + } + + #[test] + fn delete() { + 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 = Client::new(mock_stream); + client.delete(mailbox_name).unwrap(); + assert!(client.stream.written_buf == command.as_bytes().to_vec(), "Invalid delete command"); + } + + #[test] + fn noop() { + let response = b"a1 OK NOOP completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = Client::new(mock_stream); + client.noop().unwrap(); + assert!(client.stream.written_buf == b"a1 NOOP\r\n".to_vec(), "Invalid noop command"); + } + + #[test] + fn close() { + let response = b"a1 OK CLOSE completed\r\n".to_vec(); + let mock_stream = MockStream::new(response); + let mut client = Client::new(mock_stream); + client.close().unwrap(); + assert!(client.stream.written_buf == b"a1 CLOSE\r\n".to_vec(), "Invalid close command"); + } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d9e8e84 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,99 @@ +use std::io::Error as IoError; +use std::result; +use std::fmt; +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 { + /// 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), + // Error parsing a server response. + Parse(ParseError) +} + +impl From for Error { + fn from(err: IoError) -> 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::Parse(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, + } + } +} + +#[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), + // Authentication errors. + Authentication(String) +} + +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", + ParseError::Authentication(_) => "Unable to parse authentication response" + } + } + + fn cause(&self) -> Option<&StdError> { + match *self { + _ => None + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 372205b..18ef667 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,4 +6,12 @@ extern crate openssl; extern crate regex; +pub mod authenticator; pub mod client; +pub mod error; +pub mod mailbox; + +mod parse; + +#[cfg(test)] +mod mock_stream; diff --git a/src/mailbox.rs b/src/mailbox.rs new file mode 100644 index 0000000..876bf85 --- /dev/null +++ b/src/mailbox.rs @@ -0,0 +1,32 @@ +use std::fmt; + +#[derive(Eq,PartialEq)] +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 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) + } +} diff --git a/src/mock_stream.rs b/src/mock_stream.rs new file mode 100644 index 0000000..94c28e2 --- /dev/null +++ b/src/mock_stream.rs @@ -0,0 +1,67 @@ +use std::io::{Read, Result, Write, Error, ErrorKind}; + +pub struct MockStream { + read_buf: Vec, + read_pos: usize, + pub written_buf: Vec, + err_on_read: bool +} + +impl MockStream { + pub fn new(read_buf: Vec) -> MockStream { + MockStream{ + read_buf: read_buf, + read_pos: 0, + 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")) + } + 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 + } +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..b421e36 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,155 @@ +use regex::Regex; + +use super::mailbox::Mailbox; +use super::error::{Error, ParseError, Result}; + +pub fn parse_authenticate_response(line: String) -> Result { + let authenticate_regex = Regex::new("^+(.*)\r\n").unwrap(); + + 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(line))) +} + +pub fn parse_capability(lines: Vec) -> Result> { + let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); + + //Check Ok + match parse_response_ok(lines.clone()) { + Ok(_) => (), + Err(e) => return Err(e) + }; + + 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::Parse(ParseError::Capability(lines))) +} + +pub fn parse_response_ok(lines: Vec) -> Result<()> { + match parse_response(lines) { + Ok(_) => Ok(()), + Err(e) => return Err(e) + } +} + +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(""); + match response_type { + "OK" => return Ok(lines.clone()), + "BAD" => return Err(Error::BadResponse(lines.clone())), + "NO" => return Err(Error::NoResponse(lines.clone())), + _ => {} + } + } + + Err(Error::Parse(ParseError::StatusResponse(lines.clone()))) +} + +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) +} + +#[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(); + } + + #[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(); + } +}