diff --git a/melib/Cargo.toml b/melib/Cargo.toml index a5497248..cd6ca801 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -24,3 +24,4 @@ serde_derive = "1.0.71" bincode = "1.0.1" uuid = { version = "0.6", features = ["serde", "v4"] } text_processing = { path = "../text_processing", version = "*" } +libc = {version = "0.2.59", features = ["extra_traits",]} diff --git a/melib/src/backends.rs b/melib/src/backends.rs index 0ff75b52..c1fb6f14 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -26,8 +26,8 @@ use crate::async_workers::*; use crate::conf::AccountSettings; use crate::error::{MeliError, Result}; //use mailbox::backends::imap::ImapType; -//use mailbox::backends::mbox::MboxType; use self::maildir::MaildirType; +use self::mbox::MboxType; use super::email::{Envelope, EnvelopeHash, Flag}; use std::fmt; use std::fmt::Debug; @@ -59,7 +59,10 @@ impl Backends { "maildir".to_string(), Box::new(|| Box::new(|f| Box::new(MaildirType::new(f)))), ); - //b.register("mbox".to_string(), Box::new(|| Box::new(MboxType::new("")))); + b.register( + "mbox".to_string(), + Box::new(|| Box::new(|f| Box::new(MboxType::new(f)))), + ); //b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new("")))); b } @@ -249,6 +252,9 @@ impl BackendOp for ReadOnlyOp { pub trait BackendFolder: Debug { fn hash(&self) -> FolderHash; fn name(&self) -> &str; + fn path(&self) -> &str { + self.name() + } fn change_name(&mut self, new_name: &str); fn clone(&self) -> Folder; fn children(&self) -> &Vec; diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index db38a8b1..11e72f81 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -23,59 +23,584 @@ * https://wiki2.dovecot.org/MailboxFormat/mbox */ -/* -use async::*; -use error::Result; -use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder}; -use mailbox::email::Envelope; +use crate::async_workers::{Async, AsyncBuilder, AsyncStatus}; +use crate::backends::BackendOp; +use crate::backends::FolderHash; +use crate::backends::{ + BackendFolder, Folder, MailBackend, RefreshEvent, RefreshEventConsumer, RefreshEventKind, +}; +use crate::conf::AccountSettings; +use crate::email::parser::BytesExt; +use crate::email::*; +use crate::error::{MeliError, Result}; +use fnv::FnvHashMap; +use libc; +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; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex}; + +const F_OFD_SETLKW: libc::c_int = 38; + +// Open file description locking +// # man fcntl +fn get_rw_lock_blocking(f: &File) { + let fd: libc::c_int = f.as_raw_fd(); + let mut flock: libc::flock = libc::flock { + l_type: libc::F_WRLCK as libc::c_short, + l_whence: libc::SEEK_SET as libc::c_short, + l_start: 0, + l_len: 0, /* "Specifying 0 for l_len has the special meaning: lock all bytes starting at the location + specified by l_whence and l_start through to the end of file, no matter how large the file grows." */ + l_pid: 0, /* "By contrast with traditional record locks, the l_pid field of that structure must be set to zero when using the commands described below." */ + }; + let ptr: *mut libc::flock = &mut flock; + let ret_val = unsafe { libc::fcntl(fd, F_OFD_SETLKW, ptr as *mut libc::c_void) }; + debug!(&ret_val); + 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, + name: String, + path: PathBuf, + content: Vec, + children: Vec, + parent: Option, +} + +impl BackendFolder for MboxFolder { + fn hash(&self) -> FolderHash { + self.hash + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn path(&self) -> &str { + /* We know it's valid UTF-8 because we supplied it */ + self.path.to_str().unwrap() + } + + fn change_name(&mut self, s: &str) { + self.name = s.to_string(); + } + + fn clone(&self) -> Folder { + Box::new(MboxFolder { + hash: self.hash, + name: self.name.clone(), + path: self.path.clone(), + content: self.content.clone(), + children: self.children.clone(), + parent: self.parent, + }) + } + + fn children(&self) -> &Vec { + &self.children + } + + fn parent(&self) -> Option { + self.parent + } +} /// `BackendOp` implementor for Mbox -#[derive(Debug, Default, Clone)] -pub struct MboxOp {} +#[derive(Debug, Default)] +pub struct MboxOp { + hash: EnvelopeHash, + path: PathBuf, + offset: Offset, + length: Length, + slice: Option, +} impl MboxOp { - pub fn new(_path: String) -> Self { - MboxOp {} + pub fn new(hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self { + MboxOp { + hash, + path: path.to_path_buf(), + slice: None, + offset, + length, + } } } impl BackendOp for MboxOp { fn description(&self) -> String { - unimplemented!(); + String::new() } + fn as_bytes(&mut self) -> Result<&[u8]> { - unimplemented!(); + 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()[self.offset..self.offset + self.length] + }) } + fn fetch_headers(&mut self) -> Result<&[u8]> { - unimplemented!(); + let raw = self.as_bytes()?; + let result = parser::headers_raw(raw).to_full_result()?; + Ok(result) } + fn fetch_body(&mut self) -> Result<&[u8]> { - unimplemented!(); + let raw = self.as_bytes()?; + let result = parser::body_raw(raw).to_full_result()?; + Ok(result) } + fn fetch_flags(&self) -> Flag { - unimplemented!(); + let mut flags = Flag::empty(); + let file = match std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&self.path) + { + Ok(f) => f, + Err(e) => { + debug!(e); + return flags; + } + }; + get_rw_lock_blocking(&file); + let mut buf_reader = BufReader::new(file); + let mut contents = Vec::new(); + if let Err(e) = buf_reader.read_to_end(&mut contents) { + debug!(e); + return flags; + }; + + if let Ok(headers) = parser::headers_raw(contents.as_slice()).to_full_result() { + if let Some(start) = headers.find(b"Status:") { + if let Some(end) = headers[start..].find(b"\n") { + let start = start + b"Status:".len(); + let status = headers[start..start + end].trim(); + if status.contains(&b'F') { + flags.set(Flag::FLAGGED, true); + } + if status.contains(&b'A') { + flags.set(Flag::REPLIED, true); + } + if status.contains(&b'R') { + flags.set(Flag::SEEN, true); + } + if status.contains(&b'D') { + flags.set(Flag::TRASHED, true); + } + if status.contains(&b'T') { + flags.set(Flag::DRAFT, true); + } + } + } + if let Some(start) = headers.find(b"X-Status:") { + let start = start + b"X-Status:".len(); + if let Some(end) = headers[start..].find(b"\n") { + let status = headers[start..start + end].trim(); + if status.contains(&b'F') { + flags.set(Flag::FLAGGED, true); + } + if status.contains(&b'A') { + flags.set(Flag::REPLIED, true); + } + if status.contains(&b'R') { + flags.set(Flag::SEEN, true); + } + if status.contains(&b'D') { + flags.set(Flag::TRASHED, true); + } + if status.contains(&b'T') { + flags.set(Flag::DRAFT, true); + } + } + } + } + flags } - fn set_flags(&self, f: Flag) -> Result<()> { - unimplemented!() + + fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag) -> Result<()> { + Ok(()) } } +pub fn mbox_parse( + index: Arc>>, + input: &[u8], + file_offset: usize, +) -> IResult<&[u8], Vec> { + if input.is_empty() { + return IResult::Incomplete(Needed::Unknown); + } + let mut input = input; + let mut offset = 0; + let mut index = index.lock().unwrap(); + let mut envelopes = Vec::with_capacity(32); + while !input.is_empty() { + let next_offset: Option = input.find(b"\n\nFrom "); + if let Some(len) = next_offset { + match Envelope::from_bytes(&input[..len]) { + Ok(mut env) => { + let mut flags = Flag::empty(); + if env.other_headers().contains_key("Status") { + if env.other_headers()["Status"].contains("F") { + flags.set(Flag::FLAGGED, true); + } + if env.other_headers()["Status"].contains("A") { + flags.set(Flag::REPLIED, true); + } + if env.other_headers()["Status"].contains("R") { + flags.set(Flag::SEEN, true); + } + if env.other_headers()["Status"].contains("D") { + flags.set(Flag::TRASHED, true); + } + } + if env.other_headers().contains_key("X-Status") { + if env.other_headers()["X-Status"].contains("F") { + flags.set(Flag::FLAGGED, true); + } + if env.other_headers()["X-Status"].contains("A") { + flags.set(Flag::REPLIED, true); + } + if env.other_headers()["X-Status"].contains("R") { + flags.set(Flag::SEEN, true); + } + if env.other_headers()["X-Status"].contains("D") { + flags.set(Flag::TRASHED, true); + } + if env.other_headers()["X-Status"].contains("T") { + flags.set(Flag::DRAFT, true); + } + } + env.set_flags(flags); + index.insert(env.hash(), (offset + file_offset, len)); + envelopes.push(env); + } + Err(_) => { + debug!("Could not parse mail at byte offset {}", offset); + } + } + offset += len + 2; + input = &input[len + 2..]; + } else { + match Envelope::from_bytes(input) { + Ok(mut env) => { + let mut flags = Flag::empty(); + if env.other_headers().contains_key("Status") { + if env.other_headers()["Status"].contains("F") { + flags.set(Flag::FLAGGED, true); + } + if env.other_headers()["Status"].contains("A") { + flags.set(Flag::REPLIED, true); + } + if env.other_headers()["Status"].contains("R") { + flags.set(Flag::SEEN, true); + } + if env.other_headers()["Status"].contains("D") { + flags.set(Flag::TRASHED, true); + } + } + if env.other_headers().contains_key("X-Status") { + if env.other_headers()["X-Status"].contains("F") { + flags.set(Flag::FLAGGED, true); + } + if env.other_headers()["X-Status"].contains("A") { + flags.set(Flag::REPLIED, true); + } + if env.other_headers()["X-Status"].contains("R") { + flags.set(Flag::SEEN, true); + } + if env.other_headers()["X-Status"].contains("D") { + flags.set(Flag::TRASHED, true); + } + if env.other_headers()["X-Status"].contains("T") { + flags.set(Flag::DRAFT, true); + } + } + env.set_flags(flags); + index.insert(env.hash(), (offset + file_offset, input.len())); + envelopes.push(env); + } + Err(_) => { + debug!("Could not parse mail at byte offset {}", offset); + } + } + break; + } + } + return IResult::Done(&[], envelopes); +} + +type Offset = usize; +type Length = usize; /// Mbox backend -#[derive(Debug)] -pub struct MboxType {} +#[derive(Debug, Default)] +pub struct MboxType { + path: PathBuf, + index: Arc>>, + folders: Arc>>, +} impl MailBackend for MboxType { - fn get(&self, _folder: &Folder) -> Async>> { - unimplemented!(); + fn get(&mut self, folder: &Folder) -> Async>> { + let mut w = AsyncBuilder::new(); + let handle = { + let tx = w.tx(); + let index = self.index.clone(); + let folder_path = folder.path().to_string(); + let folder_hash = folder.hash(); + let folders = self.folders.clone(); + let closure = move || { + let tx = tx.clone(); + let index = index.clone(); + let file = match std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&folder_path) + { + Ok(f) => f, + Err(e) => { + tx.send(AsyncStatus::Payload(Err(MeliError::from(e)))); + return; + } + }; + get_rw_lock_blocking(&file); + let mut buf_reader = BufReader::new(file); + let mut contents = Vec::new(); + if let Err(e) = buf_reader.read_to_end(&mut contents) { + tx.send(AsyncStatus::Payload(Err(MeliError::from(e)))); + return; + }; + + let payload = mbox_parse(index, contents.as_slice(), 0) + .to_full_result() + .map_err(|e| MeliError::from(e)); + { + let mut folder_lock = folders.lock().unwrap(); + folder_lock + .entry(folder_hash) + .and_modify(|f| f.content = contents); + } + + tx.send(AsyncStatus::Payload(payload)); + }; + Box::new(closure) + }; + w.build(handle) } - fn watch(&self, _sender: RefreshEventConsumer, _folders: &[Folder]) -> () { + + fn watch(&self, sender: RefreshEventConsumer) -> Result<()> { + let (tx, rx) = channel(); + let mut watcher = watcher(tx, std::time::Duration::from_secs(10)).unwrap(); + for f in self.folders.lock().unwrap().values() { + watcher.watch(&f.path, RecursiveMode::Recursive).unwrap(); + debug!("watching {:?}", f.path.as_path()); + } + let index = self.index.clone(); + let folders = self.folders.clone(); + std::thread::Builder::new() + .name(format!( + "watching {}", + self.path.file_name().unwrap().to_str().unwrap() + )) + .spawn(move || { + // Move `watcher` in the closure's scope so that it doesn't get dropped. + let _watcher = watcher; + let index = index; + let folders = folders; + 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 { + /* Update */ + DebouncedEvent::NoticeWrite(pathbuf) + | DebouncedEvent::Write(pathbuf) => { + let folder_hash = get_path_hash!(&pathbuf); + let file = match std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&pathbuf) + { + Ok(f) => f, + Err(_) => { + continue; + } + }; + get_rw_lock_blocking(&file); + let mut folder_lock = folders.lock().unwrap(); + let mut buf_reader = BufReader::new(file); + let mut contents = Vec::new(); + if let Err(e) = buf_reader.read_to_end(&mut contents) { + debug!(e); + continue; + }; + if contents + .starts_with(folder_lock[&folder_hash].content.as_slice()) + { + if let Ok(envelopes) = mbox_parse( + index.clone(), + &contents[folder_lock[&folder_hash].content.len()..], + folder_lock[&folder_hash].content.len(), + ) + .to_full_result() + { + for env in envelopes { + sender.send(RefreshEvent { + hash: folder_hash, + kind: RefreshEventKind::Create(Box::new(env)), + }); + } + } + } else { + sender.send(RefreshEvent { + hash: folder_hash, + kind: RefreshEventKind::Rescan, + }); + } + folder_lock + .entry(folder_hash) + .and_modify(|f| f.content = contents); + } + /* Remove */ + DebouncedEvent::NoticeRemove(pathbuf) + | DebouncedEvent::Remove(pathbuf) => { + panic!(format!("mbox folder {} was removed.", pathbuf.display())) + } + /* Envelope hasn't changed */ + DebouncedEvent::Rename(src, dest) => panic!(format!( + "mbox folder {} was renamed to {}.", + src.display(), + dest.display() + )), + /* Trigger rescan of folder */ + DebouncedEvent::Rescan => { + /* Actually should rescan all folders */ + unreachable!("Unimplemented: rescan of all folders in MboxType") + } + _ => {} + }, + Err(e) => debug!("watch error: {:?}", e), + } + } + })?; + Ok(()) + } + fn folders(&self) -> FnvHashMap { + self.folders + .lock() + .unwrap() + .iter() + .map(|(h, f)| (*h, f.clone() as Folder)) + .collect() + } + fn operation(&self, hash: EnvelopeHash, _folder_hash: FolderHash) -> Box { + let (offset, length) = { + let index = self.index.lock().unwrap(); + index[&hash] + }; + Box::new(MboxOp::new(hash, self.path.as_path(), offset, length)) + } + + fn save(&self, bytes: &[u8], folder: &str) -> Result<()> { unimplemented!(); } } impl MboxType { - pub fn new(_path: &str) -> Self { - MboxType {} + pub fn new(s: &AccountSettings) -> Self { + let path = Path::new(s.root_folder.as_str()); + if !path.exists() { + panic!( + "\"root_folder\" {} for account {} is not a valid path.", + s.root_folder.as_str(), + s.name() + ); + } + let ret = MboxType { + path: PathBuf::from(path), + ..Default::default() + }; + let name: String = ret + .path + .file_name() + .map(|f| f.to_string_lossy().into()) + .unwrap_or(String::new()); + let hash = get_path_hash!(path); + ret.folders.lock().unwrap().insert( + hash, + MboxFolder { + hash, + path: PathBuf::from(path), + name, + content: Vec::new(), + children: Vec::new(), + parent: None, + }, + ); + /* + /* Look for other mailboxes */ + let parent_folder = Path::new(path).parent().unwrap(); + let read_dir = std::fs::read_dir(parent_folder); + if read_dir.is_ok() { + for f in read_dir.unwrap() { + if f.is_err() { + continue; + } + let f = f.unwrap().path(); + if f.is_file() && f != path { + let name: String = f + .file_name() + .map(|f| f.to_string_lossy().into()) + .unwrap_or(String::new()); + let hash = get_path_hash!(f); + ret.folders.lock().unwrap().insert( + hash, + MboxFolder { + hash, + path: f, + name, + content: Vec::new(), + children: Vec::new(), + parent: None, + }, + ); + } + } + } + */ + ret } } -*/ diff --git a/melib/src/email.rs b/melib/src/email.rs index 06630b97..b7b8690d 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -775,6 +775,9 @@ impl Envelope { operation.set_flag(self, f)?; Ok(()) } + pub fn set_flags(&mut self, f: Flag) { + self.flags = f; + } pub fn flags(&self) -> Flag { self.flags } diff --git a/sample-config b/sample-config index 8e130858..90f0303f 100644 --- a/sample-config +++ b/sample-config @@ -14,6 +14,15 @@ # "drafts" = { rename="Drafts" } # "foobar-devel" = { ignore = true } # don't show notifications for this folder +# Setting up an mbox account +#[accounts.mbox] +#root_folder = "/var/mail/username" +#draft_folder = "" +#sent_folder = "" +#format = "mbox" +#index = "Compact" +#identity="username@hostname.local" + #[pager] #pager_ratio = 80 #filter = "/usr/bin/pygmentize" diff --git a/ui/Cargo.toml b/ui/Cargo.toml index fdc30a88..8c732efb 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -25,3 +25,4 @@ bincode = "1.0.1" uuid = { version = "0.6", features = ["serde", "v4"] } unicode-segmentation = "1.2.1" # >:c text_processing = { path = "../text_processing", version = "*" } +libc = {version = "0.2.59", features = ["extra_traits",]} diff --git a/ui/src/lib.rs b/ui/src/lib.rs index fed7fc92..6650112c 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -71,3 +71,53 @@ pub use crate::conf::*; pub mod workers; pub use crate::workers::*; + +pub use crate::username::*; +pub mod username { + use libc; + use std::ptr::null_mut; + /* taken from whoami-0.1.1 */ + fn getpwuid() -> libc::passwd { + let mut pwent = libc::passwd { + pw_name: null_mut(), + pw_passwd: null_mut(), + pw_uid: 0, + pw_gid: 0, + pw_gecos: null_mut(), + pw_dir: null_mut(), + pw_shell: null_mut(), + }; + let mut pwentp = null_mut(); + let mut buffer = [0i8; 16384]; // from the man page + + unsafe { + libc::getpwuid_r( + libc::geteuid(), + &mut pwent, + &mut buffer[0], + 16384, + &mut pwentp, + ); + } + + pwent + } + fn ptr_to_string(name: *mut i8) -> String { + let uname = name as *mut _ as *mut u8; + + let s; + let string; + + unsafe { + s = ::std::slice::from_raw_parts(uname, libc::strlen(name)); + string = String::from_utf8_lossy(s).to_string(); + } + + string + } + pub fn username() -> String { + let pwent = getpwuid(); + + ptr_to_string(pwent.pw_name) + } +}