Initial Commit

This commit is contained in:
Matt McCoy 2015-04-15 16:23:58 -04:00
commit 95e6676055
8 changed files with 500 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
Cargo.lock

1
.travis.yml Normal file
View file

@ -0,0 +1 @@
language: rust

17
Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "imap"
version = "0.0.1"
authors = ["Matt McCoy <mattnenterprise@yahoo.com>"]
repository = "https://github.com/mattnenterprise/rust-imap"
description = "IMAP client for Rust"
readme = "README.md"
license = "MIT"
[dependencies]
openssl = "*"
regex = "*"
[[bin]]
name = "example"
path = "example.rs"

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2015 Matt McCoy
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

71
README.md Normal file
View file

@ -0,0 +1,71 @@
rust-imap
================
IMAP Client for Rust
This client has SSL support. SSL is configured using an SSLContext that is passed into the connect method of a IMAPStream. If no SSL
support is wanted just pass in None. The library rust-openssl is used to support SSL for this project.
[![Build Status](https://travis-ci.org/mattnenterprise/rust-imap.svg)](https://travis-ci.org/mattnenterprise/rust-imap)
### Installation
Add imap via your `Cargo.toml`:
```toml
[dependencies]
imap = "*"
```
### Usage
```rs
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: {:?}, parmanent_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)
};
}
```
### License
MIT

46
example.rs Normal file
View file

@ -0,0 +1,46 @@
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: {:?}, parmanent_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)
};
}

335
src/client.rs Normal file
View file

@ -0,0 +1,335 @@
use std::net::TcpStream;
use openssl::ssl::{SslContext, SslStream};
use std::io::{Error, ErrorKind, Read, Result, Write};
use regex::Regex;
enum IMAPStreamTypes {
Basic(TcpStream),
Ssl(SslStream<TcpStream>)
}
pub struct IMAPStream {
stream: IMAPStreamTypes,
pub host: &'static str,
pub port: u16,
tag: u32,
tag_prefix: &'static str
}
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 {
pub fn connect(host: &'static str, port: u16, ssl_context: Option<SslContext>) -> Result<IMAPStream> {
let connect_string = format!("{}:{}", host, port);
let tcp_stream = TcpStream::connect(&*connect_string).unwrap();
let mut socket = match ssl_context {
Some(context) => IMAPStream { stream: IMAPStreamTypes::Ssl(SslStream::new(&context, tcp_stream).unwrap()), host: host, port: port, tag: 1, tag_prefix: "a"},
None => IMAPStream { stream: IMAPStreamTypes::Basic(tcp_stream), host: host, port: port, tag: 1, tag_prefix: "a"},
};
match socket.read_greeting() {
Ok(_) => (),
Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read greet response"))
}
Ok(socket)
}
//LOGIN
pub fn login(&mut self, username: & str, password: & str) -> Result<()> {
self.run_command_and_check_ok(format!("LOGIN {} {}", username, password).as_str())
}
//SELECT
pub fn select(&mut self, mailbox_name: &str) -> Result<IMAPMailbox> {
match self.run_command(format!("SELECT {}", mailbox_name).as_str()) {
Ok(lines) => IMAPStream::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: String::from_str(""),
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 = String::from_str(cap.at(1).unwrap());
} 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(String::from_str(cap.at(1).unwrap()));
}
}
return Ok(mailbox);
}
//EXAMINE
pub fn examine(&mut self, mailbox_name: &str) -> Result<IMAPMailbox> {
match self.run_command(format!("EXAMINE {}", mailbox_name).as_str()) {
Ok(lines) => IMAPStream::parse_select_or_examine(lines),
Err(e) => Err(e)
}
}
//FETCH
pub fn fetch(&mut self, sequence_set: &str, query: &str) -> Result<Vec<String>> {
self.run_command(format!("FETCH {} {}", sequence_set, query).as_str())
}
//NOOP
pub fn noop(&mut self) -> Result<()> {
self.run_command_and_check_ok("NOOP")
}
//LOGOUT
pub fn logout(&mut self) -> Result<()> {
self.run_command_and_check_ok("LOGOUT")
}
//CREATE
pub fn create(&mut self, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(format!("CREATE {}", mailbox_name).as_str())
}
//DELETE
pub fn delete(&mut self, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(format!("DELETE {}", mailbox_name).as_str())
}
//RENAME
pub fn rename(&mut self, current_mailbox_name: &str, new_mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(format!("RENAME {} {}", current_mailbox_name, new_mailbox_name).as_str())
}
//SUBSCRIBE
pub fn subscribe(&mut self, mailbox: &str) -> Result<()> {
self.run_command_and_check_ok(format!("SUBSCRIBE {}", mailbox).as_str())
}
//UNSUBSCRIBE
pub fn unsubscribe(&mut self, mailbox: &str) -> Result<()> {
self.run_command_and_check_ok(format!("UNSUBSCRIBE {}", mailbox).as_str())
}
//CAPABILITY
pub fn capability(&mut self) -> Result<Vec<String>> {
match self.run_command(format!("CAPABILITY").as_str()) {
Ok(lines) => IMAPStream::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| String::from_str(x)).collect());
}
}
Err(Error::new(ErrorKind::Other, "Error parsing capabilities response"))
}
//COPY
pub fn copy(&mut self, sequence_set: &str, mailbox_name: &str) -> Result<()> {
self.run_command_and_check_ok(format!("COPY {} {}", sequence_set, mailbox_name).as_str())
}
fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> {
match self.run_command(command) {
Ok(lines) => IMAPStream::parse_response_ok(lines),
Err(e) => Err(e)
}
}
fn run_command(&mut self, untagged_command: &str) -> Result<Vec<String>> {
let command = self.create_command(String::from_str(untagged_command));
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).as_str()));
}
fn write_str(&mut self, s: &str) -> Result<()> {
match self.stream {
IMAPStreamTypes::Ssl(ref mut stream) => stream.write_fmt(format_args!("{}", s)),
IMAPStreamTypes::Basic(ref mut stream) => stream.write_fmt(format_args!("{}", s)),
}
}
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self.stream {
IMAPStreamTypes::Ssl(ref mut stream) => stream.read(buf),
IMAPStreamTypes::Basic(ref mut stream) => stream.read(buf),
}
}
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 mut lines: Vec<String> = Vec::new();
while !found_tag_line {
let mut line_buffer: Vec<u8> = Vec::new();
while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) {
let byte_buffer: &mut [u8] = &mut [0];
match self.read(byte_buffer) {
Ok(_) => {},
Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")),
}
line_buffer.push(byte_buffer[0]);
}
let line = String::from_utf8(line_buffer).unwrap();
lines.push(line.clone());
if (&*line).starts_with(&*start_str) {
found_tag_line = true;
}
}
Ok(lines)
}
fn read_greeting(&mut self) -> Result<()> {
//Carriage return
let cr = 0x0d;
//Line Feed
let lf = 0x0a;
let mut line_buffer: Vec<u8> = Vec::new();
while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) {
let byte_buffer: &mut [u8] = &mut [0];
match self.read(byte_buffer) {
Ok(_) => {},
Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")),
}
line_buffer.push(byte_buffer[0]);
}
Ok(())
}
fn create_command(&mut self, command: String) -> String {
let command = format!("{}{} {}\r\n", self.tag_prefix, self.tag, command);
return command;
}
}

8
src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
#![feature(collections, convert)]
#![crate_name = "imap"]
#![crate_type = "lib"]
extern crate openssl;
extern crate regex;
pub mod client;