melib: add mailbox delete/create to IMAP

master
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 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<dyn BackendOp>;
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>>>> {
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<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
@ -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)
}
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
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<Folder> {
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<Folder> {
/* 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<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 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)?;

View File

@ -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<()> {

View File

@ -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<FolderHash>,
pub(super) children: Vec<FolderHash>,
pub separator: u8,
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
pub no_select: bool,
pub is_subscribed: bool,
@ -38,6 +40,12 @@ pub struct ImapFolder {
pub unseen: Arc<Mutex<usize>>,
}
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(())
}

View File

@ -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<ResponseCode> {
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<ResponseCode>),
No(Option<ResponseCode>),
Bad(Option<ResponseCode>),
Preauth(Option<ResponseCode>),
Bye(Option<ResponseCode>),
Ok(ResponseCode),
No(ResponseCode),
Bad(ResponseCode),
Preauth(ResponseCode),
Bye(ResponseCode),
}
impl<T: AsRef<str>> From<T> 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<Result<()>> 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<Result<()>> 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)
})

View File

@ -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) {

View File

@ -63,16 +63,6 @@ pub trait Method<OBJ: Object>: 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)]

View File

@ -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<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 {

View File

@ -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,

View File

@ -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<String> {
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.")),
}
}

View File

@ -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())))
)
);
)

View File

@ -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;

View File

@ -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(

View File

@ -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),