From 2b6f6ab42c4278cc2ff5f8c1819979d37b22ec50 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 17 Dec 2019 14:12:41 +0200 Subject: [PATCH] melib: Add BackendFolder methods, move special usage logic to backend - add count() method to return (unseen, total) counts - add is_subscribed() - add set_special_usage() and set_is_subscribed() concerns #8 --- melib/src/backends.rs | 36 +++++++++++++++ melib/src/backends/imap.rs | 30 +++++++++++-- melib/src/backends/imap/connection.rs | 37 ++++++++++++++++ melib/src/backends/imap/folder.rs | 38 ++++++++++------ melib/src/backends/imap/protocol_parser.rs | 6 +-- melib/src/backends/imap/watch.rs | 17 +++++++- melib/src/backends/jmap.rs | 5 ++- melib/src/backends/jmap/connection.rs | 27 +++++++----- melib/src/backends/jmap/folder.rs | 27 ++++++++++-- melib/src/backends/jmap/operations.rs | 2 +- melib/src/backends/jmap/protocol.rs | 8 ++-- melib/src/backends/maildir.rs | 43 ++++++++++++------ melib/src/backends/maildir/backend.rs | 31 ++++++++++++- melib/src/backends/mbox.rs | 31 ++++++++++++- melib/src/backends/notmuch.rs | 30 +++++++++++-- ui/src/components/mail/listing.rs | 28 +++--------- ui/src/components/mail/status.rs | 44 +++++++++---------- ui/src/conf/accounts.rs | 51 ++++++++++++++++------ 18 files changed, 370 insertions(+), 121 deletions(-) diff --git a/melib/src/backends.rs b/melib/src/backends.rs index 7a1b68cf..2bd18066 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -372,6 +372,26 @@ pub enum SpecialUsageMailbox { Trash, } +impl std::fmt::Display for SpecialUsageMailbox { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use SpecialUsageMailbox::*; + write!( + f, + "{}", + match self { + Normal => "Normal", + Inbox => "Inbox", + Archive => "Archive", + Drafts => "Drafts", + Flagged => "Flagged", + Junk => "Junk", + Sent => "Sent", + Trash => "Trash", + } + ) + } +} + impl Default for SpecialUsageMailbox { fn default() -> Self { SpecialUsageMailbox::Normal @@ -409,8 +429,12 @@ pub trait BackendFolder: Debug { fn clone(&self) -> Folder; fn children(&self) -> &[FolderHash]; fn parent(&self) -> Option; + fn is_subscribed(&self) -> bool; + fn set_is_subscribed(&mut self, new_val: bool) -> Result<()>; + fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()>; fn special_usage(&self) -> SpecialUsageMailbox; fn permissions(&self) -> FolderPermissions; + fn count(&self) -> Result<(usize, usize)>; } #[derive(Debug)] @@ -452,6 +476,18 @@ impl BackendFolder for DummyFolder { fn permissions(&self) -> FolderPermissions { FolderPermissions::default() } + fn is_subscribed(&self) -> bool { + true + } + fn set_is_subscribed(&mut self, _new_val: bool) -> Result<()> { + Ok(()) + } + fn set_special_usage(&mut self, _new_val: SpecialUsageMailbox) -> Result<()> { + Ok(()) + } + fn count(&self) -> Result<(usize, usize)> { + Ok((0, 0)) + } } pub fn folder_default() -> Folder { diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index dc3e39ba..53ba6cd8 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -169,9 +169,14 @@ impl MailBackend for ImapType { 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) = { + let (permissions, folder_exists, no_select, unseen) = { let f = &self.folders.read().unwrap()[&folder_hash]; - (f.permissions.clone(), f.exists.clone(), f.no_select) + ( + f.permissions.clone(), + f.exists.clone(), + f.no_select, + f.unseen.clone(), + ) }; let connection = self.connection.clone(); let closure = move |_work_context| { @@ -228,10 +233,11 @@ impl MailBackend for ImapType { ); let mut tag_lck = tag_index.write().unwrap(); + let mut our_unseen = 0; while exists > 1 { let mut envelopes = vec![]; exit_on_error!(&tx, - conn.send_command(format!("UID FETCH {}:{} (UID FLAGS ENVELOPE BODYSTRUCTURE)", std::cmp::max(exists.saturating_sub(20000), 1), exists).as_bytes()) + conn.send_command(format!("UID FETCH {}:{} (UID FLAGS ENVELOPE BODYSTRUCTURE)", std::cmp::max(exists.saturating_sub(500), 1), exists).as_bytes()) conn.read_response(&mut response) ); debug!( @@ -255,6 +261,9 @@ impl MailBackend for ImapType { h.write(folder_path.as_bytes()); env.set_hash(h.finish()); if let Some((flags, keywords)) = flags { + if !flags.contains(Flag::SEEN) { + our_unseen += 1; + } env.set_flags(flags); for f in keywords { let hash = tag_hash!(f); @@ -278,8 +287,10 @@ impl MailBackend for ImapType { tx.send(AsyncStatus::Payload(Err(e))).unwrap(); } } - exists = std::cmp::max(exists.saturating_sub(20000), 1); + exists = std::cmp::max(exists.saturating_sub(500), 1); debug!("sending payload"); + + *unseen.lock().unwrap() = our_unseen; tx.send(AsyncStatus::Payload(Ok(envelopes))).unwrap(); } drop(conn); @@ -290,6 +301,17 @@ impl MailBackend for ImapType { w.build(handle) } + fn refresh( + &mut self, + _folder_hash: FolderHash, + _sender: RefreshEventConsumer, + ) -> Result>>> { + let mut res = String::with_capacity(8 * 1024); + self.connection.lock()?.send_command(b"NOOP")?; + self.connection.lock()?.read_response(&mut res)?; + Err(MeliError::new("Unimplemented.")) + } + fn watch( &self, sender: RefreshEventConsumer, diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index a2b23989..3ee66613 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -357,6 +357,13 @@ impl ImapConnection { } pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> { + if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { + if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { + *status = Err(MeliError::new("Connection timed out")); + self.stream = Err(MeliError::new("Connection timed out")); + } + } + if let Ok(ref mut stream) = self.stream { if let Ok(_) = stream.read_lines(ret, &termination_string) { return Ok(()); @@ -381,6 +388,12 @@ impl ImapConnection { } pub fn wait_for_continuation_request(&mut self) -> Result<()> { + if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { + if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { + *status = Err(MeliError::new("Connection timed out")); + self.stream = Err(MeliError::new("Connection timed out")); + } + } if let Ok(ref mut stream) = self.stream { if let Ok(_) = stream.wait_for_continuation_request() { return Ok(()); @@ -405,6 +418,12 @@ impl ImapConnection { } pub fn send_command(&mut self, command: &[u8]) -> Result { + if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { + if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { + *status = Err(MeliError::new("Connection timed out")); + self.stream = Err(MeliError::new("Connection timed out")); + } + } if let Ok(ref mut stream) = self.stream { if let Ok(ret) = stream.send_command(command) { return Ok(ret); @@ -429,6 +448,12 @@ impl ImapConnection { } pub fn send_literal(&mut self, data: &[u8]) -> Result<()> { + if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { + if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { + *status = Err(MeliError::new("Connection timed out")); + self.stream = Err(MeliError::new("Connection timed out")); + } + } if let Ok(ref mut stream) = self.stream { if let Ok(_) = stream.send_literal(data) { return Ok(()); @@ -453,6 +478,12 @@ impl ImapConnection { } pub fn send_raw(&mut self, raw: &[u8]) -> Result<()> { + if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { + if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { + *status = Err(MeliError::new("Connection timed out")); + self.stream = Err(MeliError::new("Connection timed out")); + } + } if let Ok(ref mut stream) = self.stream { if let Ok(_) = stream.send_raw(raw) { return Ok(()); @@ -477,6 +508,12 @@ impl ImapConnection { } pub fn set_nonblocking(&mut self, val: bool) -> Result<()> { + if let (instant, ref mut status @ Ok(())) = *self.online.lock().unwrap() { + if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) { + *status = Err(MeliError::new("Connection timed out")); + self.stream = Err(MeliError::new("Connection timed out")); + } + } if let Ok(ref mut stream) = self.stream { if let Ok(_) = stream.set_nonblocking(val) { return Ok(()); diff --git a/melib/src/backends/imap/folder.rs b/melib/src/backends/imap/folder.rs index 3ca20047..399c26d2 100644 --- a/melib/src/backends/imap/folder.rs +++ b/melib/src/backends/imap/folder.rs @@ -19,7 +19,8 @@ * along with meli. If not, see . */ use crate::backends::{BackendFolder, Folder, FolderHash, FolderPermissions, SpecialUsageMailbox}; -use std::sync::{Arc, Mutex}; +use crate::error::*; +use std::sync::{Arc, Mutex, RwLock}; #[derive(Debug, Default, Clone)] pub struct ImapFolder { @@ -28,11 +29,13 @@ pub struct ImapFolder { pub(super) name: String, pub(super) parent: Option, pub(super) children: Vec, - pub usage: SpecialUsageMailbox, + pub usage: Arc>, pub no_select: bool, + pub is_subscribed: bool, pub permissions: Arc>, pub exists: Arc>, + pub unseen: Arc>, } impl BackendFolder for ImapFolder { @@ -57,21 +60,11 @@ impl BackendFolder for ImapFolder { } fn clone(&self) -> Folder { - Box::new(ImapFolder { - hash: self.hash, - path: self.path.clone(), - name: self.name.clone(), - parent: self.parent, - children: self.children.clone(), - usage: self.usage, - no_select: self.no_select, - permissions: self.permissions.clone(), - exists: self.exists.clone(), - }) + Box::new(std::clone::Clone::clone(self)) } fn special_usage(&self) -> SpecialUsageMailbox { - self.usage + *self.usage.read().unwrap() } fn parent(&self) -> Option { @@ -81,4 +74,21 @@ impl BackendFolder for ImapFolder { fn permissions(&self) -> FolderPermissions { *self.permissions.lock().unwrap() } + fn is_subscribed(&self) -> bool { + self.is_subscribed + } + fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> { + self.is_subscribed = new_val; + // FIXME: imap subscribe + Ok(()) + } + + fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> { + *self.usage.write()? = new_val; + Ok(()) + } + + fn count(&self) -> Result<(usize, usize)> { + Ok((*self.unseen.lock()?, *self.exists.lock()?)) + } } diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index f9a7eadd..7250a4c5 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -93,11 +93,11 @@ named!( if p.eq_ignore_ascii_case(b"\\NoSelect") { f.no_select = true; } else if p.eq_ignore_ascii_case(b"\\Sent") { - f.usage = SpecialUsageMailbox::Sent; + let _ = f.set_special_usage(SpecialUsageMailbox::Sent); } else if p.eq_ignore_ascii_case(b"\\Junk") { - f.usage = SpecialUsageMailbox::Trash; + let _ = f.set_special_usage(SpecialUsageMailbox::Trash); } else if p.eq_ignore_ascii_case(b"\\Drafts") { - f.usage = SpecialUsageMailbox::Drafts; + let _ = f.set_special_usage(SpecialUsageMailbox::Drafts); } } f.hash = get_path_hash!(path); diff --git a/melib/src/backends/imap/watch.rs b/melib/src/backends/imap/watch.rs index 87426cf9..56f8fe23 100644 --- a/melib/src/backends/imap/watch.rs +++ b/melib/src/backends/imap/watch.rs @@ -124,7 +124,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { .read() .unwrap() .values() - .find(|f| f.parent.is_none() && (f.usage == SpecialUsageMailbox::Inbox)) + .find(|f| f.parent.is_none() && (f.special_usage() == SpecialUsageMailbox::Inbox)) .map(std::clone::Clone::clone) { Some(folder) => folder, @@ -166,6 +166,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { hash: folder_hash, kind: RefreshEventKind::Rescan, }); + *prev_exists = 0; uid_store.uid_index.lock().unwrap().clear(); uid_store.hash_index.lock().unwrap().clear(); uid_store.byte_cache.lock().unwrap().clear(); @@ -346,6 +347,11 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { env.labels_mut().push(hash); } } + if !env.is_seen() { + *folder.unseen.lock().unwrap() += 1; + } + *folder.exists.lock().unwrap() += 1; + sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -460,6 +466,9 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { env.subject(), folder.path(), ); + if !env.is_seen() { + *folder.unseen.lock().unwrap() += 1; + } sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -626,6 +635,9 @@ fn examine_updates( env.labels_mut().push(hash); } } + if !env.is_seen() { + *folder.unseen.lock().unwrap() += 1; + } sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -697,6 +709,9 @@ fn examine_updates( env.subject(), folder.path(), ); + if !env.is_seen() { + *folder.unseen.lock().unwrap() += 1; + } sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index e1815c19..35f38a25 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -289,7 +289,10 @@ impl JmapType { s: &AccountSettings, is_subscribed: Box bool + Send + Sync>, ) -> Result> { - let online = Arc::new(Mutex::new(Err(MeliError::new("Account is uninitialised.")))); + let online = Arc::new(Mutex::new(( + std::time::Instant::now(), + Err(MeliError::new("Account is uninitialised.")), + ))); let server_conf = JmapServerConf::new(s)?; Ok(Box::new(JmapType { diff --git a/melib/src/backends/jmap/connection.rs b/melib/src/backends/jmap/connection.rs index ea520b3a..c094df24 100644 --- a/melib/src/backends/jmap/connection.rs +++ b/melib/src/backends/jmap/connection.rs @@ -33,10 +33,12 @@ pub struct JmapConnection { } impl JmapConnection { - pub fn new(server_conf: &JmapServerConf, online_status: Arc)>>) -> Result { + pub fn new( + server_conf: &JmapServerConf, + online_status: Arc)>>, + ) -> Result { use reqwest::header; let mut headers = header::HeaderMap::new(); - let connection_start = std::time::Instant::now(); headers.insert( header::ACCEPT, header::HeaderValue::from_static("application/json"), @@ -70,28 +72,29 @@ impl JmapConnection { let res_text = req.text()?; let session: JmapSession = serde_json::from_str(&res_text).map_err(|_| { - let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)", &server_conf.server_hostname); - *online_status.lock().unwrap() = (Instant::new(), Err(err.clone())); + let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)", &server_conf.server_hostname)); + *online_status.lock().unwrap() = (Instant::now(), Err(err.clone())); err - ))?; + } + )?; if !session .capabilities .contains_key("urn:ietf:params:jmap:core") { - let err = Err(MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::>().join(", ")))); - *online_status.lock().unwrap() = (Instant::new(), Err(err.clone())); - return err; + let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::>().join(", "))); + *online_status.lock().unwrap() = (Instant::now(), Err(err.clone())); + return Err(err); } if !session .capabilities .contains_key("urn:ietf:params:jmap:mail") { - let err = Err(MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::>().join(", ")))); - *online_status.lock().unwrap() = (Instant::new(), Err(err.clone())); - return err; + let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::>().join(", "))); + *online_status.lock().unwrap() = (Instant::now(), Err(err.clone())); + return Err(err); } - *online_status.lock().unwrap() = (Instant::new(), Ok(())); + *online_status.lock().unwrap() = (Instant::now(), Ok(())); let server_conf = server_conf.clone(); Ok(JmapConnection { session, diff --git a/melib/src/backends/jmap/folder.rs b/melib/src/backends/jmap/folder.rs index 87e34c91..373650e1 100644 --- a/melib/src/backends/jmap/folder.rs +++ b/melib/src/backends/jmap/folder.rs @@ -21,6 +21,7 @@ use super::*; use crate::backends::{FolderPermissions, SpecialUsageMailbox}; +use std::sync::{Arc, Mutex, RwLock}; #[derive(Debug, Clone)] pub struct JmapFolder { @@ -34,11 +35,11 @@ pub struct JmapFolder { pub parent_id: Option, pub role: Option, pub sort_order: u64, - pub total_emails: u64, + pub total_emails: Arc>, pub total_threads: u64, - pub unread_emails: u64, + pub unread_emails: Arc>, pub unread_threads: u64, - pub usage: SpecialUsageMailbox, + pub usage: Arc>, } impl BackendFolder for JmapFolder { @@ -91,4 +92,24 @@ impl BackendFolder for JmapFolder { None => SpecialUsageMailbox::Normal, } } + fn is_subscribed(&self) -> bool { + self.is_subscribed + } + fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> { + self.is_subscribed = new_val; + // FIXME: jmap subscribe + Ok(()) + } + + fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> { + *self.usage.write()? = new_val; + Ok(()) + } + + fn count(&self) -> Result<(usize, usize)> { + Ok(( + *self.unread_emails.lock()? as usize, + *self.total_emails.lock()? as usize, + )) + } } diff --git a/melib/src/backends/jmap/operations.rs b/melib/src/backends/jmap/operations.rs index 68f58e24..79d904f1 100644 --- a/melib/src/backends/jmap/operations.rs +++ b/melib/src/backends/jmap/operations.rs @@ -105,7 +105,7 @@ impl BackendOp for JmapOp { Ok(()) } - fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, value: bool) -> Result<()> { + fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> { Ok(()) } } diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index 4029e48c..6057a2e9 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -126,7 +126,7 @@ pub fn get_mailboxes(conn: &JmapConnection) -> Result::try_from(v.method_responses.remove(0))?; let GetResponse:: { list, account_id, .. @@ -163,9 +163,9 @@ pub fn get_mailboxes(conn: &JmapConnection) -> Result Result::try_from(v.method_responses.remove(0))?; let QueryResponse:: { ids, .. } = m; Ok(ids) diff --git a/melib/src/backends/maildir.rs b/melib/src/backends/maildir.rs index 177c86c2..a58d48af 100644 --- a/melib/src/backends/maildir.rs +++ b/melib/src/backends/maildir.rs @@ -33,6 +33,7 @@ use std::collections::hash_map::DefaultHasher; use std::fs; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; /// `BackendOp` implementor for Maildir #[derive(Debug)] @@ -168,7 +169,7 @@ impl<'a> BackendOp for MaildirOp { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct MaildirFolder { hash: FolderHash, name: String, @@ -176,8 +177,11 @@ pub struct MaildirFolder { path: PathBuf, parent: Option, children: Vec, - usage: SpecialUsageMailbox, + pub usage: Arc>, + pub is_subscribed: bool, permissions: FolderPermissions, + pub total: Arc>, + pub unseen: Arc>, } impl MaildirFolder { @@ -238,7 +242,8 @@ impl MaildirFolder { fs_path: pathbuf, parent, children, - usage: SpecialUsageMailbox::Normal, + usage: Arc::new(RwLock::new(SpecialUsageMailbox::Normal)), + is_subscribed: true, permissions: FolderPermissions { create_messages: !read_only, remove_messages: !read_only, @@ -249,6 +254,8 @@ impl MaildirFolder { delete_mailbox: !read_only, change_permissions: false, }, + unseen: Arc::new(Mutex::new(0)), + total: Arc::new(Mutex::new(0)), }; ret.is_valid()?; Ok(ret) @@ -274,6 +281,7 @@ impl MaildirFolder { Ok(()) } } + impl BackendFolder for MaildirFolder { fn hash(&self) -> FolderHash { self.hash @@ -296,20 +304,11 @@ impl BackendFolder for MaildirFolder { } fn clone(&self) -> Folder { - Box::new(MaildirFolder { - hash: self.hash, - name: self.name.clone(), - fs_path: self.fs_path.clone(), - path: self.path.clone(), - children: self.children.clone(), - usage: self.usage, - parent: self.parent, - permissions: self.permissions, - }) + Box::new(std::clone::Clone::clone(self)) } fn special_usage(&self) -> SpecialUsageMailbox { - self.usage + *self.usage.read().unwrap() } fn parent(&self) -> Option { @@ -319,4 +318,20 @@ impl BackendFolder for MaildirFolder { fn permissions(&self) -> FolderPermissions { self.permissions } + fn is_subscribed(&self) -> bool { + self.is_subscribed + } + fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> { + self.is_subscribed = new_val; + Ok(()) + } + + fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> { + *self.usage.write()? = new_val; + Ok(()) + } + + fn count(&self) -> Result<(usize, usize)> { + Ok((*self.unseen.lock()?, *self.total.lock()?)) + } } diff --git a/melib/src/backends/maildir/backend.rs b/melib/src/backends/maildir/backend.rs index 93c6c17d..e524b688 100644 --- a/melib/src/backends/maildir/backend.rs +++ b/melib/src/backends/maildir/backend.rs @@ -192,7 +192,7 @@ impl MailBackend for MaildirType { Ok(self .folders .iter() - .map(|(h, f)| (*h, f.clone() as Folder)) + .map(|(h, f)| (*h, BackendFolder::clone(f))) .collect()) } @@ -342,6 +342,11 @@ impl MailBackend for MaildirType { debug!("watching {:?}", root_path); let hash_indexes = self.hash_indexes.clone(); let folder_index = self.folder_index.clone(); + let folder_counts = self + .folders + .iter() + .map(|(&k, v)| (k, (v.unseen.clone(), v.total.clone()))) + .collect::>, Arc>)>>(); let handle = thread::Builder::new() .name("folder watch".to_string()) .spawn(move || { @@ -397,6 +402,10 @@ impl MailBackend for MaildirType { env.subject(), pathbuf.display() ); + if !env.is_seen() { + *folder_counts[&folder_hash].0.lock().unwrap() += 1; + } + *folder_counts[&folder_hash].1.lock().unwrap() += 1; sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -505,6 +514,8 @@ impl MailBackend for MaildirType { e.removed = true; }); + //FIXME: check if envelope was unseen to update unseen count + *folder_counts[&folder_hash].1.lock().unwrap() += 1; sender.send(RefreshEvent { hash: folder_hash, kind: Remove(hash), @@ -579,6 +590,10 @@ impl MailBackend for MaildirType { env.subject(), dest.display() ); + if !env.is_seen() { + *folder_counts[&folder_hash].0.lock().unwrap() += 1; + } + *folder_counts[&folder_hash].1.lock().unwrap() += 1; sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -777,6 +792,8 @@ impl MaildirType { // TODO: Avoid clone let folder: &MaildirFolder = &self.folders[&self.owned_folder_idx(folder)]; let folder_hash = folder.hash(); + let unseen = folder.unseen.clone(); + let total = folder.total.clone(); let tx_final = w.tx(); let path: PathBuf = folder.fs_path().into(); let name = format!("parsing {:?}", folder.name()); @@ -785,6 +802,8 @@ impl MaildirType { let folder_index = self.folder_index.clone(); let closure = move |work_context: crate::async_workers::WorkContext| { + let unseen = unseen.clone(); + let total = total.clone(); let name = name.clone(); work_context .set_name @@ -829,6 +848,8 @@ impl MaildirType { count }; for chunk in files.chunks(chunk_size) { + let unseen = unseen.clone(); + let total = total.clone(); let cache_dir = cache_dir.clone(); let tx = tx.clone(); let map = map.clone(); @@ -869,6 +890,10 @@ impl MaildirType { .lock() .unwrap() .insert(hash, folder_hash); + if !env.is_seen() { + *unseen.lock().unwrap() += 1; + } + *total.lock().unwrap() += 1; local_r.push(env); continue; } @@ -908,6 +933,10 @@ impl MaildirType { let writer = io::BufWriter::new(f); bincode::serialize_into(writer, &e).unwrap(); } + if !e.is_seen() { + *unseen.lock().unwrap() += 1; + } + *total.lock().unwrap() += 1; local_r.push(e); } else { debug!( diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index c0a254c9..8ec2ac90 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -49,7 +49,7 @@ use std::io::Read; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; const F_OFD_SETLKW: libc::c_int = 38; @@ -87,7 +87,11 @@ struct MboxFolder { content: Vec, children: Vec, parent: Option, + usage: Arc>, + is_subscribed: bool, permissions: FolderPermissions, + pub total: Arc>, + pub unseen: Arc>, } impl BackendFolder for MboxFolder { @@ -115,8 +119,12 @@ impl BackendFolder for MboxFolder { path: self.path.clone(), content: self.content.clone(), children: self.children.clone(), + usage: self.usage.clone(), + is_subscribed: self.is_subscribed, parent: self.parent, permissions: self.permissions, + unseen: self.unseen.clone(), + total: self.total.clone(), }) } @@ -129,12 +137,27 @@ impl BackendFolder for MboxFolder { } fn special_usage(&self) -> SpecialUsageMailbox { - SpecialUsageMailbox::Normal + *self.usage.read().unwrap() } fn permissions(&self) -> FolderPermissions { self.permissions } + fn is_subscribed(&self) -> bool { + self.is_subscribed + } + fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> { + self.is_subscribed = new_val; + Ok(()) + } + fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> { + *self.usage.write()? = new_val; + Ok(()) + } + + fn count(&self) -> Result<(usize, usize)> { + Ok((*self.unseen.lock()?, *self.total.lock()?)) + } } /// `BackendOp` implementor for Mbox @@ -631,6 +654,8 @@ impl MboxType { content: Vec::new(), children: Vec::new(), parent: None, + usage: Arc::new(RwLock::new(SpecialUsageMailbox::Normal)), + is_subscribed: true, permissions: FolderPermissions { create_messages: !read_only, remove_messages: !read_only, @@ -641,6 +666,8 @@ impl MboxType { delete_mailbox: !read_only, change_permissions: false, }, + unseen: Arc::new(Mutex::new(0)), + total: Arc::new(Mutex::new(0)), }, ); /* diff --git a/melib/src/backends/notmuch.rs b/melib/src/backends/notmuch.rs index 708d022a..52c50eb6 100644 --- a/melib/src/backends/notmuch.rs +++ b/melib/src/backends/notmuch.rs @@ -16,7 +16,7 @@ use std::ffi::{CStr, CString}; use std::hash::{Hash, Hasher}; use std::io::Read; use std::path::{Path, PathBuf}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; pub mod bindings; use bindings::*; @@ -67,10 +67,13 @@ struct NotmuchFolder { parent: Option, name: String, path: String, - usage: SpecialUsageMailbox, query_str: String, query: Option<*mut notmuch_query_t>, phantom: std::marker::PhantomData<&'static mut notmuch_query_t>, + usage: Arc>, + + total: Arc>, + unseen: Arc>, } impl BackendFolder for NotmuchFolder { @@ -101,12 +104,29 @@ impl BackendFolder for NotmuchFolder { } fn special_usage(&self) -> SpecialUsageMailbox { - self.usage + *self.usage.read().unwrap() } fn permissions(&self) -> FolderPermissions { FolderPermissions::default() } + + fn is_subscribed(&self) -> bool { + true + } + + fn set_is_subscribed(&mut self, _new_val: bool) -> Result<()> { + Ok(()) + } + + fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> { + *self.usage.write()? = new_val; + Ok(()) + } + + fn count(&self) -> Result<(usize, usize)> { + Ok((*self.unseen.lock()?, *self.total.lock()?)) + } } unsafe impl Send for NotmuchFolder {} @@ -162,8 +182,10 @@ impl NotmuchDb { parent: None, query: None, query_str: query_str.to_string(), - usage: SpecialUsageMailbox::Normal, + usage: Arc::new(RwLock::new(SpecialUsageMailbox::Normal)), phantom: std::marker::PhantomData, + total: Arc::new(Mutex::new(0)), + unseen: Arc::new(Mutex::new(0)), }, ); } else { diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing.rs index d2f7dd16..e792b84e 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing.rs @@ -770,17 +770,11 @@ impl Component for Listing { } else { return Some(String::new()); }; - let envelopes = account.collection.envelopes.clone(); - let envelopes = envelopes.read().unwrap(); format!( "Mailbox: {}, Messages: {}, New: {}", m.folder.name(), m.envelopes.len(), - m.envelopes - .iter() - .map(|h| &envelopes[&h]) - .filter(|e| !e.is_seen()) - .count() + m.folder.count().ok().map(|(v, _)| v).unwrap_or(0), ) }) } @@ -903,20 +897,12 @@ impl Listing { ) { match context.accounts[index].status(entries[&folder_idx].hash()) { Ok(_) => { - let account = &context.accounts[index]; - let count = account[entries[&folder_idx].hash()] - .unwrap() - .envelopes - .iter() - .filter_map(|&h| { - if account.collection.get_env(h).is_seen() { - None - } else { - Some(()) - } - }) - .count(); - lines.push((*depth, *inc, folder_idx, Some(count))); + lines.push(( + *depth, + *inc, + folder_idx, + entries[&folder_idx].count().ok().map(|(v, _)| v), + )); } Err(_) => { lines.push((*depth, *inc, folder_idx, None)); diff --git a/ui/src/components/mail/status.rs b/ui/src/components/mail/status.rs index 653e5898..d0734614 100644 --- a/ui/src/components/mail/status.rs +++ b/ui/src/components/mail/status.rs @@ -291,18 +291,12 @@ impl StatusPanel { self.content[(2, h + y + 1)].set_ch(' '); } } + let count = a.ref_folders.values().fold((0, 0), |acc, f| { + let count = f.count().unwrap_or((0, 0)); + (acc.0 + count.0, acc.1 + count.1) + }); let (mut column_width, _) = write_string_to_grid( - &format!( - "Messages total {}, unseen {}", - a.collection.len(), - a.collection - .envelopes - .read() - .unwrap() - .values() - .filter(|e| !e.is_seen()) - .count() - ), + &format!("Messages total {}, unseen {}", count.1, count.0), &mut self.content, Color::Default, Color::Default, @@ -338,17 +332,7 @@ impl StatusPanel { ); /* next column */ write_string_to_grid( - &format!( - "Messages total {}, unseen {}", - a.collection.len(), - a.collection - .envelopes - .read() - .unwrap() - .values() - .filter(|e| !e.is_seen()) - .count() - ), + "Special Mailboxes:", &mut self.content, Color::Default, Color::Default, @@ -356,6 +340,22 @@ impl StatusPanel { ((5 + column_width, y + 2), (120 - 2, y + 2)), None, ); + for (i, f) in a + .ref_folders + .values() + .filter(|f| f.special_usage() != SpecialUsageMailbox::Normal) + .enumerate() + { + write_string_to_grid( + &format!("{}: {}", f.path(), f.special_usage()), + &mut self.content, + Color::Default, + Color::Default, + Attr::Default, + ((5 + column_width, y + 3 + i), (120 - 2, y + 2)), + None, + ); + } } } } diff --git a/ui/src/conf/accounts.rs b/ui/src/conf/accounts.rs index 3c808c6c..0c0a4c03 100644 --- a/ui/src/conf/accounts.rs +++ b/ui/src/conf/accounts.rs @@ -146,6 +146,7 @@ pub struct Account { name: String, pub is_online: bool, pub(crate) folders: FnvHashMap, + pub(crate) ref_folders: FnvHashMap, pub(crate) folder_confs: FnvHashMap, pub(crate) folders_order: Vec, pub(crate) folder_names: FnvHashMap, @@ -289,6 +290,7 @@ impl Account { name, is_online: false, folders: Default::default(), + ref_folders: Default::default(), folder_confs: Default::default(), folders_order: Default::default(), folder_names: Default::default(), @@ -309,7 +311,7 @@ impl Account { } fn init(&mut self) { - let ref_folders: FnvHashMap = + let mut ref_folders: FnvHashMap = match self.backend.read().unwrap().folders() { Ok(f) => f, Err(err) => { @@ -325,7 +327,7 @@ impl Account { let mut folder_confs = FnvHashMap::default(); let mut sent_folder = None; - for f in ref_folders.values() { + for f in ref_folders.values_mut() { if !((self.settings.folder_confs.contains_key(f.path()) && self.settings.folder_confs[f.path()] .folder_conf() @@ -342,18 +344,41 @@ impl Account { continue; } - if self.settings.folder_confs.contains_key(f.path()) { - match self.settings.folder_confs[f.path()].folder_conf().usage { + if let Some(conf) = self.settings.folder_confs.get_mut(f.path()) { + conf.folder_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal { + Some(f.special_usage()) + } else { + let tmp = SpecialUsageMailbox::detect_usage(f.name()); + if tmp != Some(SpecialUsageMailbox::Normal) && tmp != None { + let _ = f.set_special_usage(tmp.unwrap()); + } + tmp + }; + match conf.folder_conf.usage { Some(SpecialUsageMailbox::Sent) => { sent_folder = Some(f.hash()); } + None => { + if f.special_usage() == SpecialUsageMailbox::Sent { + sent_folder = Some(f.hash()); + } + } _ => {} } - folder_confs.insert(f.hash(), self.settings.folder_confs[f.path()].clone()); + folder_confs.insert(f.hash(), conf.clone()); } else { let mut new = FileFolderConf::default(); new.folder_conf.subscribe = super::ToggleFlag::InternalVal(true); - new.folder_conf.usage = SpecialUsageMailbox::detect_usage(f.name()); + new.folder_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal { + Some(f.special_usage()) + } else { + let tmp = SpecialUsageMailbox::detect_usage(f.name()); + if tmp != Some(SpecialUsageMailbox::Normal) && tmp != None { + let _ = f.set_special_usage(tmp.unwrap()); + } + tmp + }; + folder_confs.insert(f.hash(), new); } folder_names.insert(f.hash(), f.path().to_string()); @@ -396,7 +421,6 @@ impl Account { workers.insert( *h, Account::new_worker( - &folder_confs, f.clone(), &mut self.backend, &self.work_context, @@ -440,6 +464,7 @@ impl Account { } self.folders = folders; + self.ref_folders = ref_folders; self.folder_confs = folder_confs; self.folders_order = folders_order; self.folder_names = folder_names; @@ -450,7 +475,6 @@ impl Account { } fn new_worker( - folder_confs: &FnvHashMap, folder: Folder, backend: &Arc>>, work_context: &WorkContext, @@ -460,11 +484,11 @@ impl Account { let mut builder = AsyncBuilder::new(); let our_tx = builder.tx(); let folder_hash = folder.hash(); - let priority = match folder_confs[&folder.hash()].folder_conf().usage { - Some(SpecialUsageMailbox::Inbox) => 0, - Some(SpecialUsageMailbox::Sent) => 1, - Some(SpecialUsageMailbox::Drafts) | Some(SpecialUsageMailbox::Trash) => 2, - Some(_) | None => { + let priority = match folder.special_usage() { + SpecialUsageMailbox::Inbox => 0, + SpecialUsageMailbox::Sent => 1, + SpecialUsageMailbox::Drafts | SpecialUsageMailbox::Trash => 2, + _ => { 3 * folder .path() .split(if folder.path().contains('/') { @@ -673,7 +697,6 @@ impl Account { let ref_folders: FnvHashMap = self.backend.read().unwrap().folders().unwrap(); let handle = Account::new_worker( - &self.folder_confs, ref_folders[&folder_hash].clone(), &mut self.backend, &self.work_context,