commit 95e66760552e1f267f7d2c66b886c7fbefd4696f Author: Matt McCoy Date: Wed Apr 15 16:23:58 2015 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..613564f --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: rust \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0fff3ad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] + +name = "imap" +version = "0.0.1" +authors = ["Matt McCoy "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20c958d --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..706d703 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/example.rs b/example.rs new file mode 100644 index 0000000..ce393bf --- /dev/null +++ b/example.rs @@ -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) + }; +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..1f55e45 --- /dev/null +++ b/src/client.rs @@ -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) +} + +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, + pub permanent_flags: Option, + pub uid_next: Option, + pub uid_validity: Option +} + +impl IMAPStream { + + pub fn connect(host: &'static str, port: u16, ssl_context: Option) -> Result { + 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 { + 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) -> Result { + let exists_regex = match Regex::new(r"^\* (\d+) EXISTS\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + let recent_regex = match Regex::new(r"^\* (\d+) RECENT\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + let flags_regex = match Regex::new(r"^\* FLAGS (.+)\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + let unseen_regex = match Regex::new(r"^OK \[UNSEEN (\d+)\](.*)\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + let uid_validity_regex = match Regex::new(r"^OK \[UIDVALIDITY (\d+)\](.*)\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + let uid_next_regex = match Regex::new(r"^OK \[UIDNEXT (\d+)\](.*)\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + let permanent_flags_regex = match Regex::new(r"^OK \[PERMANENTFLAGS (.+)\]\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + //Check Ok + match IMAPStream::parse_response_ok(lines.clone()) { + Ok(_) => (), + Err(e) => return Err(e) + }; + + let mut mailbox = IMAPMailbox{ + flags: 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::().unwrap(); + } else if recent_regex.is_match(line) { + let cap = recent_regex.captures(line).unwrap(); + mailbox.recent = cap.at(1).unwrap().parse::().unwrap(); + } else if flags_regex.is_match(line) { + let cap = flags_regex.captures(line).unwrap(); + mailbox.flags = 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::().unwrap()); + } else if uid_validity_regex.is_match(line) { + let cap = uid_validity_regex.captures(line).unwrap(); + mailbox.uid_validity = Some(cap.at(1).unwrap().parse::().unwrap()); + } else if uid_next_regex.is_match(line) { + let cap = uid_next_regex.captures(line).unwrap(); + mailbox.uid_next = Some(cap.at(1).unwrap().parse::().unwrap()); + } else if permanent_flags_regex.is_match(line) { + let cap = permanent_flags_regex.captures(line).unwrap(); + mailbox.permanent_flags = Some(String::from_str(cap.at(1).unwrap())); + } + } + + return Ok(mailbox); + } + + //EXAMINE + pub fn examine(&mut self, mailbox_name: &str) -> Result { + 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> { + 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> { + match self.run_command(format!("CAPABILITY").as_str()) { + Ok(lines) => IMAPStream::parse_capability(lines), + Err(e) => Err(e) + } + } + + fn parse_capability(lines: Vec) -> Result> { + let capability_regex = match Regex::new(r"^\* CAPABILITY (.*)\r\n") { + Ok(re) => re, + Err(err) => panic!("{}", err), + }; + + //Check Ok + match IMAPStream::parse_response_ok(lines.clone()) { + Ok(_) => (), + Err(e) => return Err(e) + }; + + for line in lines.iter() { + if capability_regex.is_match(line) { + let cap = capability_regex.captures(line).unwrap(); + let capabilities_str = cap.at(1).unwrap(); + return Ok(capabilities_str.split(' ').map(|x| 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> { + 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) -> 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 { + 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> { + //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 = Vec::new(); + + while !found_tag_line { + let mut line_buffer: Vec = Vec::new(); + while line_buffer.len() < 2 || (line_buffer[line_buffer.len()-1] != lf && line_buffer[line_buffer.len()-2] != cr) { + let byte_buffer: &mut [u8] = &mut [0]; + match self.read(byte_buffer) { + Ok(_) => {}, + Err(_) => return Err(Error::new(ErrorKind::Other, "Failed to read the response")), + } + 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 = 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; + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b1470ba --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +#![feature(collections, convert)] +#![crate_name = "imap"] +#![crate_type = "lib"] + +extern crate openssl; +extern crate regex; + +pub mod client;