Merge pull request #14 from mattnenterprise/generic-client

Generic client and other multiple additions
This commit is contained in:
Matt McCoy 2016-06-29 19:08:02 -04:00 committed by GitHub
commit a716c34d83
14 changed files with 911 additions and 308 deletions

View file

@ -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

View file

@ -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"

View file

@ -2,41 +2,27 @@ 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) => {
@ -44,14 +30,14 @@ fn main() {
println!("{}", capability);
}
},
Err(_) => println!("Error retreiving capabilities")
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);
Ok(mailbox) => {
println!("{}", mailbox);
},
Err(_) => println!("Error selecting INBOX")
Err(e) => println!("Error selecting INBOX: {}", e)
};
match imap_socket.fetch("2", "body[text]") {
@ -60,12 +46,10 @@ fn main() {
print!("{}", line);
}
},
Err(_) => println!("Error Fetching email 2")
Err(e) => println!("Error Fetching email 2: {}", e)
};
if let Err(e) = imap_socket.logout() {
println!("Error: {}", e)
};
imap_socket.logout().unwrap();
}
```

View file

@ -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)
};
}

8
examples/README.md Normal file
View file

@ -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.

40
examples/basic.rs Normal file
View file

@ -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();
}

48
examples/gmail_oauth2.rs Normal file
View file

@ -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("<access_token>")
};
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();
}

4
src/authenticator.rs Normal file
View file

@ -0,0 +1,4 @@
/// This will allow plugable authentication mechanisms.
pub trait Authenticator {
fn process(&self, String) -> String;
}

View file

@ -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<TcpStream>)
}
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<T> {
stream: T,
tag: u32
}
pub struct IMAPMailbox {
pub flags: String,
pub exists: u32,
pub recent: u32,
pub unseen: Option<u32>,
pub permanent_flags: Option<String>,
pub uid_next: Option<u32>,
pub uid_validity: Option<u32>
}
impl IMAPStream {
/// Creates an IMAP Stream.
pub fn connect<A: ToSocketAddrs>(addr: A, ssl_context: Option<SslContext>) -> Result<IMAPStream> {
impl Client<TcpStream> {
/// Creates a new client.
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> {
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<Client<SslStream<TcpStream>>> {
// 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<SslStream<TcpStream>> {
/// Creates a client with an SSL wrapper.
pub fn secure_connect<A: ToSocketAddrs>(addr: A, ssl_context: SslContext) -> Result<Client<SslStream<TcpStream>>> {
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<T: Read+Write> Client<T> {
/// Creates a new client with the underlying stream.
pub fn new(stream: T) -> Client<T> {
Client{
stream: stream,
tag: INITIAL_TAG
}
}
/// Authenticate will authenticate with the server, using the authenticator given.
pub fn authenticate<A: Authenticator>(&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<A: Authenticator>(&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<IMAPMailbox> {
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<Mailbox> {
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<String>) -> Result<IMAPMailbox> {
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::<u32>().unwrap();
} else if recent_regex.is_match(line) {
let cap = recent_regex.captures(line).unwrap();
mailbox.recent = cap.at(1).unwrap().parse::<u32>().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::<u32>().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::<u32>().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::<u32>().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<IMAPMailbox> {
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<Mailbox> {
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<Vec<String>> {
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<Vec<String>> {
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<String>) -> Result<Vec<String>> {
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,95 +215,85 @@ 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<Vec<String>> {
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<Vec<String>> {
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<Vec<String>> {
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<Vec<String>> {
// Run a command and parse the status response.
pub fn run_command_and_parse(&mut self, command: &str) -> Result<Vec<String>> {
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<String>) -> 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(());
match self.stream.write_fmt(format_args!("{}", &*command)) {
Ok(_) => Ok(()),
Err(_) => Err(Error::Io(io::Error::new(io::ErrorKind::Other, "Failed to write"))),
}
}
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<usize> {
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<Vec<String>> {
match self.run_command(untagged_command) {
Ok(_) => self.read_response(),
Err(e) => Err(e)
}
}
fn read_response(&mut self) -> Result<Vec<String>> {
//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<String> = Vec::new();
while !found_tag_line {
let mut line_buffer: Vec<u8> = 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")),
}
line_buffer.push(byte_buffer[0]);
}
let line = String::from_utf8(line_buffer).unwrap();
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;
}
},
Err(err) => return Err(err)
}
}
Ok(lines)
}
fn read_greeting(&mut self) -> Result<()> {
match self.readline() {
Ok(_) => Ok(()),
Err(err) => Err(err)
}
}
fn readline(&mut self) -> Result<Vec<u8>> {
//Carriage return
let cr = 0x0d;
//Line Feed
@ -335,24 +302,262 @@ impl IMAPStream {
let mut line_buffer: Vec<u8> = 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;
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::mock_stream::MockStream;
use super::super::mailbox::Mailbox;
#[test]
fn connect() {
let imap = IMAPStream::connect(("this-is-not-an-imap-server", 143), None);
assert!(imap.is_err());
fn read_response() {
let response = "a0 OK Logged in.\r\n";
let expected_response: Vec<String> = 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");
}
}

99
src/error.rs Normal file
View file

@ -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<T> = result::Result<T, Error>;
/// 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<String>),
/// A NO response from the IMAP server.
NoResponse(Vec<String>),
// Error parsing a server response.
Parse(ParseError)
}
impl From<IoError> for Error {
fn from(err: IoError) -> Error {
Error::Io(err)
}
}
impl From<SslError> 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<String>),
// Error parsing the cabability response.
Capability(Vec<String>),
// 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
}
}
}

View file

@ -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;

32
src/mailbox.rs Normal file
View file

@ -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<u32>,
pub permanent_flags: Option<String>,
pub uid_next: Option<u32>,
pub uid_validity: Option<u32>
}
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)
}
}

67
src/mock_stream.rs Normal file
View file

@ -0,0 +1,67 @@
use std::io::{Read, Result, Write, Error, ErrorKind};
pub struct MockStream {
read_buf: Vec<u8>,
read_pos: usize,
pub written_buf: Vec<u8>,
err_on_read: bool
}
impl MockStream {
pub fn new(read_buf: Vec<u8>) -> 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<usize> {
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<usize> {
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
}
}

155
src/parse.rs Normal file
View file

@ -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<String> {
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<String>) -> Result<Vec<String>> {
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<String>) -> Result<()> {
match parse_response(lines) {
Ok(_) => Ok(()),
Err(e) => return Err(e)
}
}
pub fn parse_response(lines: Vec<String>) -> Result<Vec<String>> {
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<String>) -> Result<Mailbox> {
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::<u32>().unwrap();
} else if recent_regex.is_match(line) {
let cap = recent_regex.captures(line).unwrap();
mailbox.recent = cap.at(1).unwrap().parse::<u32>().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::<u32>().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::<u32>().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::<u32>().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();
}
}