From 7f8c6383610c5c8595b99a35b359fe366592e195 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 15 Jan 2020 12:31:49 +0200 Subject: [PATCH] melib/imap: add mailbox creation ability --- melib/src/backends.rs | 2 +- melib/src/backends/imap.rs | 32 ++ melib/src/backends/imap/connection.rs | 384 +++++++-------------- melib/src/backends/imap/protocol_parser.rs | 119 +++++++ ui/src/conf/accounts.rs | 199 +++++++---- ui/src/execute.rs | 44 ++- ui/src/lib.rs | 1 - ui/src/plugins/backend.rs | 4 +- ui/src/state.rs | 8 +- ui/src/types.rs | 2 +- 10 files changed, 451 insertions(+), 344 deletions(-) diff --git a/melib/src/backends.rs b/melib/src/backends.rs index 245d95ce..41c471ae 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -264,7 +264,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync { fn operation(&self, hash: EnvelopeHash) -> Box; fn save(&self, bytes: &[u8], folder: &str, flags: Option) -> Result<()>; - fn create_folder(&mut self, name: NewFolderName) -> Result { + fn create_folder(&mut self, path: String) -> Result { unimplemented!() } fn tags(&self) -> Option>>> { diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index de694f53..439fa3e8 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -517,6 +517,38 @@ impl MailBackend for ImapType { None } } + + fn create_folder(&mut self, path: String) -> Result { + let mut response = String::with_capacity(8 * 1024); + if self + .folders + .read() + .unwrap() + .values() + .any(|f| f.path == path) + { + return Err(MeliError::new(format!( + "Folder named `{}` in account `{}` already exists.", + path, self.account_name, + ))); + } + let mut conn_lck = self.connection.lock()?; + conn_lck.send_command(debug!(format!("CREATE \"{}\"", path,)).as_bytes())?; + conn_lck.read_response(&mut response)?; + conn_lck.send_command(debug!(format!("SUBSCRIBE \"{}\"", path,)).as_bytes())?; + conn_lck.read_response(&mut response)?; + drop(conn_lck); + self.folders.write().unwrap().clear(); + self.folders().and_then(|f| { + debug!(f) + .into_iter() + .find(|(_, f)| f.path() == path) + .map(|f| f.1) + .ok_or(MeliError::new( + "Internal error: could not find folder after creating it?", + )) + }) + } } impl ImapType { diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index 601f39af..c24b8295 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -19,7 +19,7 @@ * along with meli. If not, see . */ -use super::protocol_parser::ImapLineSplit; +use super::protocol_parser::{ImapLineSplit, ImapResponse}; use crate::email::parser::BytesExt; use crate::error::*; use std::io::Read; @@ -27,6 +27,7 @@ use std::io::Write; extern crate native_tls; use fnv::FnvHashSet; use native_tls::TlsConnector; +use std::borrow::Cow; use std::iter::FromIterator; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; @@ -56,96 +57,6 @@ impl Drop for ImapStream { } impl ImapStream { - 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: &str) -> 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) - { - 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")?; - self.stream.write_all(self.cmd_id.to_string().as_bytes())?; - self.stream.write_all(b" ")?; - let ret = self.cmd_id; - self.cmd_id += 1; - self.stream.write_all(command)?; - self.stream.write_all(b"\r\n")?; - 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)?; - if !data.ends_with(b"\r\n") { - self.stream.write_all(b"\r\n")?; - } - self.stream.write_all(b"\r\n")?; - Ok(()) - } - - pub fn send_raw(&mut self, raw: &[u8]) -> Result<()> { - self.stream.write_all(raw)?; - self.stream.write_all(b"\r\n")?; - Ok(()) - } - - pub fn set_nonblocking(&mut self, val: bool) -> Result<()> { - self.stream.get_mut().set_nonblocking(val)?; - Ok(()) - } - pub fn new_connection(server_conf: &ImapServerConf) -> Result<(Capabilities, ImapStream)> { use std::io::prelude::*; use std::net::TcpStream; @@ -167,8 +78,8 @@ impl ImapStream { }; let mut socket = TcpStream::connect(&addr)?; - socket.set_read_timeout(Some(std::time::Duration::new(120, 0)))?; - socket.set_write_timeout(Some(std::time::Duration::new(120, 0)))?; + socket.set_read_timeout(Some(std::time::Duration::new(4, 0)))?; + socket.set_write_timeout(Some(std::time::Duration::new(4, 0)))?; let cmd_id = 0; if server_conf.use_starttls { socket.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())?; @@ -274,7 +185,7 @@ impl ImapStream { let tag_start = format!("M{} ", (ret.cmd_id - 1)); loop { - ret.read_lines(&mut res, &String::new())?; + ret.read_lines(&mut res, &String::new(), false)?; let mut should_break = false; for l in res.split_rn() { if l.starts_with("* CAPABILITY") { @@ -317,6 +228,102 @@ impl ImapStream { Ok((capabilities, ret)) } } + + pub fn read_response(&mut self, ret: &mut String) -> Result<()> { + let id = format!("M{} ", self.cmd_id - 1); + self.read_lines(ret, &id, true) + } + + pub fn read_lines( + &mut self, + ret: &mut String, + termination_string: &str, + keep_termination_string: bool, + ) -> 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) + { + debug!(&ret[last_line_idx..]); + if !keep_termination_string { + 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, false) + } + + pub fn send_command(&mut self, command: &[u8]) -> Result<()> { + let command = command.trim(); + self.stream.write_all(b"M")?; + self.stream.write_all(self.cmd_id.to_string().as_bytes())?; + self.stream.write_all(b" ")?; + self.cmd_id += 1; + self.stream.write_all(command)?; + self.stream.write_all(b"\r\n")?; + debug!("sent: M{} {}", self.cmd_id - 1, unsafe { + std::str::from_utf8_unchecked(command) + }); + Ok(()) + } + + pub fn send_literal(&mut self, data: &[u8]) -> Result<()> { + self.stream.write_all(data)?; + if !data.ends_with(b"\r\n") { + self.stream.write_all(b"\r\n")?; + } + self.stream.write_all(b"\r\n")?; + Ok(()) + } + + pub fn send_raw(&mut self, raw: &[u8]) -> Result<()> { + self.stream.write_all(raw)?; + self.stream.write_all(b"\r\n")?; + Ok(()) + } + + pub fn set_nonblocking(&mut self, val: bool) -> Result<()> { + self.stream.get_mut().set_nonblocking(val)?; + Ok(()) + } } impl ImapConnection { @@ -333,188 +340,39 @@ impl ImapConnection { } pub fn read_response(&mut self, ret: &mut String) -> Result<()> { - if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { - if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { - *status = Err(MeliError::new("Connection timed out")); - self.stream = Err(MeliError::new("Connection timed out")); - } - } - - if let Ok(ref mut stream) = self.stream { - if let Ok(_) = stream.read_response(ret) { - return Ok(()); - } - } - let new_stream = ImapStream::new_connection(&self.server_conf); - if new_stream.is_err() { - *self.online.lock().unwrap() = ( - Instant::now(), - Err(new_stream.as_ref().unwrap_err().clone()), - ); - } else { - *self.online.lock().unwrap() = (Instant::now(), Ok(())); - } - let (capabilities, mut stream) = new_stream?; - let ret = stream.read_response(ret); - if ret.is_ok() { - self.stream = Ok(stream); - self.capabilities = capabilities; - } - ret + let res = self.try_send(|s| s.read_response(ret)); + let r: Result<()> = ImapResponse::from(&ret).into(); + r } pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> { - if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { - if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { - *status = Err(MeliError::new("Connection timed out")); - self.stream = Err(MeliError::new("Connection timed out")); - } - } - - if let Ok(ref mut stream) = self.stream { - if let Ok(_) = stream.read_lines(ret, &termination_string) { - return Ok(()); - } - } - let new_stream = ImapStream::new_connection(&self.server_conf); - if new_stream.is_err() { - *self.online.lock().unwrap() = ( - Instant::now(), - Err(new_stream.as_ref().unwrap_err().clone()), - ); - } else { - *self.online.lock().unwrap() = (Instant::now(), Ok(())); - } - let (capabilities, mut stream) = new_stream?; - let ret = stream.read_lines(ret, &termination_string); - if ret.is_ok() { - self.stream = Ok(stream); - self.capabilities = capabilities; - } - ret + self.try_send(|s| s.read_lines(ret, &termination_string, false)) } pub fn wait_for_continuation_request(&mut self) -> Result<()> { - if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { - if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { - *status = Err(MeliError::new("Connection timed out")); - self.stream = Err(MeliError::new("Connection timed out")); - } - } - if let Ok(ref mut stream) = self.stream { - if let Ok(_) = stream.wait_for_continuation_request() { - return Ok(()); - } - } - let new_stream = ImapStream::new_connection(&self.server_conf); - if new_stream.is_err() { - *self.online.lock().unwrap() = ( - Instant::now(), - Err(new_stream.as_ref().unwrap_err().clone()), - ); - } else { - *self.online.lock().unwrap() = (Instant::now(), Ok(())); - } - let (capabilities, mut stream) = new_stream?; - let ret = stream.wait_for_continuation_request(); - if ret.is_ok() { - self.stream = Ok(stream); - self.capabilities = capabilities; - } - ret + self.try_send(|s| s.wait_for_continuation_request()) } - pub fn send_command(&mut self, command: &[u8]) -> Result { - if let (instant, ref mut status) = *self.online.lock().unwrap() { - if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { - *status = Err(MeliError::new("Connection timed out")); - self.stream = Err(MeliError::new("Connection timed out")); - } - } - if let Ok(ref mut stream) = self.stream { - if let Ok(ret) = stream.send_command(command) { - return Ok(ret); - } - } - let new_stream = ImapStream::new_connection(&self.server_conf); - if new_stream.is_err() { - *self.online.lock().unwrap() = ( - Instant::now(), - Err(new_stream.as_ref().unwrap_err().clone()), - ); - } else { - *self.online.lock().unwrap() = (Instant::now(), Ok(())); - } - let (capabilities, mut stream) = new_stream?; - let ret = stream.send_command(command); - if ret.is_ok() { - self.stream = Ok(stream); - self.capabilities = capabilities; - } - ret + pub fn send_command(&mut self, command: &[u8]) -> Result<()> { + self.try_send(|s| s.send_command(command)) } pub fn send_literal(&mut self, data: &[u8]) -> Result<()> { - if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { - if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { - *status = Err(MeliError::new("Connection timed out")); - self.stream = Err(MeliError::new("Connection timed out")); - } - } - if let Ok(ref mut stream) = self.stream { - if let Ok(_) = stream.send_literal(data) { - return Ok(()); - } - } - let new_stream = ImapStream::new_connection(&self.server_conf); - if new_stream.is_err() { - *self.online.lock().unwrap() = ( - Instant::now(), - Err(new_stream.as_ref().unwrap_err().clone()), - ); - } else { - *self.online.lock().unwrap() = (Instant::now(), Ok(())); - } - let (capabilities, mut stream) = new_stream?; - let ret = stream.send_literal(data); - if ret.is_ok() { - self.stream = Ok(stream); - self.capabilities = capabilities; - } - ret + self.try_send(|s| s.send_literal(data)) } pub fn send_raw(&mut self, raw: &[u8]) -> Result<()> { - if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { - if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { - *status = Err(MeliError::new("Connection timed out")); - self.stream = Err(MeliError::new("Connection timed out")); - } - } - if let Ok(ref mut stream) = self.stream { - if let Ok(_) = stream.send_raw(raw) { - return Ok(()); - } - } - let new_stream = ImapStream::new_connection(&self.server_conf); - if new_stream.is_err() { - *self.online.lock().unwrap() = ( - Instant::now(), - Err(new_stream.as_ref().unwrap_err().clone()), - ); - } else { - *self.online.lock().unwrap() = (Instant::now(), Ok(())); - } - let (capabilities, mut stream) = new_stream?; - let ret = stream.send_raw(raw); - if ret.is_ok() { - self.stream = Ok(stream); - self.capabilities = capabilities; - } - ret + self.try_send(|s| s.send_raw(raw)) } pub fn set_nonblocking(&mut self, val: bool) -> Result<()> { + self.try_send(|s| s.set_nonblocking(val)) + } + + pub fn try_send( + &mut self, + mut action: impl FnMut(&mut ImapStream) -> Result<()>, + ) -> Result<()> { if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { *status = Err(MeliError::new("Connection timed out")); @@ -522,7 +380,7 @@ impl ImapConnection { } } if let Ok(ref mut stream) = self.stream { - if let Ok(_) = stream.set_nonblocking(val) { + if let Ok(_) = action(stream) { return Ok(()); } } @@ -536,7 +394,7 @@ impl ImapConnection { *self.online.lock().unwrap() = (Instant::now(), Ok(())); } let (capabilities, mut stream) = new_stream?; - let ret = stream.set_nonblocking(val); + let ret = action(&mut stream); if ret.is_ok() { self.stream = Ok(stream); self.capabilities = capabilities; @@ -562,7 +420,7 @@ impl From for ImapBlockingConnection { .map(|s| { s.stream .get_mut() - .set_write_timeout(Some(std::time::Duration::new(5 * 60, 0))) + .set_write_timeout(Some(std::time::Duration::new(30, 0))) .expect("set_write_timeout call failed") }) .expect("set_write_timeout call failed"); @@ -571,7 +429,7 @@ impl From for ImapBlockingConnection { .map(|s| { s.stream .get_mut() - .set_read_timeout(Some(std::time::Duration::new(5 * 60, 0))) + .set_read_timeout(Some(std::time::Duration::new(30, 0))) .expect("set_read_timeout call failed") }) .expect("set_read_timeout call failed"); diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index 9c92768c..97f6f50e 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -12,6 +12,125 @@ pub struct ImapLineIterator<'a> { slice: &'a str, } +#[derive(Debug, PartialEq)] +pub enum ResponseCode { + ///The human-readable text contains a special alert that MUST be presented to the user in a fashion that calls the user's attention to the message. + Alert(String), + + ///Optionally followed by a parenthesized list of charsets. A SEARCH failed because the given charset is not supported by this implementation. If the optional list of charsets is given, this lists the charsets that are supported by this implementation. + Badcharset, + + /// Followed by a list of capabilities. This can appear in the initial OK or PREAUTH response to transmit an initial capabilities list. This makes it unnecessary for a client to send a separate CAPABILITY command if it recognizes this response. + Capability, + + /// The human-readable text represents an error in parsing the [RFC-2822] header or [MIME-IMB] headers of a message in the mailbox. + Parse(String), + + /// Followed by a parenthesized list of flags, indicates which of the known flags the client can change permanently. Any flags that are in the FLAGS untagged response, but not the PERMANENTFLAGS list, can not be set permanently. If the client attempts to STORE a flag that is not in the PERMANENTFLAGS list, the server will either ignore the change or store the state change for the remainder of the current session only. The PERMANENTFLAGS list can also include the special flag \*, which indicates that it is possible to create new keywords by attempting to store those flags in the mailbox. + Permanentflags, + + /// The mailbox is selected read-only, or its access while selected has changed from read-write to read-only. + ReadOnly, + + /// The mailbox is selected read-write, or its access while selected has changed from read-only to read-write. + ReadWrite, + + /// An APPEND or COPY attempt is failing because the target mailbox does not exist (as opposed to some other reason). This is a hint to the client that the operation can succeed if the mailbox is first created by the CREATE command. + Trycreate, + + /// Followed by a decimal number, indicates the next unique identifier value. Refer to section 2.3.1.1 for more information. + Uidnext(UID), + /// Followed by a decimal number, indicates the unique identifier validity value. Refer to section 2.3.1.1 for more information. + Uidvalidity(UID), + /// Followed by a decimal number, indicates the number of the first message without the \Seen flag set. + Unseen(usize), +} + +impl ResponseCode { + fn from(val: &str) -> Option { + if !val.starts_with("[") { + return None; + } + + let val = &val[1..]; + use ResponseCode::*; + if val.starts_with("BADCHARSET") { + Some(Badcharset) + } else if val.starts_with("READONLY") { + Some(ReadOnly) + } else if val.starts_with("READWRITE") { + Some(ReadWrite) + } else if val.starts_with("TRYCREATE") { + Some(Trycreate) + } else if val.starts_with("UIDNEXT") { + //FIXME + Some(Uidnext(0)) + } else if val.starts_with("UIDVALIDITY") { + //FIXME + Some(Uidvalidity(0)) + } else if val.starts_with("UNSEEN") { + //FIXME + Some(Unseen(0)) + } else { + let msg = &val[val.as_bytes().find(b"] ").unwrap() + 1..].trim(); + Some(Alert(msg.to_string())) + } + } +} + +#[derive(Debug, PartialEq)] +pub enum ImapResponse { + Ok(Option), + No(Option), + Bad(Option), + Preauth(Option), + Bye(Option), +} + +impl> From for ImapResponse { + fn from(val: T) -> ImapResponse { + let val: &str = val.as_ref().split_rn().last().unwrap_or(val.as_ref()); + debug!(&val); + assert!(val.starts_with("M")); + let mut val = val[val.as_bytes().find(b" ").unwrap() + 1..].trim(); + // M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n + if val.ends_with(" secs).") { + val = &val[..val.as_bytes().rfind(b"(").unwrap()]; + } + + if val.starts_with("OK") { + Self::Ok(ResponseCode::from(&val["OK ".len()..])) + } else if val.starts_with("NO") { + Self::No(ResponseCode::from(&val["NO ".len()..])) + } else if val.starts_with("BAD") { + Self::Bad(ResponseCode::from(&val["BAD ".len()..])) + } else if val.starts_with("PREAUTH") { + Self::Preauth(ResponseCode::from(&val["PREAUTH ".len()..])) + } else if val.starts_with("BYE") { + Self::Bye(ResponseCode::from(&val["BYE ".len()..])) + } else { + panic!("Unknown IMAP response: `{}`", val); + } + } +} + +impl Into> for ImapResponse { + fn into(self) -> Result<()> { + match self { + Self::Ok(_) | Self::Preauth(_) | Self::Bye(_) => Ok(()), + Self::No(Some(ResponseCode::Alert(msg))) + | Self::Bad(Some(ResponseCode::Alert(msg))) => Err(MeliError::new(msg)), + Self::No(_) => Err(MeliError::new("IMAP NO Response.")), + Self::Bad(_) => Err(MeliError::new("IMAP BAD Response.")), + } + } +} + +#[test] +fn test_imap_response() { + assert_eq!(ImapResponse::from("M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"), ImapResponse::No(Some(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string())))); +} + impl<'a> Iterator for ImapLineIterator<'a> { type Item = &'a str; diff --git a/ui/src/conf/accounts.rs b/ui/src/conf/accounts.rs index 116d1200..67df5e2a 100644 --- a/ui/src/conf/accounts.rs +++ b/ui/src/conf/accounts.rs @@ -384,7 +384,6 @@ impl Account { folder_names.insert(f.hash(), f.path().to_string()); } - let mut stack: SmallVec<[FolderHash; 8]> = SmallVec::new(); let mut tree: Vec = Vec::new(); let mut collection: Collection = Collection::new(Default::default()); for (h, f) in ref_folders.iter() { @@ -392,28 +391,6 @@ impl Account { /* Skip unsubscribed folder */ continue; } - if f.parent().is_none() { - fn rec(h: FolderHash, ref_folders: &FnvHashMap) -> FolderNode { - let mut node = FolderNode { - hash: h, - kids: Vec::new(), - }; - for &c in ref_folders[&h].children() { - node.kids.push(rec(c, ref_folders)); - } - node - }; - - tree.push(rec(*h, &ref_folders)); - for &c in f.children() { - stack.push(c); - } - while let Some(next) = stack.pop() { - for c in ref_folders[&next].children() { - stack.push(*c); - } - } - } folders.insert( *h, MailboxEntry::Parsing(Mailbox::new(f.clone(), &FnvHashMap::default()), 0, 0), @@ -430,40 +407,7 @@ impl Account { collection.threads.insert(*h, Threads::default()); } - tree.sort_unstable_by(|a, b| { - if ref_folders[&b.hash].path().eq_ignore_ascii_case("INBOX") { - std::cmp::Ordering::Greater - } else if ref_folders[&a.hash].path().eq_ignore_ascii_case("INBOX") { - std::cmp::Ordering::Less - } else { - ref_folders[&a.hash] - .path() - .cmp(&ref_folders[&b.hash].path()) - } - }); - - let mut stack: SmallVec<[Option<&FolderNode>; 8]> = SmallVec::new(); - for n in tree.iter_mut() { - folders_order.push(n.hash); - n.kids.sort_unstable_by(|a, b| { - if ref_folders[&b.hash].path().eq_ignore_ascii_case("INBOX") { - std::cmp::Ordering::Greater - } else if ref_folders[&a.hash].path().eq_ignore_ascii_case("INBOX") { - std::cmp::Ordering::Less - } else { - ref_folders[&a.hash] - .path() - .cmp(&ref_folders[&b.hash].path()) - } - }); - stack.extend(n.kids.iter().rev().map(Some)); - while let Some(Some(next)) = stack.pop() { - folders_order.push(next.hash); - stack.extend(next.kids.iter().rev().map(Some)); - } - } - drop(stack); - + build_folders_order(&folder_confs, &mut tree, &ref_folders, &mut folders_order); self.folders = folders; self.ref_folders = ref_folders; self.folder_confs = folder_confs; @@ -784,11 +728,7 @@ impl Account { self.folders.is_empty() } pub fn list_folders(&self) -> Vec { - let mut folders = if let Ok(folders) = self.backend.read().unwrap().folders() { - folders - } else { - return Vec::new(); - }; + let mut folders = self.ref_folders.clone(); let folder_confs = &self.folder_confs; //debug!("folder renames: {:?}", folder_renames); for f in folders.values_mut() { @@ -1015,7 +955,68 @@ impl Account { } pub fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> { - Err(MeliError::new("Not implemented.")) + match op { + FolderOperation::Create => { + if self.settings.account.read_only() { + Err(MeliError::new("Account is read-only.")) + } else { + let mut folder = self + .backend + .write() + .unwrap() + .create_folder(path.to_string())?; + let mut new = FileFolderConf::default(); + new.folder_conf.subscribe = super::ToggleFlag::InternalVal(true); + new.folder_conf.usage = if folder.special_usage() != SpecialUsageMailbox::Normal + { + Some(folder.special_usage()) + } else { + let tmp = SpecialUsageMailbox::detect_usage(folder.name()); + if tmp != Some(SpecialUsageMailbox::Normal) && tmp != None { + let _ = folder.set_special_usage(tmp.unwrap()); + } + tmp + }; + + self.folder_confs.insert(folder.hash(), new); + self.folder_names + .insert(folder.hash(), folder.path().to_string()); + self.folders.insert( + folder.hash(), + MailboxEntry::Parsing( + Mailbox::new(folder.clone(), &FnvHashMap::default()), + 0, + 0, + ), + ); + self.workers.insert( + folder.hash(), + Account::new_worker( + folder.clone(), + &mut self.backend, + &self.work_context, + self.notify_fn.clone(), + ), + ); + self.collection + .threads + .insert(folder.hash(), Threads::default()); + self.ref_folders.insert(folder.hash(), folder); + build_folders_order( + &self.folder_confs, + &mut self.tree, + &self.ref_folders, + &mut self.folders_order, + ); + Ok(()) + } + } + FolderOperation::Delete => Err(MeliError::new("Not implemented.")), + FolderOperation::Subscribe => Err(MeliError::new("Not implemented.")), + FolderOperation::Unsubscribe => Err(MeliError::new("Not implemented.")), + FolderOperation::Rename(_) => Err(MeliError::new("Not implemented.")), + FolderOperation::SetPermissions(_) => Err(MeliError::new("Not implemented.")), + } } pub fn folder_confs(&self, folder_hash: FolderHash) -> &FileFolderConf { @@ -1147,3 +1148,75 @@ impl IndexMut for Account { self.folders.get_mut(&self.folders_order[index]).unwrap() } } + +fn build_folders_order( + folder_confs: &FnvHashMap, + tree: &mut Vec, + ref_folders: &FnvHashMap, + folders_order: &mut Vec, +) { + let mut stack: SmallVec<[FolderHash; 8]> = SmallVec::new(); + tree.clear(); + folders_order.clear(); + for (h, f) in ref_folders.iter() { + if !folder_confs.contains_key(&h) { + continue; + } + + if f.parent().is_none() { + fn rec(h: FolderHash, ref_folders: &FnvHashMap) -> FolderNode { + let mut node = FolderNode { + hash: h, + kids: Vec::new(), + }; + for &c in ref_folders[&h].children() { + node.kids.push(rec(c, ref_folders)); + } + node + }; + + tree.push(rec(*h, &ref_folders)); + for &c in f.children() { + stack.push(c); + } + while let Some(next) = stack.pop() { + for c in ref_folders[&next].children() { + stack.push(*c); + } + } + } + } + + tree.sort_unstable_by(|a, b| { + if ref_folders[&b.hash].path().eq_ignore_ascii_case("INBOX") { + std::cmp::Ordering::Greater + } else if ref_folders[&a.hash].path().eq_ignore_ascii_case("INBOX") { + std::cmp::Ordering::Less + } else { + ref_folders[&a.hash] + .path() + .cmp(&ref_folders[&b.hash].path()) + } + }); + + let mut stack: SmallVec<[Option<&FolderNode>; 8]> = SmallVec::new(); + for n in tree.iter_mut() { + folders_order.push(n.hash); + n.kids.sort_unstable_by(|a, b| { + if ref_folders[&b.hash].path().eq_ignore_ascii_case("INBOX") { + std::cmp::Ordering::Greater + } else if ref_folders[&a.hash].path().eq_ignore_ascii_case("INBOX") { + std::cmp::Ordering::Less + } else { + ref_folders[&a.hash] + .path() + .cmp(&ref_folders[&b.hash].path()) + } + }); + stack.extend(n.kids.iter().rev().map(Some)); + while let Some(Some(next)) = stack.pop() { + folders_order.push(next.hash); + stack.extend(next.kids.iter().rev().map(Some)); + } + } +} diff --git a/ui/src/execute.rs b/ui/src/execute.rs index 2d50686a..e6932e45 100644 --- a/ui/src/execute.rs +++ b/ui/src/execute.rs @@ -23,7 +23,7 @@ */ use melib::backends::FolderOperation; pub use melib::thread::{SortField, SortOrder}; -use nom::{digit, not_line_ending}; +use nom::{digit, not_line_ending, IResult}; use std; pub mod actions; pub mod history; @@ -45,6 +45,26 @@ macro_rules! define_commands { }; } +pub fn quoted_argument(input: &[u8]) -> IResult<&[u8], &str> { + if input.is_empty() { + return IResult::Error(nom::ErrorKind::Custom(0)); + } + + if input[0] == b'"' { + let mut i = 1; + while i < input.len() { + if input[i] == b'\"' && input[i - 1] != b'\\' { + return IResult::Done(&input[i + 1..], unsafe { + std::str::from_utf8_unchecked(&input[1..i]) + }); + } + i += 1; + } + return IResult::Error(nom::ErrorKind::Custom(0)); + } else { + return map_res!(input, is_not!(" "), std::str::from_utf8); + } +} define_commands!([ { tags: ["set"], desc: "set [seen/unseen], toggles message's Seen flag.", @@ -179,9 +199,9 @@ define_commands!([ ws!(tag!("pipe")) >> bin: map_res!(is_not!(" "), std::str::from_utf8) >> is_a!(" ") - >> args: separated_list!(is_a!(" "), is_not!(" ")) + >> args: separated_list!(is_a!(" "), quoted_argument) >> ({ - View(Pipe(bin.to_string(), args.into_iter().map(|v| String::from_utf8(v.to_vec()).unwrap()).collect::>())) + View(Pipe(bin.to_string(), args.into_iter().map(String::from).collect::>())) })) | do_parse!( ws!(tag!("pipe")) >> bin: ws!(map_res!(is_not!(" "), std::str::from_utf8)) @@ -233,7 +253,7 @@ define_commands!([ named!( create_folder, do_parse!( ws!(tag!("create-folder")) - >> account: map_res!(is_not!(" "), std::str::from_utf8) + >> account: quoted_argument >> is_a!(" ") >> path: map_res!(call!(not_line_ending), std::str::from_utf8) >> (Folder(account.to_string(), path.to_string(), FolderOperation::Create)) @@ -247,7 +267,7 @@ define_commands!([ named!( sub_folder, do_parse!( ws!(tag!("subscribe-folder")) - >> account: map_res!(is_not!(" "), std::str::from_utf8) + >> account: quoted_argument >> is_a!(" ") >> path: map_res!(call!(not_line_ending), std::str::from_utf8) >> (Folder(account.to_string(), path.to_string(), FolderOperation::Subscribe)) @@ -261,7 +281,7 @@ define_commands!([ named!( unsub_folder, do_parse!( ws!(tag!("unsubscribe-folder")) - >> account: map_res!(is_not!(" "), std::str::from_utf8) + >> account: quoted_argument >> is_a!(" ") >> path: map_res!(call!(not_line_ending), std::str::from_utf8) >> (Folder(account.to_string(), path.to_string(), FolderOperation::Unsubscribe)) @@ -275,9 +295,9 @@ define_commands!([ named!( rename_folder, do_parse!( ws!(tag!("rename-folder")) - >> account: map_res!(is_not!(" "), std::str::from_utf8) + >> account: quoted_argument >> is_a!(" ") - >> src: map_res!(is_not!(" "), std::str::from_utf8) + >> src: quoted_argument >> is_a!(" ") >> dest: map_res!(call!(not_line_ending), std::str::from_utf8) >> (Folder(account.to_string(), src.to_string(), FolderOperation::Rename(dest.to_string()))) @@ -291,9 +311,9 @@ define_commands!([ named!( delete_folder, do_parse!( ws!(tag!("delete-folder")) - >> account: map_res!(is_not!(" "), std::str::from_utf8) + >> account: quoted_argument >> is_a!(" ") - >> path: map_res!(call!(not_line_ending), std::str::from_utf8) + >> path: quoted_argument >> (Folder(account.to_string(), path.to_string(), FolderOperation::Delete)) ) ); @@ -305,7 +325,7 @@ define_commands!([ named!( reindex, do_parse!( ws!(tag!("reindex")) - >> account: map_res!(is_not!(" "), std::str::from_utf8) + >> account: quoted_argument >> (AccountAction(account.to_string(), ReIndex)) ) ); @@ -329,7 +349,7 @@ define_commands!([ do_parse!( ws!(tag!("save-attachment")) >> idx: map_res!(map_res!(is_not!(" "), std::str::from_utf8), usize::from_str) - >> path: ws!(map_res!(call!(not_line_ending), std::str::from_utf8)) + >> path: ws!(quoted_argument) >> (View(SaveAttachment(idx, path.to_string()))) ) ); diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 98d23ed6..7ec3ad48 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -231,7 +231,6 @@ pub mod timer { impl PosixTimer { pub fn rearm(&mut self) { - debug!("posixtimer rearm"); let spec = itimerspec { it_interval: timespec { tv_sec: self.interval.as_secs().try_into().unwrap_or(0), diff --git a/ui/src/plugins/backend.rs b/ui/src/plugins/backend.rs index e1062ca0..4731a1ee 100644 --- a/ui/src/plugins/backend.rs +++ b/ui/src/plugins/backend.rs @@ -102,7 +102,7 @@ impl MailBackend for PluginBackend { channel.expect_ack().unwrap(); loop { let read_val: Result>>> = - debug!(channel.from_read()); + channel.from_read(); match read_val.map(Into::into).and_then(std::convert::identity) { Ok(Some(a)) => { tx.send(AsyncStatus::Payload(Ok(a @@ -300,7 +300,7 @@ impl BackendOp for PluginOp { debug!(channel.expect_ack())?; channel.write_ref(&rmpv::ValueRef::Integer(self.hash.into()))?; debug!(channel.expect_ack())?; - let bytes: Result> = debug!(channel.from_read()); + let bytes: Result> = channel.from_read(); self.bytes = Some(bytes.map(Into::into).and_then(std::convert::identity)?); if let Some(ref bytes) = self.bytes { debug!(Envelope::from_bytes(bytes.as_bytes(), None)); diff --git a/ui/src/state.rs b/ui/src/state.rs index 1067f2e0..518c242f 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -254,7 +254,6 @@ impl State { let sender = sender; loop { thread::park(); - debug!("unparked"); sender.send(ThreadEvent::Pulse).unwrap(); thread::sleep(std::time::Duration::from_millis(100)); @@ -586,6 +585,13 @@ impl State { self.context.replies.push_back(UIEvent::StatusEvent( StatusEvent::DisplayMessage(e.to_string()), )); + } else { + self.context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "{} succesfully created in `{}`", + path, account_name + )), + )); } } else { self.context.replies.push_back(UIEvent::StatusEvent( diff --git a/ui/src/types.rs b/ui/src/types.rs index e8c76ec6..c2e2a985 100644 --- a/ui/src/types.rs +++ b/ui/src/types.rs @@ -181,7 +181,7 @@ pub mod segment_tree { let height = (f64::from(u32::try_from(val.len()).unwrap_or(0))) .log2() .ceil() as u32; - let max_size = 2 * (2_usize.pow(height)) - 1; + let max_size = 2 * (2_usize.pow(height)); let mut segment_tree: SmallVec<[u8; 1024]> = SmallVec::from_iter(core::iter::repeat(0).take(max_size));