mirror of https://git.meli.delivery/meli/meli
threads
parent
8e07843c4a
commit
9946fbcbe0
@ -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(())
|
||||
}
|
||||
}
|
@ -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)));
|
||||
|
@ -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 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<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;
|
||||
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;
|
||||
}
|
||||
Some(ncurses::WchResult::Char(10)) => {
|
||||
index.show_pager();
|
||||
index.draw();
|
||||
continue;
|
||||
}
|
||||
Some(ncurses::WchResult::KeyCode(ncurses::KEY_F1)) => {
|
||||
break;
|
||||
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;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
drop(ui);
|
||||
}
|
||||
|
Loading…
Reference in New Issue