melib: add mailbox delete/create to IMAP

async
Manos Pitsidianakis 2020-02-06 01:49:18 +02:00
parent d6f04c9ed3
commit f208948651
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
14 changed files with 488 additions and 262 deletions

View File

@ -64,6 +64,17 @@ use std::sync::{Arc, RwLock};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use std; 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< pub type BackendCreator = Box<
dyn Fn( dyn Fn(
&AccountSettings, &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 { pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn is_online(&self) -> Result<()>; fn is_online(&self) -> Result<()>;
fn connect(&mut self) {} fn connect(&mut self) {}
@ -264,9 +263,6 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp>; fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp>;
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()>; fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()>;
fn create_folder(&mut self, _path: String) -> Result<Folder> {
unimplemented!()
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> { fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None None
} }
@ -275,6 +271,30 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn as_any_mut(&mut self) -> &mut dyn Any { fn as_any_mut(&mut self) -> &mut dyn Any {
unimplemented!() unimplemented!()
} }
fn create_folder(&mut self, _path: String) -> Result<Folder> {
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<Folder> {
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 /// 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 { pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
fn description(&self) -> String; fn description(&self) -> String;
fn as_bytes(&mut self) -> Result<&[u8]>; fn as_bytes(&mut self) -> Result<&[u8]>;
//fn delete(&self) -> ();
//fn copy(&self
fn fetch_flags(&self) -> Flag; fn fetch_flags(&self) -> Flag;
fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag, value: bool) -> Result<()>; 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<()>; 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)
}
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
use crate::get_path_hash;
use smallvec::SmallVec; use smallvec::SmallVec;
#[macro_use] #[macro_use]
mod protocol_parser; mod protocol_parser;
@ -158,12 +159,12 @@ impl MailBackend for ImapType {
let uid_store = self.uid_store.clone(); let uid_store = self.uid_store.clone();
let tag_index = self.tag_index.clone(); let tag_index = self.tag_index.clone();
let can_create_flags = self.can_create_flags.clone(); let can_create_flags = self.can_create_flags.clone();
let folder_path = folder.path().to_string();
let folder_hash = folder.hash(); 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]; let f = &self.folders.read().unwrap()[&folder_hash];
( (
f.permissions.clone(), f.permissions.clone(),
f.imap_path().to_string(),
f.exists.clone(), f.exists.clone(),
f.no_select, f.no_select,
f.unseen.clone(), f.unseen.clone(),
@ -374,7 +375,7 @@ impl MailBackend for ImapType {
Box::new(ImapOp::new( Box::new(ImapOp::new(
uid, uid,
self.folders.read().unwrap()[&folder_hash] self.folders.read().unwrap()[&folder_hash]
.path() .imap_path()
.to_string(), .to_string(),
self.connection.clone(), self.connection.clone(),
self.uid_store.clone(), self.uid_store.clone(),
@ -400,7 +401,7 @@ impl MailBackend for ImapType {
} }
f_result f_result
.map(|v| v.path().to_string()) .map(|v| v.imap_path().to_string())
.ok_or(MeliError::new(format!( .ok_or(MeliError::new(format!(
"Folder with name {} not found.", "Folder with name {} not found.",
folder folder
@ -425,70 +426,6 @@ impl MailBackend for ImapType {
Ok(()) 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 { fn as_any(&self) -> &dyn::std::any::Any {
self self
} }
@ -505,36 +442,191 @@ impl MailBackend for ImapType {
} }
} }
fn create_folder(&mut self, path: String) -> Result<Folder> { fn create_folder(&mut self, mut path: String) -> Result<Folder> {
let mut response = String::with_capacity(8 * 1024); /* Must transform path to something the IMAP server will accept
if self *
.folders * Each root mailbox has a hierarchy delimeter reported by the LIST entry. All paths
.read() * must use this delimeter to indicate children of this mailbox.
.unwrap() *
.values() * A new root mailbox should have the default delimeter, which can be found out by issuing
.any(|f| f.path == path) * 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!( return Err(MeliError::new(format!(
"Folder named `{}` in account `{}` already exists.", "Folder named `{}` in account `{}` already exists.",
path, self.account_name, path, self.account_name,
))); )));
} }
let mut conn_lck = self.connection.lock()?;
conn_lck.send_command(debug!(format!("CREATE \"{}\"", path,)).as_bytes())?; let mut response = String::with_capacity(8 * 1024);
conn_lck.read_response(&mut response)?; {
conn_lck.send_command(debug!(format!("SUBSCRIBE \"{}\"", path,)).as_bytes())?; let mut conn_lck = self.connection.lock()?;
conn_lck.read_response(&mut response)?;
drop(conn_lck); conn_lck.send_command(format!("CREATE \"{}\"", path,).as_bytes())?;
self.folders.write().unwrap().clear(); conn_lck.read_response(&mut response)?;
self.folders().and_then(|f| { conn_lck.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?;
debug!(f) conn_lck.read_response(&mut response)?;
.into_iter() }
.find(|(_, f)| f.path() == path) let ret: Result<()> = ImapResponse::from(&response).into();
.map(|f| f.1) ret?;
.ok_or(MeliError::new( folders.clear();
"Internal error: could not find folder after creating it?", 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<Folder> {
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 folders_lck = self.folders.read()?;
let mut response = String::with_capacity(8 * 1024); let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock()?; 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.read_response(&mut response)?;
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query).as_bytes())?; conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query).as_bytes())?;
conn.read_response(&mut response)?; conn.read_response(&mut response)?;

View File

@ -340,8 +340,14 @@ impl ImapConnection {
pub fn read_response(&mut self, ret: &mut String) -> Result<()> { pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
self.try_send(|s| s.read_response(ret))?; self.try_send(|s| s.read_response(ret))?;
let r: Result<()> = ImapResponse::from(&ret).into(); let r: ImapResponse = ImapResponse::from(&ret);
r 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<()> { pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> {

View File

@ -25,10 +25,12 @@ use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct ImapFolder { pub struct ImapFolder {
pub(super) hash: FolderHash, pub(super) hash: FolderHash,
pub(super) imap_path: String,
pub(super) path: String, pub(super) path: String,
pub(super) name: String, pub(super) name: String,
pub(super) parent: Option<FolderHash>, pub(super) parent: Option<FolderHash>,
pub(super) children: Vec<FolderHash>, pub(super) children: Vec<FolderHash>,
pub separator: u8,
pub usage: Arc<RwLock<SpecialUsageMailbox>>, pub usage: Arc<RwLock<SpecialUsageMailbox>>,
pub no_select: bool, pub no_select: bool,
pub is_subscribed: bool, pub is_subscribed: bool,
@ -38,6 +40,12 @@ pub struct ImapFolder {
pub unseen: Arc<Mutex<usize>>, pub unseen: Arc<Mutex<usize>>,
} }
impl ImapFolder {
pub fn imap_path(&self) -> &str {
&self.imap_path
}
}
impl BackendFolder for ImapFolder { impl BackendFolder for ImapFolder {
fn hash(&self) -> FolderHash { fn hash(&self) -> FolderHash {
self.hash self.hash
@ -79,7 +87,6 @@ impl BackendFolder for ImapFolder {
} }
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> { fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val; self.is_subscribed = new_val;
// FIXME: imap subscribe
Ok(()) Ok(())
} }

View File

@ -21,9 +21,8 @@
use super::*; use super::*;
use crate::email::parser::BytesExt; use crate::email::parser::BytesExt;
use crate::get_path_hash;
use nom::{digit, is_digit, rest, IResult}; use nom::{digit, is_digit, rest, IResult};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug)] #[derive(Debug)]
@ -65,54 +64,55 @@ pub enum ResponseCode {
Uidvalidity(UID), Uidvalidity(UID),
/// Followed by a decimal number, indicates the number of the first message without the \Seen flag set. /// Followed by a decimal number, indicates the number of the first message without the \Seen flag set.
Unseen(usize), Unseen(usize),
None,
} }
impl ResponseCode { impl ResponseCode {
fn from(val: &str) -> Option<ResponseCode> { fn from(val: &str) -> ResponseCode {
use ResponseCode::*;
if !val.starts_with("[") { if !val.starts_with("[") {
return None; return None;
} }
let val = &val[1..]; let val = &val[1..];
use ResponseCode::*;
if val.starts_with("BADCHARSET") { if val.starts_with("BADCHARSET") {
Some(Badcharset) Badcharset
} else if val.starts_with("READONLY") { } else if val.starts_with("READONLY") {
Some(ReadOnly) ReadOnly
} else if val.starts_with("READWRITE") { } else if val.starts_with("READWRITE") {
Some(ReadWrite) ReadWrite
} else if val.starts_with("TRYCREATE") { } else if val.starts_with("TRYCREATE") {
Some(Trycreate) Trycreate
} else if val.starts_with("UIDNEXT") { } else if val.starts_with("UIDNEXT") {
//FIXME //FIXME
Some(Uidnext(0)) Uidnext(0)
} else if val.starts_with("UIDVALIDITY") { } else if val.starts_with("UIDVALIDITY") {
//FIXME //FIXME
Some(Uidvalidity(0)) Uidvalidity(0)
} else if val.starts_with("UNSEEN") { } else if val.starts_with("UNSEEN") {
//FIXME //FIXME
Some(Unseen(0)) Unseen(0)
} else { } else {
let msg = &val[val.as_bytes().find(b"] ").unwrap() + 1..].trim(); let msg = &val[val.as_bytes().find(b"] ").unwrap() + 1..].trim();
Some(Alert(msg.to_string())) Alert(msg.to_string())
} }
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum ImapResponse { pub enum ImapResponse {
Ok(Option<ResponseCode>), Ok(ResponseCode),
No(Option<ResponseCode>), No(ResponseCode),
Bad(Option<ResponseCode>), Bad(ResponseCode),
Preauth(Option<ResponseCode>), Preauth(ResponseCode),
Bye(Option<ResponseCode>), Bye(ResponseCode),
} }
impl<T: AsRef<str>> From<T> for ImapResponse { impl<T: AsRef<str>> From<T> for ImapResponse {
fn from(val: T) -> ImapResponse { fn from(val: T) -> ImapResponse {
let val: &str = val.as_ref().split_rn().last().unwrap_or(val.as_ref()); let val: &str = val.as_ref().split_rn().last().unwrap_or(val.as_ref());
debug!(&val); debug!(&val);
assert!(val.starts_with("M"));
let mut val = val[val.as_bytes().find(b" ").unwrap() + 1..].trim(); 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 // 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).") { if val.ends_with(" secs).") {
@ -139,8 +139,9 @@ impl Into<Result<()>> for ImapResponse {
fn into(self) -> Result<()> { fn into(self) -> Result<()> {
match self { match self {
Self::Ok(_) | Self::Preauth(_) | Self::Bye(_) => Ok(()), Self::Ok(_) | Self::Preauth(_) | Self::Bye(_) => Ok(()),
Self::No(Some(ResponseCode::Alert(msg))) Self::No(ResponseCode::Alert(msg)) | Self::Bad(ResponseCode::Alert(msg)) => {
| Self::Bad(Some(ResponseCode::Alert(msg))) => Err(MeliError::new(msg)), Err(MeliError::new(msg))
}
Self::No(_) => Err(MeliError::new("IMAP NO Response.")), Self::No(_) => Err(MeliError::new("IMAP NO Response.")),
Self::Bad(_) => Err(MeliError::new("IMAP BAD Response.")), Self::Bad(_) => Err(MeliError::new("IMAP BAD Response.")),
} }
@ -149,7 +150,7 @@ impl Into<Result<()>> for ImapResponse {
#[test] #[test]
fn test_imap_response() { 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> { impl<'a> Iterator for ImapLineIterator<'a> {
@ -206,13 +207,7 @@ macro_rules! dbg_dmp (
dbg_dmp!($i, call!($f)); 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 (\HasNoChildren) "." INBOX.Sent
* LIST (\HasChildren) "." INBOX * LIST (\HasChildren) "." INBOX
@ -243,14 +238,20 @@ named!(
let _ = f.set_special_usage(SpecialUsageMailbox::Drafts); let _ = f.set_special_usage(SpecialUsageMailbox::Drafts);
} }
} }
f.hash = get_path_hash!(path); f.imap_path = String::from_utf8_lossy(path).into();
f.path = String::from_utf8_lossy(path).into(); f.hash = get_path_hash!(&f.imap_path);
f.name = if let Some(pos) = path.iter().rposition(|&c| c == separator) { f.path = if separator == b'/' {
f.parent = Some(get_path_hash!(&path[..pos])); f.imap_path.clone()
String::from_utf8_lossy(&path[pos + 1..]).into()
} else { } 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) debug!(f)
}) })

View File

@ -149,7 +149,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
folder_hash, folder_hash,
work_context, work_context,
thread_id, 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) conn.read_response(&mut response)
); );
debug!("select response {}", &response); debug!("select response {}", &response);
@ -531,7 +531,7 @@ fn examine_updates(
folder_hash, folder_hash,
work_context, work_context,
thread_id, 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) conn.read_response(&mut response)
); );
match protocol_parser::select_response(&response) { match protocol_parser::select_response(&response) {

View File

@ -63,16 +63,6 @@ pub trait Method<OBJ: Object>: Serialize {
const NAME: &'static str; 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"]; static USING: &'static [&'static str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
#[derive(Serialize)] #[derive(Serialize)]

View File

@ -34,12 +34,7 @@ extern crate notify;
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::time::Duration; 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 fnv::{FnvHashMap, FnvHashSet, FnvHasher};
use std::collections::hash_map::DefaultHasher;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@ -48,6 +43,7 @@ use std::ops::{Deref, DerefMut};
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::result; use std::result;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
@ -132,7 +128,6 @@ macro_rules! path_is_new {
}; };
} }
#[macro_export]
macro_rules! get_path_hash { macro_rules! get_path_hash {
($path:expr) => {{ ($path:expr) => {{
let mut path = $path.clone(); let mut path = $path.clone();
@ -145,9 +140,7 @@ macro_rules! get_path_hash {
path.pop(); path.pop();
}; };
let mut hasher = DefaultHasher::new(); crate::get_path_hash!(path)
path.hash(&mut hasher);
hasher.finish()
}}; }};
} }
@ -642,6 +635,64 @@ impl MailBackend for MaildirType {
fn as_any(&self) -> &dyn::std::any::Any { fn as_any(&self) -> &dyn::std::any::Any {
self self
} }
fn create_folder(&mut self, new_path: String) -> Result<Folder> {
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<Folder> {
Err(MeliError::new("Unimplemented."))
}
fn set_folder_permissions(
&mut self,
_folder_hash: FolderHash,
_val: crate::backends::FolderPermissions,
) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
} }
impl MaildirType { impl MaildirType {

View File

@ -34,6 +34,7 @@ use crate::conf::AccountSettings;
use crate::email::parser::BytesExt; use crate::email::parser::BytesExt;
use crate::email::*; use crate::email::*;
use crate::error::{MeliError, Result}; use crate::error::{MeliError, Result};
use crate::get_path_hash;
use crate::shellexpand::ShellExpandTrait; use crate::shellexpand::ShellExpandTrait;
use fnv::FnvHashMap; use fnv::FnvHashMap;
use libc; use libc;
@ -41,9 +42,7 @@ use memmap::{Mmap, Protection};
use nom::{IResult, Needed}; use nom::{IResult, Needed};
extern crate notify; extern crate notify;
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::collections::hash_map::DefaultHasher;
use std::fs::File; use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::BufReader; use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
@ -74,14 +73,6 @@ fn get_rw_lock_blocking(f: &File) {
assert!(-1 != ret_val); assert!(-1 != ret_val);
} }
macro_rules! get_path_hash {
($path:expr) => {{
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
#[derive(Debug)] #[derive(Debug)]
struct MboxFolder { struct MboxFolder {
hash: FolderHash, hash: FolderHash,

View File

@ -27,8 +27,8 @@ use super::{AccountConf, FileFolderConf};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext}; use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
use melib::backends::{ use melib::backends::{
BackendOp, Backends, Folder, FolderHash, FolderOperation, MailBackend, NotifyFn, ReadOnlyOp, BackendOp, Backends, Folder, FolderHash, MailBackend, NotifyFn, ReadOnlyOp, RefreshEvent,
RefreshEvent, RefreshEventConsumer, RefreshEventKind, SpecialUsageMailbox, RefreshEventConsumer, RefreshEventKind, SpecialUsageMailbox,
}; };
use melib::error::{MeliError, Result}; use melib::error::{MeliError, Result};
use melib::mailbox::*; use melib::mailbox::*;
@ -964,67 +964,112 @@ impl Account {
&self.collection.threads[&f].thread_nodes()[&h] &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<String> {
use crate::execute::actions::FolderOperation;
if self.settings.account.read_only() {
return Err(MeliError::new("Account is read-only."));
}
match op { match op {
FolderOperation::Create => { FolderOperation::Create(path) => {
if self.settings.account.read_only() { let mut folder = self
Err(MeliError::new("Account is read-only.")) .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 { } else {
let mut folder = self let tmp = SpecialUsageMailbox::detect_usage(folder.name());
.backend if tmp != Some(SpecialUsageMailbox::Normal) && tmp != None {
.write() let _ = folder.set_special_usage(tmp.unwrap());
.unwrap() }
.create_folder(path.to_string())?; tmp
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_confs.insert(folder.hash(), new);
self.folder_names self.folder_names
.insert(folder.hash(), folder.path().to_string()); .insert(folder.hash(), folder.path().to_string());
self.folders.insert( self.folders.insert(
folder.hash(), folder.hash(),
MailboxEntry::Parsing( MailboxEntry::Parsing(
Mailbox::new(folder.clone(), &FnvHashMap::default()), Mailbox::new(folder.clone(), &FnvHashMap::default()),
0, 0,
0, 0,
), ),
); );
self.workers.insert( self.workers.insert(
folder.hash(), folder.hash(),
Account::new_worker( Account::new_worker(
folder.clone(), folder.clone(),
&mut self.backend, &mut self.backend,
&self.work_context, &self.work_context,
self.notify_fn.clone(), self.notify_fn.clone(),
), ),
); );
self.collection self.collection
.threads .threads
.insert(folder.hash(), Threads::default()); .insert(folder.hash(), Threads::default());
self.ref_folders.insert(folder.hash(), folder); self.ref_folders = self.backend.read().unwrap().folders()?;
build_folders_order( build_folders_order(
&self.folder_confs, &self.folder_confs,
&mut self.tree, &mut self.tree,
&self.ref_folders, &self.ref_folders,
&mut self.folders_order, &mut self.folders_order,
); );
Ok(()) Ok(format!("`{}` successfully created.", &path))
}
} }
FolderOperation::Delete => Err(MeliError::new("Not implemented.")), FolderOperation::Delete(path) => {
FolderOperation::Subscribe => Err(MeliError::new("Not implemented.")), if self.ref_folders.len() == 1 {
FolderOperation::Unsubscribe => Err(MeliError::new("Not implemented.")), return Err(MeliError::new("Cannot delete only mailbox."));
FolderOperation::Rename(_) => Err(MeliError::new("Not implemented.")), }
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.")), FolderOperation::SetPermissions(_) => Err(MeliError::new("Not implemented.")),
} }
} }

View File

@ -21,11 +21,11 @@
/*! A parser module for user commands passed through the Execute mode. /*! A parser module for user commands passed through the Execute mode.
*/ */
use melib::backends::FolderOperation;
pub use melib::thread::{SortField, SortOrder}; pub use melib::thread::{SortField, SortOrder};
use nom::{digit, not_line_ending, IResult}; use nom::{digit, not_line_ending, IResult};
use std; use std;
pub mod actions; pub mod actions;
use actions::FolderOperation;
pub mod history; pub mod history;
pub use crate::actions::AccountAction::{self, *}; pub use crate::actions::AccountAction::{self, *};
pub use crate::actions::Action::{self, *}; pub use crate::actions::Action::{self, *};
@ -78,7 +78,7 @@ define_commands!([
map!(ws!(tag!("seen")), |_| Listing(SetSeen)) map!(ws!(tag!("seen")), |_| Listing(SetSeen))
| map!(ws!(tag!("unseen")), |_| Listing(SetUnseen)) | 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")) ws!(tag!("create-folder"))
>> account: quoted_argument >> account: quoted_argument
>> is_a!(" ") >> is_a!(" ")
>> path: map_res!(call!(not_line_ending), std::str::from_utf8) >> path: quoted_argument
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Create)) >> (Folder(account.to_string(), FolderOperation::Create(path.to_string())))
) )
); );
) )
@ -269,8 +269,8 @@ define_commands!([
ws!(tag!("subscribe-folder")) ws!(tag!("subscribe-folder"))
>> account: quoted_argument >> account: quoted_argument
>> is_a!(" ") >> is_a!(" ")
>> path: map_res!(call!(not_line_ending), std::str::from_utf8) >> path: quoted_argument
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Subscribe)) >> (Folder(account.to_string(), FolderOperation::Subscribe(path.to_string())))
) )
); );
) )
@ -283,8 +283,8 @@ define_commands!([
ws!(tag!("unsubscribe-folder")) ws!(tag!("unsubscribe-folder"))
>> account: quoted_argument >> account: quoted_argument
>> is_a!(" ") >> is_a!(" ")
>> path: map_res!(call!(not_line_ending), std::str::from_utf8) >> path: quoted_argument
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Unsubscribe)) >> (Folder(account.to_string(), FolderOperation::Unsubscribe(path.to_string())))
) )
); );
) )
@ -299,8 +299,8 @@ define_commands!([
>> is_a!(" ") >> is_a!(" ")
>> src: quoted_argument >> src: quoted_argument
>> is_a!(" ") >> is_a!(" ")
>> dest: map_res!(call!(not_line_ending), std::str::from_utf8) >> dest: quoted_argument
>> (Folder(account.to_string(), src.to_string(), FolderOperation::Rename(dest.to_string()))) >> (Folder(account.to_string(), FolderOperation::Rename(src.to_string(), dest.to_string())))
) )
); );
) )
@ -314,7 +314,7 @@ define_commands!([
>> account: quoted_argument >> account: quoted_argument
>> is_a!(" ") >> is_a!(" ")
>> path: quoted_argument >> path: quoted_argument
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Delete)) >> (Folder(account.to_string(), FolderOperation::Delete(path.to_string())))
) )
); );
) )

View File

@ -24,7 +24,7 @@
*/ */
use crate::components::Component; use crate::components::Component;
use melib::backends::{FolderHash, FolderOperation}; use melib::backends::FolderHash;
pub use melib::thread::{SortField, SortOrder}; pub use melib::thread::{SortField, SortOrder};
use melib::{Draft, EnvelopeHash}; use melib::{Draft, EnvelopeHash};
@ -86,6 +86,17 @@ pub enum AccountAction {
ReIndex, ReIndex,
} }
#[derive(Debug)]
pub enum FolderOperation {
Create(NewFolderPath),
Delete(FolderPath),
Subscribe(FolderPath),
Unsubscribe(FolderPath),
Rename(FolderPath, NewFolderPath),
// Placeholder
SetPermissions(FolderPath),
}
#[derive(Debug)] #[derive(Debug)]
pub enum Action { pub enum Action {
Listing(ListingAction), Listing(ListingAction),
@ -99,9 +110,10 @@ pub enum Action {
SetEnv(String, String), SetEnv(String, String),
PrintEnv(String), PrintEnv(String),
Compose(ComposeAction), Compose(ComposeAction),
Folder(AccountName, FolderPath, FolderOperation), Folder(AccountName, FolderOperation),
AccountAction(AccountName, AccountAction), AccountAction(AccountName, AccountAction),
} }
type AccountName = String; type AccountName = String;
type FolderPath = String; type FolderPath = String;
type NewFolderPath = String;

View File

@ -612,24 +612,27 @@ impl State {
), ),
)); ));
} }
Folder(account_name, path, op) => { Folder(account_name, op) => {
if let Some(account) = self if let Some(account) = self
.context .context
.accounts .accounts
.iter_mut() .iter_mut()
.find(|a| a.name() == account_name) .find(|a| a.name() == account_name)
{ {
if let Err(e) = account.folder_operation(&path, op) { match account.folder_operation(op) {
self.context.replies.push_back(UIEvent::StatusEvent( Err(err) => {
StatusEvent::DisplayMessage(e.to_string()), self.context.replies.push_back(UIEvent::StatusEvent(
)); StatusEvent::DisplayMessage(err.to_string()),
} else { ));
self.context.replies.push_back(UIEvent::StatusEvent( }
StatusEvent::DisplayMessage(format!( Ok(msg) => {
"{} succesfully created in `{}`", self.context.replies.push_back(UIEvent::StatusEvent(
path, account_name StatusEvent::DisplayMessage(format!(
)), "`{}`: {}",
)); account_name, msg
)),
));
}
} }
} else { } else {
self.context.replies.push_back(UIEvent::StatusEvent( self.context.replies.push_back(UIEvent::StatusEvent(

View File

@ -111,6 +111,8 @@ pub enum UIEvent {
Action(Action), Action(Action),
StatusEvent(StatusEvent), StatusEvent(StatusEvent),
MailboxUpdate((usize, FolderHash)), // (account_idx, mailbox_idx) MailboxUpdate((usize, FolderHash)), // (account_idx, mailbox_idx)
MailboxDelete((usize, FolderHash)),
MailboxCreate((usize, FolderHash)),
ComponentKill(Uuid), ComponentKill(Uuid),
WorkerProgress(FolderHash), WorkerProgress(FolderHash),
StartupCheck(FolderHash), StartupCheck(FolderHash),