diff --git a/melib/src/mailbox/backends/maildir.rs b/melib/src/mailbox/backends/maildir/backend.rs similarity index 57% rename from melib/src/mailbox/backends/maildir.rs rename to melib/src/mailbox/backends/maildir/backend.rs index 4561bea96..53138899a 100644 --- a/melib/src/mailbox/backends/maildir.rs +++ b/melib/src/mailbox/backends/maildir/backend.rs @@ -20,18 +20,19 @@ */ extern crate bincode; +extern crate fnv; +extern crate notify; extern crate xdg; +use super::{MaildirFolder, MaildirOp}; use async::*; use conf::AccountSettings; -use error::{MeliError, Result}; +use error::Result; use mailbox::backends::{ BackendFolder, BackendOp, Folder, MailBackend, RefreshEvent, RefreshEventConsumer, + RefreshEventKind::*, }; -use mailbox::email::parser; -use mailbox::email::{Envelope, Flag}; - -extern crate notify; +use mailbox::email::{Envelope, EnvelopeHash}; use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use std::time::Duration; @@ -42,148 +43,84 @@ use std::sync::mpsc::channel; //use std::time::Duration; use std::thread; extern crate crossbeam; -use memmap::{Mmap, Protection}; +use self::fnv::{FnvHashMap, FnvHasher}; use std::collections::hash_map::DefaultHasher; +use std::ffi::OsStr; use std::fs; use std::hash::{Hash, Hasher}; use std::io; use std::io::Read; use std::os::unix::ffi::OsStrExt; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use std::result; use std::sync::{Arc, Mutex}; -extern crate fnv; -use self::fnv::{FnvHashMap, FnvHasher}; -/// `BackendOp` implementor for Maildir -#[derive(Debug)] -pub struct MaildirOp { - hash_index: Arc>>, - hash: u64, - slice: Option, -} - -impl Clone for MaildirOp { - fn clone(&self) -> Self { - MaildirOp { - hash_index: self.hash_index.clone(), - hash: self.hash, - slice: None, - } - } -} - -impl MaildirOp { - pub fn new(hash: u64, hash_index: Arc>>) -> Self { - MaildirOp { - hash_index, - hash, - slice: None, - } - } - fn path(&self) -> PathBuf { - let hash_index = self.hash_index.clone(); - let map = hash_index.lock().unwrap(); - map.get(&self.hash).unwrap().1.clone() - } -} - -impl<'a> BackendOp for MaildirOp { - fn description(&self) -> String { - format!("Path of file: {}", self.path().display()) - } - fn as_bytes(&mut self) -> Result<&[u8]> { - if self.slice.is_none() { - self.slice = Some(Mmap::open_path(self.path(), Protection::Read)?); - } - /* Unwrap is safe since we use ? above. */ - Ok(unsafe { self.slice.as_ref().unwrap().as_slice() }) - } - fn fetch_headers(&mut self) -> Result<&[u8]> { - let raw = self.as_bytes()?; - let result = parser::headers_raw(raw).to_full_result()?; - Ok(result) - } - fn fetch_body(&mut self) -> Result<&[u8]> { - let raw = self.as_bytes()?; - let result = parser::headers_raw(raw).to_full_result()?; - Ok(result) - } - fn fetch_flags(&self) -> Flag { - let mut flag = Flag::default(); - let path = self.path(); - let path = path.to_str().unwrap(); // Assume UTF-8 validity - if !path.contains(":2,") { - return flag; - } - - for f in path.chars().rev() { - match f { - ',' => break, - 'D' => flag |= Flag::DRAFT, - 'F' => flag |= Flag::FLAGGED, - 'P' => flag |= Flag::PASSED, - 'R' => flag |= Flag::REPLIED, - 'S' => flag |= Flag::SEEN, - 'T' => flag |= Flag::TRASHED, - _ => panic!(), - } - } - - flag - } - - fn set_flag(&mut self, envelope: &mut Envelope, f: &Flag) -> Result<()> { - let path = self.path(); - let path = path.to_str().unwrap(); // Assume UTF-8 validity - let idx: usize = path - .rfind(":2,") - .ok_or_else(|| MeliError::new(format!("Invalid email filename: {:?}", self)))? - + 3; - let mut new_name: String = path[..idx].to_string(); - let mut flags = self.fetch_flags(); - if !(flags & *f).is_empty() { - return Ok(()); - } - flags.toggle(*f); - if !(flags & Flag::DRAFT).is_empty() { - new_name.push('D'); - } - if !(flags & Flag::FLAGGED).is_empty() { - new_name.push('F'); - } - if !(flags & Flag::PASSED).is_empty() { - new_name.push('P'); - } - if !(flags & Flag::REPLIED).is_empty() { - new_name.push('R'); - } - if !(flags & Flag::SEEN).is_empty() { - new_name.push('S'); - } - if !(flags & Flag::TRASHED).is_empty() { - new_name.push('T'); - } - - fs::rename(&path, &new_name)?; - let hash = envelope.hash(); - let hash_index = self.hash_index.clone(); - let mut map = hash_index.lock().unwrap(); - map.get_mut(&hash).unwrap().1 = PathBuf::from(new_name); - Ok(()) - } -} +type HashIndex = Arc>>; /// Maildir backend https://cr.yp.to/proto/maildir.html #[derive(Debug)] pub struct MaildirType { name: String, folders: Vec, - hash_index: Arc>>, + hash_index: HashIndex, path: PathBuf, } +macro_rules! path_is_new { + ($path:expr) => { + if $path.is_dir() { + false + } else { + let mut iter = $path.components().rev(); + iter.next(); + iter.next(); + iter.next() == Some(Component::Normal(OsStr::new("new"))) + } + }; +} +macro_rules! get_path_hash { + ($path:expr) => {{ + if $path.is_dir() { + if $path.ends_with("cur") | $path.ends_with("new") { + $path.pop(); + } + } else { + $path.pop(); + $path.pop(); + }; + eprintln!(" got event in {}", $path.display()); + + let mut hasher = DefaultHasher::new(); + $path.hash(&mut hasher); + hasher.finish() + }}; +} + +fn get_file_hash(file: &Path) -> EnvelopeHash { + let mut buf = Vec::new(); + let mut f = fs::File::open(&file).unwrap_or_else(|_| panic!("Can't open {}", file.display())); + f.read_to_end(&mut buf) + .unwrap_or_else(|_| panic!("Can't read {}", file.display())); + let mut hasher = FnvHasher::default(); + hasher.write(file.as_os_str().as_bytes()); + hasher.write(&buf); + hasher.finish() +} + +fn move_to_cur(p: PathBuf) { + let mut new = p.clone(); + { + let file_name = p.file_name().unwrap(); + new.pop(); + new.pop(); + + new.push("cur"); + new.push(file_name); + } + fs::rename(p, new).unwrap(); +} + impl MailBackend for MaildirType { fn folders(&self) -> Vec { self.folders.iter().map(|f| f.clone()).collect() @@ -194,6 +131,8 @@ impl MailBackend for MaildirType { fn watch(&self, sender: RefreshEventConsumer) -> Result<()> { let (tx, rx) = channel(); let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); + let root_path = self.path.to_path_buf(); + let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap(); for f in &self.folders { if f.is_valid().is_err() { continue; @@ -206,6 +145,7 @@ impl MailBackend for MaildirType { p.push("new"); watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); } + let map = self.hash_index.clone(); thread::Builder::new() .name("folder watch".to_string()) .spawn(move || { @@ -213,26 +153,127 @@ impl MailBackend for MaildirType { let _watcher = watcher; loop { match rx.recv() { + /* + * Event types: + * + * pub enum RefreshEventKind { + * Update(EnvelopeHash, Envelope), // Old hash, new envelope + * Create(Envelope), + * Remove(EnvelopeHash), + * Rescan, + * } + */ Ok(event) => match event { - DebouncedEvent::Create(mut pathbuf) - | DebouncedEvent::Remove(mut pathbuf) => { - if pathbuf.is_dir() { - if pathbuf.ends_with("cur") | pathbuf.ends_with("new") { - pathbuf.pop(); - } + /* Create */ + DebouncedEvent::Create(mut pathbuf) => { + if path_is_new!(pathbuf) { + move_to_cur(pathbuf); + continue; + } + let file_name = pathbuf + .as_path() + .strip_prefix(&root_path) + .unwrap() + .to_path_buf(); + if let Some(env) = add_path_to_index( + &map, + pathbuf.as_path(), + &cache_dir, + file_name, + ) { + sender.send(RefreshEvent { + hash: get_path_hash!(pathbuf), + kind: Create(env), + }); } else { - pathbuf.pop(); - pathbuf.pop(); + continue; + } + } + /* Update */ + DebouncedEvent::NoticeWrite(mut pathbuf) + | DebouncedEvent::Write(mut pathbuf) => { + let file_name = pathbuf + .as_path() + .strip_prefix(&root_path) + .unwrap() + .to_path_buf(); + /* Linear search in hash_index to find old hash */ + let old_hash: EnvelopeHash = { + let mut map_lock = map.lock().unwrap(); + if let Some((k, v)) = + map_lock.iter_mut().find(|(_, v)| v.1 == pathbuf) + { + v.1 = pathbuf.clone(); + *k + } else { + /* Did we just miss a Create event? In any case, create + * envelope. */ + if let Some(env) = add_path_to_index( + &map, + pathbuf.as_path(), + &cache_dir, + file_name, + ) { + sender.send(RefreshEvent { + hash: get_path_hash!(pathbuf), + kind: Create(env), + }); + } + return; + } + }; + let new_hash: EnvelopeHash = get_file_hash(pathbuf.as_path()); + let mut map_lock = map.lock().unwrap(); + if map_lock.get_mut(&new_hash).is_none() { + let op = Box::new(MaildirOp::new(new_hash, map.clone())); + if let Some(env) = Envelope::from_token(op, new_hash) { + map_lock.insert(new_hash, (0, pathbuf.clone())); + + /* Send Write notice */ + + sender.send(RefreshEvent { + hash: get_path_hash!(pathbuf), + kind: Update(old_hash, env), + }); + } + } + } + /* Remove */ + DebouncedEvent::NoticeRemove(mut pathbuf) + | DebouncedEvent::Remove(mut pathbuf) => { + let map = map.lock().unwrap(); + let hash: EnvelopeHash = if let Some((k, _)) = + map.iter().find(|(_, v)| v.1 == pathbuf) + { + *k + } else { + continue; }; - eprintln!(" got event in {}", pathbuf.display()); - let mut hasher = DefaultHasher::new(); - pathbuf.hash(&mut hasher); sender.send(RefreshEvent { - folder: format!("{}", pathbuf.display()), - hash: hasher.finish(), + hash: get_path_hash!(pathbuf), + kind: Remove(hash), }); } + /* Envelope hasn't changed, so handle this here */ + DebouncedEvent::Rename(_, mut dest) => { + let new_hash: EnvelopeHash = get_file_hash(dest.as_path()); + let mut map = map.lock().unwrap(); + if let Some(v) = map.get_mut(&new_hash) { + v.1 = dest; + } else { + /* Maybe a re-read should be triggered here just to be safe. */ + sender.send(RefreshEvent { + hash: get_path_hash!(dest), + kind: Rescan, + }); + } + } + /* Trigger rescan of folder */ + DebouncedEvent::Rescan => { + /* Actually should rescan all folders */ + unreachable!("Unimplemented: rescan of all folders in MaildirType") + } _ => {} }, Err(e) => eprintln!("watch error: {:?}", e), @@ -241,7 +282,7 @@ impl MailBackend for MaildirType { })?; Ok(()) } - fn operation(&self, hash: u64) -> Box { + fn operation(&self, hash: EnvelopeHash) -> Box { Box::new(MaildirOp::new(hash, self.hash_index.clone())) } } @@ -296,12 +337,12 @@ impl MaildirType { } } fn owned_folder_idx(&self, folder: &Folder) -> usize { - for (idx, f) in self.folders.iter().enumerate() { - if f.hash() == folder.hash() { - return idx; - } - } - unreachable!() + self.folders + .iter() + .enumerate() + .find(|(_, f)| f.hash() == folder.hash()) + .unwrap() + .0 } pub fn multicore(&mut self, cores: usize, folder: &Folder) -> Async>> { @@ -321,6 +362,15 @@ impl MaildirType { .name(name.clone()) .spawn(move || { let cache_dir = cache_dir.clone(); + { + path.push("new"); + for d in path.read_dir()? { + if let Ok(p) = d { + move_to_cur(p.path()); + } + } + path.pop(); + } path.push("cur"); let iter = path.read_dir()?; let count = path.read_dir()?.count(); @@ -382,20 +432,7 @@ impl MaildirType { } } { - let hash = { - let mut buf = Vec::new(); - let mut f = fs::File::open(&file) - .unwrap_or_else(|_| { - panic!("Can't open {}", file.display()) - }); - f.read_to_end(&mut buf).unwrap_or_else(|_| { - panic!("Can't read {}", file.display()) - }); - let mut hasher = FnvHasher::default(); - hasher.write(file.as_os_str().as_bytes()); - hasher.write(&buf); - hasher.finish() - }; + let hash = get_file_hash(file); { let mut map = map.lock().unwrap(); if (*map).contains_key(&hash) { @@ -454,67 +491,37 @@ impl MaildirType { } } -#[derive(Debug, Default)] -pub struct MaildirFolder { - hash: u64, - name: String, - path: PathBuf, - children: Vec, -} - -impl MaildirFolder { - pub fn new(path: String, file_name: String, children: Vec) -> Result { - let pathbuf = PathBuf::from(path); - let mut h = DefaultHasher::new(); - pathbuf.hash(&mut h); - - let ret = MaildirFolder { - hash: h.finish(), - name: file_name, - path: pathbuf, - children, - }; - ret.is_valid()?; - Ok(ret) - } - pub fn path(&self) -> &Path { - self.path.as_path() - } - fn is_valid(&self) -> Result<()> { - let path = self.path(); - let mut p = PathBuf::from(path); - for d in &["cur", "new", "tmp"] { - p.push(d); - if !p.is_dir() { - return Err(MeliError::new(format!( - "{} is not a valid maildir folder", - path.display() - ))); - } - p.pop(); +fn add_path_to_index( + map: &HashIndex, + path: &Path, + cache_dir: &xdg::BaseDirectories, + file_name: PathBuf, +) -> Option { + let env: Envelope; + let hash = get_file_hash(path); + { + let mut map = map.lock().unwrap(); + if (*map).contains_key(&hash) { + return None; } - Ok(()) - } -} -impl BackendFolder for MaildirFolder { - fn hash(&self) -> u64 { - self.hash - } - fn name(&self) -> &str { - &self.name - } - fn change_name(&mut self, s: &str) { - self.name = s.to_string(); - } - fn children(&self) -> &Vec { - &self.children - } - fn clone(&self) -> Folder { - Box::new(MaildirFolder { - hash: self.hash, - name: self.name.clone(), - path: self.path.clone(), - children: self.children.clone(), - }) + (*map).insert(hash, (0, path.to_path_buf())); } + let op = Box::new(MaildirOp::new(hash, map.clone())); + if let Some(e) = Envelope::from_token(op, hash) { + if let Ok(cached) = cache_dir.place_cache_file(file_name) { + /* place result in cache directory */ + let f = match fs::File::create(cached) { + Ok(f) => f, + Err(e) => { + panic!("{}", e); + } + }; + let writer = io::BufWriter::new(f); + bincode::serialize_into(writer, &e).unwrap(); + } + env = e; + } else { + return None; + } + Some(env) } diff --git a/melib/src/mailbox/backends/maildir/mod.rs b/melib/src/mailbox/backends/maildir/mod.rs new file mode 100644 index 000000000..2957afb00 --- /dev/null +++ b/melib/src/mailbox/backends/maildir/mod.rs @@ -0,0 +1,225 @@ +/* + * meli - mailbox module. + * + * Copyright 2017 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +extern crate fnv; +use self::fnv::FnvHashMap; + +mod backend; +pub use self::backend::*; + +use error::{MeliError, Result}; +use mailbox::backends::*; +use mailbox::email::parser; +use mailbox::email::{Envelope, Flag}; + +use memmap::{Mmap, Protection}; +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)] +pub struct MaildirOp { + hash_index: Arc>>, + hash: EnvelopeHash, + slice: Option, +} + +impl Clone for MaildirOp { + fn clone(&self) -> Self { + MaildirOp { + hash_index: self.hash_index.clone(), + hash: self.hash, + slice: None, + } + } +} + +impl MaildirOp { + pub fn new( + hash: EnvelopeHash, + hash_index: Arc>>, + ) -> Self { + MaildirOp { + hash_index, + hash, + slice: None, + } + } + fn path(&self) -> PathBuf { + let hash_index = self.hash_index.clone(); + let map = hash_index.lock().unwrap(); + map.get(&self.hash).unwrap().1.clone() + } +} + +impl<'a> BackendOp for MaildirOp { + fn description(&self) -> String { + format!("Path of file: {}", self.path().display()) + } + fn as_bytes(&mut self) -> Result<&[u8]> { + if self.slice.is_none() { + self.slice = Some(Mmap::open_path(self.path(), Protection::Read)?); + } + /* Unwrap is safe since we use ? above. */ + Ok(unsafe { self.slice.as_ref().unwrap().as_slice() }) + } + fn fetch_headers(&mut self) -> Result<&[u8]> { + let raw = self.as_bytes()?; + let result = parser::headers_raw(raw).to_full_result()?; + Ok(result) + } + fn fetch_body(&mut self) -> Result<&[u8]> { + let raw = self.as_bytes()?; + let result = parser::headers_raw(raw).to_full_result()?; + Ok(result) + } + fn fetch_flags(&self) -> Flag { + let mut flag = Flag::default(); + let path = self.path(); + let path = path.to_str().unwrap(); // Assume UTF-8 validity + if !path.contains(":2,") { + return flag; + } + + for f in path.chars().rev() { + match f { + ',' => break, + 'D' => flag |= Flag::DRAFT, + 'F' => flag |= Flag::FLAGGED, + 'P' => flag |= Flag::PASSED, + 'R' => flag |= Flag::REPLIED, + 'S' => flag |= Flag::SEEN, + 'T' => flag |= Flag::TRASHED, + _ => panic!(), + } + } + + flag + } + + fn set_flag(&mut self, envelope: &mut Envelope, f: &Flag) -> Result<()> { + let path = self.path(); + let path = path.to_str().unwrap(); // Assume UTF-8 validity + let idx: usize = path + .rfind(":2,") + .ok_or_else(|| MeliError::new(format!("Invalid email filename: {:?}", self)))? + + 3; + let mut new_name: String = path[..idx].to_string(); + let mut flags = self.fetch_flags(); + if !(flags & *f).is_empty() { + return Ok(()); + } + flags.toggle(*f); + if !(flags & Flag::DRAFT).is_empty() { + new_name.push('D'); + } + if !(flags & Flag::FLAGGED).is_empty() { + new_name.push('F'); + } + if !(flags & Flag::PASSED).is_empty() { + new_name.push('P'); + } + if !(flags & Flag::REPLIED).is_empty() { + new_name.push('R'); + } + if !(flags & Flag::SEEN).is_empty() { + new_name.push('S'); + } + if !(flags & Flag::TRASHED).is_empty() { + new_name.push('T'); + } + + fs::rename(&path, &new_name)?; + let hash = envelope.hash(); + let hash_index = self.hash_index.clone(); + let mut map = hash_index.lock().unwrap(); + map.get_mut(&hash).unwrap().1 = PathBuf::from(new_name); + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct MaildirFolder { + hash: FolderHash, + name: String, + path: PathBuf, + children: Vec, +} + +impl MaildirFolder { + pub fn new(path: String, file_name: String, children: Vec) -> Result { + let pathbuf = PathBuf::from(path); + let mut h = DefaultHasher::new(); + pathbuf.hash(&mut h); + + let ret = MaildirFolder { + hash: h.finish(), + name: file_name, + path: pathbuf, + children, + }; + ret.is_valid()?; + Ok(ret) + } + pub fn path(&self) -> &Path { + self.path.as_path() + } + fn is_valid(&self) -> Result<()> { + let path = self.path(); + let mut p = PathBuf::from(path); + for d in &["cur", "new", "tmp"] { + p.push(d); + if !p.is_dir() { + return Err(MeliError::new(format!( + "{} is not a valid maildir folder", + path.display() + ))); + } + p.pop(); + } + Ok(()) + } +} +impl BackendFolder for MaildirFolder { + fn hash(&self) -> FolderHash { + self.hash + } + fn name(&self) -> &str { + &self.name + } + fn change_name(&mut self, s: &str) { + self.name = s.to_string(); + } + fn children(&self) -> &Vec { + &self.children + } + fn clone(&self) -> Folder { + Box::new(MaildirFolder { + hash: self.hash, + name: self.name.clone(), + path: self.path.clone(), + children: self.children.clone(), + }) + } +} diff --git a/melib/src/mailbox/backends/mod.rs b/melib/src/mailbox/backends/mod.rs index 6a1901c73..819551e84 100644 --- a/melib/src/mailbox/backends/mod.rs +++ b/melib/src/mailbox/backends/mod.rs @@ -28,7 +28,7 @@ use error::Result; //use mailbox::backends::imap::ImapType; //use mailbox::backends::mbox::MboxType; use mailbox::backends::maildir::MaildirType; -use mailbox::email::{Envelope, Flag}; +use mailbox::email::{Envelope, EnvelopeHash, Flag}; use std::fmt; use std::fmt::Debug; @@ -79,18 +79,27 @@ impl Backends { } } +#[derive(Debug)] +pub enum RefreshEventKind { + Update(EnvelopeHash, Envelope), + Create(Envelope), + Remove(FolderHash), + Rescan, +} + +#[derive(Debug)] pub struct RefreshEvent { - hash: u64, - folder: String, + hash: FolderHash, + kind: RefreshEventKind, } impl RefreshEvent { - pub fn hash(&self) -> u64 { + pub fn hash(&self) -> FolderHash { self.hash } - - pub fn folder(&self) -> &str { - self.folder.as_str() + pub fn kind(self) -> RefreshEventKind { + /* consumes self! */ + self.kind } } @@ -112,7 +121,7 @@ pub trait MailBackend: ::std::fmt::Debug { fn get(&mut self, folder: &Folder) -> Async>>; fn watch(&self, sender: RefreshEventConsumer) -> Result<()>; fn folders(&self) -> Vec; - fn operation(&self, hash: u64) -> Box; + fn operation(&self, hash: EnvelopeHash) -> Box; //login function } @@ -191,7 +200,7 @@ impl fmt::Debug for BackendOpGenerator { } pub trait BackendFolder: Debug { - fn hash(&self) -> u64; + fn hash(&self) -> FolderHash; fn name(&self) -> &str; fn change_name(&mut self, &str); fn clone(&self) -> Folder; @@ -204,7 +213,7 @@ struct DummyFolder { } impl BackendFolder for DummyFolder { - fn hash(&self) -> u64 { + fn hash(&self) -> FolderHash { 0 } fn name(&self) -> &str { @@ -224,4 +233,5 @@ pub fn folder_default() -> Folder { }) } +pub type FolderHash = u64; pub type Folder = Box; diff --git a/melib/src/mailbox/email/mod.rs b/melib/src/mailbox/email/mod.rs index 3d93e6ff5..66f54bea8 100644 --- a/melib/src/mailbox/email/mod.rs +++ b/melib/src/mailbox/email/mod.rs @@ -260,6 +260,9 @@ impl EnvelopeWrapper { } } +pub type UnixTimestamp = u64; +pub type EnvelopeHash = u64; + /// `Envelope` represents all the data of an email we need to know. /// /// Attachments (the email's body) is parsed on demand with `body`. @@ -280,16 +283,16 @@ pub struct Envelope { in_reply_to: Option, references: Option, - timestamp: u64, + timestamp: UnixTimestamp, thread: usize, - hash: u64, + hash: EnvelopeHash, flags: Flag, } impl Envelope { - pub fn new(hash: u64) -> Self { + pub fn new(hash: EnvelopeHash) -> Self { Envelope { date: String::new(), from: Vec::new(), @@ -320,7 +323,7 @@ impl Envelope { } Err(MeliError::new("Couldn't parse mail.")) } - pub fn from_token(mut operation: Box, hash: u64) -> Option { + pub fn from_token(mut operation: Box, hash: EnvelopeHash) -> Option { let mut e = Envelope::new(hash); e.flags = operation.fetch_flags(); if let Ok(bytes) = operation.as_bytes() { @@ -331,7 +334,7 @@ impl Envelope { } None } - pub fn hash(&self) -> u64 { + pub fn hash(&self) -> EnvelopeHash { self.hash } pub fn populate_headers(&mut self, bytes: &[u8]) -> Result<()> { @@ -437,7 +440,7 @@ impl Envelope { let headers = operation.fetch_headers()?; self.populate_headers(headers) } - pub fn date(&self) -> u64 { + pub fn date(&self) -> UnixTimestamp { self.timestamp } @@ -672,7 +675,7 @@ impl Envelope { self.thread = new_val; } pub fn set_datetime(&mut self, new_val: chrono::DateTime) -> () { - self.timestamp = new_val.timestamp() as u64; + self.timestamp = new_val.timestamp() as UnixTimestamp; } pub fn set_flag(&mut self, f: Flag, mut operation: Box) -> Result<()> { operation.set_flag(self, &f)?; diff --git a/melib/src/mailbox/mod.rs b/melib/src/mailbox/mod.rs index 5e31c62b7..49a2cb337 100644 --- a/melib/src/mailbox/mod.rs +++ b/melib/src/mailbox/mod.rs @@ -95,4 +95,30 @@ impl Mailbox { pub fn thread(&self, i: usize) -> &Container { &self.threads[i] } + + pub fn update(&mut self, old_hash: EnvelopeHash, envelope: Envelope) { + if let Some(i) = self.collection.iter().position(|e| e.hash() == old_hash) { + self.collection[i] = envelope; + } else { + panic!() + } + } + + pub fn insert(&mut self, envelope: Envelope) -> &Envelope { + self.collection.push(envelope); + // TODO: Update threads. + eprintln!("Inserted envelope"); + &self.collection[self.collection.len() - 1] + } + + pub fn remove(&mut self, envelope_hash: EnvelopeHash) { + if let Some(i) = self + .collection + .iter() + .position(|e| e.hash() == envelope_hash) + { + self.collection.remove(i); + } + // eprintln!("envelope_hash: {}\ncollection:\n{:?}", envelope_hash, self.collection); + } } diff --git a/melib/src/mailbox/thread.rs b/melib/src/mailbox/thread.rs index 17c8ac0c9..d10f8377f 100644 --- a/melib/src/mailbox/thread.rs +++ b/melib/src/mailbox/thread.rs @@ -82,8 +82,6 @@ impl FromStr for SortOrder { } } -type UnixTimestamp = u64; - /// A `Container` struct is needed to describe the thread tree forest during creation of threads. /// Because of Rust's memory model, we store indexes of Envelopes inside a collection instead of /// references and every reference is passed through the `Container` owner (a `Vec`). diff --git a/src/bin.rs b/src/bin.rs index 9fdd10af0..1c963c13b 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -62,7 +62,7 @@ fn main() { /* Register some reasonably useful interfaces */ let menu = Entity::from(Box::new(AccountMenu::new(&state.context.accounts))); - let listing = CompactListing::new(); + let listing = PlainListing::new(); let b = Entity::from(Box::new(listing)); let tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))])); let window = Entity::from(tabs); @@ -138,8 +138,8 @@ fn main() { }, } }, - ThreadEvent::RefreshMailbox { hash : h } => { - state.hash_to_folder(h); + ThreadEvent::RefreshMailbox(event) => { + state.refresh_event(event); state.redraw(); }, ThreadEvent::UIEvent(UIEventType::ChangeMode(f)) => { diff --git a/ui/src/components/notifications.rs b/ui/src/components/notifications.rs index dfab70bb4..d9a4fe6c7 100644 --- a/ui/src/components/notifications.rs +++ b/ui/src/components/notifications.rs @@ -40,10 +40,15 @@ impl fmt::Display for XDGNotifications { impl Component for XDGNotifications { fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {} fn process_event(&mut self, event: &UIEvent, _context: &mut Context) -> bool { - if let UIEventType::Notification(ref t) = event.event_type { + if let UIEventType::Notification(ref title, ref body) = event.event_type { notify_Notification::new() - .summary("Refresh Event") - .body(t) + .summary( + title + .as_ref() + .map(|v| v.as_str()) + .unwrap_or("Refresh Event"), + ) + .body(body) .icon("dialog-information") .show() .unwrap(); diff --git a/ui/src/state.rs b/ui/src/state.rs index 8312e53fc..566d4d9ca 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -29,6 +29,8 @@ */ use super::*; +use melib::backends::FolderHash; + use chan::{Receiver, Sender}; use fnv::FnvHashMap; use std::io::Write; @@ -74,7 +76,7 @@ impl InputHandler { /// A context container for loaded settings, accounts, UI changes, etc. pub struct Context { pub accounts: Vec, - mailbox_hashes: FnvHashMap, + mailbox_hashes: FnvHashMap, pub settings: Settings, pub runtime_settings: Settings, @@ -101,34 +103,9 @@ impl Context { self.input.restore(self.sender.clone()); } pub fn account_status(&mut self, idx_a: usize, idx_m: usize) -> result::Result { - let s = self.accounts[idx_a].status(idx_m)?; - match s { - LoadMailboxResult::New(event) => { - eprintln!("setting up notification"); - let (idx_a, idx_m) = self.mailbox_hashes[&event.folder]; - let subjects = { - let mut ret = Vec::with_capacity(event.index.len()); - eprintln!("index is {:?}", &event.index); - for &i in &event.index { - ret.push( - self.accounts[idx_a][idx_m].as_ref().unwrap().collection[i].subject(), - ); - } - ret - }; - self.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::Notification(format!( - "Update in {}/{}, indexes {:?}", - self.accounts[idx_a].name(), - self.accounts[idx_a][idx_m].as_ref().unwrap().folder.name(), - subjects - )), - }); - Ok(true) - } - LoadMailboxResult::Loaded => Ok(true), - LoadMailboxResult::Refresh => Ok(false), + match self.accounts[idx_a].status(idx_m) { + Ok(()) => Ok(true), + Err(n) => Err(n), } } } @@ -285,9 +262,16 @@ impl State { * we match the hash to the index of the mailbox, request a reload * and startup a thread to remind us to poll it every now and then till it's finished. */ - pub fn hash_to_folder(&mut self, hash: u64) { + pub fn refresh_event(&mut self, event: RefreshEvent) { + let hash = event.hash(); if let Some(&(idxa, idxm)) = self.context.mailbox_hashes.get(&hash) { - self.context.accounts[idxa].reload(idxm); + if let Some(notification) = self.context.accounts[idxa].reload(event, idxm) { + self.context.replies.push_back(UIEvent { + id: 0, + event_type: notification, + }); + } + let (startup_tx, startup_rx) = chan::async(); let startup_thread = { let sender = self.context.sender.clone(); diff --git a/ui/src/types/accounts.rs b/ui/src/types/accounts.rs index 7245c282e..eeeec60bf 100644 --- a/ui/src/types/accounts.rs +++ b/ui/src/types/accounts.rs @@ -25,23 +25,14 @@ use async::*; use conf::AccountConf; -use mailbox::backends::{Backends, Folder, MailBackend, RefreshEventConsumer}; +use mailbox::backends::{ + Backends, Folder, MailBackend, RefreshEvent, RefreshEventConsumer, RefreshEventKind, +}; use mailbox::*; use melib::error::Result; -use std::mem; use std::ops::{Index, IndexMut}; use std::result; - -pub enum LoadMailboxResult { - New(NewMailEvent), - Refresh, - Loaded, -} - -pub struct NewMailEvent { - pub folder: u64, - pub index: Vec, -} +use types::UIEventType::{self, Notification}; pub type Worker = Option>>>; @@ -83,10 +74,36 @@ impl Account { backend, } } - pub fn reload(&mut self, idx: usize) { - let ref_folders: Vec = self.backend.folders(); - let handle = self.backend.get(&ref_folders[idx]); - self.workers[idx] = Some(handle); + pub fn reload(&mut self, event: RefreshEvent, idx: usize) -> Option { + let kind = event.kind(); + let mailbox: &mut Mailbox = self.folders[idx].as_mut().unwrap().as_mut().unwrap(); + match kind { + RefreshEventKind::Update(old_hash, envelope) => { + mailbox.update(old_hash, envelope); + } + RefreshEventKind::Create(envelope) => { + let env: &Envelope = mailbox.insert(envelope); + let ref_folders: Vec = self.backend.folders(); + return Some(Notification( + Some("New mail".into()), + format!( + "Subject: {:15}:\n{:15}\nFrom: {:15}", + ref_folders[idx].name(), + env.subject(), + env.field_from_to_string() + ), + )); + } + RefreshEventKind::Remove(envelope_hash) => { + mailbox.remove(envelope_hash); + } + RefreshEventKind::Rescan => { + let ref_folders: Vec = self.backend.folders(); + let handle = self.backend.get(&ref_folders[idx]); + self.workers[idx] = Some(handle); + } + } + return None; } pub fn watch(&self, r: RefreshEventConsumer) -> () { self.backend.watch(r).unwrap(); @@ -119,13 +136,7 @@ impl Account { &mut self.workers } - fn load_mailbox( - &mut self, - index: usize, - envelopes: Result>, - ) -> LoadMailboxResult { - let mut ret: LoadMailboxResult = LoadMailboxResult::Refresh; - + fn load_mailbox(&mut self, index: usize, envelopes: Result>) { // TODO: Cleanup this function let folders = self.backend.folders(); let folder = &folders[index]; @@ -133,50 +144,7 @@ impl Account { let id = self.sent_folder.unwrap(); if id == index { /* ======================== */ - let old_m = mem::replace( - &mut self.folders[index], - Some(Mailbox::new(folder, &None, envelopes)), - ); - if let Some(old_m) = old_m { - if self.folders[index].is_some() && old_m.is_ok() { - let diff = self.folders[index] - .as_ref() - .map(|v| v.as_ref().unwrap().collection.len()) - .unwrap_or(0) - .saturating_sub( - old_m.as_ref().map(|v| v.collection.len()).unwrap_or(0), - ); - if diff > 0 { - let mut index = old_m - .as_ref() - .unwrap() - .collection - .iter() - .zip( - &self.folders[index] - .as_ref() - .unwrap() - .as_ref() - .unwrap() - .collection, - ) - .count(); - ret = LoadMailboxResult::New(NewMailEvent { - folder: folder.hash(), - index: (index.saturating_sub(1) - ..(self.folders[index] - .as_ref() - .unwrap() - .as_ref() - .unwrap() - .collection - .len() - .saturating_sub(1))) - .collect(), - }); - } - } - } + self.folders[index] = Some(Mailbox::new(folder, &None, envelopes)); /* ======================== */ } else { let (sent, cur) = { @@ -194,97 +162,20 @@ impl Account { sent[0] = Some(Mailbox::new(sent_path, &None, envelopes.clone())); } /* ======================== */ - let old_m = - mem::replace(&mut cur[0], Some(Mailbox::new(folder, &sent[0], envelopes))); - if let Some(old_m) = old_m { - if cur[0].is_some() && old_m.is_ok() { - let diff = cur[0] - .as_ref() - .map(|v| v.as_ref().unwrap().collection.len()) - .unwrap_or(0) - .saturating_sub( - old_m.as_ref().map(|v| v.collection.len()).unwrap_or(0), - ); - if diff > 0 { - let mut index = old_m - .as_ref() - .unwrap() - .collection - .iter() - .zip(&cur[0].as_ref().unwrap().as_ref().unwrap().collection) - .count(); - ret = LoadMailboxResult::New(NewMailEvent { - folder: folder.hash(), - index: (index.saturating_sub(1) - ..(cur[0] - .as_ref() - .unwrap() - .as_ref() - .unwrap() - .collection - .len()) - .saturating_sub(1)) - .collect(), - }); - } - } - } + cur[0] = Some(Mailbox::new(folder, &sent[0], envelopes)); /* ======================== */ } } else { /* ======================== */ - let old_m = mem::replace( - &mut self.folders[index], - Some(Mailbox::new(folder, &None, envelopes)), - ); - if let Some(old_m) = old_m { - if self.folders[index].is_some() && old_m.is_ok() { - let diff = self.folders[index] - .as_ref() - .map(|v| v.as_ref().unwrap().collection.len()) - .unwrap_or(0) - .saturating_sub(old_m.as_ref().map(|v| v.collection.len()).unwrap_or(0)); - if diff > 0 { - let mut index = old_m - .as_ref() - .unwrap() - .collection - .iter() - .zip( - &self.folders[index] - .as_ref() - .unwrap() - .as_ref() - .unwrap() - .collection, - ) - .count(); - ret = LoadMailboxResult::New(NewMailEvent { - folder: folder.hash(), - index: (index.saturating_sub(1) - ..(self.folders[index] - .as_ref() - .unwrap() - .as_ref() - .unwrap() - .collection - .len() - .saturating_sub(1))) - .collect(), - }); - } - } - } + self.folders[index] = Some(Mailbox::new(folder, &None, envelopes)); /* ======================== */ }; - - ret } - pub fn status(&mut self, index: usize) -> result::Result { + pub fn status(&mut self, index: usize) -> result::Result<(), usize> { match self.workers[index].as_mut() { None => { - return Ok(LoadMailboxResult::Loaded); + return Ok(()); } Some(ref mut w) => match w.poll() { Ok(AsyncStatus::NoUpdate) => { diff --git a/ui/src/types/mod.rs b/ui/src/types/mod.rs index 0978f51a1..29f908e3d 100644 --- a/ui/src/types/mod.rs +++ b/ui/src/types/mod.rs @@ -20,7 +20,7 @@ */ pub mod accounts; -pub use self::accounts::{Account, LoadMailboxResult}; +pub use self::accounts::Account; #[macro_use] mod position; #[macro_use] @@ -57,16 +57,14 @@ pub enum ThreadEvent { /// User input. Input(Key), /// A watched folder has been refreshed. - RefreshMailbox { - hash: u64, - }, + RefreshMailbox(RefreshEvent), UIEvent(UIEventType), //Decode { _ }, // For gpg2 signature check } impl From for ThreadEvent { fn from(event: RefreshEvent) -> Self { - ThreadEvent::RefreshMailbox { hash: event.hash() } + ThreadEvent::RefreshMailbox(event) } } @@ -89,7 +87,7 @@ pub enum UIEventType { ChangeMailbox(usize), ChangeMode(UIMode), Command(String), - Notification(String), + Notification(Option, String), Action(Action), StatusEvent(StatusEvent), MailboxUpdate((usize, usize)), // (account_idx, mailbox_idx)