From 9946fbcbe0bfc495adfe51c4a804d23fb449cae6 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Fri, 1 Sep 2017 15:24:32 +0300 Subject: [PATCH] threads --- Cargo.toml | 12 +- src/conf/mod.rs | 136 ++++++++++++ src/error.rs | 37 +++ src/mailbox/email.rs | 371 +++++++++++++++++++++++++++++++ src/mailbox/maildir.rs | 92 ++++++++ src/mailbox/mod.rs | 494 ++++++++++++++++++++++------------------- src/mailbox/parser.rs | 176 +++++++++++++++ src/main.rs | 110 +++++++-- src/ui/index.rs | 350 +++++++++++++++++++++-------- src/ui/mod.rs | 17 +- src/ui/pager.rs | 117 +++++----- 11 files changed, 1499 insertions(+), 413 deletions(-) create mode 100644 src/conf/mod.rs create mode 100644 src/error.rs create mode 100644 src/mailbox/email.rs create mode 100644 src/mailbox/maildir.rs create mode 100644 src/mailbox/parser.rs diff --git a/Cargo.toml b/Cargo.toml index d5eea1624..f27be8c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,19 @@ version = "0.1.0" authors = ["Manos Pitsidianakis "] [dependencies] -mailparse = "0.5.1" -maildir = "0.1.1" chrono = "0.4" +xdg = "2.1.0" +config = "0.6" +serde_derive = "^1.0.8" +serde = "^1.0.8" +nom = "3.2.0" +memmap = "*" +base64 = "*" [dependencies.ncurses] features = ["wide"] optional = false version = "5.86.0" + +[profile.release] +lto = true diff --git a/src/conf/mod.rs b/src/conf/mod.rs new file mode 100644 index 000000000..e3f52362b --- /dev/null +++ b/src/conf/mod.rs @@ -0,0 +1,136 @@ +/* + * meli - configuration 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 xdg; +extern crate config; + +use std::collections::HashMap; +use std::io; +use std::fs; +use std::path::{PathBuf, Path}; + +#[derive(Debug)] +enum MailFormat { + Maildir +} + +impl MailFormat { + pub fn from_str(x: &str) -> MailFormat { + match x { + "maildir" | "Maildir" | + "MailDir" => { MailFormat::Maildir }, + _ => { panic!("Unrecognizable mail format");} + } + } +} + +#[derive(Debug, Deserialize)] +struct FileAccount { + folders: String, + format: String, + sent_folder: String, + threaded : bool, +} + +#[derive(Debug, Deserialize, Default)] +struct FileSettings { + accounts: HashMap, +} + +#[derive(Debug)] +pub struct Account { + pub folders: Vec, + format: MailFormat, + pub sent_folder: String, + threaded : bool, +} +#[derive(Debug)] +pub struct Settings { + pub accounts: HashMap, +} + + +use self::config::{Config, File, FileFormat}; +impl FileSettings { + pub fn new() -> FileSettings { + let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").unwrap(); + let config_path = xdg_dirs.place_config_file("config") + .expect("cannot create configuration directory"); + //let setts = Config::default().merge(File::new(config_path.to_str().unwrap_or_default(), config::FileFormat::Toml)).unwrap(); + let mut s = Config::new(); + let s = s.merge(File::new(config_path.to_str().unwrap(), FileFormat::Toml)); + + match s.is_ok() { //.unwrap_or(Settings { }); + true => { s.unwrap().deserialize().unwrap() }, + false => { + eprintln!("{:?}",s.err().unwrap()); + let mut buf = String::new(); + io::stdin().read_line(&mut buf).expect("Failed to read line"); + FileSettings { ..Default::default() } }, + } + } +} + +impl Settings { + pub fn new() -> Settings { + let fs = FileSettings::new(); + let mut s: HashMap = HashMap::new(); + + for (id, x) in fs.accounts { + let mut folders = Vec::new(); + fn recurse_folders>(folders: &mut Vec, p: P) { + for mut f in fs::read_dir(p).unwrap() { + for f in f.iter_mut().next() { + { + let path = f.path(); + if path.ends_with("cur") || path.ends_with("new") || + path.ends_with("tmp") { + continue; + } + if path.is_dir() { + folders.push(path.to_str().unwrap().to_string()); + recurse_folders(folders, path); + } + } + } + + } + }; + let path = PathBuf::from(&x.folders); + if path.is_dir() { + folders.push(path.to_str().unwrap().to_string()); + } + recurse_folders(&mut folders, &x.folders); + s.insert(id.clone(), Account { + folders: folders, + format: MailFormat::from_str(&x.format), + sent_folder: x.sent_folder.clone(), + threaded: x.threaded, + }); + + + } + + Settings { accounts: s } + + + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..482a3f776 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,37 @@ +use std::error::Error; +use std::fmt; +use std::result; +use std::io; + +pub type Result = result::Result; + +#[derive(Debug)] +pub struct MeliError { + details: String +} + +impl MeliError { + pub fn new(msg: M) -> MeliError where M: Into { + MeliError{details: msg.into()} + } +} + +impl fmt::Display for MeliError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f,"{}",self.details) + } +} + +impl Error for MeliError { + fn description(&self) -> &str { + &self.details + } +} + +impl From for MeliError { + #[inline] + fn from(kind: io::Error) -> MeliError { + MeliError::new(kind.description()) + } +} + diff --git a/src/mailbox/email.rs b/src/mailbox/email.rs new file mode 100644 index 000000000..9d0713750 --- /dev/null +++ b/src/mailbox/email.rs @@ -0,0 +1,371 @@ +use std::string::String; +use memmap::{Mmap, Protection}; +use std; +use std::cmp::Ordering; +use std::fmt; +use std::option::Option; + +use std::io::prelude::*; +use mailbox::parser::*; + +use chrono; +use chrono::TimeZone; + +/* Helper struct to return slices from a struct on demand */ +#[derive(Clone,Debug)] +struct StrBuilder { + offset: usize, + length: usize, +} + +pub trait StrBuild { + fn new(&str, &str) -> Self; + fn get_raw(&self) -> &str; + fn get_val(&self) -> &str; +} + +#[derive(Clone)] +pub struct MessageID (String, StrBuilder); + +impl StrBuild for MessageID { + fn new(string: &str, slice: &str) -> Self { + let offset = string.find(slice).unwrap(); + MessageID (string.to_string(), StrBuilder { + offset: offset, + length: slice.len() + 1, + }) + } + fn get_raw(&self) -> &str { + let offset = self.1.offset; + let length = self.1.length; + &self.0[offset..length] + } + fn get_val(&self) -> &str { + &self.0 + } +} + +#[test] +fn test_strbuilder() { + let m_id = "<20170825132332.6734-1-el13635@mail.ntua.gr>"; + let (_, slice) = message_id(m_id.as_bytes()).unwrap(); + assert_eq!(MessageID::new(m_id, slice), MessageID (m_id.to_string(), StrBuilder{offset: 1, length: 43})); +} + +impl PartialEq for MessageID { + fn eq(&self, other: &MessageID) -> bool { + self.get_raw() == other.get_raw() + } +} +impl fmt::Debug for MessageID { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.get_raw()) + } +} + +#[derive(Clone,Debug)] +struct References { + raw: String, + refs: Vec, +} + +/* A very primitive mail object */ +#[derive(Clone,Debug)] +pub struct Mail { + date: String, + from: Option, + to: Option, + body: String, + subject: Option, + message_id: Option, + in_reply_to: Option, + references: Option, + + datetime: Option>, + + thread: usize, +} + +impl Mail { + pub fn get_date(&self) -> i64 { + match self.datetime { + Some(v) => v.timestamp(), + None => 0, + } + } + pub fn get_datetime(&self) -> chrono::DateTime { + self.datetime.unwrap_or(chrono::FixedOffset::west(0).ymd(1970, 1, 1).and_hms(0, 0, 0)) + } + pub fn get_date_as_str(&self) -> &str { + &self.date + } + pub fn get_from(&self) -> &str { + match self.from { + Some(ref s) => s, + None => "", + } + } + pub fn get_to(&self) -> &str { + match self.to { + Some(ref s) => s, + None => "", + } + } + pub fn get_body(&self) -> &str { + &self.body + } + pub fn get_subject(&self) -> &str { + match self.subject { + Some(ref s) => s, + _ => "" + } + } + pub fn get_in_reply_to(&self) -> &str { + match self.in_reply_to { + Some(ref s) => s.get_val(), + _ => "" + } + } + pub fn get_in_reply_to_raw(&self) -> &str { + match self.in_reply_to { + Some(ref s) => s.get_raw(), + _ => "" + } + } + pub fn get_message_id(&self) -> &str { + match self.message_id { + Some(ref s) => s.get_val(), + _ => "", + } + } + pub fn get_message_id_raw(&self) -> &str { + match self.message_id { + Some(ref s) => s.get_raw(), + _ => "", + } + } + fn set_date(&mut self, new_val: String) -> () { + self.date = new_val; + } + fn set_from(&mut self, new_val: String) -> () { + self.from = Some(new_val); + } + fn set_to(&mut self, new_val: String) -> () { + self.to = Some(new_val); + } + fn set_in_reply_to(&mut self, new_val: &str) -> () { + let slice = match message_id(new_val.as_bytes()).to_full_result() { + Ok(v) => { v }, + Err(v) => { eprintln!("{} {:?}",new_val, v); + self.in_reply_to = None; + return; } + }; + self.in_reply_to = Some(MessageID::new(new_val, slice)); + } + fn set_subject(&mut self, new_val: String) -> () { + self.subject = Some(new_val); + } + fn set_message_id(&mut self, new_val: &str) -> () { + let slice = match message_id(new_val.as_bytes()).to_full_result() { + Ok(v) => { v }, + Err(v) => { eprintln!("{} {:?}",new_val, v); + self.message_id = None; + return; } + }; + self.message_id = Some(MessageID::new(new_val, slice)); + } + fn push_references(&mut self, new_val: &str) -> () { + let slice = match message_id(new_val.as_bytes()).to_full_result() { + Ok(v) => { v }, + Err(v) => { eprintln!("{} {:?}",new_val, v); + return; } + }; + let new_ref = MessageID::new(new_val, slice); + match self.references { + Some(ref mut s) => { + if s.refs.contains(&new_ref) { + return; + } + s.refs.push(new_ref); + }, + None => { + let mut v = Vec::new(); + v.push(new_ref); + self.references = Some(References { raw: "".to_string(), refs: v, }); + } + } + + } + fn set_references(&mut self, new_val: String) -> () { + match self.references { + Some(ref mut s) => { + s.raw = new_val; + }, + None => { + let v = Vec::new(); + self.references = Some(References { raw: new_val, refs: v, }); + } + } + } + pub fn get_references<'a>(&'a self) -> Vec<&'a MessageID> { + match self.references { + Some(ref s) => s.refs.iter().fold(Vec::with_capacity(s.refs.len()), |mut acc, x| { acc.push(&x); acc }), + None => Vec::new(), + } + } + pub fn set_body(&mut self, new_val: String) -> () { + self.body = new_val; + } + pub fn get_thread(&self) -> usize { + self.thread + } + pub fn set_thread(&mut self, new_val: usize) -> () { + self.thread = new_val; + } + pub fn set_datetime(&mut self, new_val: Option>) -> () { + self.datetime = new_val; + } + pub fn new() -> Self { + Mail { + date: "".to_string(), + from: None, + to: None, + body: "".to_string(), + subject: None, + message_id: None, + in_reply_to: None, + references: None, + + datetime: None, + + thread: 0, + } + } + pub fn from(path: std::string::String) -> Option { + let f = Mmap::open_path(path.clone(), Protection::Read).unwrap(); + let file = unsafe { f.as_slice() }; + let (headers, body) = match mail(file).to_full_result() { + Ok(v) => v, + Err(_) => { + eprintln!("error in parsing"); + let path = std::path::PathBuf::from(&path); + + let mut czc = std::fs::File::open(path).unwrap(); + let mut buffer = Vec::new(); + let _ = czc.read_to_end(&mut buffer); + eprintln!("\n-------------------------------"); + eprintln!("{}\n", std::string::String::from_utf8_lossy(&buffer)); + eprintln!("-------------------------------\n"); + + return None; } + }; + let mut mail = Mail::new(); + let mut in_reply_to = None; + let mut datetime = None; + + for (name, value) in headers { + if value.len() == 1 && value[0].is_empty() { + continue; + } + match name { + "To" => { + let value = value.iter().fold(String::new(), |mut acc, x| { acc.push_str(x); acc }); + let parse_result = subject(value.as_bytes()); + let value = match parse_result.is_done() { + true => { + parse_result.to_full_result().unwrap() + }, + false => { + "".to_string() + }, + }; + mail.set_to(value); + }, + "From" => { + let value = value.iter().fold(String::new(), |mut acc, x| { acc.push_str(x); acc }); + let parse_result = subject(value.as_bytes()); + let value = match parse_result.is_done() { + true => { + parse_result.to_full_result().unwrap() + }, + false => { + "".to_string() + }, + }; + mail.set_from(value); + }, + "Subject" => { + let value = value.iter().fold(String::new(), |mut acc, x| { acc.push_str(" "); acc.push_str(x); acc }); + let parse_result = subject(value.trim().as_bytes()); + let value = match parse_result.is_done() { + true => { + parse_result.to_full_result().unwrap() + }, + false => { + "".to_string() + }, + }; + mail.set_subject(value); + }, + "Message-ID" | "Message-Id" | "Message-id" | "message-id" => { + mail.set_message_id(&value.iter().fold(String::new(), |mut acc, x| { acc.push_str(x); acc })); + }, + "References" => { + let folded_value = value.iter().fold(String::new(), |mut acc, x| { acc.push_str(x); acc }); + { + let parse_result = references(&folded_value.as_bytes()); + match parse_result.is_done() { + true => { + for v in parse_result.to_full_result().unwrap() { + mail.push_references(v); + } + }, + _ => {} + } + } + mail.set_references(folded_value); + }, + "In-Reply-To" | "In-reply-to" | "In-Reply-to" | "in-reply-to" => { + let value = value.iter().fold(String::new(), |mut acc, x| { acc.push_str(x); acc }); + mail.set_in_reply_to(&value); + in_reply_to = Some(value); }, + "Date" => { + let value = value.iter().fold(String::new(), |mut acc, x| { acc.push_str(x); acc }); + mail.set_date(value.clone()); + datetime = Some(value); + }, + _ => {}, + } + }; + match in_reply_to { + Some(ref mut x) => { + mail.push_references(x); + }, + None => {}, + } + mail.set_body(String::from_utf8_lossy(body).into_owned()); + if datetime.is_some() { + mail.set_datetime(date(&datetime.unwrap())); + } + + Some(mail) + } +} + +impl Eq for Mail {} +impl Ord for Mail { + fn cmp(&self, other: &Mail) -> Ordering { + self.get_datetime().cmp(&other.get_datetime()) + } +} +impl PartialOrd for Mail { + fn partial_cmp(&self, other: &Mail) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Mail { + fn eq(&self, other: &Mail) -> bool { + self.get_message_id_raw() == other.get_message_id_raw() + } +} diff --git a/src/mailbox/maildir.rs b/src/mailbox/maildir.rs new file mode 100644 index 000000000..3f8a41ef2 --- /dev/null +++ b/src/mailbox/maildir.rs @@ -0,0 +1,92 @@ +/* + * 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 . + */ +//use std::io::prelude::*; +//use std::fs::File; +use std::path::PathBuf; +use super::email::Mail; +use error::{MeliError, Result}; + + +pub trait MailBackend { + fn get(&self) -> Result>; +} +pub struct MaildirType { + path: String, +} + +impl MailBackend for MaildirType { + fn get(&self) -> Result> { + MaildirType::is_valid(&self.path)?; + let mut path = PathBuf::from(&self.path); + path.push("cur"); + let iter = path.read_dir()?; + let count = path.read_dir()?.count(); + let mut r = Vec::with_capacity(count); + for e in iter { + //eprintln!("{:?}", e); + let e = e.and_then(|x| { + let path = x.path(); + Ok(path.to_str().unwrap().to_string()) + })?; + match Mail::from(e) { + Some(e) => {r.push(e);}, + None => {} + } + /* + + f.read_to_end(&mut buffer)?; + eprintln!("{:?}", String::from_utf8(buffer.clone()).unwrap()); + let m = match Email::parse(&buffer) { + Ok((v, rest)) => match rest.len() { + 0 => v, + _ => + { eprintln!("{:?}", String::from_utf8(rest.to_vec()).unwrap()); +panic!("didn't parse"); }, + }, + Err(v) => panic!(v), + }; + + r.push(m); + */ + + } + Ok(r) + } +} + +impl MaildirType { + pub fn new(path: &str) -> Self { + MaildirType { + path: path.to_string() + } + } + fn is_valid(path: &str) -> Result<()> { + let mut p = PathBuf::from(path); + for d in ["cur", "new", "tmp"].iter() { + p.push(d); + if !p.is_dir() { + return Err(MeliError::new(format!("{} is not a valid maildir folder", path))); + } + p.pop(); + } + Ok(()) + } +} diff --git a/src/mailbox/mod.rs b/src/mailbox/mod.rs index 5806bfc74..acbe0981d 100644 --- a/src/mailbox/mod.rs +++ b/src/mailbox/mod.rs @@ -2,7 +2,7 @@ * meli - mailbox module. * * Copyright 2017 Manos Pitsidianakis - * + * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify @@ -19,27 +19,27 @@ * along with meli. If not, see . */ -extern crate maildir; -extern crate mailparse; -use self::mailparse::*; -use std::cmp::Ordering; +//use std::cmp::Ordering; +//use std::fmt; use std::option::Option; use std::collections::HashMap; use std; +mod maildir; +pub mod email; +mod parser; +pub use self::email::*; +use mailbox::maildir::MailBackend; +use error::Result; + + type UnixTimestamp = i64; -pub struct Mail { - entry: maildir::MailEntry, - subject: std::string::String, - pub message_id: std::string::String, - pub references: Vec, - date: UnixTimestamp, - thread: usize, -} /*a Mailbox represents a folder of mail. Currently only Maildir is supported.*/ +#[derive(Debug)] pub struct Mailbox{ + pub path: String, pub collection: Box>, pub threaded_collection: Vec, threads: Vec, @@ -55,15 +55,16 @@ pub struct Mailbox{ * nonexistent in our Mailbox but we know it exists (for example we have a copy * of a reply to a mail but we don't have its copy. */ -#[derive(Clone,Copy,Debug)] +#[derive(Clone, Copy, Debug)] pub struct Thread { id: usize, - message: Option, + message: Option, parent: Option, first_child: Option, next_sibling: Option, date: UnixTimestamp, indentation: usize, + show_subject: bool, } impl Thread { @@ -73,6 +74,9 @@ impl Thread { pub fn get_parent(&self) -> Option { self.parent } + pub fn has_parent(&self) -> bool { + self.parent.is_some() + } pub fn get_first_child(&self) -> Option { self.first_child } @@ -88,237 +92,301 @@ impl Thread { pub fn has_message(&self) -> bool { self.message.is_some() } - pub fn set_indentation(&mut self, i: usize) { + fn set_indentation(&mut self, i: usize) { self.indentation = i; } pub fn get_indentation(&self) -> usize { self.indentation } - pub fn is_descendant(&self, threads: &Vec, other: Thread) -> bool { + fn is_descendant(&self, threads: &Vec, other: &Thread) -> bool { + if self == other { + return true; + } match self.first_child { - Some(v) => { - if threads[v] == other { - return true; - } - if threads[v].clone().is_descendant(&threads, other) { + Some(v) => { + if threads[v].is_descendant(threads, other) { return true; } }, None => {} - } + }; match self.next_sibling { - Some(v) => { - if threads[v] == other { - return true; - } - if threads[v].clone().is_descendant(threads, other) { + Some(v) => { + if threads[v].is_descendant(threads, other) { return true; } }, None => {} - } - + }; return false; - - + } + fn set_show_subject(&mut self, set: bool) -> () { + self.show_subject = set; + } + pub fn get_show_subject(&self) -> bool { + self.show_subject } } impl PartialEq for Thread { fn eq(&self, other: &Thread) -> bool { - self.id == other.id + match (self.message, other.message) { + (Some(s), Some(o)) => { + s == o + }, + _ => { + self.id == other.id + } + } } } -impl Mailbox { - pub fn new(path: &str) -> Mailbox { - let maildir = maildir::Maildir::from(path); - let iter = maildir.list_cur(); - let mut collection: Box> = Box::new(Vec::new()); - let mut threads: Vec = Vec::new(); - let mut id_table: HashMap = HashMap::new(); - let mut idx = 0; - for x in iter { - let mut e = x.unwrap(); - let d = e.headers().unwrap().get_first_value("Date").unwrap(); - - let m_id = match e.headers().unwrap().get_first_value("Message-Id") { - Ok(v) => { - match v { - Some(v) => { - v - }, - None => idx.to_string() - } - } - Err(_) => { - idx.to_string() - } - }; - let mut references: Vec = Vec::new(); - match e.headers().unwrap().get_first_value("References") { - Ok(v) => { - match v { - Some(v) => { - references.append(&mut v.split_whitespace().map(|x| x.to_string()).collect()); - } - None => {} - } - } - Err(_) => { - } - }; - match e.headers().unwrap().get_first_value("In-Reply-To:") { - Ok(v) => { - match v { - Some(v) => { - references.push(v); - } - None => {} - } - } - Err(_) => { - } - }; - let subject = match e.headers().unwrap().get_first_value("Subject") { - Ok(v) => { - match v - { - Some(v) => v.clone(), - None => std::string::String::from("") - } - }, - Err(x) => panic!(x) - }; - collection.push( - Mail { - entry: e, - subject: subject, - references: references, - message_id: m_id, - date: dateparse(&d.unwrap()).unwrap(), - thread: 0, - }); - idx += 1; - } - idx = 0; - - collection.sort_by(|a, b| b.date.cmp(&a.date)); - for (i, x) in collection.iter_mut().enumerate() { - let x_index; - let m_id = x.message_id.clone(); - if id_table.contains_key(&m_id) { - let c = id_table.get(&m_id).unwrap(); - /* the already existing Thread should be empty, since we're - * seeing this message for the first time - * Store this message in the Thread's message slot. */ - threads[*c].message = Some(i); - threads[*c].date = x.date; - x.thread = *c; - x_index = *c; - } else { - /* Create a new Thread object holding this message */ - threads.push( - Thread { - message: Some(i), - id: idx, - parent: None, - first_child: None, - next_sibling: None, - date: x.date, - indentation: 0, - }); - x_index = idx; - x.thread = idx; - id_table.insert(m_id, x_index); - idx += 1; - } - /* For each element in the message's References field: - * - * Find a Thread object for the given Message-ID: - * If there's one in id_table use that; - * Otherwise, make (and index) one with a null Message. - * - * Link the References field's Threads together in the order implied by the References header. - * If they are already linked, don't change the existing links. - * Do not add a link if adding that link would introduce a loop: that is, before asserting A->B, search down the children of B to see if A is reachable, and also search down the children of A to see if B is reachable. If either is already reachable as a child of the other, don't add the link. - */ - if x.references.len() == 0 { +fn build_collection(threads: &mut Vec, id_table: &mut HashMap, collection: &mut Box>) -> () { + for (i, x) in collection.iter_mut().enumerate() { + let x_index; /* x's index in threads */ + let m_id = x.get_message_id_raw().to_string(); + if id_table.contains_key(&m_id) { + let t = *(id_table.get(&m_id).unwrap()); + /* the already existing Thread should be empty, since we're + * seeing this message for the first time */ + if threads[t].message.is_some() { + /* skip duplicate message-id, but this should be handled instead */ continue; } - let r_to = x.references[x.references.len() - 1].clone(); - let parent_id = - if id_table.contains_key(&r_to) { - - let p = id_table.get(&r_to).unwrap(); - if threads[*p].is_descendant(&threads, threads[x_index]) || - threads[x_index].is_descendant(&threads, threads[*p]) { - continue; - } - if threads[*p].first_child.is_none() { - threads[*p].first_child = Some(x_index); - } else { - let mut fc = threads[*p].first_child.unwrap(); - while threads[fc].next_sibling.is_some() { - fc = threads[fc].next_sibling.unwrap(); + x_index = t; + /* Store this message in the Thread's message slot. */ + threads[t].date = x.get_date(); + x.set_thread(t); + threads[t].message = Some(i); + } else { + /* Create a new Thread object holding this message */ + x_index = threads.len(); + threads.push( + Thread { + message: Some(i), + id: x_index, + parent: None, + first_child: None, + next_sibling: None, + date: x.get_date(), + indentation: 0, + show_subject: true, + }); + x.set_thread(x_index); + id_table.insert(m_id, x_index); + } + /* For each element in the message's References field: + * + * Find a Thread object for the given Message-ID: + * If there's one in id_table use that; + * Otherwise, make (and index) one with a null Message + * + * Link the References field's Threads together in the order implied by the References header. + * If they are already linked, don't change the existing links. + * Do not add a link if adding that link would introduce a loop: that is, before asserting A->B, search down the children of B to see if A is reachable, and also search down the children of A to see if B is reachable. If either is already reachable as a child of the other, don't add the link. + */ + let mut curr_ref = x_index; + 'ref_loop: for &r in x.get_references().iter().rev() { + let parent_id = + if id_table.contains_key(r.get_raw()) { + let p = *(id_table.get(r.get_raw()).unwrap()); + if !(threads[p].is_descendant(&threads, &threads[curr_ref]) || + threads[curr_ref].is_descendant(&threads, &threads[p])) { + threads[curr_ref].parent = Some(p); + if threads[p].first_child.is_none() { + threads[p].first_child = Some(curr_ref); + } else { + let mut child_iter = threads[p].first_child.unwrap(); + while threads[child_iter].next_sibling.is_some() { + threads[child_iter].parent = Some(p); + child_iter = threads[child_iter].next_sibling.unwrap(); + } + threads[child_iter].next_sibling = Some(curr_ref); + threads[child_iter].parent = Some(p); } - threads[fc].next_sibling = Some(x_index); - threads[fc].parent = Some(*p); } - *p + p } else { + let idx = threads.len(); threads.push( Thread { message: None, id: idx, parent: None, - first_child: Some(x_index), + first_child: Some(curr_ref), next_sibling: None, - date: x.date, + date: x.get_date(), indentation: 0, + show_subject: true, }); - id_table.insert(r_to.clone(), idx); - idx += 1; - idx-1 + id_table.insert(r.get_raw().to_string(), idx); + idx }; /* update thread date */ let mut parent_iter = parent_id; - loop { + 'date: loop { let mut p = &mut threads[parent_iter]; - p.date = x.date; - if p.parent.is_none() { - break; - } else { - parent_iter = p.get_parent().unwrap(); + if p.date < x.get_date() { + p.date = x.get_date(); + } + match p.parent { + Some(p) => { parent_iter = p; }, + None => { break 'date; }, + } + } + if threads[curr_ref].parent.is_none() { + threads[curr_ref].parent = Some(parent_id); + } + curr_ref = parent_id; + } + } +} + +impl Mailbox { + pub fn new(path: &str, sent_folder: Option<&str>) -> Result { + let mut collection: Box> = Box::new(maildir::MaildirType::new(path).get()?); + /* To reconstruct thread information from the mails we need: */ + + /* a vector to hold thread members */ + let mut threads: Vec = Vec::with_capacity((collection.len() as f64 * 1.2) as usize); + /* A hash table of Message IDs */ + let mut id_table: HashMap = HashMap::with_capacity(collection.len()); + + collection.sort_by(|a, b| a.get_date().cmp(&b.get_date())); + /* Add each message to id_table and threads, and link them together according to the + * References / In-Reply-To headers */ + build_collection(&mut threads, &mut id_table, &mut collection); + let mut idx = collection.len(); + let mut tidx = threads.len(); + /* Link messages from Sent folder if they are relevant to this folder. + * This means that + * - if a message from Sent is a reply to a message in this folder, we will + * add it to the threading (but not the collection; non-threading users shouldn't care + * about this) + * - if a message in this folder is a reply to a message we sent, we will add it to the + * threading + */ + if sent_folder.is_some() { + for mut x in maildir::MaildirType::new(sent_folder.unwrap()).get().unwrap() { + if id_table.contains_key(x.get_message_id_raw()) || + (!x.get_in_reply_to_raw().is_empty() && id_table.contains_key(x.get_in_reply_to_raw())) { + collection.push(x.clone()); + idx += 1; + } + if id_table.contains_key(x.get_message_id_raw()) { + let c = *(id_table.get(x.get_message_id_raw()).unwrap()); + if threads[c].message.is_some() { + /* skip duplicate message-id, but this should be handled instead */ + continue; + } + threads[c].message = Some(idx-1); + assert!(threads[c].has_children()); + threads[c].date = x.get_date(); + x.set_thread(c); + } + if !x.get_in_reply_to_raw().is_empty() && id_table.contains_key(x.get_in_reply_to_raw()) { + let p = *(id_table.get(x.get_in_reply_to_raw()).unwrap()); + let c = if !id_table.contains_key(x.get_message_id_raw()) { + threads.push( + Thread { + message: Some(idx-1), + id: tidx, + parent: Some(p), + first_child: None, + next_sibling: None, + date: x.get_date(), + indentation: 0, + show_subject: true, + }); + id_table.insert(x.get_message_id_raw().to_string(), tidx); + x.set_thread(tidx); + tidx += 1; + tidx - 1 + } else { + *(id_table.get(x.get_message_id_raw()).unwrap()) + }; + threads[c].parent = Some(p); + if threads[p].is_descendant(&threads, &threads[c]) || + threads[c].is_descendant(&threads, &threads[p]) { + continue; + } + if threads[p].first_child.is_none() { + threads[p].first_child = Some(c); + } else { + let mut fc = threads[p].first_child.unwrap(); + while threads[fc].next_sibling.is_some() { + threads[fc].parent = Some(p); + fc = threads[fc].next_sibling.unwrap(); + } + threads[fc].next_sibling = Some(c); + threads[fc].parent = Some(p); + } + /* update thread date */ + let mut parent_iter = p; + 'date: loop { + let mut p = &mut threads[parent_iter]; + if p.date < x.get_date() { + p.date = x.get_date(); + } + match p.parent { + Some(p) => { parent_iter = p; }, + None => { break 'date; }, + } + } } } - threads[x_index].parent = Some(parent_id); } /* Walk over the elements of id_table, and gather a list of the Thread objects that have - * no parents. */ - let mut root_set = Vec::new(); - for (_,v) in id_table.iter() { + * no parents. These are the root messages of each thread */ + let mut root_set = Vec::with_capacity(collection.len()); + 'root_set: for (_,v) in id_table.iter() { if threads[*v].parent.is_none() { + if !threads[*v].has_message() && threads[*v].has_children() && !threads[threads[*v].first_child.unwrap()].has_sibling() { + /* Do not promote the children if doing so would promote them to the root set + * -- unless there is only one child, in which case, do. */ + root_set.push(threads[*v].first_child.unwrap()); + continue 'root_set; + } root_set.push(*v); } } root_set.sort_by(|a, b| threads[*b].date.cmp(&threads[*a].date)); - let mut threaded_collection: Vec = Vec::new(); - fn build_threaded(threads: &mut Vec, indentation: usize, threaded: &mut Vec, index: usize) { - - let thread = threads[index]; - - if thread.has_message() { - threads[index].set_indentation(indentation); - if !threaded.contains(&index) { - threaded.push(index); + /* Group messages together by thread in a collection so we can print them together */ + let mut threaded_collection: Vec = Vec::with_capacity(collection.len()); + fn build_threaded(threads: &mut Vec, indentation: usize, threaded: &mut Vec, i: usize, root_subject_idx: usize, collection: &Vec) { + let thread = threads[i]; + if threads[root_subject_idx].has_message() { + let root_subject = collection[threads[root_subject_idx].get_message().unwrap()].get_subject(); + /* If the Container has no Message, but does have children, remove this container but + * promote its children to this level (that is, splice them in to the current child + * list.) */ + if indentation > 0 && thread.has_message() { + let subject = collection[thread.get_message().unwrap()].get_subject(); + if subject == root_subject || subject.starts_with("Re: ") && subject.ends_with(root_subject) { + threads[i].set_show_subject(false); + } } } + if thread.has_parent() && !threads[thread.get_parent().unwrap()].has_message() { + threads[i].parent = None; + } + let indentation = + if thread.has_message() { + threads[i].set_indentation(indentation); + if !threaded.contains(&i) { + threaded.push(i); + } + indentation + 1 + } else if indentation > 0 { + indentation + } else { + indentation + 1 + }; if thread.has_children() { let mut fc = thread.get_first_child().unwrap(); loop { - build_threaded(threads, indentation + 1, threaded, fc); + build_threaded(threads, indentation, threaded, fc, i, collection); let thread_ = threads[fc]; if !thread_.has_sibling() { break; @@ -328,16 +396,18 @@ impl Mailbox { } } for i in &root_set { - build_threaded(&mut threads, 0, &mut threaded_collection, *i); + build_threaded(&mut threads, 0, &mut threaded_collection, *i, *i, &collection); } let length = collection.len(); - Mailbox { + + Ok(Mailbox { + path: path.to_string(), collection: collection, threads: threads, length: length, threaded_collection: threaded_collection, - } + }) } pub fn get_length(&self) -> usize { self.length @@ -349,43 +419,11 @@ impl Mailbox { pub fn get_mail_and_thread(&mut self, i: usize) -> (&mut Mail, Thread) { let ref mut x = self.collection.as_mut_slice()[i]; - let thread = self.threads[x.get_thread()].clone(); + let thread = self.threads[x.get_thread()]; (x, thread) } - pub fn get_thread(&self, i: usize) -> Thread { - self.threads[i].clone() + pub fn get_thread(&self, i: usize) -> &Thread { + &self.threads[i] } } -impl Mail { - pub fn get_entry(&mut self) -> &mut maildir::MailEntry { - &mut self.entry - } - pub fn get_date(&self) -> i64 { - self.date - } - pub fn get_subject(&self) -> &str { - &self.subject - } - pub fn get_thread(&self) -> usize { - self.thread - } -} - -impl Eq for Mail {} -impl Ord for Mail { - fn cmp(&self, other: &Mail) -> Ordering { - self.date.cmp(&other.date) - } -} -impl PartialOrd for Mail { - fn partial_cmp(&self, other: &Mail) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for Mail { - fn eq(&self, other: &Mail) -> bool { - self.date == other.date - } -} diff --git a/src/mailbox/parser.rs b/src/mailbox/parser.rs new file mode 100644 index 000000000..354c0b1d8 --- /dev/null +++ b/src/mailbox/parser.rs @@ -0,0 +1,176 @@ +//use memmap::{Mmap, Protection}; +use std; +use base64; +use chrono; +use nom::le_u8; + +/* Wow this sucks! */ +named!(quoted_printable_byte, do_parse!( + p: map_res!(preceded!(tag!("="), verify!(complete!(take!(2)), |s: &[u8]| { ::nom::is_hex_digit(s[0]) && ::nom::is_hex_digit(s[1]) })), std::str::from_utf8) >> + ( { + u8::from_str_radix(p, 16).unwrap() + } ))); + + +// Parser definition + +/* A header can span multiple lines, eg: + * + * Received: from -------------------- (-------------------------) + * by --------------------- (--------------------- [------------------]) (-----------------------) + * with ESMTP id ------------ for <------------------->; + * Tue, 5 Jan 2016 21:30:44 +0100 (CET) + */ + +/* + * if a header value is a Vec<&str>, this is the tail of that Vector + */ +named!(valuelist<&str>, + map_res!(delimited!(alt_complete!(tag!("\t") | tag!(" ")), take_until!("\n"), tag!("\n")), std::str::from_utf8) + ); + +/* Parse the value part of the header -> Vec<&str> */ +named!(value>, + do_parse!( + head: map_res!(terminated!(take_until!("\n"), tag!("\n")), std::str::from_utf8) >> + tail: many0!(valuelist) >> + ( { + let tail_len = tail.len(); + let tail: Vec<&str> = tail.iter().map(|v| { v.trim()}).collect(); + let mut result = Vec::with_capacity(1 + tail.len()); + result.push(head.trim()); + if tail_len == 1 && tail[0] == "" { + result + } else { + tail.iter().fold(result, |mut acc, x| { acc.push(x); acc}) + } + } ) + )); + +/* Parse the name part of the header -> &str */ +named!(name<&str>, + terminated!(verify!(map_res!(take_until1!(":"), std::str::from_utf8), | v: &str | { !v.contains("\n")} ), tag!(":"))); + +/* Parse a single header as a tuple -> (&str, Vec<&str>) */ +named!(header<(&str, std::vec::Vec<&str>)>, + pair!(complete!(name), complete!(value))); +/* Parse all headers -> Vec<(&str, Vec<&str>)> */ +named!(headers)>>, + many1!(complete!(header))); + +named!(pub mail<(std::vec::Vec<(&str, std::vec::Vec<&str>)>, &[u8])>, + separated_pair!(headers, tag!("\n"), take_while!(call!(|_| { true })))); + //pair!(headers, take_while!(call!(|_| { true })))); + +/* try chrono parse_from_str with several formats + * https://docs.rs/chrono/0.4.0/chrono/struct.DateTime.html#method.parse_from_str + */ + +/* Header parsers */ + +/* Encoded words + *"=?charset?encoding?encoded text?=". + */ +named!(utf8_token_base64, do_parse!( + encoded: complete!(delimited!(tag_no_case!("=?UTF-8?B?"), take_until1!("?="), tag!("?="))) >> + ( { + match base64::decode(encoded) { + Ok(ref v) => { String::from_utf8_lossy(v).into_owned() + }, + Err(_) => { String::from_utf8_lossy(encoded).into_owned() } + } + } ) + )); + +named!(utf8_token_quoted_p_raw<&[u8], &[u8]>, + complete!(delimited!(tag_no_case!("=?UTF-8?q?"), take_until1!("?="), tag!("?=")))); + +//named!(utf8_token_quoted_p, escaped_transform!(call!(alpha), '=', quoted_printable_byte)); + +named!(utf8_token_quoted_p, do_parse!( + raw: call!(utf8_token_quoted_p_raw) >> + ( { + named!(get_bytes>, dbg!(many0!(alt!(quoted_printable_byte | le_u8)))); + let bytes = get_bytes(raw).to_full_result().unwrap(); + String::from_utf8_lossy(&bytes).into_owned() + } ))); + +named!(utf8_token, alt_complete!( + utf8_token_base64 | + call!(utf8_token_quoted_p))); + +named!(utf8_token_list, ws!(do_parse!( + list: separated_nonempty_list!(complete!(tag!(" ")), utf8_token) >> + ( { + let list_len = list.iter().fold(0, |mut acc, x| { acc+=x.len(); acc }); + list.iter().fold(String::with_capacity(list_len), |mut acc, x| { acc.push_str(x); acc}) + } ) + ))); +named!(ascii_token, do_parse!( + word: alt!(terminated!(take_until1!("=?"), peek!(tag_no_case!("=?UTF-8?"))) | take_while!(call!(|_| { true }))) >> + ( { + String::from_utf8_lossy(word).into_owned() + + } ))); + +/* Lots of copying here. TODO: fix it */ +named!(pub subject, ws!(do_parse!( + list: many0!(alt_complete!( utf8_token_list | ascii_token)) >> + ( { + let list_len = list.iter().fold(0, |mut acc, x| { acc+=x.len(); acc }); + let s = list.iter().fold(String::with_capacity(list_len), |mut acc, x| { acc.push_str(x); acc.push_str(" "); acc}); + s.trim().to_string() + } ) + + ))); + +#[test] +fn test_subject() { + let subject_s = "list.free.de mailing list memberships reminder".as_bytes(); + assert_eq!((&b""[..], "list.free.de mailing list memberships reminder".to_string()), subject(subject_s).unwrap()); + let subject_s = "=?UTF-8?B?zp3Orc6/IM+Az4HOv8+Dz4nPgM65zrrPjCDOvM6uzr3Phc68zrEgzrHPhs6v?= =?UTF-8?B?z4fOuM63?=".as_bytes(); + assert_eq!((&b""[..], "Νέο προσωπικό μήνυμα αφίχθη".to_string()), subject(subject_s).unwrap()); +} +fn eat_comments(input: &str) -> String { + let mut in_comment = false; + input.chars().fold(String::with_capacity(input.len()), |mut acc, x| { + if x == '(' && !in_comment { + in_comment = true; + acc + } else if x == ')' && in_comment { + in_comment = false; + acc + } else if in_comment { + acc + } else { + acc.push(x); acc + } + }) +} + +#[test] +fn test_eat_comments() { + let s = "Mon (Lundi), 4(quatre)May (Mai) 1998(1998-05-04)03 : 04 : 12 +0000"; + assert_eq!(eat_comments(s), "Mon , 4May 199803 : 04 : 12 +0000"); + let s = "Thu, 31 Aug 2017 13:43:37 +0000 (UTC)"; + assert_eq!(eat_comments(s), "Thu, 31 Aug 2017 13:43:37 +0000 "); +} +/* Date should tokenize input and convert the tokens, right now we expect input will have no extra + * spaces in between tokens */ +pub fn date(input: &str) -> Option> { + chrono::DateTime::parse_from_rfc2822(eat_comments(input).trim()).ok() +} + +#[test] +fn test_date() { + let s = "Thu, 31 Aug 2017 13:43:37 +0000 (UTC)"; + let _s = "Thu, 31 Aug 2017 13:43:37 +0000"; + assert_eq!(date(s).unwrap(), date(_s).unwrap()); +} + +named!(pub message_id<&str>, + map_res!(complete!(delimited!(tag!("<"), take_until1!(">"), tag!(">"))), std::str::from_utf8) + ); + +named!(pub references>, many0!(preceded!(is_not!("<"), message_id))); + diff --git a/src/main.rs b/src/main.rs index 6f7008754..2ac7ccffd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,39 +1,101 @@ +/* + * meli - main.rs + * + * 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 ncurses; -extern crate maildir; -extern crate mailparse; +pub mod mailbox; mod ui; +mod conf; +mod error; use ui::index::*; +use mailbox::*; +use conf::*; + +#[macro_use] +extern crate serde_derive; +/* parser */ +#[macro_use] +extern crate nom; +extern crate chrono; +extern crate base64; +extern crate memmap; fn main() { let locale_conf = ncurses::LcCategory::all; ncurses::setlocale(locale_conf, "en_US.UTF-8"); - ui::initialize(); - let mailbox = Mailbox::new("PATH"); - let mut index = Index::new(mailbox); - ncurses::refresh(); + let set = Settings::new(); + let ui = ui::TUI::initialize(); + //let mailbox = Mailbox::new("/home/epilys/Downloads/rust/nutt/Inbox4"); + let mut j = 0; + let folder_length = set.accounts.get("norn").unwrap().folders.len(); + 'main : loop { + ncurses::touchwin(ncurses::stdscr()); + ncurses::mv(0,0); + let mailbox = Mailbox::new(&set.accounts.get("norn").unwrap().folders[j], + Some(&set.accounts.get("norn").unwrap().sent_folder)); + let mut index: Box = match mailbox { + Ok(v) => { + Box::new(Index::new(v)) + }, + Err(v) => { + Box::new(ErrorWindow::new(v)) + } + }; + //eprintln!("{:?}", set); + ncurses::refresh(); - index.draw(); + index.draw(); - let mut ch; - loop { - ch = ncurses::get_wch(); - match ch { - Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP)) | - Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN)) => { - index.scroll(k); - continue; + let mut ch; + 'inner : loop { + ch = ncurses::get_wch(); + match ch { + Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP)) | + Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN)) => { + index.handle_input(k); + continue; + } + Some(ncurses::WchResult::Char(k @ 10)) => { + index.handle_input(k as i32); + continue; + } + Some(ncurses::WchResult::KeyCode(ncurses::KEY_F1)) | + Some(ncurses::WchResult::Char(113)) => { + break 'main; + }, + Some(ncurses::WchResult::Char(74)) => { + if j < folder_length - 1 { + j += 1; + break 'inner; + } + }, + Some(ncurses::WchResult::Char(75)) => { + if j > 0 { + j -= 1; + break 'inner; + } + }, + _ => {} } - Some(ncurses::WchResult::Char(10)) => { - index.show_pager(); - index.draw(); - continue; - } - Some(ncurses::WchResult::KeyCode(ncurses::KEY_F1)) => { - break; - } - _ => {} } } + drop(ui); } diff --git a/src/ui/index.rs b/src/ui/index.rs index f8e9c4f6f..a82a13bb6 100644 --- a/src/ui/index.rs +++ b/src/ui/index.rs @@ -19,12 +19,51 @@ * along with meli. If not, see . */ extern crate ncurses; -extern crate maildir; -extern crate mailparse; extern crate chrono; +use mailbox::email::Mail; use mailbox::*; +use error::MeliError; +use std::error::Error; -use self::chrono::NaiveDateTime; +//use self::chrono::NaiveDateTime; + +pub trait Window { + fn draw(&mut self) -> (); + fn handle_input(&mut self, input: i32) -> (); +} + +pub struct ErrorWindow { + description: String, + + win: ncurses::WINDOW, +} + +impl Window for ErrorWindow { + fn draw(&mut self) -> () { + ncurses::waddstr(self.win, &self.description); + ncurses::wrefresh(self.win); + } + fn handle_input(&mut self, _: i32) -> () { + + + } +} +impl ErrorWindow { + pub fn new(err: MeliError) -> Self { + /* + let mut screen_height = 0; + let mut screen_width = 0; + /* Get the screen bounds. */ + ncurses::getmaxyx(ncurses::stdscr(), &mut screen_height, &mut screen_width); + // let win = ncurses::newwin( ncurses::LINES(), ncurses::COLS()-30, 0, 30); + */ + let win = ncurses::newwin(0, 0, 0, 0); + ErrorWindow { + description: err.description().to_string(), + win: win, + } + } +} /* Index represents a UI list of mails */ pub struct Index { @@ -35,68 +74,90 @@ pub struct Index { screen_width: i32, screen_height: i32, + /* threading */ threaded: bool, + cursor_idx: usize, } -impl Index { - pub fn new(mailbox: Mailbox) -> Index { - let mut screen_height = 0; - let mut screen_width = 0; - /* Get the screen bounds. */ - ncurses::getmaxyx(ncurses::stdscr(), &mut screen_height, &mut screen_width); - // let win = ncurses::newwin( ncurses::LINES(), ncurses::COLS()-30, 0, 30); - let win = ncurses::newwin(0, 0, 0, 0); - ncurses::getmaxyx(win, &mut screen_height, &mut screen_width); - //eprintln!("length is {}\n", length); - let pad = ncurses::newpad(mailbox.get_length() as i32, screen_width); - ncurses::wbkgd( - pad, - ' ' as ncurses::chtype | - ncurses::COLOR_PAIR(super::COLOR_PAIR_DEFAULT) as ncurses::chtype, - ); - Index { - mailbox: mailbox, - win: win, - pad: pad, - screen_width: 0, - screen_height: 0, - threaded: true, - cursor_idx: 0, - } - } - pub fn draw(&mut self) { + +impl Window for Index { + fn draw(&mut self) { + if self.mailbox.get_length() == 0 { + return; + } let mut x = 0; let mut y = 0; ncurses::getbegyx(self.win, &mut y, &mut x); ncurses::wclear(self.pad); - let mut idx = 0; if self.threaded { + let mut indentations : Vec = Vec::with_capacity(6); /* Draw threaded view. */ - for i in self.mailbox.threaded_collection.iter() { + let mut iter = self.mailbox.threaded_collection.iter().enumerate().peekable(); + /* This is just a desugared for loop so that we can use .peek() */ + loop { + match iter.next() { + Some((idx, i)) => { + let container = self.mailbox.get_thread(*i); + let indentation = container.get_indentation(); + + assert_eq!(container.has_message(), true); + match iter.peek() { + Some(&(_, x)) if self.mailbox.get_thread(*x).get_indentation() == indentation => { + indentations.pop(); + indentations.push(true); + }, + _ => { + indentations.pop(); + indentations.push(false); + } + } + if container.has_sibling() { + indentations.pop(); + indentations.push(true); + } + let x = &self.mailbox.collection[container.get_message().unwrap()]; + Index::draw_entry(self.pad, x, idx, indentation, container.has_sibling(), container.has_parent(), idx == self.cursor_idx, container.get_show_subject(), Some(&indentations)); + match iter.peek() { + Some(&(_, x)) if self.mailbox.get_thread(*x).get_indentation() > indentation => { + indentations.push(false); + }, + Some(&(_, x)) if self.mailbox.get_thread(*x).get_indentation() < indentation => { + for _ in 0..(indentation - self.mailbox.get_thread(*x).get_indentation()) { + indentations.pop(); + } + }, + _ => { + } + } + }, + None => break, + } + } + /* + for (idx, i) in self.mailbox.threaded_collection.iter().enumerate() { let container = self.mailbox.get_thread(*i); - assert!(container.has_message(), true); + assert_eq!(container.has_message(), true); + if container.has_sibling() { + indentations.pop(); + indentations.push(true); + } let x = &self.mailbox.collection[container.get_message().unwrap()]; - if idx == self.cursor_idx { - Index::draw_entry(self.pad, x, idx, container.get_indentation(), false, true); + Index::draw_entry(self.pad, x, idx, container.get_indentation(), container.has_sibling(), idx == self.cursor_idx, container.get_show_subject(), Some(&indentations)); + if container.has_children() { + indentations.push(false); } else { - Index::draw_entry(self.pad, x, idx, container.get_indentation(), false, false); + indentations.pop(); } - idx += 1; - } + */ } else { - for x in self.mailbox.collection.as_mut_slice() { - if idx == self.cursor_idx { - Index::draw_entry(self.pad, x, idx, 0, false, true); - } else { - Index::draw_entry(self.pad, x, idx, 0, false, false); - } - idx += 1; + for (idx, x) in self.mailbox.collection.as_mut_slice().iter().enumerate() { + Index::draw_entry(self.pad, x, idx, 0, false, false, idx == self.cursor_idx, true, None); } } ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); @@ -113,7 +174,10 @@ impl Index { ); } - pub fn scroll(&mut self, motion: i32) { + fn handle_input(&mut self, motion: i32) { + if self.mailbox.get_length() == 0 { + return; + } ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); if self.screen_height == 0 { return; @@ -129,14 +193,18 @@ impl Index { } else { return; } - } + }, ncurses::KEY_DOWN => { if self.cursor_idx < self.mailbox.get_length() - 1 { self.cursor_idx += 1; } else { return; } - } + }, + 10 => { + self.show_pager(); + self.draw(); + }, _ => { return; } @@ -146,26 +214,41 @@ impl Index { ncurses::wmove(self.pad, self.cursor_idx as i32, 0); /* Borrow x from self.mailbox in separate scopes or else borrow checker complains */ { - let i: usize = - if self.threaded { - self.mailbox.get_threaded_mail(self.cursor_idx) - } else { - self.cursor_idx - }; - let (ref mut x, thread) = self.mailbox.get_mail_and_thread(i); - Index::draw_entry(self.pad, x, self.cursor_idx, thread.get_indentation(), false, true); + let pair = super::COLOR_PAIR_CURSOR; + ncurses::wchgat(self.pad, -1, 0, pair); } /* Draw previous highlighted entry normally */ ncurses::wmove(self.pad, prev_idx as i32, 0); { - let i: usize = - if self.threaded { - self.mailbox.get_threaded_mail(prev_idx) - } else { - prev_idx + let pair = match self.threaded { + true if prev_idx % 2 == 0 => super::COLOR_PAIR_THREAD_EVEN, + true => super::COLOR_PAIR_THREAD_ODD, + false => super::COLOR_PAIR_DEFAULT, }; - let (ref mut x, thread) = self.mailbox.get_mail_and_thread(i); - Index::draw_entry(self.pad, x, prev_idx, thread.get_indentation(), false, false); + ncurses::wchgat(self.pad, 32, 0, pair); + ncurses::wmove(self.pad, prev_idx as i32, 32); + /* If first character in subject column is space, we need to check for indentation + * characters and highlight them appropriately */ + if (ncurses::winch(self.pad) & ncurses::A_CHARTEXT()) == ' ' as u64 { + let mut x = 32; + loop { + match ncurses::mvwinch(self.pad, prev_idx as i32, x) & ncurses::A_CHARTEXT() { + 32 => { /* ASCII code for space */ + ncurses::wchgat(self.pad, x, 0, pair); + }, + 62 => { /* ASCII code for '>' */ + ncurses::wchgat(self.pad, x, 0, super::COLOR_PAIR_THREAD_INDENT); + ncurses::wmove(self.pad, prev_idx as i32, x + 1); + break; + } + _ => { + ncurses::wchgat(self.pad, x, 0, super::COLOR_PAIR_THREAD_INDENT); + }, + } + x += 1; + } + } + ncurses::wchgat(self.pad, -1, 0, pair); } /* Calculate the pad row of the first entry to be displayed in the window */ @@ -189,9 +272,9 @@ impl Index { */ if pminrow != pminrow_prev && pminrow + self.screen_height > self.mailbox.get_length() as i32 { - /* touch Index window (tell ncurses to redraw the entire window in - * next refresh) */ - ncurses::touchwin(self.win); + /* touch dead entries in index (tell ncurses to redraw the empty lines next refresh) */ + let live_entries = self.mailbox.get_length() as i32 - pminrow; + ncurses::wredrawln(self.win, live_entries, self.screen_height); ncurses::wrefresh(self.win); } ncurses::prefresh( @@ -204,43 +287,120 @@ impl Index { self.screen_width - 1, ); } - fn draw_entry(win: ncurses::WINDOW, mail: &Mail, i: usize, indent: usize, - has_sibling: bool, highlight: bool) { - if highlight { - ncurses::wattron(win, - ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); +} +impl Index { + pub fn new(mailbox: Mailbox) -> Index { + let mut screen_height = 0; + let mut screen_width = 0; + /* Get the screen bounds. */ + ncurses::getmaxyx(ncurses::stdscr(), &mut screen_height, &mut screen_width); + // let win = ncurses::newwin( ncurses::LINES(), ncurses::COLS()-30, 0, 30); + let win = ncurses::newwin(0, 0, 0, 0); + ncurses::getmaxyx(win, &mut screen_height, &mut screen_width); + //eprintln!("length is {}\n", length); + let mailbox_length = mailbox.get_length(); + let pad = ncurses::newpad(mailbox_length as i32, screen_width); + ncurses::wbkgd( + pad, + ' ' as ncurses::chtype | + ncurses::COLOR_PAIR(super::COLOR_PAIR_DEFAULT) as ncurses::chtype, + ); + if mailbox_length == 0 { + ncurses::printw(&format!("Mailbox {} is empty.\n", mailbox.path)); + ncurses::refresh(); } + let mut color = true; + let mut thread_color = Vec::with_capacity(mailbox_length); + for i in &mailbox.threaded_collection { + let container = mailbox.get_thread(*i); + if !container.has_parent() { + color = !color; + } + thread_color.push(color); + } + Index { + mailbox: mailbox, + win: win, + pad: pad, + screen_width: 0, + screen_height: 0, + threaded: true, + cursor_idx: 0, + } + } + + /* draw_entry() doesn't take &mut self because borrow checker complains if it's called from + * another method. */ + fn draw_entry(win: ncurses::WINDOW, mail: &Mail, i: usize, indent: usize, + has_sibling: bool, has_parent: bool, highlight: bool, + show_subject: bool, indentations: Option<&Vec>) { + /* TODO: use addchstr */ + let pair = + if highlight { + super::COLOR_PAIR_CURSOR + } else if i % 2 == 0 { + super::COLOR_PAIR_THREAD_EVEN + } else { + super::COLOR_PAIR_THREAD_ODD + }; + let attr = ncurses::COLOR_PAIR(pair); + ncurses::wattron(win, attr); + ncurses::waddstr(win, &format!("{}\t", i)); - let dt = NaiveDateTime::from_timestamp(mail.get_date(), 0); - ncurses::waddstr(win, &dt.format("%Y-%m-%d %H:%M:%S").to_string()); + ncurses::waddstr(win, &mail.get_datetime().format("%Y-%m-%d %H:%M:%S").to_string()); ncurses::waddch(win, '\t' as u64); - for _ in 0..indent { - ncurses::waddch(win, ' ' as u64); + for i in 0..indent { + if indentations.is_some() && indentations.unwrap().len() > i && indentations.unwrap()[i] { + ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT)); + ncurses::waddstr(win, "│"); + ncurses::wattroff(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT)); + ncurses::wattron(win, attr); + } else { + ncurses::waddch(win, ' ' as u64); + } + if i > 0 { + ncurses::waddch(win, ' ' as u64); + } } if indent > 0 { - ncurses::wattron(win, - ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT)); - if has_sibling { - ncurses::waddstr(win, "│"); + ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT)); + if has_sibling && has_parent { + ncurses::waddstr(win, "├"); + } else if has_sibling { + ncurses::waddstr(win, "┬"); } else { ncurses::waddstr(win, "└"); } - ncurses::waddstr(win, "->"); - ncurses::wattroff(win, - ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT)); + ncurses::waddstr(win, "─>"); + ncurses::wattroff(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT)); } - if highlight { - ncurses::wattron(win, - ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); + ncurses::wattron(win, attr); + if show_subject { + ncurses::waddstr(win, &format!("{:.85}",mail.get_subject())); + /* + if indent == 0 { + if mail.get_subject().chars().count() < 85 { + for _ in 0..(85 - mail.get_subject().chars().count()) { + ncurses::waddstr(win, "▔"); + } + } + ncurses::waddstr(win,"▔"); + }*/ } - ncurses::waddstr(win, &format!("{:.85}",mail.get_subject())); - if highlight { - ncurses::wattroff(win, - ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); - } - ncurses::waddstr(win, "\n"); + let mut screen_height = 0; + let mut screen_width = 0; + /* Get the screen bounds. */ + let mut x = 0; + let mut y = 0; + ncurses::getmaxyx(win, &mut screen_height, &mut screen_width); + ncurses::getyx(win, &mut y, &mut x); + ncurses::waddstr(win, &" ".repeat((screen_width - x) as usize)); + ncurses::wattroff(win, attr); } - pub fn show_pager(&mut self) { + fn show_pager(&mut self) { + if self.mailbox.get_length() == 0 { + return; + } ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); let x: &mut Mail; @@ -251,7 +411,7 @@ impl Index { } else { x = &mut self.mailbox.collection[self.cursor_idx]; } - let mut pager = super::pager::Pager::new(self.win, &mut x.get_entry()); + let mut pager = super::pager::Pager::new(self.win, x); pager.scroll(ncurses::KEY_DOWN); pager.scroll(ncurses::KEY_UP); let mut ch = ncurses::getch(); @@ -267,14 +427,12 @@ impl Index { } ch = ncurses::getch(); } - drop(pager); // drop pager before next refresh - ncurses::wrefresh(self.win); } } impl Drop for Index { fn drop(&mut self) { + ncurses::wclear(self.win); ncurses::delwin(self.win); ncurses::delwin(self.pad); - ncurses::endwin(); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e302bcd94..9f5b5c104 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -26,7 +26,13 @@ pub static COLOR_PAIR_DEFAULT: i16 = 1; pub static COLOR_PAIR_CURSOR: i16 = 2; pub static COLOR_PAIR_HEADERS: i16 = 3; pub static COLOR_PAIR_THREAD_INDENT: i16 = 4; -pub fn initialize() { +pub static COLOR_PAIR_THREAD_ODD: i16 = 5; +pub static COLOR_PAIR_THREAD_EVEN: i16 = 6; + +pub struct TUI; + +impl TUI { +pub fn initialize() -> Self { /* start ncurses */ ncurses::initscr(); ncurses::keypad(ncurses::stdscr(), true); @@ -40,9 +46,18 @@ pub fn initialize() { ncurses::init_pair(COLOR_PAIR_CURSOR, 251, 235); ncurses::init_pair(COLOR_PAIR_HEADERS, 33, 0); ncurses::init_pair(COLOR_PAIR_THREAD_INDENT, 5, 0); + ncurses::init_pair(COLOR_PAIR_THREAD_ODD, 15, 0); + ncurses::init_pair(COLOR_PAIR_THREAD_EVEN, 15, 233); /* Set the window's background color. */ ncurses::bkgd( ' ' as ncurses::chtype | ncurses::COLOR_PAIR(COLOR_PAIR_DEFAULT) as ncurses::chtype, ); + TUI {} +} +} +impl Drop for TUI { + fn drop(&mut self) { + ncurses::endwin(); + } } diff --git a/src/ui/pager.rs b/src/ui/pager.rs index e5bf57e5e..509135969 100644 --- a/src/ui/pager.rs +++ b/src/ui/pager.rs @@ -19,9 +19,8 @@ * along with meli. If not, see . */ extern crate ncurses; -extern crate maildir; -extern crate mailparse; -use self::mailparse::*; + +use super::super::mailbox; /* Pager represents the part of the UI that shows the mail headers and body for * viewing */ @@ -36,7 +35,7 @@ pub struct Pager { impl Pager { pub fn new(parent: ncurses::WINDOW, - entry: &mut maildir::MailEntry) -> Pager { + entry: &mut mailbox::Mail) -> Pager { let mut screen_height = 0; let mut screen_width = 0; ncurses::getmaxyx(parent, &mut screen_height, &mut screen_width); @@ -136,50 +135,56 @@ impl Pager { w - 1, ); } - fn print_entry_headers(win: ncurses::WINDOW, mail: &mut maildir::MailEntry) -> i32 { + fn print_entry_headers(win: ncurses::WINDOW, mail: &mut mailbox::Mail) -> i32 { let mut i = 0; ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_HEADERS)); ncurses::waddstr(win, "Date: "); ncurses::waddstr( win, - &mail.headers() - .unwrap() - .get_first_value("Date") - .unwrap() - .unwrap(), + mail.get_date_as_str() ); ncurses::waddstr(win, "\n"); i += 1; ncurses::waddstr(win, "From: "); ncurses::waddstr( win, - &mail.headers() - .unwrap() - .get_first_value("From") - .unwrap() - .unwrap(), + &mail.get_from(), ); ncurses::waddstr(win, "\n"); i += 1; ncurses::waddstr(win, "To: "); ncurses::waddstr( win, - &mail.headers() - .unwrap() - .get_first_value("To") - .unwrap() - .unwrap(), + &mail.get_to(), ); ncurses::waddstr(win, "\n"); i += 1; ncurses::waddstr(win, "Subject: "); ncurses::waddstr( win, - &mail.headers() - .unwrap() - .get_first_value("Subject") - .unwrap() - .unwrap(), + &mail.get_subject(), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::waddstr(win, "Message-ID: "); + ncurses::waddstr( + win, + &mail.get_message_id_raw(), + //&mail.get_message_id(), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::waddstr(win, "References: "); + ncurses::waddstr( + win, + &format!("{:?}", mail.get_references()), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::waddstr(win, "In-Reply-To: "); + ncurses::waddstr( + win, + &mail.get_in_reply_to_raw(), ); ncurses::waddstr(win, "\n"); i += 1; @@ -189,7 +194,7 @@ impl Pager { } fn print_entry_content( win: ncurses::WINDOW, - mail: &mut maildir::MailEntry, + mail: &mut mailbox::Mail, height: i32) -> (ncurses::WINDOW, i32, i32) { let mut h = 0; let mut w = 0; @@ -199,44 +204,31 @@ impl Pager { let mut y = 0; /* y,x coordinates of upper left corner of win */ ncurses::getparyx(win, &mut y, &mut x); - match &mail.parsed() { - &Ok(ref v) => { - match &v.get_body() { - &Ok(ref b) => { - let lines: Vec<&str> = b.split('\n').collect(); - let lines_length = lines.len(); - let pad = ncurses::newpad(lines_length as i32, 1024); - ncurses::wclear(pad); - for l in lines { - ncurses::waddstr(pad, &l.replace("%", "%%")); - ncurses::waddstr(pad, "\n"); - } - /* - * ┌ ┏━━━━━━━━━┓ ┐ - * │ ┃ ┃ │ - * y ┃ ┃ │ - * │ ┃ ┃ │ - * ├ x━━━━━━━━━┫ ┐ │ index - * │ ┃ ┃ │ │ - * h ┃ ┃ │ pager │ - * └ ┗━━━━━━━━━w ┘ ┘ - */ - ncurses::pnoutrefresh(pad, 0, 0, y + height, x, y + height - 1, w - 1); - return (pad, lines_length as i32, height); - } - _ => { - return (ncurses::newpad(0, 0), 0, height); - } - } - } - _ => { - return (ncurses::newpad(0, 0), 0, height); - } + let body = mail.get_body(); + let lines: Vec<&str> = body.trim().split('\n').collect(); + let lines_length = lines.len(); + let pad = ncurses::newpad(lines_length as i32, 1024); + ncurses::wclear(pad); + for l in lines { + ncurses::waddstr(pad, &l.replace("%", "%%")); + ncurses::waddstr(pad, "\n"); } + /* + * ┌ ┏━━━━━━━━━┓ ┐ + * │ ┃ ┃ │ + * y ┃ ┃ │ + * │ ┃ ┃ │ + * ├ x━━━━━━━━━┫ ┐ │ index + * │ ┃ ┃ │ │ + * h ┃ ┃ │ pager │ + * └ ┗━━━━━━━━━w ┘ ┘ + */ + ncurses::pnoutrefresh(pad, 0, 0, y + height, x, y + height - 1, w - 1); + return (pad, lines_length as i32, height); } fn print_entry( win: ncurses::WINDOW, - mail: &mut maildir::MailEntry) -> (ncurses::WINDOW, i32, i32) { + mail: &mut mailbox::Mail) -> (ncurses::WINDOW, i32, i32) { let header_height = Pager::print_entry_headers(win, mail); Pager::print_entry_content(win, mail, header_height + 2) } @@ -244,8 +236,9 @@ impl Pager { impl Drop for Pager { fn drop(&mut self) { - ncurses::wclear(self.win); - ncurses::delwin(self.win); ncurses::delwin(self.pad); + ncurses::wclear(self.win); + ncurses::wrefresh(self.win); + ncurses::delwin(self.win); } }