melib: add mailbox delete/create to IMAP
parent
d6f04c9ed3
commit
f208948651
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 response = String::with_capacity(8 * 1024);
|
||||
{
|
||||
let mut conn_lck = self.connection.lock()?;
|
||||
conn_lck.send_command(debug!(format!("CREATE \"{}\"", path,)).as_bytes())?;
|
||||
|
||||
conn_lck.send_command(format!("CREATE \"{}\"", path,).as_bytes())?;
|
||||
conn_lck.read_response(&mut response)?;
|
||||
conn_lck.send_command(debug!(format!("SUBSCRIBE \"{}\"", path,)).as_bytes())?;
|
||||
conn_lck.send_command(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 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)?;
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,21 +964,30 @@ impl Account {
|
|||
&self.collection.threads[&f].thread_nodes()[&h]
|
||||
}
|
||||
|
||||
pub fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> {
|
||||
match op {
|
||||
FolderOperation::Create => {
|
||||
pub fn folder_operation(
|
||||
&mut self,
|
||||
op: crate::execute::actions::FolderOperation,
|
||||
) -> Result<String> {
|
||||
use crate::execute::actions::FolderOperation;
|
||||
if self.settings.account.read_only() {
|
||||
Err(MeliError::new("Account is read-only."))
|
||||
} else {
|
||||
return Err(MeliError::new("Account is read-only."));
|
||||
}
|
||||
match op {
|
||||
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
|
||||
{
|
||||
new.folder_conf.usage = if folder.special_usage() != SpecialUsageMailbox::Normal {
|
||||
Some(folder.special_usage())
|
||||
} else {
|
||||
let tmp = SpecialUsageMailbox::detect_usage(folder.name());
|
||||
|
@ -1011,20 +1020,56 @@ impl Account {
|
|||
self.collection
|
||||
.threads
|
||||
.insert(folder.hash(), Threads::default());
|
||||
self.ref_folders.insert(folder.hash(), folder);
|
||||
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(())
|
||||
Ok(format!("`{}` successfully created.", &path))
|
||||
}
|
||||
FolderOperation::Delete(path) => {
|
||||
if self.ref_folders.len() == 1 {
|
||||
return Err(MeliError::new("Cannot delete only mailbox."));
|
||||
}
|
||||
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.")),
|
||||
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.")),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())))
|
||||
)
|
||||
);
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
|
15
src/state.rs
15
src/state.rs
|
@ -612,25 +612,28 @@ 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) {
|
||||
match account.folder_operation(op) {
|
||||
Err(err) => {
|
||||
self.context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(e.to_string()),
|
||||
StatusEvent::DisplayMessage(err.to_string()),
|
||||
));
|
||||
} else {
|
||||
}
|
||||
Ok(msg) => {
|
||||
self.context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"{} succesfully created in `{}`",
|
||||
path, account_name
|
||||
"`{}`: {}",
|
||||
account_name, msg
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue