diff --git a/melib/src/backends.rs b/melib/src/backends.rs index c68e2dfb..5d83fef6 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -64,6 +64,17 @@ use std::sync::{Arc, RwLock}; use fnv::FnvHashMap; use std; +#[macro_export] +macro_rules! get_path_hash { + ($path:expr) => {{ + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + $path.hash(&mut hasher); + hasher.finish() + }}; +} + pub type BackendCreator = Box< dyn Fn( &AccountSettings, @@ -232,18 +243,6 @@ impl NotifyFn { } } -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum FolderOperation { - Create, - Delete, - Subscribe, - Unsubscribe, - Rename(NewFolderName), - SetPermissions(FolderPermissions), -} - -type NewFolderName = String; - pub trait MailBackend: ::std::fmt::Debug + Send + Sync { fn is_online(&self) -> Result<()>; fn connect(&mut self) {} @@ -264,9 +263,6 @@ 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, _path: String) -> Result { - unimplemented!() - } fn tags(&self) -> Option>>> { None } @@ -275,6 +271,30 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any { unimplemented!() } + + fn create_folder(&mut self, _path: String) -> Result { + Err(MeliError::new("Unimplemented.")) + } + + fn delete_folder(&mut self, _folder_hash: FolderHash) -> Result<()> { + Err(MeliError::new("Unimplemented.")) + } + + fn set_folder_subscription(&mut self, _folder_hash: FolderHash, _val: bool) -> Result<()> { + Err(MeliError::new("Unimplemented.")) + } + + fn rename_folder(&mut self, _folder_hash: FolderHash, _new_path: String) -> Result { + Err(MeliError::new("Unimplemented.")) + } + + fn set_folder_permissions( + &mut self, + _folder_hash: FolderHash, + _val: FolderPermissions, + ) -> Result<()> { + Err(MeliError::new("Unimplemented.")) + } } /// A `BackendOp` manages common operations for the various mail backends. They only live for the @@ -319,8 +339,6 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync { pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send { fn description(&self) -> String; fn as_bytes(&mut self) -> Result<&[u8]>; - //fn delete(&self) -> (); - //fn copy(&self fn fetch_flags(&self) -> Flag; fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag, value: bool) -> Result<()>; fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()>; @@ -536,3 +554,9 @@ impl Default for FolderPermissions { } } } + +impl std::fmt::Display for FolderPermissions { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:#?}", self) + } +} diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index e20d9ad9..56f683b0 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -19,6 +19,7 @@ * along with meli. If not, see . */ +use crate::get_path_hash; use smallvec::SmallVec; #[macro_use] mod protocol_parser; @@ -158,12 +159,12 @@ impl MailBackend for ImapType { let uid_store = self.uid_store.clone(); let tag_index = self.tag_index.clone(); let can_create_flags = self.can_create_flags.clone(); - let folder_path = folder.path().to_string(); let folder_hash = folder.hash(); - let (permissions, folder_exists, no_select, unseen) = { + let (permissions, folder_path, folder_exists, no_select, unseen) = { let f = &self.folders.read().unwrap()[&folder_hash]; ( f.permissions.clone(), + f.imap_path().to_string(), f.exists.clone(), f.no_select, f.unseen.clone(), @@ -374,7 +375,7 @@ impl MailBackend for ImapType { Box::new(ImapOp::new( uid, self.folders.read().unwrap()[&folder_hash] - .path() + .imap_path() .to_string(), self.connection.clone(), self.uid_store.clone(), @@ -400,7 +401,7 @@ impl MailBackend for ImapType { } f_result - .map(|v| v.path().to_string()) + .map(|v| v.imap_path().to_string()) .ok_or(MeliError::new(format!( "Folder with name {} not found.", folder @@ -425,70 +426,6 @@ impl MailBackend for ImapType { Ok(()) } - /* - fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> { - use FolderOperation::*; - - match ( - &op, - self.folders - .read() - .unwrap() - .values() - .any(|f| f.path == path), - ) { - (Create, true) => { - return Err(MeliError::new(format!( - "Folder named `{}` in account `{}` already exists.", - path, self.account_name, - ))); - } - (op, false) if *op != Create => { - return Err(MeliError::new(format!( - "No folder named `{}` in account `{}`", - path, self.account_name, - ))); - } - _ => {} - } - - let mut response = String::with_capacity(8 * 1024); - match op { - Create => { - let mut conn = self.connection.lock()?; - conn.send_command(format!("CREATE \"{}\"", path,).as_bytes())?; - conn.read_response(&mut response)?; - conn.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?; - conn.read_response(&mut response)?; - } - Rename(dest) => { - let mut conn = self.connection.lock()?; - conn.send_command(format!("RENAME \"{}\" \"{}\"", path, dest).as_bytes())?; - conn.read_response(&mut response)?; - } - Delete => { - let mut conn = self.connection.lock()?; - conn.send_command(format!("DELETE \"{}\"", path,).as_bytes())?; - conn.read_response(&mut response)?; - } - Subscribe => { - let mut conn = self.connection.lock()?; - conn.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?; - conn.read_response(&mut response)?; - } - Unsubscribe => { - let mut conn = self.connection.lock()?; - conn.send_command(format!("UNSUBSCRIBE \"{}\"", path,).as_bytes())?; - conn.read_response(&mut response)?; - } - SetPermissions(_new_val) => { - unimplemented!(); - } - } - Ok(()) - } - */ - fn as_any(&self) -> &dyn::std::any::Any { self } @@ -505,36 +442,191 @@ impl MailBackend for ImapType { } } - 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) - { + fn create_folder(&mut self, mut path: String) -> Result { + /* Must transform path to something the IMAP server will accept + * + * Each root mailbox has a hierarchy delimeter reported by the LIST entry. All paths + * must use this delimeter to indicate children of this mailbox. + * + * A new root mailbox should have the default delimeter, which can be found out by issuing + * an empty LIST command as described in RFC3501: + * C: A101 LIST "" "" + * S: * LIST (\Noselect) "/" "" + * + * The default delimiter for us is '/' just like UNIX paths. I apologise if this + * decision is unpleasant for you. + */ + + let mut folders = self.folders.write().unwrap(); + for root_folder in folders.values().filter(|f| f.parent.is_none()) { + if path.starts_with(&root_folder.name) { + debug!("path starts with {:?}", &root_folder); + path = path.replace( + '/', + (root_folder.separator as char).encode_utf8(&mut [0; 4]), + ); + break; + } + } + + if folders.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?", + + let mut response = String::with_capacity(8 * 1024); + { + let mut conn_lck = self.connection.lock()?; + + conn_lck.send_command(format!("CREATE \"{}\"", path,).as_bytes())?; + conn_lck.read_response(&mut response)?; + conn_lck.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?; + conn_lck.read_response(&mut response)?; + } + let ret: Result<()> = ImapResponse::from(&response).into(); + ret?; + folders.clear(); + drop(folders); + self.folders().map_err(|err| format!("Mailbox create was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?; + let new_hash = get_path_hash!(path.as_str()); + Ok(BackendFolder::clone( + &self.folders.read().unwrap()[&new_hash], + )) + } + + fn delete_folder(&mut self, folder_hash: FolderHash) -> Result<()> { + let mut folders = self.folders.write().unwrap(); + let permissions = folders[&folder_hash].permissions(); + if !permissions.delete_mailbox { + return Err(MeliError::new(format!("You do not have permission to delete `{}`. Set permissions for this mailbox are {}", folders[&folder_hash].name(), permissions))); + } + let mut response = String::with_capacity(8 * 1024); + { + let mut conn_lck = self.connection.lock()?; + if folders[&folder_hash].is_subscribed() { + conn_lck.send_command( + format!("UNSUBSCRIBE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(), + )?; + conn_lck.read_response(&mut response)?; + } + + if !folders[&folder_hash].no_select { + /* make sure mailbox is not selected before it gets deleted, otherwise + * connection gets dropped by server */ + if conn_lck + .capabilities + .iter() + .any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT")) + { + conn_lck.send_command( + format!("UNSELECT \"{}\"", folders[&folder_hash].imap_path()).as_bytes(), + )?; + conn_lck.read_response(&mut response)?; + } else { + conn_lck.send_command( + format!("SELECT \"{}\"", folders[&folder_hash].imap_path()).as_bytes(), + )?; + conn_lck.read_response(&mut response)?; + conn_lck.send_command( + format!("EXAMINE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(), + )?; + conn_lck.read_response(&mut response)?; + } + } + conn_lck.send_command( + debug!(format!("DELETE \"{}\"", folders[&folder_hash].imap_path())).as_bytes(), + )?; + conn_lck.read_response(&mut response)?; + } + let ret: Result<()> = ImapResponse::from(&response).into(); + if ret.is_ok() { + folders.clear(); + drop(folders); + self.folders().map_err(|err| format!("Mailbox delete was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?; + } + ret + } + + fn set_folder_subscription(&mut self, folder_hash: FolderHash, new_val: bool) -> Result<()> { + let mut folders = self.folders.write().unwrap(); + if folders[&folder_hash].is_subscribed() == new_val { + return Ok(()); + } + + let mut response = String::with_capacity(8 * 1024); + { + let mut conn_lck = self.connection.lock()?; + if new_val { + conn_lck.send_command( + format!("SUBSCRIBE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(), + )?; + } else { + conn_lck.send_command( + format!("UNSUBSCRIBE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(), + )?; + } + conn_lck.read_response(&mut response)?; + } + + let ret: Result<()> = ImapResponse::from(&response).into(); + if ret.is_ok() { + folders.entry(folder_hash).and_modify(|entry| { + let _ = entry.set_is_subscribed(new_val); + }); + } + ret + } + + fn rename_folder(&mut self, folder_hash: FolderHash, mut new_path: String) -> Result { + let mut folders = self.folders.write().unwrap(); + let permissions = folders[&folder_hash].permissions(); + if !permissions.delete_mailbox { + return Err(MeliError::new(format!("You do not have permission to rename folder `{}` (rename is equivalent to delete + create). Set permissions for this mailbox are {}", folders[&folder_hash].name(), permissions))); + } + let mut response = String::with_capacity(8 * 1024); + if folders[&folder_hash].separator != b'/' { + new_path = new_path.replace( + '/', + (folders[&folder_hash].separator as char).encode_utf8(&mut [0; 4]), + ); + } + { + let mut conn_lck = self.connection.lock()?; + conn_lck.send_command( + debug!(format!( + "RENAME \"{}\" \"{}\"", + folders[&folder_hash].imap_path(), + new_path )) - }) + .as_bytes(), + )?; + conn_lck.read_response(&mut response)?; + } + let new_hash = get_path_hash!(new_path.as_str()); + let ret: Result<()> = ImapResponse::from(&response).into(); + ret?; + folders.clear(); + drop(folders); + self.folders().map_err(|err| format!("Mailbox rename was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?; + Ok(BackendFolder::clone( + &self.folders.read().unwrap()[&new_hash], + )) + } + + fn set_folder_permissions( + &mut self, + folder_hash: FolderHash, + _val: crate::backends::FolderPermissions, + ) -> Result<()> { + let folders = self.folders.write().unwrap(); + let permissions = folders[&folder_hash].permissions(); + if !permissions.change_permissions { + return Err(MeliError::new(format!("You do not have permission to change permissions for folder `{}`. Set permissions for this mailbox are {}", folders[&folder_hash].name(), permissions))); + } + + Err(MeliError::new("Unimplemented.")) } } @@ -707,7 +799,9 @@ impl ImapType { let folders_lck = self.folders.read()?; let mut response = String::with_capacity(8 * 1024); let mut conn = self.connection.lock()?; - conn.send_command(format!("EXAMINE \"{}\"", folders_lck[&folder_hash].path()).as_bytes())?; + conn.send_command( + format!("EXAMINE \"{}\"", folders_lck[&folder_hash].imap_path()).as_bytes(), + )?; conn.read_response(&mut response)?; conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query).as_bytes())?; conn.read_response(&mut response)?; diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index edcde379..20b39437 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -340,8 +340,14 @@ impl ImapConnection { pub fn read_response(&mut self, ret: &mut String) -> Result<()> { self.try_send(|s| s.read_response(ret))?; - let r: Result<()> = ImapResponse::from(&ret).into(); - r + let r: ImapResponse = ImapResponse::from(&ret); + if let ImapResponse::Bye(ref response_code) = r { + self.stream = Err(MeliError::new(format!( + "Offline: received BYE: {:?}", + response_code + ))); + } + r.into() } pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> { diff --git a/melib/src/backends/imap/folder.rs b/melib/src/backends/imap/folder.rs index 399c26d2..4eb45529 100644 --- a/melib/src/backends/imap/folder.rs +++ b/melib/src/backends/imap/folder.rs @@ -25,10 +25,12 @@ use std::sync::{Arc, Mutex, RwLock}; #[derive(Debug, Default, Clone)] pub struct ImapFolder { pub(super) hash: FolderHash, + pub(super) imap_path: String, pub(super) path: String, pub(super) name: String, pub(super) parent: Option, pub(super) children: Vec, + pub separator: u8, pub usage: Arc>, pub no_select: bool, pub is_subscribed: bool, @@ -38,6 +40,12 @@ pub struct ImapFolder { pub unseen: Arc>, } +impl ImapFolder { + pub fn imap_path(&self) -> &str { + &self.imap_path + } +} + impl BackendFolder for ImapFolder { fn hash(&self) -> FolderHash { self.hash @@ -79,7 +87,6 @@ impl BackendFolder for ImapFolder { } fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> { self.is_subscribed = new_val; - // FIXME: imap subscribe Ok(()) } diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index ac6c4057..3819b28e 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -21,9 +21,8 @@ use super::*; use crate::email::parser::BytesExt; +use crate::get_path_hash; use nom::{digit, is_digit, rest, IResult}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; use std::str::FromStr; #[derive(Debug)] @@ -65,54 +64,55 @@ pub enum ResponseCode { Uidvalidity(UID), /// Followed by a decimal number, indicates the number of the first message without the \Seen flag set. Unseen(usize), + + None, } impl ResponseCode { - fn from(val: &str) -> Option { + fn from(val: &str) -> ResponseCode { + use ResponseCode::*; if !val.starts_with("[") { return None; } let val = &val[1..]; - use ResponseCode::*; if val.starts_with("BADCHARSET") { - Some(Badcharset) + Badcharset } else if val.starts_with("READONLY") { - Some(ReadOnly) + ReadOnly } else if val.starts_with("READWRITE") { - Some(ReadWrite) + ReadWrite } else if val.starts_with("TRYCREATE") { - Some(Trycreate) + Trycreate } else if val.starts_with("UIDNEXT") { //FIXME - Some(Uidnext(0)) + Uidnext(0) } else if val.starts_with("UIDVALIDITY") { //FIXME - Some(Uidvalidity(0)) + Uidvalidity(0) } else if val.starts_with("UNSEEN") { //FIXME - Some(Unseen(0)) + Unseen(0) } else { let msg = &val[val.as_bytes().find(b"] ").unwrap() + 1..].trim(); - Some(Alert(msg.to_string())) + Alert(msg.to_string()) } } } #[derive(Debug, PartialEq)] pub enum ImapResponse { - Ok(Option), - No(Option), - Bad(Option), - Preauth(Option), - Bye(Option), + Ok(ResponseCode), + No(ResponseCode), + Bad(ResponseCode), + Preauth(ResponseCode), + Bye(ResponseCode), } 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).") { @@ -139,8 +139,9 @@ 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(ResponseCode::Alert(msg)) | Self::Bad(ResponseCode::Alert(msg)) => { + Err(MeliError::new(msg)) + } Self::No(_) => Err(MeliError::new("IMAP NO Response.")), Self::Bad(_) => Err(MeliError::new("IMAP BAD Response.")), } @@ -149,7 +150,7 @@ impl Into> for ImapResponse { #[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())))); + 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(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string()))); } impl<'a> Iterator for ImapLineIterator<'a> { @@ -206,13 +207,7 @@ macro_rules! dbg_dmp ( 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 @@ -243,14 +238,20 @@ named!( let _ = f.set_special_usage(SpecialUsageMailbox::Drafts); } } - 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() + f.imap_path = String::from_utf8_lossy(path).into(); + f.hash = get_path_hash!(&f.imap_path); + f.path = if separator == b'/' { + f.imap_path.clone() } else { - f.path.clone() + f.imap_path.replace(separator as char, "/") }; + f.name = if let Some(pos) = f.imap_path.as_bytes().iter().rposition(|&c| c == separator) { + f.parent = Some(get_path_hash!(&f.imap_path[..pos])); + f.imap_path[pos + 1..].to_string() + } else { + f.imap_path.clone() + }; + f.separator = separator; debug!(f) }) diff --git a/melib/src/backends/imap/watch.rs b/melib/src/backends/imap/watch.rs index 27519c14..b8a31864 100644 --- a/melib/src/backends/imap/watch.rs +++ b/melib/src/backends/imap/watch.rs @@ -149,7 +149,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { folder_hash, work_context, thread_id, - conn.send_command(format!("SELECT \"{}\"", folder.path()).as_bytes()) + conn.send_command(format!("SELECT \"{}\"", folder.imap_path()).as_bytes()) conn.read_response(&mut response) ); debug!("select response {}", &response); @@ -531,7 +531,7 @@ fn examine_updates( folder_hash, work_context, thread_id, - conn.send_command(format!("EXAMINE \"{}\"", folder.path()).as_bytes()) + conn.send_command(format!("EXAMINE \"{}\"", folder.imap_path()).as_bytes()) conn.read_response(&mut response) ); match protocol_parser::select_response(&response) { diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index 10bb7092..253c3d96 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -63,16 +63,6 @@ pub trait Method: Serialize { const NAME: &'static str; } -macro_rules! get_path_hash { - ($path:expr) => {{ - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - $path.hash(&mut hasher); - hasher.finish() - }}; -} - static USING: &'static [&'static str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"]; #[derive(Serialize)] diff --git a/melib/src/backends/maildir/backend.rs b/melib/src/backends/maildir/backend.rs index 6426abfc..829fcb3d 100644 --- a/melib/src/backends/maildir/backend.rs +++ b/melib/src/backends/maildir/backend.rs @@ -34,12 +34,7 @@ extern crate notify; use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use std::time::Duration; -use std::sync::mpsc::channel; -//use std::sync::mpsc::sync_channel; -//use std::sync::mpsc::SyncSender; -//use std::time::Duration; use fnv::{FnvHashMap, FnvHashSet, FnvHasher}; -use std::collections::hash_map::DefaultHasher; use std::ffi::OsStr; use std::fs; use std::hash::{Hash, Hasher}; @@ -48,6 +43,7 @@ use std::ops::{Deref, DerefMut}; use std::os::unix::fs::PermissionsExt; use std::path::{Component, Path, PathBuf}; use std::result; +use std::sync::mpsc::channel; use std::sync::{Arc, Mutex}; use std::thread; @@ -132,7 +128,6 @@ macro_rules! path_is_new { }; } -#[macro_export] macro_rules! get_path_hash { ($path:expr) => {{ let mut path = $path.clone(); @@ -145,9 +140,7 @@ macro_rules! get_path_hash { path.pop(); }; - let mut hasher = DefaultHasher::new(); - path.hash(&mut hasher); - hasher.finish() + crate::get_path_hash!(path) }}; } @@ -642,6 +635,64 @@ impl MailBackend for MaildirType { fn as_any(&self) -> &dyn::std::any::Any { self } + + fn create_folder(&mut self, new_path: String) -> Result { + let mut path = self.path.clone(); + path.push(&new_path); + if !path.starts_with(&self.path) { + return Err(MeliError::new(format!("Path given (`{}`) is absolute. Please provide a path relative to the account's root folder.", &new_path))); + } + + std::fs::create_dir(&path)?; + /* create_dir does not create intermediate directories (like `mkdir -p`), so the parent must be a valid + * folder at this point. */ + + let parent = path.parent().and_then(|p| { + self.folders + .iter() + .find(|(_, f)| f.fs_path == p) + .map(|item| *item.0) + }); + + let folder_hash = get_path_hash!(&path); + let new_folder = MaildirFolder { + hash: folder_hash, + path: PathBuf::from(&new_path), + name: new_path, + fs_path: path, + parent, + children: vec![], + usage: Default::default(), + is_subscribed: true, + permissions: Default::default(), + unseen: Default::default(), + total: Default::default(), + }; + + let ret = BackendFolder::clone(debug!(&new_folder)); + self.folders.insert(folder_hash, new_folder); + Ok(ret) + } + + fn delete_folder(&mut self, _folder_hash: FolderHash) -> Result<()> { + Err(MeliError::new("Unimplemented.")) + } + + fn set_folder_subscription(&mut self, _folder_hash: FolderHash, _val: bool) -> Result<()> { + Err(MeliError::new("Unimplemented.")) + } + + fn rename_folder(&mut self, _folder_hash: FolderHash, _new_path: String) -> Result { + Err(MeliError::new("Unimplemented.")) + } + + fn set_folder_permissions( + &mut self, + _folder_hash: FolderHash, + _val: crate::backends::FolderPermissions, + ) -> Result<()> { + Err(MeliError::new("Unimplemented.")) + } } impl MaildirType { diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index 03c697a5..46fedf6c 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -34,6 +34,7 @@ use crate::conf::AccountSettings; use crate::email::parser::BytesExt; use crate::email::*; use crate::error::{MeliError, Result}; +use crate::get_path_hash; use crate::shellexpand::ShellExpandTrait; use fnv::FnvHashMap; use libc; @@ -41,9 +42,7 @@ use memmap::{Mmap, Protection}; use nom::{IResult, Needed}; extern crate notify; use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; -use std::collections::hash_map::DefaultHasher; use std::fs::File; -use std::hash::{Hash, Hasher}; use std::io::BufReader; use std::io::Read; use std::os::unix::io::AsRawFd; @@ -74,14 +73,6 @@ fn get_rw_lock_blocking(f: &File) { assert!(-1 != ret_val); } -macro_rules! get_path_hash { - ($path:expr) => {{ - let mut hasher = DefaultHasher::new(); - $path.hash(&mut hasher); - hasher.finish() - }}; -} - #[derive(Debug)] struct MboxFolder { hash: FolderHash, diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs index 8ed24e9d..f18f4042 100644 --- a/src/conf/accounts.rs +++ b/src/conf/accounts.rs @@ -27,8 +27,8 @@ use super::{AccountConf, FileFolderConf}; use fnv::FnvHashMap; use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext}; use melib::backends::{ - BackendOp, Backends, Folder, FolderHash, FolderOperation, MailBackend, NotifyFn, ReadOnlyOp, - RefreshEvent, RefreshEventConsumer, RefreshEventKind, SpecialUsageMailbox, + BackendOp, Backends, Folder, FolderHash, MailBackend, NotifyFn, ReadOnlyOp, RefreshEvent, + RefreshEventConsumer, RefreshEventKind, SpecialUsageMailbox, }; use melib::error::{MeliError, Result}; use melib::mailbox::*; @@ -964,67 +964,112 @@ impl Account { &self.collection.threads[&f].thread_nodes()[&h] } - pub fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> { + pub fn folder_operation( + &mut self, + op: crate::execute::actions::FolderOperation, + ) -> Result { + use crate::execute::actions::FolderOperation; + if self.settings.account.read_only() { + return Err(MeliError::new("Account is read-only.")); + } match op { - FolderOperation::Create => { - if self.settings.account.read_only() { - Err(MeliError::new("Account is read-only.")) + FolderOperation::Create(path) => { + let mut folder = self + .backend + .write() + .unwrap() + .create_folder(path.to_string())?; + self.sender + .send(ThreadEvent::UIEvent(UIEvent::MailboxCreate(( + self.index, + folder.hash(), + )))) + .unwrap(); + 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 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 - }; + 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(()) - } + 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 = self.backend.read().unwrap().folders()?; + build_folders_order( + &self.folder_confs, + &mut self.tree, + &self.ref_folders, + &mut self.folders_order, + ); + Ok(format!("`{}` successfully created.", &path)) } - 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::Delete(path) => { + if self.ref_folders.len() == 1 { + return Err(MeliError::new("Cannot delete only mailbox.")); + } + let folder_hash = if let Some((folder_hash, _)) = + self.ref_folders.iter().find(|(_, f)| f.path() == path) + { + *folder_hash + } else { + return Err(MeliError::new("Mailbox with that path not found.")); + }; + self.backend.write().unwrap().delete_folder(folder_hash)?; + self.sender + .send(ThreadEvent::UIEvent(UIEvent::MailboxDelete(( + self.index, + folder_hash, + )))) + .unwrap(); + self.folders.remove(&folder_hash); + self.ref_folders = self.backend.read().unwrap().folders()?; + self.folder_confs.remove(&folder_hash); + if let Some(pos) = self.folders_order.iter().position(|&h| h == folder_hash) { + self.folders_order.remove(pos); + } + self.folder_names.remove(&folder_hash); + if let Some(pos) = self.tree.iter().position(|n| n.hash == folder_hash) { + self.tree.remove(pos); + } + if self.sent_folder == Some(folder_hash) { + self.sent_folder = None; + } + self.collection.threads.remove(&folder_hash); + self.workers.remove(&folder_hash); // FIXME Kill worker as well + + // FIXME remove from settings as well + + Ok(format!("'`{}` has been deleted.", &path)) + } + 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.")), } } diff --git a/src/execute.rs b/src/execute.rs index bb7ec388..aeb8fc7c 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -21,11 +21,11 @@ /*! A parser module for user commands passed through the Execute mode. */ -use melib::backends::FolderOperation; pub use melib::thread::{SortField, SortOrder}; use nom::{digit, not_line_ending, IResult}; use std; pub mod actions; +use actions::FolderOperation; pub mod history; pub use crate::actions::AccountAction::{self, *}; pub use crate::actions::Action::{self, *}; @@ -78,7 +78,7 @@ define_commands!([ map!(ws!(tag!("seen")), |_| Listing(SetSeen)) | map!(ws!(tag!("unseen")), |_| Listing(SetUnseen)) ) - ) | map!(ws!(tag!("delete")), |_| Listing(Delete)) + ) | map!(preceded!(tag!("delete"), eof!()), |_| Listing(Delete)) ) ); ) }, @@ -255,8 +255,8 @@ define_commands!([ ws!(tag!("create-folder")) >> 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)) + >> path: quoted_argument + >> (Folder(account.to_string(), FolderOperation::Create(path.to_string()))) ) ); ) @@ -269,8 +269,8 @@ define_commands!([ ws!(tag!("subscribe-folder")) >> 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)) + >> path: quoted_argument + >> (Folder(account.to_string(), FolderOperation::Subscribe(path.to_string()))) ) ); ) @@ -283,8 +283,8 @@ define_commands!([ ws!(tag!("unsubscribe-folder")) >> 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)) + >> path: quoted_argument + >> (Folder(account.to_string(), FolderOperation::Unsubscribe(path.to_string()))) ) ); ) @@ -299,8 +299,8 @@ define_commands!([ >> is_a!(" ") >> 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()))) + >> dest: quoted_argument + >> (Folder(account.to_string(), FolderOperation::Rename(src.to_string(), dest.to_string()))) ) ); ) @@ -314,7 +314,7 @@ define_commands!([ >> account: quoted_argument >> is_a!(" ") >> path: quoted_argument - >> (Folder(account.to_string(), path.to_string(), FolderOperation::Delete)) + >> (Folder(account.to_string(), FolderOperation::Delete(path.to_string()))) ) ); ) diff --git a/src/execute/actions.rs b/src/execute/actions.rs index 2580f474..ba5eb029 100644 --- a/src/execute/actions.rs +++ b/src/execute/actions.rs @@ -24,7 +24,7 @@ */ use crate::components::Component; -use melib::backends::{FolderHash, FolderOperation}; +use melib::backends::FolderHash; pub use melib::thread::{SortField, SortOrder}; use melib::{Draft, EnvelopeHash}; @@ -86,6 +86,17 @@ pub enum AccountAction { ReIndex, } +#[derive(Debug)] +pub enum FolderOperation { + Create(NewFolderPath), + Delete(FolderPath), + Subscribe(FolderPath), + Unsubscribe(FolderPath), + Rename(FolderPath, NewFolderPath), + // Placeholder + SetPermissions(FolderPath), +} + #[derive(Debug)] pub enum Action { Listing(ListingAction), @@ -99,9 +110,10 @@ pub enum Action { SetEnv(String, String), PrintEnv(String), Compose(ComposeAction), - Folder(AccountName, FolderPath, FolderOperation), + Folder(AccountName, FolderOperation), AccountAction(AccountName, AccountAction), } type AccountName = String; type FolderPath = String; +type NewFolderPath = String; diff --git a/src/state.rs b/src/state.rs index 7b4b8b5e..7e8b11fb 100644 --- a/src/state.rs +++ b/src/state.rs @@ -612,24 +612,27 @@ impl State { ), )); } - Folder(account_name, path, op) => { + Folder(account_name, op) => { if let Some(account) = self .context .accounts .iter_mut() .find(|a| a.name() == account_name) { - if let Err(e) = account.folder_operation(&path, op) { - 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 - )), - )); + match account.folder_operation(op) { + Err(err) => { + self.context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(err.to_string()), + )); + } + Ok(msg) => { + self.context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "`{}`: {}", + account_name, msg + )), + )); + } } } else { self.context.replies.push_back(UIEvent::StatusEvent( diff --git a/src/types.rs b/src/types.rs index 8dc3638b..bbae7d07 100644 --- a/src/types.rs +++ b/src/types.rs @@ -111,6 +111,8 @@ pub enum UIEvent { Action(Action), StatusEvent(StatusEvent), MailboxUpdate((usize, FolderHash)), // (account_idx, mailbox_idx) + MailboxDelete((usize, FolderHash)), + MailboxCreate((usize, FolderHash)), ComponentKill(Uuid), WorkerProgress(FolderHash), StartupCheck(FolderHash),