diff --git a/melib/src/async_workers.rs b/melib/src/async_workers.rs index b3d7b49d..7eb49d2b 100644 --- a/melib/src/async_workers.rs +++ b/melib/src/async_workers.rs @@ -39,7 +39,7 @@ use crossbeam::{ use std::fmt; use std::sync::Arc; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct WorkContext { pub new_work: Sender, pub set_name: Sender<(std::thread::ThreadId, String)>, diff --git a/melib/src/backends.rs b/melib/src/backends.rs index 06bdf251..f1841a0b 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -44,7 +44,7 @@ use fnv::FnvHashMap; use std; pub type BackendCreator = - Box bool>) -> Box>; + Box bool + Send + Sync>) -> Box>; /// A hashmap containing all available mail backends. /// An abstraction over any available backends. @@ -108,7 +108,7 @@ pub enum RefreshEventKind { /// Rename(old_hash, new_hash) Rename(EnvelopeHash, EnvelopeHash), Create(Box), - Remove(FolderHash), + Remove(EnvelopeHash), Rescan, Failure(MeliError), } @@ -177,6 +177,7 @@ pub enum FolderOperation { type NewFolderName = String; pub trait MailBackend: ::std::fmt::Debug { + fn is_online(&self) -> bool; fn get(&mut self, folder: &Folder) -> Async>>; fn watch( &self, diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index 74cc7937..adcb151c 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -65,14 +65,30 @@ pub struct ImapServerConf { pub danger_accept_invalid_certs: bool, } +struct IsSubscribedFn(Box bool + Send + Sync>); + +impl std::fmt::Debug for IsSubscribedFn { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "IsSubscribedFn Box") + } +} + +impl std::ops::Deref for IsSubscribedFn { + type Target = Box bool + Send + Sync>; + fn deref(&self) -> &Box bool + Send + Sync> { + &self.0 + } +} type Capabilities = FnvHashSet>; #[derive(Debug)] pub struct ImapType { account_name: String, + online: Arc>, + is_subscribed: Arc, connection: Arc>, server_conf: ImapServerConf, - folders: FnvHashMap, + folders: Arc>>, hash_index: Arc>>, uid_index: Arc>>, @@ -80,6 +96,9 @@ pub struct ImapType { } impl MailBackend for ImapType { + fn is_online(&self) -> bool { + *self.online.lock().unwrap() + } fn get(&mut self, folder: &Folder) -> Async>> { macro_rules! exit_on_error { ($tx:expr,$($result:expr)+) => { @@ -97,7 +116,7 @@ impl MailBackend for ImapType { let uid_index = self.uid_index.clone(); let folder_path = folder.path().to_string(); let folder_hash = folder.hash(); - let folder_exists = self.folders[&folder_hash].exists.clone(); + let folder_exists = self.folders.lock().unwrap()[&folder_hash].exists.clone(); let connection = self.connection.clone(); let closure = move |_work_context| { let connection = connection.clone(); @@ -189,6 +208,7 @@ impl MailBackend for ImapType { let folders = self.folders.clone(); let conn = ImapConnection::new_connection(&self.server_conf); let main_conn = self.connection.clone(); + let is_online = self.online.clone(); let hash_index = self.hash_index.clone(); let uid_index = self.uid_index.clone(); let handle = std::thread::Builder::new() @@ -201,6 +221,7 @@ impl MailBackend for ImapType { .unwrap(); let kit = ImapWatchKit { conn, + is_online, main_conn, hash_index, uid_index, @@ -218,55 +239,20 @@ impl MailBackend for ImapType { } fn folders(&self) -> FnvHashMap { - if !self.folders.is_empty() { - return self - .folders + let mut folders = self.folders.lock().unwrap(); + if !folders.is_empty() { + return folders .iter() .map(|(h, f)| (*h, Box::new(Clone::clone(f)) as Folder)) .collect(); } - - let mut folders: FnvHashMap = Default::default(); - let mut res = String::with_capacity(8 * 1024); - let mut conn = self.connection.lock().unwrap(); - conn.send_command(b"LIST \"\" \"*\"").unwrap(); - conn.read_response(&mut res).unwrap(); - debug!("out: {}", &res); - for l in res.lines().map(|l| l.trim()) { - if let Ok(mut folder) = - protocol_parser::list_folder_result(l.as_bytes()).to_full_result() - { - if let Some(parent) = folder.parent { - if folders.contains_key(&parent) { - folders - .entry(parent) - .and_modify(|e| e.children.push(folder.hash)); - } else { - /* Insert dummy parent entry, populating only the children field. Later - * when we encounter the parent entry we will swap its children with - * dummy's */ - folders.insert( - parent, - ImapFolder { - children: vec![folder.hash], - ..ImapFolder::default() - }, - ); - } - } - - if folders.contains_key(&folder.hash) { - let entry = folders.entry(folder.hash).or_default(); - std::mem::swap(&mut entry.children, &mut folder.children); - std::mem::swap(entry, &mut folder); - } else { - folders.insert(folder.hash, folder); - } - } else { - debug!("parse error for {:?}", l); - } + *folders = ImapType::imap_folders(&self.connection); + folders.retain(|_, f| (self.is_subscribed)(f.path())); + let keys = folders.keys().cloned().collect::>(); + for f in folders.values_mut() { + f.children.retain(|c| keys.contains(c)); } - debug!(&folders); + *self.online.lock().unwrap() = true; folders .iter() .map(|(h, f)| (*h, Box::new(Clone::clone(f)) as Folder)) @@ -277,25 +263,31 @@ impl MailBackend for ImapType { let (uid, folder_hash) = self.hash_index.lock().unwrap()[&hash]; Box::new(ImapOp::new( uid, - self.folders[&folder_hash].path().to_string(), + self.folders.lock().unwrap()[&folder_hash] + .path() + .to_string(), self.connection.clone(), self.byte_cache.clone(), )) } fn save(&self, bytes: &[u8], folder: &str, flags: Option) -> Result<()> { - let path = self - .folders - .values() - .find(|v| v.name == folder) - .ok_or(MeliError::new(""))?; + let path = { + let folders = self.folders.lock().unwrap(); + + folders + .values() + .find(|v| v.name == folder) + .map(|v| v.path().to_string()) + .ok_or(MeliError::new(""))? + }; let mut response = String::with_capacity(8 * 1024); let mut conn = self.connection.lock().unwrap(); let flags = flags.unwrap_or(Flag::empty()); conn.send_command( format!( "APPEND \"{}\" ({}) {{{}}}", - path.path(), + &path, flags_to_imap_list!(flags), bytes.len() ) @@ -311,7 +303,14 @@ impl MailBackend for ImapType { fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> { use FolderOperation::*; - match (&op, self.folders.values().any(|f| f.path == path)) { + match ( + &op, + self.folders + .lock() + .unwrap() + .values() + .any(|f| f.path == path), + ) { (Create, true) => { return Err(MeliError::new(format!( "Folder named `{}` in account `{}` already exists.", @@ -391,7 +390,10 @@ macro_rules! get_conf_val { } impl ImapType { - pub fn new(s: &AccountSettings, is_subscribed: Box bool>) -> Self { + pub fn new( + s: &AccountSettings, + is_subscribed: Box bool + Send + Sync>, + ) -> Self { debug!(s); let server_hostname = get_conf_val!(s["server_hostname"]); let server_username = get_conf_val!(s["server_username"]); @@ -416,28 +418,18 @@ impl ImapType { }; let connection = ImapConnection::new_connection(&server_conf); - let mut m = ImapType { + ImapType { account_name: s.name().to_string(), + online: Arc::new(Mutex::new(false)), server_conf, + is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)), - folders: Default::default(), + folders: Arc::new(Mutex::new(Default::default())), connection: Arc::new(Mutex::new(connection)), hash_index: Default::default(), uid_index: Default::default(), byte_cache: Default::default(), - }; - - m.folders = m.imap_folders(); - m.folders.retain(|_, f| is_subscribed(f.path())); - let keys = m - .folders - .keys() - .cloned() - .collect::>(); - for f in m.folders.values_mut() { - f.children.retain(|c| keys.contains(c)); } - m } pub fn shell(&mut self) { @@ -472,10 +464,12 @@ impl ImapType { } } - pub fn imap_folders(&self) -> FnvHashMap { + pub fn imap_folders( + connection: &Arc>, + ) -> FnvHashMap { let mut folders: FnvHashMap = Default::default(); let mut res = String::with_capacity(8 * 1024); - let mut conn = self.connection.lock().unwrap(); + let mut conn = connection.lock().unwrap(); conn.send_command(b"LIST \"\" \"*\"").unwrap(); conn.read_response(&mut res).unwrap(); debug!("out: {}", &res); diff --git a/melib/src/backends/imap/watch.rs b/melib/src/backends/imap/watch.rs index f3dfab78..8cae6154 100644 --- a/melib/src/backends/imap/watch.rs +++ b/melib/src/backends/imap/watch.rs @@ -24,10 +24,11 @@ use std::sync::{Arc, Mutex}; /// Arguments for IMAP watching functions pub struct ImapWatchKit { pub conn: ImapConnection, + pub is_online: Arc>, pub main_conn: Arc>, pub hash_index: Arc>>, pub uid_index: Arc>>, - pub folders: FnvHashMap, + pub folders: Arc>>, pub sender: RefreshEventConsumer, pub work_context: WorkContext, } @@ -49,6 +50,7 @@ macro_rules! exit_on_error { pub fn poll_with_examine(kit: ImapWatchKit) { debug!("poll with examine"); let ImapWatchKit { + is_online, mut conn, main_conn, hash_index, @@ -57,6 +59,12 @@ pub fn poll_with_examine(kit: ImapWatchKit) { sender, work_context, } = kit; + loop { + if *is_online.lock().unwrap() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } let mut response = String::with_capacity(8 * 1024); let thread_id: std::thread::ThreadId = std::thread::current().id(); loop { @@ -65,6 +73,7 @@ pub fn poll_with_examine(kit: ImapWatchKit) { .send((thread_id, "sleeping...".to_string())) .unwrap(); std::thread::sleep(std::time::Duration::from_millis(5 * 60 * 1000)); + let folders = folders.lock().unwrap(); for folder in folders.values() { work_context .set_status @@ -94,6 +103,7 @@ pub fn idle(kit: ImapWatchKit) { * minutes wake up and poll the others */ let ImapWatchKit { mut conn, + is_online, main_conn, hash_index, uid_index, @@ -101,10 +111,19 @@ pub fn idle(kit: ImapWatchKit) { sender, work_context, } = kit; + loop { + if *is_online.lock().unwrap() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } let thread_id: std::thread::ThreadId = std::thread::current().id(); - let folder: &ImapFolder = folders + let folder: ImapFolder = folders + .lock() + .unwrap() .values() .find(|f| f.parent.is_none() && f.path().eq_ignore_ascii_case("INBOX")) + .map(std::clone::Clone::clone) .unwrap(); let folder_hash = folder.hash(); let mut response = String::with_capacity(8 * 1024); @@ -185,7 +204,7 @@ pub fn idle(kit: ImapWatchKit) { iter.conn.send_raw(b"DONE") iter.conn.read_response(&mut response) ); - for (hash, folder) in &folders { + for (hash, folder) in folders.lock().unwrap().iter() { if *hash == folder_hash { /* Skip INBOX */ continue; diff --git a/melib/src/backends/maildir/backend.rs b/melib/src/backends/maildir/backend.rs index 6ac0e62d..f6b4af18 100644 --- a/melib/src/backends/maildir/backend.rs +++ b/melib/src/backends/maildir/backend.rs @@ -183,6 +183,9 @@ fn move_to_cur(p: PathBuf) -> Result { } impl MailBackend for MaildirType { + fn is_online(&self) -> bool { + true + } fn folders(&self) -> FnvHashMap { self.folders .iter() diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index 132db42a..327c3eb4 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -367,6 +367,9 @@ pub struct MboxType { } impl MailBackend for MboxType { + fn is_online(&self) -> bool { + true + } fn get(&mut self, folder: &Folder) -> Async>> { let mut w = AsyncBuilder::new(); let handle = { diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing.rs index a2682146..fca74936 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing.rs @@ -184,6 +184,10 @@ impl fmt::Display for Listing { impl Component for Listing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + for a in context.accounts.iter_mut() { + a.is_online(); + } + if !self.is_dirty() { return; } @@ -218,7 +222,22 @@ impl Component for Listing { .push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right)))); } self.dirty = false; + if right_component_width == total_cols { + if !context.accounts[self.cursor_pos.0].is_online() { + clear_area(grid, area); + write_string_to_grid( + "offline", + grid, + Color::Byte(243), + Color::Default, + Attr::Default, + area, + false, + ); + context.dirty_areas.push_back(area); + return; + } match self.component { Compact(ref mut l) => l.draw(grid, area, context), Plain(ref mut l) => l.draw(grid, area, context), @@ -229,6 +248,20 @@ impl Component for Listing { self.draw_menu(grid, area, context); } else { self.draw_menu(grid, (upper_left, (mid, get_y(bottom_right))), context); + if !context.accounts[self.cursor_pos.0].is_online() { + clear_area(grid, (set_x(upper_left, mid + 1), bottom_right)); + write_string_to_grid( + "offline", + grid, + Color::Byte(243), + Color::Default, + Attr::Default, + (set_x(upper_left, mid + 1), bottom_right), + false, + ); + context.dirty_areas.push_back(area); + return; + } match self.component { Compact(ref mut l) => { l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context) @@ -337,21 +370,26 @@ impl Component for Listing { } _ => return false, } - let folder_hash = - context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1]; - /* Check if per-folder configuration overrides general configuration */ - if let Some(index_style) = context - .accounts - .get(self.cursor_pos.0) - .and_then(|account| account.folder_confs(folder_hash).conf_override.index_style) + + /* Account might have no folders yet if it's offline */ + if let Some(&folder_hash) = context.accounts[self.cursor_pos.0] + .folders_order + .get(self.cursor_pos.1) { - self.component.set_style(index_style); - } else if let Some(index_style) = context - .accounts - .get(self.cursor_pos.0) - .and_then(|account| Some(account.settings.conf.index_style())) - { - self.component.set_style(index_style); + /* Check if per-folder configuration overrides general configuration */ + if let Some(index_style) = + context.accounts.get(self.cursor_pos.0).and_then(|account| { + account.folder_confs(folder_hash).conf_override.index_style + }) + { + self.component.set_style(index_style); + } else if let Some(index_style) = context + .accounts + .get(self.cursor_pos.0) + .and_then(|account| Some(account.settings.conf.index_style())) + { + self.component.set_style(index_style); + } } context .replies @@ -740,6 +778,15 @@ impl Listing { ); if lines.is_empty() { + write_string_to_grid( + "offline", + grid, + Color::Byte(243), + Color::Default, + Attr::Default, + (pos_inc(upper_left, (0, 1)), bottom_right), + false, + ); return 0; } diff --git a/ui/src/conf/accounts.rs b/ui/src/conf/accounts.rs index 6b6db111..425a3810 100644 --- a/ui/src/conf/accounts.rs +++ b/ui/src/conf/accounts.rs @@ -25,7 +25,7 @@ use super::{AccountConf, FolderConf}; use fnv::FnvHashMap; -use melib::async_workers::{Async, AsyncBuilder, AsyncStatus}; +use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext}; use melib::backends::{ BackendOp, Backends, Folder, FolderHash, FolderOperation, MailBackend, NotifyFn, ReadOnlyOp, RefreshEvent, RefreshEventConsumer, RefreshEventKind, SpecialUseMailbox, @@ -118,6 +118,7 @@ impl MailboxEntry { pub struct Account { pub index: usize, name: String, + is_online: bool, pub(crate) folders: FnvHashMap, pub(crate) folder_confs: FnvHashMap, pub(crate) folders_order: Vec, @@ -129,6 +130,7 @@ pub struct Account { pub(crate) address_book: AddressBook, pub(crate) workers: FnvHashMap, + pub(crate) work_context: WorkContext, pub(crate) settings: AccountConf, pub(crate) runtime_settings: AccountConf, @@ -199,40 +201,84 @@ impl Account { name: String, settings: AccountConf, map: &Backends, + work_context: WorkContext, notify_fn: NotifyFn, ) -> Self { let s = settings.clone(); - let mut backend = map.get(settings.account().format())( + let backend = map.get(settings.account().format())( settings.account(), Box::new(move |path: &str| { s.folder_confs.contains_key(path) && s.folder_confs[path].subscribe.is_true() }), ); - let mut ref_folders: FnvHashMap = backend.folders(); + let notify_fn = Arc::new(notify_fn); + + let data_dir = xdg::BaseDirectories::with_profile("meli", &name).unwrap(); + let address_book = if let Ok(data) = data_dir.place_data_file("addressbook") { + if data.exists() { + let reader = io::BufReader::new(fs::File::open(data).unwrap()); + let result: result::Result = serde_json::from_reader(reader); + if let Ok(data_t) = result { + data_t + } else { + AddressBook::new(name.clone()) + } + } else { + AddressBook::new(name.clone()) + } + } else { + AddressBook::new(name.clone()) + }; + let mut ret = Account { + index, + name, + is_online: false, + folders: Default::default(), + folder_confs: Default::default(), + folders_order: Default::default(), + folder_names: Default::default(), + tree: Default::default(), + address_book, + sent_folder: Default::default(), + collection: Default::default(), + workers: Default::default(), + work_context, + runtime_settings: settings.clone(), + settings, + backend, + notify_fn, + + event_queue: VecDeque::with_capacity(8), + }; + + ret.is_online(); + ret + } + fn init(&mut self) { + let mut ref_folders: FnvHashMap = self.backend.folders(); let mut folders: FnvHashMap = FnvHashMap::with_capacity_and_hasher(ref_folders.len(), Default::default()); let mut folders_order: Vec = Vec::with_capacity(ref_folders.len()); let mut workers: FnvHashMap = FnvHashMap::default(); - let notify_fn = Arc::new(notify_fn); let mut folder_names = FnvHashMap::default(); let mut folder_confs = FnvHashMap::default(); let mut sent_folder = None; for f in ref_folders.values_mut() { - if !settings.folder_confs.contains_key(f.path()) - || settings.folder_confs[f.path()].subscribe.is_false() + if !self.settings.folder_confs.contains_key(f.path()) + || self.settings.folder_confs[f.path()].subscribe.is_false() { /* Skip unsubscribed folder */ continue; } - match settings.folder_confs[f.path()].usage { + match self.settings.folder_confs[f.path()].usage { Some(SpecialUseMailbox::Sent) => { sent_folder = Some(f.hash()); } _ => {} } - folder_confs.insert(f.hash(), settings.folder_confs[f.path()].clone()); + folder_confs.insert(f.hash(), self.settings.folder_confs[f.path()].clone()); folder_names.insert(f.hash(), f.path().to_string()); } @@ -240,8 +286,8 @@ impl Account { let mut tree: Vec = Vec::new(); let mut collection: Collection = Collection::new(Default::default()); for (h, f) in ref_folders.iter() { - if !settings.folder_confs.contains_key(f.path()) - || settings.folder_confs[f.path()].subscribe.is_false() + if !self.settings.folder_confs.contains_key(f.path()) + || self.settings.folder_confs[f.path()].subscribe.is_false() { /* Skip unsubscribed folder */ continue; @@ -274,7 +320,13 @@ impl Account { ); workers.insert( *h, - Account::new_worker(&settings, f.clone(), &mut backend, notify_fn.clone()), + Account::new_worker( + &self.settings, + f.clone(), + &mut self.backend, + &self.work_context, + self.notify_fn.clone(), + ), ); collection.threads.insert(*h, Threads::default()); } @@ -292,47 +344,21 @@ impl Account { } } - let data_dir = xdg::BaseDirectories::with_profile("meli", &name).unwrap(); - let address_book = if let Ok(data) = data_dir.place_data_file("addressbook") { - if data.exists() { - let reader = io::BufReader::new(fs::File::open(data).unwrap()); - let result: result::Result = serde_json::from_reader(reader); - if let Ok(data_t) = result { - data_t - } else { - AddressBook::new(name.clone()) - } - } else { - AddressBook::new(name.clone()) - } - } else { - AddressBook::new(name.clone()) - }; - - Account { - index, - name, - folders, - folder_confs, - folders_order, - folder_names, - tree, - address_book, - sent_folder, - collection, - workers, - settings: settings.clone(), - runtime_settings: settings, - backend, - notify_fn, - - event_queue: VecDeque::with_capacity(8), - } + self.folders = folders; + self.folder_confs = folder_confs; + self.folders_order = folders_order; + self.folder_names = folder_names; + self.tree = tree; + self.sent_folder = sent_folder; + self.collection = collection; + self.workers = workers; } + fn new_worker( settings: &AccountConf, folder: Folder, backend: &mut Box, + work_context: &WorkContext, notify_fn: Arc, ) -> Worker { let mailbox_handle = backend.get(&folder); @@ -361,7 +387,7 @@ impl Account { * threads' closures this could be avoided, but it requires green threads. */ builder.set_priority(priority).set_is_static(true); - let w = builder.build(Box::new(move |work_context| { + let mut w = builder.build(Box::new(move |work_context| { let name = format!("Parsing {}", folder.path()); let mut mailbox_handle = mailbox_handle.clone(); let work = mailbox_handle.work().unwrap(); @@ -397,6 +423,9 @@ impl Account { } } })); + if let Some(w) = w.work() { + work_context.new_work.send(w).unwrap(); + } Some(w) } pub fn reload( @@ -485,6 +514,7 @@ impl Account { &self.settings, ref_folders[&folder_hash].clone(), &mut self.backend, + &self.work_context, self.notify_fn.clone(), ); self.workers.insert(folder_hash, handle); @@ -758,6 +788,15 @@ impl Account { .find(|(_, f)| f.usage == Some(special_use)); ret.as_ref().map(|r| r.0.as_str()) } + + pub fn is_online(&mut self) -> bool { + let ret = self.backend.is_online(); + if ret != self.is_online && ret { + self.init(); + } + self.is_online = ret; + ret + } } impl Index for Account { diff --git a/ui/src/state.rs b/ui/src/state.rs index e0db9e2b..a20480ee 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -171,6 +171,8 @@ impl State { let termrows = termsize.map(|(_, h)| h); let cols = termcols.unwrap_or(0) as usize; let rows = termrows.unwrap_or(0) as usize; + + let work_controller = WorkController::new(sender.clone()); let mut accounts: Vec = settings .accounts .iter() @@ -182,6 +184,7 @@ impl State { n.to_string(), a_s.clone(), &backends, + work_controller.get_context(), NotifyFn::new(Box::new(move |f: FolderHash| { sender .send(ThreadEvent::UIEvent(UIEvent::StartupCheck(f))) @@ -210,7 +213,7 @@ impl State { dirty_areas: VecDeque::with_capacity(5), replies: VecDeque::with_capacity(5), temp_files: Vec::new(), - work_controller: WorkController::new(sender.clone()), + work_controller, sender, receiver, @@ -225,16 +228,6 @@ impl State { s.grid.set_ascii_drawing(true); } - for a in s.context.accounts.iter_mut() { - for worker in a.workers.values_mut() { - if let Some(worker) = worker.as_mut() { - if let Some(w) = worker.work() { - s.context.work_controller.queue.add_work(w); - } - } - } - } - s.switch_to_alternate_screen(); debug!("inserting mailbox hashes:"); {