Merge pull request #14 from mattnenterprise/generic-client
Generic client and other multiple additions
This commit is contained in:
commit
a716c34d83
14 changed files with 911 additions and 308 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
76
README.md
76
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.
|
||||
|
||||
|
||||
[](https://travis-ci.org/mattnenterprise/rust-imap)
|
||||
[](https://crates.io/crates/imap)
|
||||
[](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();
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
46
example.rs
46
example.rs
|
|
@ -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
8
examples/README.md
Normal 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
40
examples/basic.rs
Normal 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
48
examples/gmail_oauth2.rs
Normal 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
4
src/authenticator.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/// This will allow plugable authentication mechanisms.
|
||||
pub trait Authenticator {
|
||||
fn process(&self, String) -> String;
|
||||
}
|
||||
629
src/client.rs
629
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<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,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<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(());
|
||||
}
|
||||
}
|
||||
|
||||
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<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")),
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
#[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<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
99
src/error.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
32
src/mailbox.rs
Normal 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
67
src/mock_stream.rs
Normal 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
155
src/parse.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue