From 0812242f60a96b2093307b3f125dfc4f42a64f2d Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Fri, 28 Jun 2019 19:34:40 +0300 Subject: [PATCH] Add IMAP backend TODOs: new message events (untagged responses) --- melib/Cargo.toml | 1 + melib/src/backends.rs | 9 +- melib/src/backends/imap.rs | 846 +++++++++++++++++++-- melib/src/backends/imap/connection.rs | 182 +++++ melib/src/backends/imap/folder.rs | 66 ++ melib/src/backends/imap/operations.rs | 257 +++++++ melib/src/backends/imap/protocol_parser.rs | 414 ++++++++++ melib/src/backends/imap/tokens.rs | 571 ++++++++++++++ melib/src/backends/maildir/backend.rs | 29 +- melib/src/backends/mbox.rs | 6 +- melib/src/conf.rs | 3 + melib/src/email.rs | 7 +- melib/src/email/attachments.rs | 4 + melib/src/email/parser.rs | 44 +- testing/Cargo.toml | 4 + testing/src/imap_conn.rs | 35 + ui/src/components/mail/compose.rs | 1 + ui/src/conf.rs | 3 + ui/src/conf/accounts.rs | 6 +- 19 files changed, 2421 insertions(+), 67 deletions(-) create mode 100644 melib/src/backends/imap/connection.rs create mode 100644 melib/src/backends/imap/folder.rs create mode 100644 melib/src/backends/imap/operations.rs create mode 100644 melib/src/backends/imap/protocol_parser.rs create mode 100644 melib/src/backends/imap/tokens.rs create mode 100644 testing/src/imap_conn.rs diff --git a/melib/Cargo.toml b/melib/Cargo.toml index 0e53cc3cf..9a85e4e33 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -19,6 +19,7 @@ notify = "4.0.1" notify-rust = "^3" termion = "1.5.1" xdg = "2.1.0" +native-tls = "0.2" serde = "1.0.71" serde_derive = "1.0.71" bincode = "1.0.1" diff --git a/melib/src/backends.rs b/melib/src/backends.rs index 49c881c1c..8b6bb4a42 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -22,6 +22,7 @@ pub mod imap; pub mod maildir; pub mod mbox; +pub use self::imap::ImapType; use crate::async_workers::*; use crate::conf::AccountSettings; use crate::error::{MeliError, Result}; @@ -63,7 +64,10 @@ impl Backends { "mbox".to_string(), Box::new(|| Box::new(|f| Box::new(MboxType::new(f)))), ); - //b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new("")))); + b.register( + "imap".to_string(), + Box::new(|| Box::new(|f| Box::new(ImapType::new(f)))), + ); b } @@ -161,11 +165,10 @@ pub trait MailBackend: ::std::fmt::Debug { fn folders(&self) -> FnvHashMap; fn operation(&self, hash: EnvelopeHash, folder_hash: FolderHash) -> Box; - fn save(&self, bytes: &[u8], folder: &str) -> Result<()>; + fn save(&self, bytes: &[u8], folder: &str, flags: Option) -> Result<()>; fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> { Ok(()) } - //login function } /// A `BackendOp` manages common operations for the various mail backends. They only live for the diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index 6f5ab1abf..fcb1931e2 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -1,7 +1,7 @@ /* - * meli - mailbox module. + * meli - imap module. * - * Copyright 2017 Manos Pitsidianakis + * Copyright 2019 Manos Pitsidianakis * * This file is part of meli. * @@ -19,56 +19,816 @@ * along with meli. If not, see . */ -/* -use async::*; -use error::Result; -use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder}; -use mailbox::email::Envelope; +#[macro_use] +mod protocol_parser; +pub use protocol_parser::{UntaggedResponse::*, *}; +mod folder; +pub use folder::*; +mod operations; +pub use operations::*; +mod connection; +pub use connection::*; -/// `BackendOp` implementor for Imap -#[derive(Debug, Default, Clone)] -pub struct ImapOp {} +extern crate native_tls; -impl ImapOp { - pub fn new(_path: String) -> Self { - ImapOp {} - } -} +use crate::async_workers::{Async, AsyncBuilder, AsyncStatus}; +use crate::backends::BackendOp; +use crate::backends::FolderHash; +use crate::backends::RefreshEvent; +use crate::backends::RefreshEventKind::{self, *}; +use crate::backends::{BackendFolder, Folder, MailBackend, RefreshEventConsumer}; +use crate::conf::AccountSettings; +use crate::email::*; +use crate::error::{MeliError, Result}; +use fnv::{FnvHashMap, FnvHashSet}; +use native_tls::TlsConnector; +use std::iter::FromIterator; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +pub type UID = usize; - -impl BackendOp for ImapOp { - fn description(&self) -> String { - unimplemented!(); - } - fn as_bytes(&mut self) -> Result<&[u8]> { - unimplemented!(); - } - fn fetch_headers(&mut self) -> Result<&[u8]> { - unimplemented!(); - } - fn fetch_body(&mut self) -> Result<&[u8]> { - unimplemented!(); - } - fn fetch_flags(&self) -> Flag { - unimplemented!(); - } -} - -/// Imap backend #[derive(Debug)] -pub struct ImapType {} +pub struct ImapType { + account_name: String, + server_hostname: String, + server_username: String, + server_password: String, + connection: Arc>, + capabilities: FnvHashSet>, + folders: FnvHashMap, + folder_connections: FnvHashMap>>, + hash_index: Arc>>, + uid_index: Arc>>, +} impl MailBackend for ImapType { - fn get(&self, _folder: &Folder) -> Async>> { - unimplemented!(); + fn get(&mut self, folder: &Folder) -> Async>> { + macro_rules! exit_on_error { + ($tx:expr,$($result:expr)+) => { + $(if let Err(e) = $result { + $tx.send(AsyncStatus::Payload(Err(e))); + std::process::exit(1); + })+ + }; + }; + + let mut w = AsyncBuilder::new(); + let handle = { + let tx = w.tx(); + let hash_index = self.hash_index.clone(); + let uid_index = self.uid_index.clone(); + let folder_path = folder.path().to_string(); + let folder_hash = folder.hash(); + let connection = self.folder_connections[&folder_hash].clone(); + let closure = move || { + let connection = connection.clone(); + let tx = tx.clone(); + let mut response = String::with_capacity(8 * 1024); + { + let mut conn = connection.lock().unwrap(); + + debug!("locked for get {}", folder_path); + exit_on_error!(&tx, + conn.send_command(format!("EXAMINE {}", folder_path).as_bytes()) + conn.read_response(&mut response) + ); + } + let examine_response = protocol_parser::select_response(&response) + .to_full_result() + .map_err(MeliError::from); + exit_on_error!(&tx, examine_response); + let mut exists: usize = match examine_response.unwrap() { + SelectResponse::Ok(ok) => ok.exists, + SelectResponse::Bad(b) => b.exists, + }; + + while exists > 1 { + let mut envelopes = vec![]; + { + let mut conn = connection.lock().unwrap(); + exit_on_error!(&tx, + conn.send_command(format!("UID FETCH {}:{} (FLAGS RFC822.HEADER)", std::cmp::max(exists.saturating_sub(10000), 1), exists).as_bytes()) + conn.read_response(&mut response) + ); + } + debug!( + "fetch response is {} bytes and {} lines", + response.len(), + response.lines().collect::>().len() + ); + match protocol_parser::uid_fetch_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + debug!("responses len is {}", v.len()); + for (uid, flags, b) in v { + if let Ok(e) = Envelope::from_bytes(&b, flags) { + hash_index + .lock() + .unwrap() + .insert(e.hash(), (uid, folder_hash)); + uid_index.lock().unwrap().insert(uid, e.hash()); + envelopes.push(e); + } + } + } + Err(e) => { + debug!(&e); + tx.send(AsyncStatus::Payload(Err(e))); + } + } + exists = std::cmp::max(exists.saturating_sub(10000), 1); + debug!("sending payload"); + tx.send(AsyncStatus::Payload(Ok(envelopes))); + } + tx.send(AsyncStatus::Finished); + }; + Box::new(closure) + }; + w.build(handle) } - fn watch(&self, _sender: RefreshEventConsumer, _folders: &[Folder]) -> () { - unimplemented!(); + + fn watch(&self, sender: RefreshEventConsumer) -> Result<()> { + macro_rules! exit_on_error { + ($sender:expr, $folder_hash:ident, $($result:expr)+) => { + $(if let Err(e) = $result { + debug!("failure: {}", e.to_string()); + $sender.send(RefreshEvent { + hash: $folder_hash, + kind: RefreshEventKind::Failure(e), + }); + std::process::exit(1); + })+ + }; + }; + let has_idle: bool = self.capabilities.contains(&b"IDLE"[0..]); + let sender = Arc::new(sender); + for f in self.folders.values() { + let mut conn = self.new_connection(); + let main_conn = self.connection.clone(); + let f_path = f.path().to_string(); + let hash_index = self.hash_index.clone(); + let uid_index = self.uid_index.clone(); + let folder_hash = f.hash(); + let sender = sender.clone(); + std::thread::Builder::new() + .name(format!( + "{},{}: imap connection", + self.account_name.as_str(), + f_path.as_str() + )) + .spawn(move || { + let mut response = String::with_capacity(8 * 1024); + exit_on_error!( + sender.as_ref(), + folder_hash, + conn.read_response(&mut response) + conn.send_command(format!("SELECT {}", f_path).as_bytes()) + conn.read_response(&mut response) + ); + debug!("select response {}", &response); + let mut prev_exists = match protocol_parser::select_response(&response) + .to_full_result() + .map_err(MeliError::from) + { + Ok(SelectResponse::Bad(bad)) => { + debug!(bad); + panic!("could not select mailbox"); + } + Ok(SelectResponse::Ok(ok)) => { + debug!(&ok); + ok.exists + } + Err(e) => { + debug!("{:?}", e); + panic!("could not select mailbox"); + } + }; + if has_idle { + exit_on_error!(sender.as_ref(), folder_hash, conn.send_command(b"IDLE")); + let mut iter = ImapBlockingConnection::from(conn); + let mut beat = std::time::Instant::now(); + let _26_mins = std::time::Duration::from_secs(26 * 60); + while let Some(line) = iter.next() { + let now = std::time::Instant::now(); + if now.duration_since(beat) >= _26_mins { + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.set_nonblocking(true) + iter.conn.send_raw(b"DONE") + iter.conn.read_response(&mut response) + ); + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.send_command(b"IDLE") + iter.conn.set_nonblocking(false) + ); + { + exit_on_error!( + sender.as_ref(), + folder_hash, + main_conn.lock().unwrap().send_command(b"NOOP") + main_conn.lock().unwrap().read_response(&mut response) + ); + } + beat = now; + } + match protocol_parser::untagged_responses(line.as_slice()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(Some(Recent(_))) => { + /* UID SEARCH RECENT */ + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.set_nonblocking(true) + iter.conn.send_raw(b"DONE") + iter.conn.read_response(&mut response) + iter.conn.send_command(b"UID SEARCH RECENT") + iter.conn.read_response(&mut response) + ); + match protocol_parser::search_results_raw(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(&[]) => { + debug!("UID SEARCH RECENT returned no results"); + } + Ok(v) => { + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.send_command( + &[b"UID FETCH", v, b"(FLAGS RFC822.HEADER)"] + .join(&b' '), + ) + iter.conn.read_response(&mut response) + ); + debug!(&response); + match protocol_parser::uid_fetch_response( + response.as_bytes(), + ) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + for (uid, flags, b) in v { + if let Ok(env) = + Envelope::from_bytes(&b, flags) + { + hash_index.lock().unwrap().insert( + env.hash(), + (uid, folder_hash), + ); + uid_index + .lock() + .unwrap() + .insert(uid, env.hash()); + debug!( + "Create event {} {} {}", + env.hash(), + env.subject(), + f_path.as_str() + ); + sender.send(RefreshEvent { + hash: folder_hash, + kind: Create(Box::new(env)), + }); + } + } + } + Err(e) => { + debug!(e); + } + } + } + Err(e) => { + debug!( + "UID SEARCH RECENT err: {}\nresp: {}", + e.to_string(), + &response + ); + } + } + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.send_command(b"IDLE") + iter.conn.set_nonblocking(false) + ); + } + Ok(Some(Expunge(n))) => { + debug!("expunge {}", n); + } + Ok(Some(Exists(n))) => { + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.set_nonblocking(true) + iter.conn.send_raw(b"DONE") + iter.conn.read_response(&mut response) + ); + /* UID FETCH ALL UID, cross-ref, then FETCH difference headers + * */ + debug!("exists {}", n); + if n > prev_exists { + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.send_command( + &[ + b"FETCH", + format!("{}:{}", prev_exists + 1, n).as_bytes(), + b"(UID FLAGS RFC822.HEADER)", + ] + .join(&b' '), + ) + iter.conn.read_response(&mut response) + ); + match protocol_parser::uid_fetch_response( + response.as_bytes(), + ) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + for (uid, flags, b) in v { + if let Ok(env) = Envelope::from_bytes(&b, flags) + { + hash_index + .lock() + .unwrap() + .insert(env.hash(), (uid, folder_hash)); + uid_index + .lock() + .unwrap() + .insert(uid, env.hash()); + debug!( + "Create event {} {} {}", + env.hash(), + env.subject(), + f_path.as_str() + ); + sender.send(RefreshEvent { + hash: folder_hash, + kind: Create(Box::new(env)), + }); + } + } + } + Err(e) => { + debug!(e); + } + } + + prev_exists = n; + } else if n < prev_exists { + prev_exists = n; + } + exit_on_error!( + sender.as_ref(), + folder_hash, + iter.conn.send_command(b"IDLE") + iter.conn.set_nonblocking(false) + ); + } + Ok(None) | Err(_) => {} + } + } + debug!("failure"); + sender.send(RefreshEvent { + hash: folder_hash, + kind: RefreshEventKind::Failure(MeliError::new("conn_error")), + }); + return; + } else { + loop { + { + exit_on_error!( + sender.as_ref(), + folder_hash, + main_conn.lock().unwrap().send_command(b"NOOP") + main_conn.lock().unwrap().read_response(&mut response) + ); + } + exit_on_error!( + sender.as_ref(), + folder_hash, + conn.send_command(b"NOOP") + conn.read_response(&mut response) + ); + for r in response.lines() { + // FIXME mimic IDLE + debug!(&r); + } + std::thread::sleep(std::time::Duration::from_millis(10 * 1000)); + } + } + })?; + } + Ok(()) } + + fn folders(&self) -> FnvHashMap { + if !self.folders.is_empty() { + return self + .folders + .iter() + .map(|(h, f)| (*h, f.clone() as Folder)) + .collect(); + } + + let mut folders: FnvHashMap = Default::default(); + let mut res = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(b"LIST \"\" \"*\"").unwrap(); + conn.read_response(&mut res).unwrap(); + debug!("out: {}", &res); + for l in res.lines().map(|l| l.trim()) { + if let Ok(mut folder) = + protocol_parser::list_folder_result(l.as_bytes()).to_full_result() + { + if let Some(parent) = folder.parent { + if folders.contains_key(&parent) { + folders + .entry(parent) + .and_modify(|e| e.children.push(folder.hash)); + } else { + /* Insert dummy parent entry, populating only the children field. Later + * when we encounter the parent entry we will swap its children with + * dummy's */ + folders.insert( + parent, + ImapFolder { + children: vec![folder.hash], + ..ImapFolder::default() + }, + ); + } + } + + if folders.contains_key(&folder.hash) { + let entry = folders.entry(folder.hash).or_default(); + std::mem::swap(&mut entry.children, &mut folder.children); + std::mem::swap(entry, &mut folder); + } else { + folders.insert(folder.hash, folder); + } + } else { + debug!("parse error for {:?}", l); + } + } + debug!(&folders); + folders + .iter() + .map(|(h, f)| (*h, f.clone() as Folder)) + .collect() + } + + fn operation(&self, hash: EnvelopeHash, _folder_hash: FolderHash) -> Box { + let (uid, folder_hash) = self.hash_index.lock().unwrap()[&hash]; + Box::new(ImapOp::new( + uid, + self.folders[&folder_hash].path().to_string(), + self.connection.clone(), + )) + } + + fn save(&self, bytes: &[u8], folder: &str, flags: Option) -> Result<()> { + let path = self + .folders + .values() + .find(|v| v.name == folder) + .ok_or(MeliError::new(""))?; + let mut response = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + let flags = flags.unwrap_or(Flag::empty()); + conn.send_command( + format!( + "APPEND \"{}\" ({}) {{{}}}", + path.path(), + flags_to_imap_list!(flags), + bytes.len() + ) + .as_bytes(), + )?; + // wait for "+ Ready for literal data" reply + conn.wait_for_continuation_request()?; + conn.send_literal(bytes)?; + conn.read_response(&mut response)?; + Ok(()) + } +} + +fn lookup_ipv4(host: &str, port: u16) -> Result { + use std::net::ToSocketAddrs; + + let addrs = (host, port).to_socket_addrs()?; + for addr in addrs { + if let SocketAddr::V4(_) = addr { + return Ok(addr); + } + } + + Err(MeliError::new("Cannot lookup address")) +} + +macro_rules! get_conf_val { + ($s:ident[$var:literal]) => { + $s.extra.get($var).unwrap_or_else(|| { + eprintln!( + "IMAP connection for {} requires the field `{}` set", + $s.name.as_str(), + $var + ); + std::process::exit(1); + }) + }; } impl ImapType { - pub fn new(_path: &str) -> Self { - ImapType {} + pub fn new(s: &AccountSettings) -> Self { + use std::io::prelude::*; + use std::net::TcpStream; + debug!(s); + let path = get_conf_val!(s["server_hostname"]); + + let connector = TlsConnector::builder(); + let connector = connector.build().unwrap(); + + let addr = if let Ok(a) = lookup_ipv4(path, 143) { + a + } else { + eprintln!("Could not lookup address {}", &path); + std::process::exit(1); + }; + + let mut socket = TcpStream::connect(&addr).unwrap(); + let cmd_id = 0; + socket + .write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes()) + .unwrap(); + let mut buf = vec![0; 1024]; + let mut response = String::with_capacity(1024); + let mut cap_flag = false; + loop { + let len = socket.read(&mut buf).unwrap(); + response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) }); + if !cap_flag { + if response.starts_with("* OK [CAPABILITY") && response.find("\r\n").is_some() { + if let Some(pos) = response.as_bytes().find(b"\r\n") { + response.drain(0..pos + 2); + cap_flag = true; + } + } else if response.starts_with("* OK ") && response.find("\r\n").is_some() { + if let Some(pos) = response.as_bytes().find(b"\r\n") { + response.drain(0..pos + 2); + } + } + } + if cap_flag && response == "M0 OK Begin TLS negotiation now.\r\n" { + break; + } + } + + socket + .set_nonblocking(true) + .expect("set_nonblocking call failed"); + socket + .set_read_timeout(Some(std::time::Duration::new(120, 0))) + .unwrap(); + let stream = { + let mut conn_result = connector.connect(path, socket); + if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) = conn_result { + let mut midhandshake_stream = Some(midhandshake_stream); + loop { + match midhandshake_stream.take().unwrap().handshake() { + Ok(r) => { + conn_result = Ok(r); + break; + } + Err(native_tls::HandshakeError::WouldBlock(stream)) => { + midhandshake_stream = Some(stream); + } + p => { + p.unwrap(); + } + } + } + } + conn_result.unwrap() + }; + + let mut m = ImapType { + account_name: s.name().to_string(), + server_hostname: get_conf_val!(s["server_hostname"]).to_string(), + server_username: get_conf_val!(s["server_username"]).to_string(), + server_password: get_conf_val!(s["server_password"]).to_string(), + folders: Default::default(), + connection: Arc::new(Mutex::new(ImapConnection { cmd_id, stream })), + folder_connections: Default::default(), + hash_index: Default::default(), + uid_index: Default::default(), + capabilities: Default::default(), + }; + + let mut conn = m.connection.lock().unwrap(); + conn.send_command( + format!( + "LOGIN \"{}\" \"{}\"", + get_conf_val!(s["server_username"]), + get_conf_val!(s["server_password"]) + ) + .as_bytes(), + ) + .unwrap(); + let mut res = String::with_capacity(8 * 1024); + conn.read_lines(&mut res, String::new()).unwrap(); + std::io::stderr().write(res.as_bytes()).unwrap(); + m.capabilities = match protocol_parser::capabilities(res.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(c) => { + eprintln!("cap len {}", c.len()); + + FnvHashSet::from_iter(c.into_iter().map(|s| s.to_vec())) + } + Err(e) => { + eprintln!( + "Could not login in account `{}`: {}", + m.account_name.as_str(), + e + ); + std::process::exit(1); + } + }; + debug!(m + .capabilities + .iter() + .map(|s| String::from_utf8(s.to_vec()).unwrap()) + .collect::>()); + drop(conn); + + m.folders = m.imap_folders(); + for f in m.folders.keys() { + m.folder_connections + .insert(*f, Arc::new(Mutex::new(m.new_connection()))); + } + m } -}*/ + + pub fn shell(&mut self) { + self.folders(); + let mut conn = self.connection.lock().unwrap(); + let mut res = String::with_capacity(8 * 1024); + + let mut input = String::new(); + loop { + use std::io; + input.clear(); + + match io::stdin().read_line(&mut input) { + Ok(_) => { + conn.send_command(input.as_bytes()).unwrap(); + conn.read_response(&mut res).unwrap(); + debug!("out: {}", &res); + if input.trim().eq_ignore_ascii_case("logout") { + break; + } + } + Err(error) => debug!("error: {}", error), + } + } + } + + fn new_connection(&self) -> ImapConnection { + use std::io::prelude::*; + use std::net::TcpStream; + let path = &self.server_hostname; + + let connector = TlsConnector::builder(); + let connector = connector.build().unwrap(); + + let addr = if let Ok(a) = lookup_ipv4(path, 143) { + a + } else { + eprintln!("Could not lookup address {}", &path); + std::process::exit(1); + }; + + let mut socket = TcpStream::connect(&addr).unwrap(); + let cmd_id = 0; + socket + .write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes()) + .unwrap(); + + let mut buf = vec![0; 1024]; + let mut response = String::with_capacity(1024); + let mut cap_flag = false; + loop { + let len = socket.read(&mut buf)?; + response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) }); + if !cap_flag { + if response.starts_with("* OK [CAPABILITY") && response.find("\r\n").is_some() { + if let Some(pos) = response.as_bytes().find(b"\r\n") { + response.drain(0..pos + 2); + cap_flag = true; + } + } else if response.starts_with("* OK ") && response.find("\r\n").is_some() { + if let Some(pos) = response.as_bytes().find(b"\r\n") { + response.drain(0..pos + 2); + } + } + } + if cap_flag && response == "M0 OK Begin TLS negotiation now.\r\n" { + break; + } + } + + socket + .set_nonblocking(true) + .expect("set_nonblocking call failed"); + socket + .set_read_timeout(Some(std::time::Duration::new(120, 0))) + .unwrap(); + let stream = { + let mut conn_result = connector.connect(path, socket); + if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) = conn_result { + let mut midhandshake_stream = Some(midhandshake_stream); + loop { + match midhandshake_stream.take().unwrap().handshake() { + Ok(r) => { + conn_result = Ok(r); + break; + } + Err(native_tls::HandshakeError::WouldBlock(stream)) => { + midhandshake_stream = Some(stream); + } + p => { + p.unwrap(); + } + } + } + } + conn_result.unwrap() + }; + let mut ret = ImapConnection { cmd_id, stream }; + ret.send_command( + format!( + "LOGIN \"{}\" \"{}\"", + &self.server_username, &self.server_password + ) + .as_bytes(), + ) + .unwrap(); + ret + } + + pub fn imap_folders(&self) -> FnvHashMap { + let mut folders: FnvHashMap = Default::default(); + let mut res = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(b"LIST \"\" \"*\"").unwrap(); + conn.read_response(&mut res).unwrap(); + debug!("out: {}", &res); + for l in res.lines().map(|l| l.trim()) { + if let Ok(mut folder) = + protocol_parser::list_folder_result(l.as_bytes()).to_full_result() + { + if let Some(parent) = folder.parent { + if folders.contains_key(&parent) { + folders + .entry(parent) + .and_modify(|e| e.children.push(folder.hash)); + } else { + /* Insert dummy parent entry, populating only the children field. Later + * when we encounter the parent entry we will swap its children with + * dummy's */ + folders.insert( + parent, + ImapFolder { + children: vec![folder.hash], + ..ImapFolder::default() + }, + ); + } + } + if folders.contains_key(&folder.hash) { + let entry = folders.entry(folder.hash).or_default(); + std::mem::swap(&mut entry.children, &mut folder.children); + *entry = folder; + } else { + folders.insert(folder.hash, folder); + } + } else { + debug!("parse error for {:?}", l); + } + } + debug!(folders) + } +} diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs new file mode 100644 index 000000000..7a7cd1d36 --- /dev/null +++ b/melib/src/backends/imap/connection.rs @@ -0,0 +1,182 @@ +/* + * meli - imap module. + * + * Copyright 2017 - 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use crate::email::parser::BytesExt; +use crate::error::*; +use std::io::Read; +use std::io::Write; + +#[derive(Debug)] +pub struct ImapConnection { + pub cmd_id: usize, + pub stream: native_tls::TlsStream, +} + +impl Drop for ImapConnection { + fn drop(&mut self) { + self.send_command(b"LOGOUT").ok().take(); + } +} + +impl ImapConnection { + pub fn read_response(&mut self, ret: &mut String) -> Result<()> { + let id = format!("M{} ", self.cmd_id - 1); + self.read_lines(ret, id) + } + + pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> { + let mut buf: [u8; 1024] = [0; 1024]; + ret.clear(); + let mut last_line_idx: usize = 0; + loop { + match self.stream.read(&mut buf) { + Ok(0) => break, + Ok(b) => { + ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) }); + if let Some(mut pos) = ret[last_line_idx..].rfind("\r\n") { + if ret[last_line_idx..].starts_with("* BYE") { + return Err(MeliError::new("Disconnected")); + } + if let Some(prev_line) = + ret[last_line_idx..pos + last_line_idx].rfind("\r\n") + { + last_line_idx += prev_line + 2; + pos -= prev_line + 2; + } + if pos + "\r\n".len() == ret[last_line_idx..].len() { + if !termination_string.is_empty() + && ret[last_line_idx..].starts_with(termination_string.as_str()) + { + debug!(&ret[last_line_idx..]); + ret.replace_range(last_line_idx.., ""); + break; + } else if termination_string.is_empty() { + break; + } + } + last_line_idx += pos + 2; + } + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + continue; + } + Err(e) => return Err(MeliError::from(e)), + } + } + Ok(()) + } + + pub fn wait_for_continuation_request(&mut self) -> Result<()> { + let term = "+ ".to_string(); + let mut ret = String::new(); + self.read_lines(&mut ret, term) + } + + pub fn send_command(&mut self, command: &[u8]) -> Result { + let command = command.trim(); + self.stream.write_all(b"M").unwrap(); + self.stream + .write_all(self.cmd_id.to_string().as_bytes()) + .unwrap(); + self.stream.write_all(b" ").unwrap(); + let ret = self.cmd_id; + self.cmd_id += 1; + self.stream.write_all(command).unwrap(); + self.stream.write_all(b"\r\n").unwrap(); + debug!("sent: M{} {}", self.cmd_id - 1, unsafe { + std::str::from_utf8_unchecked(command) + }); + Ok(ret) + } + + pub fn send_literal(&mut self, data: &[u8]) -> Result<()> { + self.stream.write_all(data).unwrap(); + if !data.ends_with(b"\r\n") { + self.stream.write_all(b"\r\n").unwrap(); + } + self.stream.write_all(b"\r\n").unwrap(); + Ok(()) + } + + pub fn send_raw(&mut self, raw: &[u8]) -> Result<()> { + self.stream.write_all(raw).unwrap(); + self.stream.write_all(b"\r\n").unwrap(); + Ok(()) + } + + pub fn set_nonblocking(&mut self, val: bool) -> Result<()> { + self.stream.get_mut().set_nonblocking(val)?; + Ok(()) + } +} + +pub struct ImapBlockingConnection { + buf: [u8; 1024], + result: Vec, + prev_res_length: usize, + pub conn: ImapConnection, +} + +impl From for ImapBlockingConnection { + fn from(mut conn: ImapConnection) -> Self { + conn.stream + .get_mut() + .set_nonblocking(false) + .expect("set_nonblocking call failed"); + ImapBlockingConnection { + buf: [0; 1024], + conn, + prev_res_length: 0, + result: Vec::with_capacity(8 * 1024), + } + } +} + +impl ImapBlockingConnection { + pub fn into_conn(self) -> ImapConnection { + self.conn + } +} + +impl Iterator for ImapBlockingConnection { + type Item = Vec; + fn next(&mut self) -> Option { + self.result.drain(0..self.prev_res_length); + self.prev_res_length = 0; + loop { + match self.conn.stream.read(&mut self.buf) { + Ok(0) => continue, + Ok(b) => { + self.result.extend_from_slice(&self.buf[0..b]); + debug!(unsafe { std::str::from_utf8_unchecked(&self.result) }); + if let Some(pos) = self.result.find(b"\r\n") { + self.prev_res_length = pos + b"\r\n".len(); + return Some(self.result[0..self.prev_res_length].to_vec()); + } + } + Err(e) => { + debug!(e); + return None; + } + } + } + } +} diff --git a/melib/src/backends/imap/folder.rs b/melib/src/backends/imap/folder.rs new file mode 100644 index 000000000..285504a4c --- /dev/null +++ b/melib/src/backends/imap/folder.rs @@ -0,0 +1,66 @@ +/* + * meli - imap module. + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ +use crate::backends::{BackendFolder, Folder, FolderHash}; + +#[derive(Debug, Default)] +pub struct ImapFolder { + pub(super) hash: FolderHash, + pub(super) path: String, + pub(super) name: String, + pub(super) parent: Option, + pub(super) children: Vec, +} + +impl BackendFolder for ImapFolder { + fn hash(&self) -> FolderHash { + self.hash + } + + fn name(&self) -> &str { + &self.name + } + + fn path(&self) -> &str { + &self.path + } + + fn change_name(&mut self, s: &str) { + self.name = s.to_string(); + } + + fn children(&self) -> &Vec { + &self.children + } + + fn clone(&self) -> Folder { + Box::new(ImapFolder { + hash: self.hash, + path: self.path.clone(), + name: self.name.clone(), + parent: self.parent, + children: self.children.clone(), + }) + } + + fn parent(&self) -> Option { + self.parent + } +} diff --git a/melib/src/backends/imap/operations.rs b/melib/src/backends/imap/operations.rs new file mode 100644 index 000000000..c653a19f8 --- /dev/null +++ b/melib/src/backends/imap/operations.rs @@ -0,0 +1,257 @@ +/* + * meli - imap module. + * + * Copyright 2017 - 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; + +use crate::backends::BackendOp; +use crate::email::*; +use crate::error::{MeliError, Result}; +use std::cell::Cell; +use std::sync::{Arc, Mutex}; + +/// `BackendOp` implementor for Imap +#[derive(Debug, Clone)] +pub struct ImapOp { + uid: usize, + bytes: Option, + headers: Option, + body: Option, + folder_path: String, + flags: Cell>, + connection: Arc>, +} + +impl ImapOp { + pub fn new(uid: usize, folder_path: String, connection: Arc>) -> Self { + ImapOp { + uid, + connection, + bytes: None, + headers: None, + body: None, + folder_path, + flags: Cell::new(None), + } + } +} + +impl BackendOp for ImapOp { + fn description(&self) -> String { + unimplemented!(); + } + + fn as_bytes(&mut self) -> Result<&[u8]> { + if self.bytes.is_none() { + let mut response = String::with_capacity(8 * 1024); + { + let mut conn = self.connection.lock().unwrap(); + conn.send_command(format!("SELECT {}", self.folder_path).as_bytes()); + conn.read_response(&mut response); + conn.send_command(format!("UID FETCH {} (FLAGS RFC822)", self.uid).as_bytes())?; + conn.read_response(&mut response)?; + } + debug!( + "fetch response is {} bytes and {} lines", + response.len(), + response.lines().collect::>().len() + ); + match protocol_parser::uid_fetch_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + if v.len() != 1 { + debug!("responses len is {}", v.len()); + /* TODO: Trigger cache invalidation here. */ + return Err(MeliError::new(format!( + "message with UID {} was not found", + self.uid + ))); + } + let (uid, flags, b) = v[0]; + assert_eq!(uid, self.uid); + if flags.is_some() { + self.flags.set(flags); + } + self.bytes = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() }); + } + Err(e) => return Err(e), + } + } + Ok(self.bytes.as_ref().unwrap().as_bytes()) + } + + fn fetch_headers(&mut self) -> Result<&[u8]> { + if self.bytes.is_some() { + let result = + parser::headers_raw(self.bytes.as_ref().unwrap().as_bytes()).to_full_result()?; + return Ok(result); + } + if self.headers.is_none() { + let mut response = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(format!("UID FETCH {} (FLAGS RFC822.HEADER)", self.uid).as_bytes())?; + conn.read_response(&mut response)?; + debug!( + "fetch response is {} bytes and {} lines", + response.len(), + response.lines().collect::>().len() + ); + match protocol_parser::uid_fetch_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + if v.len() != 1 { + debug!("responses len is {}", v.len()); + /* TODO: Trigger cache invalidation here. */ + return Err(MeliError::new(format!( + "message with UID {} was not found", + self.uid + ))); + } + let (uid, flags, b) = v[0]; + assert_eq!(uid, self.uid); + if flags.is_some() { + self.flags.set(flags); + } + self.body = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() }); + } + Err(e) => return Err(e), + } + } + Ok(self.body.as_ref().unwrap().as_bytes()) + } + + fn fetch_body(&mut self) -> Result<&[u8]> { + if self.bytes.is_some() { + let result = + parser::body_raw(self.bytes.as_ref().unwrap().as_bytes()).to_full_result()?; + return Ok(result); + } + if self.body.is_none() { + let mut response = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(format!("UID FETCH {} (FLAGS RFC822.TEXT)", self.uid).as_bytes())?; + conn.read_response(&mut response)?; + debug!( + "fetch response is {} bytes and {} lines", + response.len(), + response.lines().collect::>().len() + ); + match protocol_parser::uid_fetch_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + if v.len() != 1 { + debug!("responses len is {}", v.len()); + /* TODO: Trigger cache invalidation here. */ + return Err(MeliError::new(format!( + "message with UID {} was not found", + self.uid + ))); + } + let (uid, flags, b) = v[0]; + assert_eq!(uid, self.uid); + if flags.is_some() { + self.flags.set(flags); + } + self.body = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() }); + } + Err(e) => return Err(e), + } + } + Ok(self.body.as_ref().unwrap().as_bytes()) + } + + fn fetch_flags(&self) -> Flag { + if self.flags.get().is_some() { + return self.flags.get().unwrap(); + } + let mut response = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(format!("UID FETCH {} FLAGS", self.uid).as_bytes()) + .unwrap(); + conn.read_response(&mut response).unwrap(); + debug!( + "fetch response is {} bytes and {} lines", + response.len(), + response.lines().collect::>().len() + ); + match protocol_parser::uid_fetch_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + if v.len() != 1 { + debug!("responses len is {}", v.len()); + /* TODO: Trigger cache invalidation here. */ + panic!(format!("message with UID {} was not found", self.uid)); + } + let (uid, flags, _) = v[0]; + assert_eq!(uid, self.uid); + if flags.is_some() { + self.flags.set(flags); + } + } + Err(e) => Err(e).unwrap(), + } + self.flags.get().unwrap() + } + + fn set_flag(&mut self, _envelope: &mut Envelope, flag: Flag) -> Result<()> { + let mut response = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(format!("SELECT \"{}\"", &self.folder_path,).as_bytes())?; + conn.read_response(&mut response)?; + debug!(&response); + conn.send_command( + format!( + "UID STORE {} FLAGS.SILENT ({})", + self.uid, + flags_to_imap_list!(flag) + ) + .as_bytes(), + )?; + conn.read_response(&mut response)?; + debug!(&response); + match protocol_parser::uid_fetch_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from) + { + Ok(v) => { + if v.len() == 1 { + debug!("responses len is {}", v.len()); + let (uid, flags, _) = v[0]; + assert_eq!(uid, self.uid); + if flags.is_some() { + self.flags.set(flags); + } + } + } + Err(e) => Err(e).unwrap(), + } + conn.send_command(format!("EXAMINE \"{}\"", &self.folder_path,).as_bytes())?; + conn.read_response(&mut response)?; + Ok(()) + } +} diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs new file mode 100644 index 000000000..3b722d46f --- /dev/null +++ b/melib/src/backends/imap/protocol_parser.rs @@ -0,0 +1,414 @@ +use super::*; +use nom::{digit, is_digit, rest, IResult}; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; + +macro_rules! dbg_dmp ( + ($i: expr, $submac:ident!( $($args:tt)* )) => ( + { + let l = line!(); + match $submac!($i, $($args)*) { + nom::IResult::Error(a) => { + debug!("Error({:?}) at l.{} by ' {} '\n{}", a, l, stringify!($submac!($($args)*)), unsafe{ std::str::from_utf8_unchecked($i) }); + nom::IResult::Error(a) + }, + nom::IResult::Incomplete(a) => { + debug!("Incomplete({:?}) at {} by ' {} '\n{}", a, l, stringify!($submac!($($args)*)), unsafe{ std::str::from_utf8_unchecked($i) }); + nom::IResult::Incomplete(a) + }, + a => a + } + } + ); + + ($i:expr, $f:ident) => ( + dbg_dmp!($i, call!($f)); + ); +); +macro_rules! get_path_hash { + ($path:expr) => {{ + let mut hasher = DefaultHasher::new(); + $path.hash(&mut hasher); + hasher.finish() + }}; +} +/* +* LIST (\HasNoChildren) "." INBOX.Sent +* LIST (\HasChildren) "." INBOX + */ +named!( + pub list_folder_result, + do_parse!( + ws!(tag!("* LIST (")) + >> properties: take_until!(&b")"[0..]) + >> tag!(b") ") + >> separator: delimited!(tag!(b"\""), take!(1), tag!(b"\"")) + >> take!(1) + >> path: alt_complete!(delimited!(tag!("\""), is_not!("\""), tag!("\"")) | call!(rest)) + >> ({ + let separator: u8 = separator[0]; + let mut f = ImapFolder::default(); + f.hash = get_path_hash!(path); + f.path = String::from_utf8_lossy(path).into(); + f.name = if let Some(pos) = path.iter().rposition(|&c| c == separator) { + f.parent = Some(get_path_hash!(&path[..pos])); + String::from_utf8_lossy(&path[pos + 1..]).into() + } else { + f.path.clone() + }; + + debug!(f) + }) + ) +); + +named!( + my_flags, + do_parse!( + flags: + separated_nonempty_list!( + tag!(" "), + preceded!(tag!("\\"), is_not!(")")) + ) + >> ({ + let mut ret = Flag::default(); + for f in flags { + match f { + b"Answered" => { + ret.set(Flag::REPLIED, true); + } + b"Flagged" => { + ret.set(Flag::FLAGGED, true); + } + b"Deleted" => { + ret.set(Flag::TRASHED, true); + } + b"Seen" => { + ret.set(Flag::SEEN, true); + } + b"Draft" => { + ret.set(Flag::DRAFT, true); + } + f => { + debug!("unknown Flag token value: {}", unsafe { + std::str::from_utf8_unchecked(f) + }); + } + } + } + ret + }) + ) +); + +/* + * + * * 1 FETCH (FLAGS (\Seen) UID 1 RFC822.HEADER {5224} +*/ +named!( + pub uid_fetch_response, &[u8])>>, + many0!( + do_parse!( + tag!("* ") + >> take_while!(call!(is_digit)) + >> tag!(" FETCH (") + >> uid_flags: permutation!(preceded!(ws!(tag!("UID ")), map_res!(digit, |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) })), opt!(preceded!(ws!(tag!("FLAGS ")), delimited!(tag!("("), byte_flags, tag!(")"))))) + >> is_not!("{") + >> body: length_bytes!(delimited!(tag!("{"), map_res!(digit, |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) }), tag!("}\r\n"))) + >> tag!(")\r\n") + >> ((uid_flags.0, uid_flags.1, body)) + ) + ) +); + +macro_rules! flags_to_imap_list { + ($flags:ident) => {{ + let mut ret = String::new(); + if !($flags & Flag::REPLIED).is_empty() { + ret.push_str("\\Answered"); + } + if !($flags & Flag::FLAGGED).is_empty() { + if !ret.is_empty() { + ret.push(' '); + } + ret.push_str("\\Flagged"); + } + if !($flags & Flag::TRASHED).is_empty() { + if !ret.is_empty() { + ret.push(' '); + } + ret.push_str("\\Deleted"); + } + if !($flags & Flag::SEEN).is_empty() { + if !ret.is_empty() { + ret.push(' '); + } + ret.push_str("\\Seen"); + } + if !($flags & Flag::DRAFT).is_empty() { + if !ret.is_empty() { + ret.push(' '); + } + ret.push_str("\\Draft"); + } + ret + }}; +} + +/* Input Example: + * ============== + * + * "M0 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE] Logged in\r\n" + */ + +named!( + pub capabilities>, + do_parse!( + take_until!("[CAPABILITY ") + >> tag!("[CAPABILITY ") + >> ret: terminated!(separated_nonempty_list_complete!(tag!(" "), is_not!(" ]")), tag!("]")) + >> take_until!("\r\n") + >> tag!("\r\n") + >> ({ ret }) + ) +); + +/// This enum represents the server's untagged responses detailed in `7. Server Responses` of RFC 3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1 +pub enum UntaggedResponse { + /// ```text + /// 7.4.1. EXPUNGE Response + /// + /// The EXPUNGE response reports that the specified message sequence + /// number has been permanently removed from the mailbox. The message + /// sequence number for each successive message in the mailbox is + /// immediately decremented by 1, and this decrement is reflected in + /// message sequence numbers in subsequent responses (including other + /// untagged EXPUNGE responses). + /// + /// The EXPUNGE response also decrements the number of messages in the + /// mailbox; it is not necessary to send an EXISTS response with the + /// new value. + /// + /// As a result of the immediate decrement rule, message sequence + /// numbers that appear in a set of successive EXPUNGE responses + /// depend upon whether the messages are removed starting from lower + /// numbers to higher numbers, or from higher numbers to lower + /// numbers. For example, if the last 5 messages in a 9-message + /// mailbox are expunged, a "lower to higher" server will send five + /// untagged EXPUNGE responses for message sequence number 5, whereas + /// a "higher to lower server" will send successive untagged EXPUNGE + /// responses for message sequence numbers 9, 8, 7, 6, and 5. + /// + /// An EXPUNGE response MUST NOT be sent when no command is in + /// progress, nor while responding to a FETCH, STORE, or SEARCH + /// command. This rule is necessary to prevent a loss of + /// synchronization of message sequence numbers between client and + /// server. A command is not "in progress" until the complete command + /// has been received; in particular, a command is not "in progress" + /// during the negotiation of command continuation. + /// + /// Note: UID FETCH, UID STORE, and UID SEARCH are different + /// commands from FETCH, STORE, and SEARCH. An EXPUNGE + /// response MAY be sent during a UID command. + /// + /// The update from the EXPUNGE response MUST be recorded by the + /// client. + /// ``` + Expunge(usize), + /// ```text + /// 7.3.1. EXISTS Response + /// + /// The EXISTS response reports the number of messages in the mailbox. + /// This response occurs as a result of a SELECT or EXAMINE command, + /// and if the size of the mailbox changes (e.g., new messages). + /// + /// The update from the EXISTS response MUST be recorded by the + /// client. + /// ``` + Exists(usize), + /// ```text + /// 7.3.2. RECENT Response + /// The RECENT response reports the number of messages with the + /// \Recent flag set. This response occurs as a result of a SELECT or + /// EXAMINE command, and if the size of the mailbox changes (e.g., new + /// messages). + /// ``` + Recent(usize), +} + +named!( + pub untagged_responses>, + do_parse!( + tag!("* ") + >> num: map_res!(digit, |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) }) + >> tag!(" ") + >> tag: take_until!("\r\n") + >> tag!("\r\n") + >> ({ + + use UntaggedResponse::*; + match tag { + b"EXPUNGE" => Some(Expunge(num)), + b"EXISTS" => Some(Exists(num)), + b"RECENT" => Some(Recent(num)), + _ => { + debug!("unknown untagged_response: {}", unsafe { std::str::from_utf8_unchecked(tag) }); + None + } + } + }) + ) +); + + +named!( + pub search_results>, + alt_complete!(do_parse!( tag!("* SEARCH") + >> list: separated_nonempty_list_complete!(tag!(" "), map_res!(is_not!("\r\n"), |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) })) + >> tag!("\r\n") + >> ({ list })) | + do_parse!(tag!("* SEARCH\r\n") >> ({ Vec::new() }))) +); + +named!( + pub search_results_raw<&[u8]>, + alt_complete!(do_parse!( tag!("* SEARCH ") + >> list: take_until!("\r\n") + >> tag!("\r\n") + >> ({ list })) | + do_parse!(tag!("* SEARCH\r\n") >> ({ &b""[0..] }))) +); + + +#[derive(Debug, Clone)] +pub enum SelectResponse { + Ok(SelectResponseOk), + Bad(SelectResponseBad), +} + +#[derive(Debug, Default, Clone)] +pub struct SelectResponseOk { + pub exists: usize, + pub recent: usize, + pub flags: Flag, + pub unseen: usize, + pub uidvalidity: usize, + pub uidnext: usize, + pub permanentflags: Flag, +} + +#[derive(Debug, Default, Clone)] +pub struct SelectResponseBad { + pub exists: usize, + pub recent: usize, + pub flags: Flag, +} + +/* + * Example: C: A142 SELECT INBOX + * S: * 172 EXISTS + * S: * 1 RECENT + * S: * OK [UNSEEN 12] Message 12 is first unseen + * S: * OK [UIDVALIDITY 3857529045] UIDs valid + * S: * OK [UIDNEXT 4392] Predicted next UID + * S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) + * S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited + * S: A142 OK [READ-WRITE] SELECT completed + */ + +/* + * + * * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) + * * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted. + * * 45 EXISTS + * * 0 RECENT + * * OK [UNSEEN 16] First unseen. + * * OK [UIDVALIDITY 1554422056] UIDs valid + * * OK [UIDNEXT 50] Predicted next UID + */ +pub fn select_response(input: &str) -> IResult<&str, SelectResponse> { + if input.contains("* OK") { + let mut ret = SelectResponseOk::default(); + for l in input.split("\r\n") { + if l.starts_with("* ") && l.ends_with(" EXISTS") { + ret.exists = usize::from_str(&l["* ".len()..l.len()-" EXISTS".len()]).unwrap(); + } else if l.starts_with("* ") && l.ends_with(" RECENT") { + ret.recent = usize::from_str(&l["* ".len()..l.len()-" RECENT".len()]).unwrap(); + } else if l.starts_with("* FLAGS (") { + ret.flags = flags(&l["* FLAGS (".len()..l.len() - ")".len()]).to_full_result().unwrap(); + } else if l.starts_with("* OK [UNSEEN ") { + ret.unseen = usize::from_str(&l["* OK [UNSEEN ".len()..l.find(']').unwrap()]).unwrap(); + } else if l.starts_with("* OK [UIDVALIDITY ") { + ret.uidvalidity = usize::from_str(&l["* OK [UIDVALIDITY ".len()..l.find(']').unwrap()]).unwrap(); + } else if l.starts_with("* OK [UIDNEXT ") { + ret.uidnext = usize::from_str(&l["* OK [UIDNEXT ".len()..l.find(']').unwrap()]).unwrap(); + } else if l.starts_with("* OK [PERMANENTFLAGS (") { + ret.permanentflags = flags(&l["* OK [PERMANENTFLAGS (".len()..l.find(')').unwrap()]).to_full_result().unwrap(); + } else if !l.is_empty() { + debug!("select response: {}", l); + } + } + IResult::Done(&""[0..], SelectResponse::Ok(ret)) + } else { + let mut ret = SelectResponseBad::default(); + for l in input.split("\r\n") { + if l.starts_with("* ") && l.ends_with(" EXISTS") { + ret.exists = usize::from_str(&l["* ".len()..l.len()-" EXISTS".len()]).unwrap(); + } else if l.starts_with("* ") && l.ends_with(" RECENT") { + ret.recent = usize::from_str(&l["* ".len()..l.len()-" RECENT".len()]).unwrap(); + } else if l.starts_with("* FLAGS (") { + ret.flags = flags(&l["* FLAGS (".len()..l.len() - ")".len()]).to_full_result().unwrap(); + } else if !l.is_empty() { + debug!("select response: {}", l); + } + } + IResult::Done(&""[0..], SelectResponse::Bad(ret)) + } +} + +pub fn flags(input: &str) -> IResult<&str, Flag> { + let mut ret = Flag::default(); + + let mut input = input; + while input.starts_with("\\") { + input = &input[1..]; + let match_end = input.find(|c: char| !c.is_ascii_alphabetic()).or_else(|| input.find(" ")).unwrap_or(input.len()); + match &input[..match_end] { + "Answered" => { + ret.set(Flag::REPLIED, true); + } + "Flagged" => { + ret.set(Flag::FLAGGED, true); + } + "Deleted" => { + ret.set(Flag::TRASHED, true); + } + "Seen" => { + ret.set(Flag::SEEN, true); + } + "Draft" => { + ret.set(Flag::DRAFT, true); + } + f => { + debug!("unknown Flag token value: {}", f); + break; + } + } + input = &input[match_end..]; + if input.starts_with(" \\") { + input = &input[1..]; + } + } + IResult::Done(input, ret) +} + +pub fn byte_flags(input: &[u8]) -> IResult<&[u8], Flag> { + let i = unsafe{ std::str::from_utf8_unchecked(input) }; + match flags(i) { + IResult::Done(rest, ret) => IResult::Done(rest.as_bytes(), ret), + IResult::Error(e) => IResult::Error(e), + IResult::Incomplete(e) => IResult::Incomplete(e), + + } +} diff --git a/melib/src/backends/imap/tokens.rs b/melib/src/backends/imap/tokens.rs new file mode 100644 index 000000000..86ba8ae57 --- /dev/null +++ b/melib/src/backends/imap/tokens.rs @@ -0,0 +1,571 @@ +use std::fmt; +use std::num::NonZeroUsize; + +trait Join { + fn join(&self, sep: char) -> String; +} + +impl Join for [T] +where + T: fmt::Display, +{ + fn join(&self, sep: char) -> String { + if self.is_empty() { + String::from("") + } else if self.len() == 1 { + format!("{}", self[0]) + } else { + format!("{}{}{}", self[0], sep, self[1..].join(sep)) + } + } +} + +struct Search { + charset: Option, + search_keys: Vec, +} + +impl fmt::Display for Search { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "SEARCH{} {}", + if let Some(ch) = self.charset.as_ref() { + format!(" CHARSET {}", ch) + } else { + format!("") + }, + self.search_keys.join(' ') + ) + } +} + +enum SearchKey { + All, + Answered, + Bcc(String), + Before(String), + Body(String), + Cc(String), + Deleted, + Flagged, + From(String), + Keyword(FlagKeyword), + New, + Old, + On(String), + Recent, + Seen, + Since(String), + Subject(String), + Text(String), + To(String), + Unanswered, + Undeleted, + Unflagged, + Unkeyword(FlagKeyword), + Unseen, + Draft, + Header(String, String), //HeaderFldName + Larger(u64), + Not(Box), + Or(Box, Box), + SentBefore(String), //Date + SentOn(String), //Date + SentSince(String), //Date + Smaller(u64), + Uid(SequenceSet), + Undraft, + SequenceSet(SequenceSet), + And(Vec), +} +impl fmt::Display for SearchKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + SearchKey::All => format!("ALL"), + SearchKey::Answered => format!("ANSWERED"), + SearchKey::Bcc(ref s) => format!("BCC {}", s), + SearchKey::Before(ref s) => format!("BEFORE {}", s), + SearchKey::Body(ref s) => format!("BODY {}", s), + SearchKey::Cc(ref s) => format!("CC {}", s), + SearchKey::Deleted => format!("DELETED"), + SearchKey::Flagged => format!("FLAGGED"), + SearchKey::From(ref s) => format!("FROM {}", s), + SearchKey::Keyword(ref s) => format!("KEYWORD {}", s), + SearchKey::New => format!("NEW"), + SearchKey::Old => format!("OLD"), + SearchKey::On(ref s) => format!("ON {}", s), + SearchKey::Recent => format!("RECENT"), + SearchKey::Seen => format!("SEEN"), + SearchKey::Since(ref s) => format!("SINCE {}", s), + SearchKey::Subject(ref s) => format!("SUBJECT {}", s), + SearchKey::Text(ref s) => format!("TEXT {}", s), + SearchKey::To(ref s) => format!("TO {}", s), + SearchKey::Unanswered => format!("UNANSWERED"), + SearchKey::Undeleted => format!("UNDELETED"), + SearchKey::Unflagged => format!("UNFLAGGED"), + SearchKey::Unkeyword(ref s) => format!("UNKEYWORD {}", s), + SearchKey::Unseen => format!("UNSEEN"), + SearchKey::Draft => format!("DRAFT"), + SearchKey::Header(ref name, ref value) => format!("HEADER {} {}", name, value), + SearchKey::Larger(ref s) => format!("LARGER {}", s), + SearchKey::Not(ref s) => format!("NOT {}", s), + SearchKey::Or(ref a, ref b) => format!("OR {} {}", a, b), + SearchKey::SentBefore(ref s) => format!("SENTBEFORE {}", s), + SearchKey::SentOn(ref s) => format!("SENTON {}", s), + SearchKey::SentSince(ref s) => format!("SENTSINCE {}", s), + SearchKey::Smaller(ref s) => format!("SMALLER {}", s), + SearchKey::Uid(ref s) => format!("UID {}", s), + SearchKey::Undraft => format!("UNDRAFT"), + SearchKey::SequenceSet(ref s) => format!("SEQUENCESET {}", s), + SearchKey::And(ref s) => format!("({})", s.join(' ')), + } + ) + } +} + +struct Delete { + mailbox: Mailbox, +} + +impl fmt::Display for Delete { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DELETE {}", self.mailbox) + } +} + +struct Examine { + mailbox: Mailbox, +} + +impl fmt::Display for Examine { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EXAMINE {}", self.mailbox) + } +} + +struct Select { + mailbox: Mailbox, +} + +impl fmt::Display for Select { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SELECT {}", self.mailbox) + } +} + +struct List { + mailbox: Mailbox, + list: String, +} + +impl fmt::Display for List { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "LIST {} \"{}\"", + if self.mailbox.is_empty() { + format!("\"\"") + } else { + format!("{}", self.mailbox) + }, + self.list.as_str() + ) + } +} + +struct Lsub { + mailbox: Mailbox, + list: String, +} + +impl fmt::Display for Lsub { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "LSUB {} \"{}\"", self.mailbox, self.list) + } +} + +enum StatusAttribute { + Messages, + Recent, + UidNext, + UidValidity, + Unseen, +} + +impl fmt::Display for StatusAttribute { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + StatusAttribute::Messages => "MESSAGES", + StatusAttribute::Recent => "RECENT", + StatusAttribute::UidNext => "UIDNEXT", + StatusAttribute::UidValidity => "UIDVALIDITY", + StatusAttribute::Unseen => "UNSEEN", + } + ) + } +} + +struct Status { + mailbox: Mailbox, + status_attributes: Vec, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "STATUS {} ({})", + self.mailbox, + self.status_attributes.join(' ') + ) + } +} + +struct Store { + sequence_set: SequenceSet, + //store_att_flags: +} + +impl fmt::Display for Store { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + //write!(f, "STORE {}", self.sequence_set) + } +} + +struct Unsubscribe { + mailbox: Mailbox, +} + +impl fmt::Display for Unsubscribe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "UNSUBSCRIBE {}", self.mailbox) + } +} + +struct Subscribe { + mailbox: Mailbox, +} + +impl fmt::Display for Subscribe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SUBSCRIBE {}", self.mailbox) + } +} + +struct Copy { + sequence_set: SequenceSet, + mailbox: Mailbox, +} + +impl fmt::Display for Copy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "COPY {} {}", self.sequence_set, self.mailbox) + } +} + +struct Create { + mailbox: Mailbox, +} + +impl fmt::Display for Create { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE {}", self.mailbox) + } +} + +struct Rename { + from: Mailbox, + to: Mailbox, +} + +impl fmt::Display for Rename { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RENAME {} {}", self.from, self.to) + } +} + +struct Append { + mailbox: Mailbox, + flag_list: Vec, + date_time: Option, + literal: String, +} + +impl fmt::Display for Append { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "APPEND {}{}{} {}", + self.mailbox, + if self.flag_list.is_empty() { + String::from("") + } else { + format!(" {}", self.flag_list.join(' ')) + }, + if let Some(date_time) = self.date_time.as_ref() { + format!(" {}", date_time) + } else { + String::from("") + }, + self.literal.as_str() + ) + } +} + +struct Fetch { + sequence_set: SequenceSet, +} + +impl fmt::Display for Fetch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FETCH {}", self.sequence_set) + } +} + +enum Flag { + Answered, + Flagged, + Deleted, + Seen, + Draft, + /*atom */ + X(String), +} + +impl fmt::Display for Flag { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "\\{}", + match self { + Flag::Answered => "Answered", + Flag::Flagged => "Flagged", + Flag::Deleted => "Deleted", + Flag::Seen => "Seen", + Flag::Draft => "Draft", + Flag::X(ref c) => c.as_str(), + } + ) + } +} + +enum Uid { + Copy(Copy), + Fetch(Fetch), + Search(Search), + Store(Store), +} + +impl fmt::Display for Uid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "UID {}", + match self { + Uid::Copy(ref c) => format!("{}", c), + Uid::Fetch(ref c) => format!("{}", c), + Uid::Search(ref c) => format!("{}", c), + Uid::Store(ref c) => format!("{}", c), + } + ) + } +} + +enum CommandSelect { + Check, + Close, + Expunge, + Copy(Copy), + Fetch(Fetch), + Store(Store), + Uid(Uid), + Search(Search), +} + +impl fmt::Display for CommandSelect { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + CommandSelect::Check => format!("CHECK"), + CommandSelect::Close => format!("CLOSE"), + CommandSelect::Expunge => format!("EXPUNGE"), + CommandSelect::Copy(ref c) => format!("{}", c), + CommandSelect::Fetch(ref c) => format!("{}", c), + CommandSelect::Store(ref c) => format!("{}", c), + CommandSelect::Uid(ref c) => format!("{}", c), + CommandSelect::Search(ref c) => format!("{}", c), + } + ) + } +} + +/// Valid in all states +enum CommandAny { + Capability, + Logout, + Noop, + XCommand(String), +} + +impl fmt::Display for CommandAny { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + CommandAny::Capability => format!("CAPABILITY"), + CommandAny::Logout => format!("LOGOUT"), + CommandAny::Noop => format!("NOOP"), + CommandAny::XCommand(ref x) => format!("{}", x), + } + ) + } +} + +enum CommandAuth { + Append(Append), + Create(Create), + Delete(Delete), + Examine(Examine), + List(List), + Lsub(Lsub), + Rename(Rename), + Select(Select), + Status(Status), + Subscribe(Subscribe), + Unsubscribe(Unsubscribe), +} + +impl fmt::Display for CommandAuth { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + CommandAuth::Append(ref c) => c.to_string(), + CommandAuth::Create(ref c) => c.to_string(), + CommandAuth::Delete(ref c) => c.to_string(), + CommandAuth::Examine(ref c) => c.to_string(), + CommandAuth::List(ref c) => c.to_string(), + CommandAuth::Lsub(ref c) => c.to_string(), + CommandAuth::Rename(ref c) => c.to_string(), + CommandAuth::Select(ref c) => c.to_string(), + CommandAuth::Status(ref c) => c.to_string(), + CommandAuth::Subscribe(ref c) => c.to_string(), + CommandAuth::Unsubscribe(ref c) => c.to_string(), + } + ) + } +} + +enum CommandNonAuth { + Login(String, String), + Authenticate(String, String), + StartTls, +} + +impl fmt::Display for CommandNonAuth { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CommandNonAuth::Login(ref userid, ref password) => { + write!(f, "LOGIN \"{}\" \"{}\"", userid, password) + } + CommandNonAuth::Authenticate(ref auth_type, ref base64) => { + write!(f, "AUTHENTICATE \"{}\" \"{}\"", auth_type, base64) + } + CommandNonAuth::StartTls => write!(f, "STARTTLS"), + } + } +} + +enum Command { + Any(CommandAny), + Auth(CommandAuth), + NonAuth(CommandNonAuth), + Select(CommandSelect), +} +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Command::Any(c) => write!(f, "{}", c), + Command::Auth(c) => write!(f, "{}", c), + Command::NonAuth(c) => write!(f, "{}", c), + Command::Select(c) => write!(f, "{}", c), + } + } +} + +pub(super) struct ImapCommand { + tag: usize, + command: Command, +} + +impl fmt::Display for ImapCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}\r\n", self.tag, self.command) + } +} + +enum SeqNumber { + MsgNumber(NonZeroUsize), + UID(NonZeroUsize), + /** "*" represents the largest number in use. In + the case of message sequence numbers, it is the number of messages in a + non-empty mailbox. In the case of unique identifiers, it is the unique + identifier of the last message in the mailbox or, if the mailbox is empty, the + mailbox's current UIDNEXT value **/ + Largest, +} + +impl fmt::Display for SeqNumber { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SeqNumber::MsgNumber(n) => write!(f, "{}", n), + SeqNumber::UID(u) => write!(f, "{}", u), + SeqNumber::Largest => write!(f, "*"), + } + } +} + +struct SeqRange(SeqNumber, SeqNumber); +impl fmt::Display for SeqRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + +struct SequenceSet { + numbers: Vec, + ranges: Vec, +} +impl fmt::Display for SequenceSet { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}{}", + if self.numbers.is_empty() { + String::from("") + } else { + self.numbers.join(',') + }, + if self.ranges.is_empty() { + String::from("") + } else { + self.ranges.join(',') + } + ) + } +} + +type Mailbox = String; +type FlagKeyword = String; diff --git a/melib/src/backends/maildir/backend.rs b/melib/src/backends/maildir/backend.rs index 1b7c47f85..615f1ceaa 100644 --- a/melib/src/backends/maildir/backend.rs +++ b/melib/src/backends/maildir/backend.rs @@ -26,7 +26,7 @@ use super::{ use super::{MaildirFolder, MaildirOp}; use crate::async_workers::*; use crate::conf::AccountSettings; -use crate::email::{Envelope, EnvelopeHash}; +use crate::email::{Envelope, EnvelopeHash, Flag}; use crate::error::{MeliError, Result}; extern crate notify; @@ -464,7 +464,7 @@ impl MailBackend for MaildirType { Box::new(MaildirOp::new(hash, self.hash_indexes.clone(), folder_hash)) } - fn save(&self, bytes: &[u8], folder: &str) -> Result<()> { + fn save(&self, bytes: &[u8], folder: &str, flags: Option) -> Result<()> { for f in self.folders.values() { if f.name == folder { let mut path = f.fs_path.clone(); @@ -484,13 +484,34 @@ impl MailBackend for MaildirType { .duration_since(std::time::SystemTime::UNIX_EPOCH) .unwrap() .as_millis(); - path.push(&format!( + let mut filename = format!( "{}.{:x}_{}.{}:2,", timestamp, u128::from_be_bytes(rand_buf), std::process::id(), hostn_buf.trim() - )); + ); + if let Some(flags) = flags { + if !(flags & Flag::DRAFT).is_empty() { + filename.push('D'); + } + if !(flags & Flag::FLAGGED).is_empty() { + filename.push('F'); + } + if !(flags & Flag::PASSED).is_empty() { + filename.push('P'); + } + if !(flags & Flag::REPLIED).is_empty() { + filename.push('R'); + } + if !(flags & Flag::SEEN).is_empty() { + filename.push('S'); + } + if !(flags & Flag::TRASHED).is_empty() { + filename.push('T'); + } + } + path.push(filename); } debug!("saving at {}", path.display()); let file = fs::File::create(path).unwrap(); diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index 11e72f81d..39c4363f0 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -262,7 +262,7 @@ pub fn mbox_parse( while !input.is_empty() { let next_offset: Option = input.find(b"\n\nFrom "); if let Some(len) = next_offset { - match Envelope::from_bytes(&input[..len]) { + match Envelope::from_bytes(&input[..len], None) { Ok(mut env) => { let mut flags = Flag::empty(); if env.other_headers().contains_key("Status") { @@ -307,7 +307,7 @@ pub fn mbox_parse( offset += len + 2; input = &input[len + 2..]; } else { - match Envelope::from_bytes(input) { + match Envelope::from_bytes(input, None) { Ok(mut env) => { let mut flags = Flag::empty(); if env.other_headers().contains_key("Status") { @@ -534,7 +534,7 @@ impl MailBackend for MboxType { Box::new(MboxOp::new(hash, self.path.as_path(), offset, length)) } - fn save(&self, bytes: &[u8], folder: &str) -> Result<()> { + fn save(&self, _bytes: &[u8], _folder: &str, _flags: Option) -> Result<()> { unimplemented!(); } } diff --git a/melib/src/conf.rs b/melib/src/conf.rs index 5614bf92d..3b70c6a42 100644 --- a/melib/src/conf.rs +++ b/melib/src/conf.rs @@ -18,6 +18,7 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ +use std::collections::hash_map::HashMap; #[derive(Debug, Serialize, Default, Clone)] pub struct AccountSettings { @@ -29,6 +30,8 @@ pub struct AccountSettings { pub read_only: bool, pub display_name: Option, pub subscribed_folders: Vec, + #[serde(flatten)] + pub extra: HashMap, } impl AccountSettings { diff --git a/melib/src/email.rs b/melib/src/email.rs index 29671724b..4a16e81de 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -280,7 +280,7 @@ impl Deref for EnvelopeWrapper { impl EnvelopeWrapper { pub fn new(buffer: Vec) -> Result { Ok(EnvelopeWrapper { - envelope: Envelope::from_bytes(&buffer)?, + envelope: Envelope::from_bytes(&buffer, None)?, buffer, }) } @@ -373,12 +373,15 @@ impl Envelope { self.hash = new_hash; } - pub fn from_bytes(bytes: &[u8]) -> Result { + pub fn from_bytes(bytes: &[u8], flags: Option) -> Result { let mut h = DefaultHasher::new(); h.write(bytes); let mut e = Envelope::new(h.finish()); let res = e.populate_headers(bytes).ok(); if res.is_some() { + if let Some(f) = flags { + e.flags = f; + } return Ok(e); } Err(MeliError::new("Couldn't parse mail.")) diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index e2d3dad26..04fec24cf 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -164,6 +164,10 @@ impl AttachmentBuilder { } pub fn subattachments(raw: &[u8], boundary: &[u8]) -> Vec { + if raw.is_empty() { + return Vec::new(); + } + match parser::attachments(raw, boundary).to_full_result() { Ok(attachments) => { let mut vec = Vec::with_capacity(attachments.len()); diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index 8322708d4..89ea8d4ab 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -119,6 +119,11 @@ fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> { || i + 1 == input_len) { return IResult::Done(&input[(i + 1)..], &input[0..i]); + } else if input[i..].starts_with(b"\r\n") + && (((i + 2) < input_len && input[i + 2] != b' ' && input[i + 2] != b'\t') + || i + 2 == input_len) + { + return IResult::Done(&input[(i + 2)..], &input[0..i]); } } IResult::Incomplete(Needed::Unknown) @@ -128,7 +133,7 @@ fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> { fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { if input.is_empty() { return IResult::Incomplete(Needed::Unknown); - } else if input.starts_with(b"\n") { + } else if input.starts_with(b"\n") || input.starts_with(b"\r\n") { return IResult::Error(error_code!(ErrorKind::Custom(43))); } let mut ptr = 0; @@ -152,6 +157,11 @@ fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { if ptr >= input.len() { return IResult::Error(error_code!(ErrorKind::Custom(43))); } + } else if input[ptr..].starts_with(b"\r\n") { + ptr += 2; + if ptr > input.len() { + return IResult::Error(error_code!(ErrorKind::Custom(43))); + } } while input[ptr] == b' ' || input[ptr] == b'\t' { ptr += 1; @@ -169,13 +179,17 @@ fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { if input.is_empty() { return IResult::Incomplete(Needed::Unknown); - } else if input.starts_with(b"\n") { + } else if input.starts_with(b"\n") || input.starts_with(b"\r\n") { return IResult::Error(error_code!(ErrorKind::Custom(43))); } let mut ptr = 0; let mut name: &[u8] = &input[0..0]; for (i, x) in input.iter().enumerate() { - if *x == b':' || *x == b'\n' { + if input[i..].starts_with(b"\r\n") { + name = &input[0..i]; + ptr = i + 2; + break; + } else if *x == b':' || *x == b'\n' { name = &input[0..i]; ptr = i; break; @@ -201,7 +215,17 @@ fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { if ptr >= input.len() { return IResult::Incomplete(Needed::Unknown); } - if input[ptr] != b' ' && input[ptr] != b'\t' { + if input.len() > ptr && input[ptr] != b' ' && input[ptr] != b'\t' { + IResult::Done(&input[ptr..], (name, b"")) + } else { + IResult::Error(error_code!(ErrorKind::Custom(43))) + } + } else if input[ptr..].starts_with(b"\r\n") { + ptr += 2; + if ptr > input.len() { + return IResult::Incomplete(Needed::Unknown); + } + if input.len() > ptr && input[ptr] != b' ' && input[ptr] != b'\t' { IResult::Done(&input[ptr..], (name, b"")) } else { IResult::Error(error_code!(ErrorKind::Custom(43))) @@ -224,8 +248,10 @@ pub fn headers_raw(input: &[u8]) -> IResult<&[u8], &[u8]> { return IResult::Incomplete(Needed::Unknown); } for (i, x) in input.iter().enumerate() { - if *x == b'\n' && i + 1 < input.len() && input[i + 1] == b'\n' { + if input[i..].starts_with(b"\n\n") { return IResult::Done(&input[(i + 1)..], &input[0..=i]); + } else if input[i..].starts_with(b"\r\n\r\n") { + return IResult::Done(&input[(i + 2)..], &input[0..=i]); } } IResult::Error(error_code!(ErrorKind::Custom(43))) @@ -233,12 +259,12 @@ pub fn headers_raw(input: &[u8]) -> IResult<&[u8], &[u8]> { named!(pub body_raw<&[u8]>, do_parse!( - take_until1!("\n\n") >> + alt_complete!(take_until1!("\n\n") | take_until1!("\r\n\r\n")) >> body: take_while!(call!(|_| true)) >> ( { body } ))); named!(pub mail<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>, - separated_pair!(headers, tag!(b"\n"), take_while!(call!(|_| true)))); + separated_pair!(headers, alt_complete!(tag!(b"\n") | tag!(b"\r\n")), take_while!(call!(|_| true)))); named!(pub attachment<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>, do_parse!( opt!(is_a!(" \n\t\r")) >> @@ -543,7 +569,7 @@ named!(pub address_list, ws!(do_parse!( let mut i = 0; list.iter().fold(String::with_capacity(string_len), |acc, x| { - let mut acc = acc + &String::from_utf8_lossy(x.replace(b"\n", b"").replace(b"\t", b" ").trim()); + let mut acc = acc + &String::from_utf8_lossy(x.replace(b"\n", b"").replace(b"\r", b"").replace(b"\t", b" ").trim()); if i != list_len - 1 { acc.push_str(" "); i+=1; @@ -626,7 +652,7 @@ fn attachments_f<'a>(input: &'a [u8], boundary: &[u8]) -> IResult<&'a [u8], Vec< input = &input[b_start - 2..]; if &input[0..2] == b"--" { input = &input[2 + boundary.len()..]; - if &input[0..1] != b"\n" { + if input[0] != b'\n' && !input[0..].starts_with(b"\r\n") { continue; } input = &input[1..]; diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 6880f2050..22ba9a59e 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -12,6 +12,10 @@ path = "src/email_parse.rs" name = "linebreak" path = "src/linebreak.rs" +[[bin]] +name = "imapconn" +path = "src/imap_conn.rs" + [dependencies] melib = { path = "../melib", version = "*" } diff --git a/testing/src/imap_conn.rs b/testing/src/imap_conn.rs new file mode 100644 index 000000000..af700c0e7 --- /dev/null +++ b/testing/src/imap_conn.rs @@ -0,0 +1,35 @@ +extern crate melib; +use melib::*; +use std::collections::HashMap; + +use melib::backends::ImapType; +use melib::AccountSettings; +use melib::Result; + +fn main() -> Result<()> { + let mut args = std::env::args().skip(1).collect::>(); + if args.len() != 3 { + eprintln!("Usage: imap_conn server_hostname server_username server_password"); + std::process::exit(1); + } + + let (a, b, c) = ( + std::mem::replace(&mut args[0], String::new()), + std::mem::replace(&mut args[1], String::new()), + std::mem::replace(&mut args[2], String::new()), + ); + let set = AccountSettings { + extra: [ + ("server_hostname".to_string(), a), + ("server_username".to_string(), b), + ("server_password".to_string(), c), + ] + .iter() + .cloned() + .collect(), + ..Default::default() + }; + let mut imap = ImapType::new(&set); + imap.shell(); + Ok(()) +} diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 006c6dcfc..1bbf730a8 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -775,6 +775,7 @@ pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) -> .settings .conf() .sent_folder(), + Some(Flag::SEEN), ) { debug!("{:?} could not save sent msg", e); context.replies.push_back(UIEvent::Notification( diff --git a/ui/src/conf.rs b/ui/src/conf.rs index f141559a0..a55208e84 100644 --- a/ui/src/conf.rs +++ b/ui/src/conf.rs @@ -127,6 +127,8 @@ pub struct FileAccount { sent_folder: String, draft_folder: String, identity: String, + #[serde(flatten)] + pub extra: HashMap, #[serde(default = "none")] display_name: Option, @@ -161,6 +163,7 @@ impl From for AccountConf { read_only: x.read_only, display_name, subscribed_folders: x.subscribed_folders.clone(), + extra: x.extra.clone(), }; let root_path = PathBuf::from(acc.root_folder.as_str()); diff --git a/ui/src/conf/accounts.rs b/ui/src/conf/accounts.rs index b879ea341..fa8d94ed4 100644 --- a/ui/src/conf/accounts.rs +++ b/ui/src/conf/accounts.rs @@ -574,16 +574,16 @@ impl Account { } let finalize = draft.finalise()?; self.backend - .save(&finalize.as_bytes(), &self.settings.conf.draft_folder) + .save(&finalize.as_bytes(), &self.settings.conf.draft_folder, None) } - pub fn save(&self, bytes: &[u8], folder: &str) -> Result<()> { + pub fn save(&self, bytes: &[u8], folder: &str, flags: Option) -> Result<()> { if self.settings.account.read_only() { return Err(MeliError::new(format!( "Account {} is read-only.", self.name.as_str() ))); } - self.backend.save(bytes, folder) + self.backend.save(bytes, folder, flags) } pub fn iter_mailboxes(&self) -> MailboxIterator { MailboxIterator {