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
|
- libdw-dev
|
||||||
after_success:
|
after_success:
|
||||||
- travis-cargo --only stable doc-upload
|
- 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"
|
openssl = "0.7.13"
|
||||||
regex = "0.1.71"
|
regex = "0.1.71"
|
||||||
|
|
||||||
[[bin]]
|
[dev-dependencies]
|
||||||
name = "example"
|
base64 = "0.2.0"
|
||||||
path = "example.rs"
|
|
||||||
|
|
|
||||||
42
README.md
42
README.md
|
|
@ -2,41 +2,27 @@ rust-imap
|
||||||
================
|
================
|
||||||
IMAP Client for Rust
|
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://travis-ci.org/mattnenterprise/rust-imap)
|
||||||
[](https://crates.io/crates/imap)
|
[](https://crates.io/crates/imap)
|
||||||
|
[](https://coveralls.io/github/mattnenterprise/rust-imap?branch=master)
|
||||||
|
|
||||||
[Documentation](http://mattnenterprise.github.io/rust-imap)
|
[Documentation](http://mattnenterprise.github.io/rust-imap)
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
Add imap via your `Cargo.toml`:
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
imap = "*"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
Here is a basic example of using the client. See the examples directory for more examples.
|
||||||
```rust
|
```rust
|
||||||
extern crate imap;
|
extern crate imap;
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
|
|
||||||
use openssl::ssl::{SslContext, SslMethod};
|
use openssl::ssl::{SslContext, SslMethod};
|
||||||
use imap::client::IMAPStream;
|
use imap::client::Client;
|
||||||
use imap::client::IMAPMailbox;
|
|
||||||
|
|
||||||
|
// 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() {
|
fn main() {
|
||||||
let mut imap_socket = match IMAPStream::connect("imap.gmail.com", 993, Some(SslContext::new(SslMethod::Sslv23).unwrap())) {
|
let mut imap_socket = Client::secure_connect(("imap.gmail.com", 993), SslContext::new(SslMethod::Sslv23).unwrap()).unwrap();
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => panic!("{}", e)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = imap_socket.login("username", "password") {
|
imap_socket.login("username", "password").unwrap();
|
||||||
println!("Error: {}", e)
|
|
||||||
};
|
|
||||||
|
|
||||||
match imap_socket.capability() {
|
match imap_socket.capability() {
|
||||||
Ok(capabilities) => {
|
Ok(capabilities) => {
|
||||||
|
|
@ -44,14 +30,14 @@ fn main() {
|
||||||
println!("{}", capability);
|
println!("{}", capability);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(_) => println!("Error retreiving capabilities")
|
Err(e) => println!("Error parsing capability: {}", e)
|
||||||
};
|
};
|
||||||
|
|
||||||
match imap_socket.select("INBOX") {
|
match imap_socket.select("INBOX") {
|
||||||
Ok(IMAPMailbox{flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity}) => {
|
Ok(mailbox) => {
|
||||||
println!("flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?}, uid_next: {:?}, uid_validity: {:?}", flags, exists, recent, unseen, permanent_flags, uid_next, uid_validity);
|
println!("{}", mailbox);
|
||||||
},
|
},
|
||||||
Err(_) => println!("Error selecting INBOX")
|
Err(e) => println!("Error selecting INBOX: {}", e)
|
||||||
};
|
};
|
||||||
|
|
||||||
match imap_socket.fetch("2", "body[text]") {
|
match imap_socket.fetch("2", "body[text]") {
|
||||||
|
|
@ -60,12 +46,10 @@ fn main() {
|
||||||
print!("{}", line);
|
print!("{}", line);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(_) => println!("Error Fetching email 2")
|
Err(e) => println!("Error Fetching email 2: {}", e)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = imap_socket.logout() {
|
imap_socket.logout().unwrap();
|
||||||
println!("Error: {}", e)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
623
src/client.rs
623
src/client.rs
|
|
@ -1,151 +1,151 @@
|
||||||
use std::net::{TcpStream, ToSocketAddrs};
|
use std::net::{TcpStream, ToSocketAddrs};
|
||||||
use openssl::ssl::{SslContext, SslStream};
|
use openssl::ssl::{SslContext, SslStream};
|
||||||
use std::io::{Error, ErrorKind, Read, Result, Write};
|
use std::io::{self, Read, Write};
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
enum IMAPStreamTypes {
|
use super::mailbox::Mailbox;
|
||||||
Basic(TcpStream),
|
use super::authenticator::Authenticator;
|
||||||
Ssl(SslStream<TcpStream>)
|
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.
|
/// Stream to interface with the IMAP server. This interface is only for the command stream.
|
||||||
pub struct IMAPStream {
|
pub struct Client<T> {
|
||||||
stream: IMAPStreamTypes,
|
stream: T,
|
||||||
tag: u32,
|
tag: u32
|
||||||
tag_prefix: &'static str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IMAPMailbox {
|
impl Client<TcpStream> {
|
||||||
pub flags: String,
|
/// Creates a new client.
|
||||||
pub exists: u32,
|
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> {
|
||||||
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> {
|
|
||||||
match TcpStream::connect(addr) {
|
match TcpStream::connect(addr) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
let mut socket = match ssl_context {
|
let mut socket = Client::new(stream);
|
||||||
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"},
|
|
||||||
};
|
|
||||||
|
|
||||||
try!(socket.read_greeting());
|
try!(socket.read_greeting());
|
||||||
Ok(socket)
|
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)
|
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.
|
/// Log in to the IMAP server.
|
||||||
pub fn login(&mut self, username: & str, password: & str) -> Result<()> {
|
pub fn login(&mut self, username: & str, password: & str) -> Result<()> {
|
||||||
self.run_command_and_check_ok(&format!("LOGIN {} {}", username, password).to_string())
|
self.run_command_and_check_ok(&format!("LOGIN {} {}", username, password).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects a mailbox
|
/// Selects a mailbox
|
||||||
pub fn select(&mut self, mailbox_name: &str) -> Result<IMAPMailbox> {
|
pub fn select(&mut self, mailbox_name: &str) -> Result<Mailbox> {
|
||||||
match self.run_command(&format!("SELECT {}", mailbox_name).to_string()) {
|
match self.run_command_and_read_response(&format!("SELECT {}", mailbox_name).to_string()) {
|
||||||
Ok(lines) => IMAPStream::parse_select_or_examine(lines),
|
Ok(lines) => parse_select_or_examine(lines),
|
||||||
Err(e) => Err(e)
|
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
|
/// Examine is identical to Select, but the selected mailbox is identified as read-only
|
||||||
pub fn examine(&mut self, mailbox_name: &str) -> Result<IMAPMailbox> {
|
pub fn examine(&mut self, mailbox_name: &str) -> Result<Mailbox> {
|
||||||
match self.run_command(&format!("EXAMINE {}", mailbox_name).to_string()) {
|
match self.run_command_and_read_response(&format!("EXAMINE {}", mailbox_name).to_string()) {
|
||||||
Ok(lines) => IMAPStream::parse_select_or_examine(lines),
|
Ok(lines) => parse_select_or_examine(lines),
|
||||||
Err(e) => Err(e)
|
Err(e) => Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch retreives data associated with a message in the mailbox.
|
/// Fetch retreives data associated with a message in the mailbox.
|
||||||
pub fn fetch(&mut self, sequence_set: &str, query: &str) -> Result<Vec<String>> {
|
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.
|
/// Noop always succeeds, and it does nothing.
|
||||||
|
|
@ -187,39 +187,16 @@ impl IMAPStream {
|
||||||
|
|
||||||
/// Capability requests a listing of capabilities that the server supports.
|
/// Capability requests a listing of capabilities that the server supports.
|
||||||
pub fn capability(&mut self) -> Result<Vec<String>> {
|
pub fn capability(&mut self) -> Result<Vec<String>> {
|
||||||
match self.run_command(&format!("CAPABILITY").to_string()) {
|
match self.run_command_and_read_response(&format!("CAPABILITY").to_string()) {
|
||||||
Ok(lines) => IMAPStream::parse_capability(lines),
|
Ok(lines) => parse_capability(lines),
|
||||||
Err(e) => Err(e)
|
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
|
/// Expunge permanently removes all messages that have the \Deleted flag set from the currently
|
||||||
/// selected mailbox.
|
/// selected mailbox.
|
||||||
pub fn expunge(&mut self) -> Result<()> {
|
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.
|
/// 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())
|
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<()> {
|
pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> {
|
||||||
match self.run_command(command) {
|
match self.run_command_and_read_response(command) {
|
||||||
Ok(lines) => IMAPStream::parse_response_ok(lines),
|
Ok(lines) => parse_response_ok(lines),
|
||||||
Err(e) => Err(e)
|
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());
|
let command = self.create_command(untagged_command.to_string());
|
||||||
|
|
||||||
match self.write_str(&*command) {
|
match self.stream.write_fmt(format_args!("{}", &*command)) {
|
||||||
Ok(_) => (),
|
Ok(_) => Ok(()),
|
||||||
Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to write")),
|
Err(_) => Err(Error::Io(io::Error::new(io::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()));
|
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(),
|
||||||
fn write_str(&mut self, s: &str) -> Result<()> {
|
Err(e) => Err(e)
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_response(&mut self) -> Result<Vec<String>> {
|
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 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();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
|
||||||
while !found_tag_line {
|
while !found_tag_line {
|
||||||
let mut line_buffer: Vec<u8> = Vec::new();
|
match self.readline() {
|
||||||
while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) {
|
Ok(raw_data) => {
|
||||||
let byte_buffer: &mut [u8] = &mut [0];
|
let line = String::from_utf8(raw_data).unwrap();
|
||||||
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();
|
|
||||||
|
|
||||||
lines.push(line.clone());
|
lines.push(line.clone());
|
||||||
|
|
||||||
if (&*line).starts_with(&*start_str) {
|
if (&*line).starts_with(&*start_str) {
|
||||||
found_tag_line = true;
|
found_tag_line = true;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(err) => return Err(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(lines)
|
Ok(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_greeting(&mut self) -> Result<()> {
|
fn read_greeting(&mut self) -> Result<()> {
|
||||||
|
match self.readline() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readline(&mut self) -> Result<Vec<u8>> {
|
||||||
//Carriage return
|
//Carriage return
|
||||||
let cr = 0x0d;
|
let cr = 0x0d;
|
||||||
//Line Feed
|
//Line Feed
|
||||||
|
|
@ -335,24 +302,262 @@ impl IMAPStream {
|
||||||
let mut line_buffer: Vec<u8> = Vec::new();
|
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) {
|
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];
|
let byte_buffer: &mut [u8] = &mut [0];
|
||||||
match self.read(byte_buffer) {
|
match self.stream.read(byte_buffer) {
|
||||||
Ok(_) => {},
|
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]);
|
line_buffer.push(byte_buffer[0]);
|
||||||
}
|
}
|
||||||
|
Ok(line_buffer)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_command(&mut self, command: String) -> String {
|
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;
|
return command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use super::super::mock_stream::MockStream;
|
||||||
|
use super::super::mailbox::Mailbox;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn connect() {
|
fn read_response() {
|
||||||
let imap = IMAPStream::connect(("this-is-not-an-imap-server", 143), None);
|
let response = "a0 OK Logged in.\r\n";
|
||||||
assert!(imap.is_err());
|
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 openssl;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
|
|
||||||
|
pub mod authenticator;
|
||||||
pub mod client;
|
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