threads
parent
8e07843c4a
commit
9946fbcbe0
12
Cargo.toml
12
Cargo.toml
|
@ -4,11 +4,19 @@ version = "0.1.0"
|
||||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mailparse = "0.5.1"
|
|
||||||
maildir = "0.1.1"
|
|
||||||
chrono = "0.4"
|
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]
|
[dependencies.ncurses]
|
||||||
features = ["wide"]
|
features = ["wide"]
|
||||||
optional = false
|
optional = false
|
||||||
version = "5.86.0"
|
version = "5.86.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String, FileAccount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Account {
|
||||||
|
pub folders: Vec<String>,
|
||||||
|
format: MailFormat,
|
||||||
|
pub sent_folder: String,
|
||||||
|
threaded : bool,
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub accounts: HashMap<String, Account>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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<String, Account> = HashMap::new();
|
||||||
|
|
||||||
|
for (id, x) in fs.accounts {
|
||||||
|
let mut folders = Vec::new();
|
||||||
|
fn recurse_folders<P: AsRef<Path>>(folders: &mut Vec<String>, 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 }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::result;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
pub type Result<T> = result::Result<T, MeliError>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MeliError {
|
||||||
|
details: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeliError {
|
||||||
|
pub fn new<M>(msg: M) -> MeliError where M: Into<String> {
|
||||||
|
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<io::Error> for MeliError {
|
||||||
|
#[inline]
|
||||||
|
fn from(kind: io::Error) -> MeliError {
|
||||||
|
MeliError::new(kind.description())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<MessageID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A very primitive mail object */
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
pub struct Mail {
|
||||||
|
date: String,
|
||||||
|
from: Option<String>,
|
||||||
|
to: Option<String>,
|
||||||
|
body: String,
|
||||||
|
subject: Option<String>,
|
||||||
|
message_id: Option<MessageID>,
|
||||||
|
in_reply_to: Option<MessageID>,
|
||||||
|
references: Option<References>,
|
||||||
|
|
||||||
|
datetime: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||||
|
|
||||||
|
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<chrono::FixedOffset> {
|
||||||
|
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<chrono::DateTime<chrono::FixedOffset>>) -> () {
|
||||||
|
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<Self> {
|
||||||
|
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<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Mail {
|
||||||
|
fn eq(&self, other: &Mail) -> bool {
|
||||||
|
self.get_message_id_raw() == other.get_message_id_raw()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
//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<Vec<Mail>>;
|
||||||
|
}
|
||||||
|
pub struct MaildirType {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailBackend for MaildirType {
|
||||||
|
fn get(&self) -> Result<Vec<Mail>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
* meli - mailbox module.
|
* meli - mailbox module.
|
||||||
*
|
*
|
||||||
* Copyright 2017 Manos Pitsidianakis
|
* Copyright 2017 Manos Pitsidianakis
|
||||||
*
|
*
|
||||||
* This file is part of meli.
|
* This file is part of meli.
|
||||||
*
|
*
|
||||||
* meli is free software: you can redistribute it and/or modify
|
* meli is free software: you can redistribute it and/or modify
|
||||||
|
@ -19,27 +19,27 @@
|
||||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
extern crate maildir;
|
//use std::cmp::Ordering;
|
||||||
extern crate mailparse;
|
//use std::fmt;
|
||||||
use self::mailparse::*;
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::option::Option;
|
use std::option::Option;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std;
|
use std;
|
||||||
|
mod maildir;
|
||||||
|
pub mod email;
|
||||||
|
mod parser;
|
||||||
|
pub use self::email::*;
|
||||||
|
use mailbox::maildir::MailBackend;
|
||||||
|
use error::Result;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type UnixTimestamp = i64;
|
type UnixTimestamp = i64;
|
||||||
|
|
||||||
pub struct Mail {
|
|
||||||
entry: maildir::MailEntry,
|
|
||||||
subject: std::string::String,
|
|
||||||
pub message_id: std::string::String,
|
|
||||||
pub references: Vec<std::string::String>,
|
|
||||||
date: UnixTimestamp,
|
|
||||||
thread: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*a Mailbox represents a folder of mail. Currently only Maildir is supported.*/
|
/*a Mailbox represents a folder of mail. Currently only Maildir is supported.*/
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Mailbox{
|
pub struct Mailbox{
|
||||||
|
pub path: String,
|
||||||
pub collection: Box<Vec<Mail>>,
|
pub collection: Box<Vec<Mail>>,
|
||||||
pub threaded_collection: Vec<usize>,
|
pub threaded_collection: Vec<usize>,
|
||||||
threads: Vec<Thread>,
|
threads: Vec<Thread>,
|
||||||
|
@ -55,15 +55,16 @@ pub struct Mailbox{
|
||||||
* nonexistent in our Mailbox but we know it exists (for example we have a copy
|
* 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.
|
* of a reply to a mail but we don't have its copy.
|
||||||
*/
|
*/
|
||||||
#[derive(Clone,Copy,Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Thread {
|
pub struct Thread {
|
||||||
id: usize,
|
id: usize,
|
||||||
message: Option<usize>,
|
message: Option<usize>,
|
||||||
parent: Option<usize>,
|
parent: Option<usize>,
|
||||||
first_child: Option<usize>,
|
first_child: Option<usize>,
|
||||||
next_sibling: Option<usize>,
|
next_sibling: Option<usize>,
|
||||||
date: UnixTimestamp,
|
date: UnixTimestamp,
|
||||||
indentation: usize,
|
indentation: usize,
|
||||||
|
show_subject: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Thread {
|
impl Thread {
|
||||||
|
@ -73,6 +74,9 @@ impl Thread {
|
||||||
pub fn get_parent(&self) -> Option<usize> {
|
pub fn get_parent(&self) -> Option<usize> {
|
||||||
self.parent
|
self.parent
|
||||||
}
|
}
|
||||||
|
pub fn has_parent(&self) -> bool {
|
||||||
|
self.parent.is_some()
|
||||||
|
}
|
||||||
pub fn get_first_child(&self) -> Option<usize> {
|
pub fn get_first_child(&self) -> Option<usize> {
|
||||||
self.first_child
|
self.first_child
|
||||||
}
|
}
|
||||||
|
@ -88,237 +92,301 @@ impl Thread {
|
||||||
pub fn has_message(&self) -> bool {
|
pub fn has_message(&self) -> bool {
|
||||||
self.message.is_some()
|
self.message.is_some()
|
||||||
}
|
}
|
||||||
pub fn set_indentation(&mut self, i: usize) {
|
fn set_indentation(&mut self, i: usize) {
|
||||||
self.indentation = i;
|
self.indentation = i;
|
||||||
}
|
}
|
||||||
pub fn get_indentation(&self) -> usize {
|
pub fn get_indentation(&self) -> usize {
|
||||||
self.indentation
|
self.indentation
|
||||||
}
|
}
|
||||||
pub fn is_descendant(&self, threads: &Vec<Thread>, other: Thread) -> bool {
|
fn is_descendant(&self, threads: &Vec<Thread>, other: &Thread) -> bool {
|
||||||
|
if self == other {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
match self.first_child {
|
match self.first_child {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
if threads[v] == other {
|
if threads[v].is_descendant(threads, other) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if threads[v].clone().is_descendant(&threads, other) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {}
|
None => {}
|
||||||
}
|
};
|
||||||
match self.next_sibling {
|
match self.next_sibling {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
if threads[v] == other {
|
if threads[v].is_descendant(threads, other) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if threads[v].clone().is_descendant(threads, other) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {}
|
None => {}
|
||||||
}
|
};
|
||||||
|
|
||||||
return false;
|
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 {
|
impl PartialEq for Thread {
|
||||||
fn eq(&self, other: &Thread) -> bool {
|
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 {
|
fn build_collection(threads: &mut Vec<Thread>, id_table: &mut HashMap<std::string::String, usize>, collection: &mut Box<Vec<Mail>>) -> () {
|
||||||
pub fn new(path: &str) -> Mailbox {
|
for (i, x) in collection.iter_mut().enumerate() {
|
||||||
let maildir = maildir::Maildir::from(path);
|
let x_index; /* x's index in threads */
|
||||||
let iter = maildir.list_cur();
|
let m_id = x.get_message_id_raw().to_string();
|
||||||
let mut collection: Box<Vec<Mail>> = Box::new(Vec::new());
|
if id_table.contains_key(&m_id) {
|
||||||
let mut threads: Vec<Thread> = Vec::new();
|
let t = *(id_table.get(&m_id).unwrap());
|
||||||
let mut id_table: HashMap<std::string::String, usize> = HashMap::new();
|
/* the already existing Thread should be empty, since we're
|
||||||
let mut idx = 0;
|
* seeing this message for the first time */
|
||||||
for x in iter {
|
if threads[t].message.is_some() {
|
||||||
let mut e = x.unwrap();
|
/* skip duplicate message-id, but this should be handled instead */
|
||||||
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<std::string::String> = 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 {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let r_to = x.references[x.references.len() - 1].clone();
|
x_index = t;
|
||||||
let parent_id =
|
/* Store this message in the Thread's message slot. */
|
||||||
if id_table.contains_key(&r_to) {
|
threads[t].date = x.get_date();
|
||||||
|
x.set_thread(t);
|
||||||
let p = id_table.get(&r_to).unwrap();
|
threads[t].message = Some(i);
|
||||||
if threads[*p].is_descendant(&threads, threads[x_index]) ||
|
} else {
|
||||||
threads[x_index].is_descendant(&threads, threads[*p]) {
|
/* Create a new Thread object holding this message */
|
||||||
continue;
|
x_index = threads.len();
|
||||||
}
|
threads.push(
|
||||||
if threads[*p].first_child.is_none() {
|
Thread {
|
||||||
threads[*p].first_child = Some(x_index);
|
message: Some(i),
|
||||||
} else {
|
id: x_index,
|
||||||
let mut fc = threads[*p].first_child.unwrap();
|
parent: None,
|
||||||
while threads[fc].next_sibling.is_some() {
|
first_child: None,
|
||||||
fc = threads[fc].next_sibling.unwrap();
|
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 {
|
} else {
|
||||||
|
let idx = threads.len();
|
||||||
threads.push(
|
threads.push(
|
||||||
Thread {
|
Thread {
|
||||||
message: None,
|
message: None,
|
||||||
id: idx,
|
id: idx,
|
||||||
parent: None,
|
parent: None,
|
||||||
first_child: Some(x_index),
|
first_child: Some(curr_ref),
|
||||||
next_sibling: None,
|
next_sibling: None,
|
||||||
date: x.date,
|
date: x.get_date(),
|
||||||
indentation: 0,
|
indentation: 0,
|
||||||
|
show_subject: true,
|
||||||
});
|
});
|
||||||
id_table.insert(r_to.clone(), idx);
|
id_table.insert(r.get_raw().to_string(), idx);
|
||||||
idx += 1;
|
idx
|
||||||
idx-1
|
|
||||||
};
|
};
|
||||||
/* update thread date */
|
/* update thread date */
|
||||||
let mut parent_iter = parent_id;
|
let mut parent_iter = parent_id;
|
||||||
loop {
|
'date: loop {
|
||||||
let mut p = &mut threads[parent_iter];
|
let mut p = &mut threads[parent_iter];
|
||||||
p.date = x.date;
|
if p.date < x.get_date() {
|
||||||
if p.parent.is_none() {
|
p.date = x.get_date();
|
||||||
break;
|
}
|
||||||
} else {
|
match p.parent {
|
||||||
parent_iter = p.get_parent().unwrap();
|
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<Mailbox> {
|
||||||
|
let mut collection: Box<Vec<Mail>> = 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<Thread> = Vec::with_capacity((collection.len() as f64 * 1.2) as usize);
|
||||||
|
/* A hash table of Message IDs */
|
||||||
|
let mut id_table: HashMap<std::string::String, usize> = 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
|
/* Walk over the elements of id_table, and gather a list of the Thread objects that have
|
||||||
* no parents. */
|
* no parents. These are the root messages of each thread */
|
||||||
let mut root_set = Vec::new();
|
let mut root_set = Vec::with_capacity(collection.len());
|
||||||
for (_,v) in id_table.iter() {
|
'root_set: for (_,v) in id_table.iter() {
|
||||||
if threads[*v].parent.is_none() {
|
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.push(*v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root_set.sort_by(|a, b| threads[*b].date.cmp(&threads[*a].date));
|
root_set.sort_by(|a, b| threads[*b].date.cmp(&threads[*a].date));
|
||||||
|
|
||||||
let mut threaded_collection: Vec<usize> = Vec::new();
|
/* Group messages together by thread in a collection so we can print them together */
|
||||||
fn build_threaded(threads: &mut Vec<Thread>, indentation: usize, threaded: &mut Vec<usize>, index: usize) {
|
let mut threaded_collection: Vec<usize> = Vec::with_capacity(collection.len());
|
||||||
|
fn build_threaded(threads: &mut Vec<Thread>, indentation: usize, threaded: &mut Vec<usize>, i: usize, root_subject_idx: usize, collection: &Vec<Mail>) {
|
||||||
let thread = threads[index];
|
let thread = threads[i];
|
||||||
|
if threads[root_subject_idx].has_message() {
|
||||||
if thread.has_message() {
|
let root_subject = collection[threads[root_subject_idx].get_message().unwrap()].get_subject();
|
||||||
threads[index].set_indentation(indentation);
|
/* If the Container has no Message, but does have children, remove this container but
|
||||||
if !threaded.contains(&index) {
|
* promote its children to this level (that is, splice them in to the current child
|
||||||
threaded.push(index);
|
* 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() {
|
if thread.has_children() {
|
||||||
let mut fc = thread.get_first_child().unwrap();
|
let mut fc = thread.get_first_child().unwrap();
|
||||||
loop {
|
loop {
|
||||||
build_threaded(threads, indentation + 1, threaded, fc);
|
build_threaded(threads, indentation, threaded, fc, i, collection);
|
||||||
let thread_ = threads[fc];
|
let thread_ = threads[fc];
|
||||||
if !thread_.has_sibling() {
|
if !thread_.has_sibling() {
|
||||||
break;
|
break;
|
||||||
|
@ -328,16 +396,18 @@ impl Mailbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i in &root_set {
|
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();
|
let length = collection.len();
|
||||||
Mailbox {
|
|
||||||
|
Ok(Mailbox {
|
||||||
|
path: path.to_string(),
|
||||||
collection: collection,
|
collection: collection,
|
||||||
threads: threads,
|
threads: threads,
|
||||||
length: length,
|
length: length,
|
||||||
threaded_collection: threaded_collection,
|
threaded_collection: threaded_collection,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
pub fn get_length(&self) -> usize {
|
pub fn get_length(&self) -> usize {
|
||||||
self.length
|
self.length
|
||||||
|
@ -349,43 +419,11 @@ impl Mailbox {
|
||||||
pub fn get_mail_and_thread(&mut self, i: usize) -> (&mut Mail, Thread) {
|
pub fn get_mail_and_thread(&mut self, i: usize) -> (&mut Mail, Thread) {
|
||||||
|
|
||||||
let ref mut x = self.collection.as_mut_slice()[i];
|
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)
|
(x, thread)
|
||||||
}
|
}
|
||||||
pub fn get_thread(&self, i: usize) -> Thread {
|
pub fn get_thread(&self, i: usize) -> &Thread {
|
||||||
self.threads[i].clone()
|
&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<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Mail {
|
|
||||||
fn eq(&self, other: &Mail) -> bool {
|
|
||||||
self.date == other.date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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<u8>, 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<Vec<&str>>,
|
||||||
|
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<std::vec::Vec<(&str, std::vec::Vec<&str>)>>,
|
||||||
|
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<String>, 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<String>, escaped_transform!(call!(alpha), '=', quoted_printable_byte));
|
||||||
|
|
||||||
|
named!(utf8_token_quoted_p<String>, do_parse!(
|
||||||
|
raw: call!(utf8_token_quoted_p_raw) >>
|
||||||
|
( {
|
||||||
|
named!(get_bytes<Vec<u8>>, 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<String>, alt_complete!(
|
||||||
|
utf8_token_base64 |
|
||||||
|
call!(utf8_token_quoted_p)));
|
||||||
|
|
||||||
|
named!(utf8_token_list<String>, 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<String>, 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<String>, 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<chrono::FixedOffset>> {
|
||||||
|
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<Vec<&str>>, many0!(preceded!(is_not!("<"), message_id)));
|
||||||
|
|
110
src/main.rs
110
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
extern crate ncurses;
|
extern crate ncurses;
|
||||||
extern crate maildir;
|
|
||||||
extern crate mailparse;
|
|
||||||
|
|
||||||
|
pub mod mailbox;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
mod conf;
|
||||||
|
mod error;
|
||||||
|
|
||||||
use ui::index::*;
|
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() {
|
fn main() {
|
||||||
let locale_conf = ncurses::LcCategory::all;
|
let locale_conf = ncurses::LcCategory::all;
|
||||||
ncurses::setlocale(locale_conf, "en_US.UTF-8");
|
ncurses::setlocale(locale_conf, "en_US.UTF-8");
|
||||||
ui::initialize();
|
let set = Settings::new();
|
||||||
let mailbox = Mailbox::new("PATH");
|
let ui = ui::TUI::initialize();
|
||||||
let mut index = Index::new(mailbox);
|
//let mailbox = Mailbox::new("/home/epilys/Downloads/rust/nutt/Inbox4");
|
||||||
ncurses::refresh();
|
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<Window> = 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;
|
let mut ch;
|
||||||
loop {
|
'inner : loop {
|
||||||
ch = ncurses::get_wch();
|
ch = ncurses::get_wch();
|
||||||
match ch {
|
match ch {
|
||||||
Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP)) |
|
Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP)) |
|
||||||
Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN)) => {
|
Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN)) => {
|
||||||
index.scroll(k);
|
index.handle_input(k);
|
||||||
continue;
|
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);
|
||||||
}
|
}
|
||||||
|
|
350
src/ui/index.rs
350
src/ui/index.rs
|
@ -19,12 +19,51 @@
|
||||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
extern crate ncurses;
|
extern crate ncurses;
|
||||||
extern crate maildir;
|
|
||||||
extern crate mailparse;
|
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
|
use mailbox::email::Mail;
|
||||||
use mailbox::*;
|
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 */
|
/* Index represents a UI list of mails */
|
||||||
pub struct Index {
|
pub struct Index {
|
||||||
|
@ -35,68 +74,90 @@ pub struct Index {
|
||||||
screen_width: i32,
|
screen_width: i32,
|
||||||
screen_height: i32,
|
screen_height: i32,
|
||||||
|
|
||||||
|
/* threading */
|
||||||
threaded: bool,
|
threaded: bool,
|
||||||
|
|
||||||
|
|
||||||
cursor_idx: usize,
|
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 x = 0;
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
ncurses::getbegyx(self.win, &mut y, &mut x);
|
ncurses::getbegyx(self.win, &mut y, &mut x);
|
||||||
|
|
||||||
ncurses::wclear(self.pad);
|
ncurses::wclear(self.pad);
|
||||||
|
|
||||||
let mut idx = 0;
|
|
||||||
if self.threaded {
|
if self.threaded {
|
||||||
|
let mut indentations : Vec<bool> = Vec::with_capacity(6);
|
||||||
/* Draw threaded view. */
|
/* 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);
|
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()];
|
let x = &self.mailbox.collection[container.get_message().unwrap()];
|
||||||
if idx == self.cursor_idx {
|
Index::draw_entry(self.pad, x, idx, container.get_indentation(), container.has_sibling(), idx == self.cursor_idx, container.get_show_subject(), Some(&indentations));
|
||||||
Index::draw_entry(self.pad, x, idx, container.get_indentation(), false, true);
|
if container.has_children() {
|
||||||
|
indentations.push(false);
|
||||||
} else {
|
} else {
|
||||||
Index::draw_entry(self.pad, x, idx, container.get_indentation(), false, false);
|
indentations.pop();
|
||||||
}
|
}
|
||||||
idx += 1;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
for x in self.mailbox.collection.as_mut_slice() {
|
for (idx, x) in self.mailbox.collection.as_mut_slice().iter().enumerate() {
|
||||||
if idx == self.cursor_idx {
|
Index::draw_entry(self.pad, x, idx, 0, false, false, idx == self.cursor_idx, true, None);
|
||||||
Index::draw_entry(self.pad, x, idx, 0, false, true);
|
|
||||||
} else {
|
|
||||||
Index::draw_entry(self.pad, x, idx, 0, false, false);
|
|
||||||
}
|
|
||||||
idx += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width);
|
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);
|
ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width);
|
||||||
if self.screen_height == 0 {
|
if self.screen_height == 0 {
|
||||||
return;
|
return;
|
||||||
|
@ -129,14 +193,18 @@ impl Index {
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
ncurses::KEY_DOWN => {
|
ncurses::KEY_DOWN => {
|
||||||
if self.cursor_idx < self.mailbox.get_length() - 1 {
|
if self.cursor_idx < self.mailbox.get_length() - 1 {
|
||||||
self.cursor_idx += 1;
|
self.cursor_idx += 1;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
10 => {
|
||||||
|
self.show_pager();
|
||||||
|
self.draw();
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -146,26 +214,41 @@ impl Index {
|
||||||
ncurses::wmove(self.pad, self.cursor_idx as i32, 0);
|
ncurses::wmove(self.pad, self.cursor_idx as i32, 0);
|
||||||
/* Borrow x from self.mailbox in separate scopes or else borrow checker complains */
|
/* Borrow x from self.mailbox in separate scopes or else borrow checker complains */
|
||||||
{
|
{
|
||||||
let i: usize =
|
let pair = super::COLOR_PAIR_CURSOR;
|
||||||
if self.threaded {
|
ncurses::wchgat(self.pad, -1, 0, pair);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
/* Draw previous highlighted entry normally */
|
/* Draw previous highlighted entry normally */
|
||||||
ncurses::wmove(self.pad, prev_idx as i32, 0);
|
ncurses::wmove(self.pad, prev_idx as i32, 0);
|
||||||
{
|
{
|
||||||
let i: usize =
|
let pair = match self.threaded {
|
||||||
if self.threaded {
|
true if prev_idx % 2 == 0 => super::COLOR_PAIR_THREAD_EVEN,
|
||||||
self.mailbox.get_threaded_mail(prev_idx)
|
true => super::COLOR_PAIR_THREAD_ODD,
|
||||||
} else {
|
false => super::COLOR_PAIR_DEFAULT,
|
||||||
prev_idx
|
|
||||||
};
|
};
|
||||||
let (ref mut x, thread) = self.mailbox.get_mail_and_thread(i);
|
ncurses::wchgat(self.pad, 32, 0, pair);
|
||||||
Index::draw_entry(self.pad, x, prev_idx, thread.get_indentation(), false, false);
|
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 */
|
/* Calculate the pad row of the first entry to be displayed in the window */
|
||||||
|
@ -189,9 +272,9 @@ impl Index {
|
||||||
*/
|
*/
|
||||||
if pminrow != pminrow_prev &&
|
if pminrow != pminrow_prev &&
|
||||||
pminrow + self.screen_height > self.mailbox.get_length() as i32 {
|
pminrow + self.screen_height > self.mailbox.get_length() as i32 {
|
||||||
/* touch Index window (tell ncurses to redraw the entire window in
|
/* touch dead entries in index (tell ncurses to redraw the empty lines next refresh) */
|
||||||
* next refresh) */
|
let live_entries = self.mailbox.get_length() as i32 - pminrow;
|
||||||
ncurses::touchwin(self.win);
|
ncurses::wredrawln(self.win, live_entries, self.screen_height);
|
||||||
ncurses::wrefresh(self.win);
|
ncurses::wrefresh(self.win);
|
||||||
}
|
}
|
||||||
ncurses::prefresh(
|
ncurses::prefresh(
|
||||||
|
@ -204,43 +287,120 @@ impl Index {
|
||||||
self.screen_width - 1,
|
self.screen_width - 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
fn draw_entry(win: ncurses::WINDOW, mail: &Mail, i: usize, indent: usize,
|
}
|
||||||
has_sibling: bool, highlight: bool) {
|
impl Index {
|
||||||
if highlight {
|
pub fn new(mailbox: Mailbox) -> Index {
|
||||||
ncurses::wattron(win,
|
let mut screen_height = 0;
|
||||||
ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR));
|
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<bool>>) {
|
||||||
|
/* 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));
|
ncurses::waddstr(win, &format!("{}\t", i));
|
||||||
let dt = NaiveDateTime::from_timestamp(mail.get_date(), 0);
|
ncurses::waddstr(win, &mail.get_datetime().format("%Y-%m-%d %H:%M:%S").to_string());
|
||||||
ncurses::waddstr(win, &dt.format("%Y-%m-%d %H:%M:%S").to_string());
|
|
||||||
ncurses::waddch(win, '\t' as u64);
|
ncurses::waddch(win, '\t' as u64);
|
||||||
for _ in 0..indent {
|
for i in 0..indent {
|
||||||
ncurses::waddch(win, ' ' as u64);
|
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 {
|
if indent > 0 {
|
||||||
ncurses::wattron(win,
|
ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT));
|
||||||
ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT));
|
if has_sibling && has_parent {
|
||||||
if has_sibling {
|
ncurses::waddstr(win, "├");
|
||||||
ncurses::waddstr(win, "│");
|
} else if has_sibling {
|
||||||
|
ncurses::waddstr(win, "┬");
|
||||||
} else {
|
} else {
|
||||||
ncurses::waddstr(win, "└");
|
ncurses::waddstr(win, "└");
|
||||||
}
|
}
|
||||||
ncurses::waddstr(win, "->");
|
ncurses::waddstr(win, "─>");
|
||||||
ncurses::wattroff(win,
|
ncurses::wattroff(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT));
|
||||||
ncurses::COLOR_PAIR(super::COLOR_PAIR_THREAD_INDENT));
|
|
||||||
}
|
}
|
||||||
if highlight {
|
ncurses::wattron(win, attr);
|
||||||
ncurses::wattron(win,
|
if show_subject {
|
||||||
ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR));
|
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()));
|
let mut screen_height = 0;
|
||||||
if highlight {
|
let mut screen_width = 0;
|
||||||
ncurses::wattroff(win,
|
/* Get the screen bounds. */
|
||||||
ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR));
|
let mut x = 0;
|
||||||
}
|
let mut y = 0;
|
||||||
ncurses::waddstr(win, "\n");
|
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,
|
ncurses::getmaxyx(self.win,
|
||||||
&mut self.screen_height, &mut self.screen_width);
|
&mut self.screen_height, &mut self.screen_width);
|
||||||
let x: &mut Mail;
|
let x: &mut Mail;
|
||||||
|
@ -251,7 +411,7 @@ impl Index {
|
||||||
} else {
|
} else {
|
||||||
x = &mut self.mailbox.collection[self.cursor_idx];
|
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_DOWN);
|
||||||
pager.scroll(ncurses::KEY_UP);
|
pager.scroll(ncurses::KEY_UP);
|
||||||
let mut ch = ncurses::getch();
|
let mut ch = ncurses::getch();
|
||||||
|
@ -267,14 +427,12 @@ impl Index {
|
||||||
}
|
}
|
||||||
ch = ncurses::getch();
|
ch = ncurses::getch();
|
||||||
}
|
}
|
||||||
drop(pager); // drop pager before next refresh
|
|
||||||
ncurses::wrefresh(self.win);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Drop for Index {
|
impl Drop for Index {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
ncurses::wclear(self.win);
|
||||||
ncurses::delwin(self.win);
|
ncurses::delwin(self.win);
|
||||||
ncurses::delwin(self.pad);
|
ncurses::delwin(self.pad);
|
||||||
ncurses::endwin();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,13 @@ pub static COLOR_PAIR_DEFAULT: i16 = 1;
|
||||||
pub static COLOR_PAIR_CURSOR: i16 = 2;
|
pub static COLOR_PAIR_CURSOR: i16 = 2;
|
||||||
pub static COLOR_PAIR_HEADERS: i16 = 3;
|
pub static COLOR_PAIR_HEADERS: i16 = 3;
|
||||||
pub static COLOR_PAIR_THREAD_INDENT: i16 = 4;
|
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 */
|
/* start ncurses */
|
||||||
ncurses::initscr();
|
ncurses::initscr();
|
||||||
ncurses::keypad(ncurses::stdscr(), true);
|
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_CURSOR, 251, 235);
|
||||||
ncurses::init_pair(COLOR_PAIR_HEADERS, 33, 0);
|
ncurses::init_pair(COLOR_PAIR_HEADERS, 33, 0);
|
||||||
ncurses::init_pair(COLOR_PAIR_THREAD_INDENT, 5, 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. */
|
/* Set the window's background color. */
|
||||||
ncurses::bkgd(
|
ncurses::bkgd(
|
||||||
' ' as ncurses::chtype | ncurses::COLOR_PAIR(COLOR_PAIR_DEFAULT) as ncurses::chtype,
|
' ' as ncurses::chtype | ncurses::COLOR_PAIR(COLOR_PAIR_DEFAULT) as ncurses::chtype,
|
||||||
);
|
);
|
||||||
|
TUI {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for TUI {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
ncurses::endwin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
117
src/ui/pager.rs
117
src/ui/pager.rs
|
@ -19,9 +19,8 @@
|
||||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
extern crate ncurses;
|
extern crate ncurses;
|
||||||
extern crate maildir;
|
|
||||||
extern crate mailparse;
|
use super::super::mailbox;
|
||||||
use self::mailparse::*;
|
|
||||||
|
|
||||||
/* Pager represents the part of the UI that shows the mail headers and body for
|
/* Pager represents the part of the UI that shows the mail headers and body for
|
||||||
* viewing */
|
* viewing */
|
||||||
|
@ -36,7 +35,7 @@ pub struct Pager {
|
||||||
|
|
||||||
impl Pager {
|
impl Pager {
|
||||||
pub fn new(parent: ncurses::WINDOW,
|
pub fn new(parent: ncurses::WINDOW,
|
||||||
entry: &mut maildir::MailEntry) -> Pager {
|
entry: &mut mailbox::Mail) -> Pager {
|
||||||
let mut screen_height = 0;
|
let mut screen_height = 0;
|
||||||
let mut screen_width = 0;
|
let mut screen_width = 0;
|
||||||
ncurses::getmaxyx(parent, &mut screen_height, &mut screen_width);
|
ncurses::getmaxyx(parent, &mut screen_height, &mut screen_width);
|
||||||
|
@ -136,50 +135,56 @@ impl Pager {
|
||||||
w - 1,
|
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;
|
let mut i = 0;
|
||||||
ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_HEADERS));
|
ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_HEADERS));
|
||||||
ncurses::waddstr(win, "Date: ");
|
ncurses::waddstr(win, "Date: ");
|
||||||
ncurses::waddstr(
|
ncurses::waddstr(
|
||||||
win,
|
win,
|
||||||
&mail.headers()
|
mail.get_date_as_str()
|
||||||
.unwrap()
|
|
||||||
.get_first_value("Date")
|
|
||||||
.unwrap()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
ncurses::waddstr(win, "\n");
|
ncurses::waddstr(win, "\n");
|
||||||
i += 1;
|
i += 1;
|
||||||
ncurses::waddstr(win, "From: ");
|
ncurses::waddstr(win, "From: ");
|
||||||
ncurses::waddstr(
|
ncurses::waddstr(
|
||||||
win,
|
win,
|
||||||
&mail.headers()
|
&mail.get_from(),
|
||||||
.unwrap()
|
|
||||||
.get_first_value("From")
|
|
||||||
.unwrap()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
ncurses::waddstr(win, "\n");
|
ncurses::waddstr(win, "\n");
|
||||||
i += 1;
|
i += 1;
|
||||||
ncurses::waddstr(win, "To: ");
|
ncurses::waddstr(win, "To: ");
|
||||||
ncurses::waddstr(
|
ncurses::waddstr(
|
||||||
win,
|
win,
|
||||||
&mail.headers()
|
&mail.get_to(),
|
||||||
.unwrap()
|
|
||||||
.get_first_value("To")
|
|
||||||
.unwrap()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
ncurses::waddstr(win, "\n");
|
ncurses::waddstr(win, "\n");
|
||||||
i += 1;
|
i += 1;
|
||||||
ncurses::waddstr(win, "Subject: ");
|
ncurses::waddstr(win, "Subject: ");
|
||||||
ncurses::waddstr(
|
ncurses::waddstr(
|
||||||
win,
|
win,
|
||||||
&mail.headers()
|
&mail.get_subject(),
|
||||||
.unwrap()
|
);
|
||||||
.get_first_value("Subject")
|
ncurses::waddstr(win, "\n");
|
||||||
.unwrap()
|
i += 1;
|
||||||
.unwrap(),
|
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");
|
ncurses::waddstr(win, "\n");
|
||||||
i += 1;
|
i += 1;
|
||||||
|
@ -189,7 +194,7 @@ impl Pager {
|
||||||
}
|
}
|
||||||
fn print_entry_content(
|
fn print_entry_content(
|
||||||
win: ncurses::WINDOW,
|
win: ncurses::WINDOW,
|
||||||
mail: &mut maildir::MailEntry,
|
mail: &mut mailbox::Mail,
|
||||||
height: i32) -> (ncurses::WINDOW, i32, i32) {
|
height: i32) -> (ncurses::WINDOW, i32, i32) {
|
||||||
let mut h = 0;
|
let mut h = 0;
|
||||||
let mut w = 0;
|
let mut w = 0;
|
||||||
|
@ -199,44 +204,31 @@ impl Pager {
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
/* y,x coordinates of upper left corner of win */
|
/* y,x coordinates of upper left corner of win */
|
||||||
ncurses::getparyx(win, &mut y, &mut x);
|
ncurses::getparyx(win, &mut y, &mut x);
|
||||||
match &mail.parsed() {
|
let body = mail.get_body();
|
||||||
&Ok(ref v) => {
|
let lines: Vec<&str> = body.trim().split('\n').collect();
|
||||||
match &v.get_body() {
|
let lines_length = lines.len();
|
||||||
&Ok(ref b) => {
|
let pad = ncurses::newpad(lines_length as i32, 1024);
|
||||||
let lines: Vec<&str> = b.split('\n').collect();
|
ncurses::wclear(pad);
|
||||||
let lines_length = lines.len();
|
for l in lines {
|
||||||
let pad = ncurses::newpad(lines_length as i32, 1024);
|
ncurses::waddstr(pad, &l.replace("%", "%%"));
|
||||||
ncurses::wclear(pad);
|
ncurses::waddstr(pad, "\n");
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* ┌ ┏━━━━━━━━━┓ ┐
|
||||||
|
* │ ┃ ┃ │
|
||||||
|
* 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(
|
fn print_entry(
|
||||||
win: ncurses::WINDOW,
|
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);
|
let header_height = Pager::print_entry_headers(win, mail);
|
||||||
Pager::print_entry_content(win, mail, header_height + 2)
|
Pager::print_entry_content(win, mail, header_height + 2)
|
||||||
}
|
}
|
||||||
|
@ -244,8 +236,9 @@ impl Pager {
|
||||||
|
|
||||||
impl Drop for Pager {
|
impl Drop for Pager {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
ncurses::wclear(self.win);
|
|
||||||
ncurses::delwin(self.win);
|
|
||||||
ncurses::delwin(self.pad);
|
ncurses::delwin(self.pad);
|
||||||
|
ncurses::wclear(self.win);
|
||||||
|
ncurses::wrefresh(self.win);
|
||||||
|
ncurses::delwin(self.win);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue