refactor + clean table system [WIP]

This commit is contained in:
Clément DOUIN 2021-01-05 23:54:39 +01:00
parent 401b51a760
commit 18042b02b8
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
4 changed files with 428 additions and 291 deletions

View file

@ -14,21 +14,9 @@ pub struct ServerInfo {
} }
impl ServerInfo { impl ServerInfo {
pub fn get_host(&self) -> &str {
&self.host
}
pub fn get_addr(&self) -> (&str, u16) { pub fn get_addr(&self) -> (&str, u16) {
(&self.host, self.port) (&self.host, self.port)
} }
pub fn get_login(&self) -> &str {
&self.login
}
pub fn get_password(&self) -> &str {
&self.password
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -40,6 +28,13 @@ pub struct Config {
} }
impl Config { impl Config {
pub fn new_from_file() -> Self {
match read_file_content() {
Err(err) => panic!(err),
Ok(content) => toml::from_str(&content).unwrap(),
}
}
pub fn email_full(&self) -> String { pub fn email_full(&self) -> String {
format!("{} <{}>", self.name, self.email) format!("{} <{}>", self.name, self.email)
} }
@ -91,10 +86,3 @@ pub fn read_file_content() -> Result<String, io::Error> {
file.read_to_string(&mut content)?; file.read_to_string(&mut content)?;
Ok(content) Ok(content)
} }
pub fn read_file() -> Config {
match read_file_content() {
Err(err) => panic!(err),
Ok(content) => toml::from_str(&content).unwrap(),
}
}

View file

@ -1,207 +1,305 @@
use imap; use imap;
use mailparse::{self, MailHeaderMap}; use mailparse::{self, MailHeaderMap};
use native_tls::{TlsConnector, TlsStream}; use native_tls::{self, TlsConnector, TlsStream};
use rfc2047_decoder; use rfc2047_decoder;
use std::net::TcpStream; use std::{error, fmt, net::TcpStream, result};
use crate::config::{Config, ServerInfo}; use crate::config;
use crate::table; use crate::table;
type ImapClient = imap::Client<TlsStream<TcpStream>>; // Email
type ImapSession = imap::Session<TlsStream<TcpStream>>;
pub fn create_tls_connector() -> TlsConnector { pub struct Uid(u32);
match native_tls::TlsConnector::new() {
Ok(connector) => connector, impl table::DisplayCell for Uid {
Err(err) => { fn styles(&self) -> &[table::Style] {
println!("The TLS connector could not be created."); &[table::RED]
panic!(err); }
}
fn value(&self) -> String {
self.0.to_string()
} }
} }
pub fn create_imap_client(server: &ServerInfo, tls: &TlsConnector) -> ImapClient { pub struct Flags<'a>(Vec<imap::types::Flag<'a>>);
match imap::connect(server.get_addr(), server.get_host(), &tls) {
Ok(client) => client, impl table::DisplayCell for Flags<'_> {
Err(err) => { fn styles(&self) -> &[table::Style] {
println!("The IMAP socket could not be opened."); &[table::WHITE]
panic!(err); }
}
fn value(&self) -> String {
use imap::types::Flag::*;
let Flags(flags) = self;
let mut flags_str = String::new();
flags_str.push_str(if !flags.contains(&Seen) { &"N" } else { &" " });
flags_str.push_str(if flags.contains(&Answered) {
&"R"
} else {
&" "
});
flags_str.push_str(if flags.contains(&Draft) { &"D" } else { &" " });
flags_str.push_str(if flags.contains(&Flagged) { &"F" } else { &" " });
flags_str
} }
} }
pub fn create_imap_sess(client: ImapClient, server: &ServerInfo) -> ImapSession { pub struct Sender(String);
match client.login(server.get_login(), server.get_password()) {
Ok(sess) => sess, impl table::DisplayCell for Sender {
Err((err, _)) => { fn styles(&self) -> &[table::Style] {
println!("The IMAP connection could not be established."); &[table::BLUE]
panic!(err); }
}
fn value(&self) -> String {
self.0.to_owned()
} }
} }
pub fn login(config: &Config) -> ImapSession { pub struct Subject(String);
let tls = create_tls_connector();
let client = create_imap_client(&config.imap, &tls);
let imap_sess = create_imap_sess(client, &config.imap);
imap_sess
}
fn subject_from_fetch(fetch: &imap::types::Fetch) -> String { impl table::DisplayCell for Subject {
let envelope = fetch.envelope().expect("envelope is missing"); fn styles(&self) -> &[table::Style] {
&[table::GREEN]
}
match &envelope.subject { fn value(&self) -> String {
None => String::new(), self.0.to_owned()
Some(bytes) => match rfc2047_decoder::decode(bytes) {
Err(_) => String::new(),
Ok(subject) => subject,
},
} }
} }
fn first_addr_from_fetch(fetch: &imap::types::Fetch) -> String { pub struct Date(String);
let envelope = fetch.envelope().expect("envelope is missing");
match &envelope.from { impl table::DisplayCell for Date {
None => String::new(), fn styles(&self) -> &[table::Style] {
Some(addresses) => match addresses.first() { &[table::YELLOW]
None => String::new(), }
Some(address) => {
let mbox = String::from_utf8(address.mailbox.expect("invalid addr mbox").to_vec())
.expect("invalid addr mbox");
let host = String::from_utf8(address.host.expect("invalid addr host").to_vec())
.expect("invalid addr host");
let email = format!("{}@{}", mbox, host);
match address.name { fn value(&self) -> String {
None => email, self.0.to_owned()
Some(name) => match rfc2047_decoder::decode(name) { }
Err(_) => email, }
Ok(name) => name,
}, pub struct Email<'a> {
uid: Uid,
flags: Flags<'a>,
from: Sender,
subject: Subject,
date: Date,
}
impl Email<'_> {
fn first_sender_from_fetch(fetch: &imap::types::Fetch) -> Option<String> {
let addr = fetch.envelope()?.from.as_ref()?.first()?;
addr.name
.and_then(|bytes| rfc2047_decoder::decode(bytes).ok())
.or_else(|| {
let mbox = String::from_utf8(addr.mailbox?.to_vec()).ok()?;
let host = String::from_utf8(addr.host?.to_vec()).ok()?;
Some(format!("{}@{}", mbox, host))
})
}
fn subject_from_fetch(fetch: &imap::types::Fetch) -> Option<String> {
fetch
.envelope()?
.subject
.and_then(|bytes| rfc2047_decoder::decode(bytes).ok())
.and_then(|subject| Some(subject.replace("\r", "")))
.and_then(|subject| Some(subject.replace("\n", "")))
}
fn date_from_fetch(fetch: &imap::types::Fetch) -> Option<String> {
fetch
.internal_date()
.and_then(|date| Some(date.to_rfc3339()))
}
}
impl<'a> table::DisplayRow for Email<'a> {
fn to_row(&self) -> Vec<table::Cell> {
use table::DisplayCell;
vec![
self.uid.to_cell(),
self.flags.to_cell(),
self.from.to_cell(),
self.subject.to_cell(),
self.date.to_cell(),
]
}
}
impl<'a> table::DisplayTable<'a, Email<'a>> for Vec<Email<'a>> {
fn cols() -> &'a [&'a str] {
&["uid", "flags", "from", "subject", "date"]
}
fn rows(&self) -> &Vec<Email<'a>> {
self
}
}
// IMAP Connector
#[derive(Debug)]
pub struct ImapConnector {
pub config: config::ServerInfo,
pub sess: imap::Session<TlsStream<TcpStream>>,
}
impl ImapConnector {
pub fn new(config: config::ServerInfo) -> Result<Self> {
let tls = TlsConnector::new()?;
let client = imap::connect(config.get_addr(), &config.host, &tls)?;
let sess = client
.login(&config.login, &config.password)
.map_err(|res| res.0)?;
Ok(Self { config, sess })
}
pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result<Vec<Email<'_>>> {
self.sess.select(mbox)?;
let uids = self
.sess
.uid_search(query)?
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>();
let emails = self
.sess
.uid_fetch(
uids[..20.min(uids.len())].join(","),
"(UID ENVELOPE INTERNALDATE)",
)?
.iter()
.map(|fetch| {
let flags = fetch.flags().iter().fold(vec![], |mut flags, flag| {
use imap::types::Flag::*;
match flag {
Seen => flags.push(Seen),
Answered => flags.push(Answered),
Draft => flags.push(Draft),
Flagged => flags.push(Flagged),
_ => (),
};
flags
});
Email {
uid: Uid(fetch.uid.unwrap()),
from: Sender(Email::first_sender_from_fetch(fetch).unwrap_or(String::new())),
subject: Subject(Email::subject_from_fetch(fetch).unwrap_or(String::new())),
date: Date(Email::date_from_fetch(fetch).unwrap_or(String::new())),
flags: Flags(flags),
} }
} })
}, .collect::<Vec<_>>();
Ok(emails)
} }
} }
fn date_from_fetch(fetch: &imap::types::Fetch) -> String { // Error wrapper
let envelope = fetch.envelope().expect("envelope is missing");
match &envelope.date { #[derive(Debug)]
None => String::new(), pub enum Error {
Some(date) => match String::from_utf8(date.to_vec()) { CreateTlsConnectorError(native_tls::Error),
Err(_) => String::new(), CreateImapSession(imap::Error),
Ok(date) => date, }
},
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::CreateTlsConnectorError(err) => err.fmt(f),
Error::CreateImapSession(err) => err.fmt(f),
}
} }
} }
pub fn read_emails(imap_sess: &mut ImapSession, mbox: &str, query: &str) -> imap::Result<()> { impl error::Error for Error {
imap_sess.select(mbox)?; fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self {
let uids = imap_sess Error::CreateTlsConnectorError(ref err) => Some(err),
.uid_search(query)? Error::CreateImapSession(ref err) => Some(err),
.iter() }
.map(|n| n.to_string())
.collect::<Vec<_>>();
let table_head = vec![
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("ID"),
),
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("FLAGS"),
),
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("FROM"),
),
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("SUBJECT"),
),
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("DATE"),
),
];
let mut table_rows = imap_sess
.uid_fetch(
uids[..20.min(uids.len())].join(","),
"(INTERNALDATE ENVELOPE UID)",
)?
.iter()
.map(|fetch| {
vec![
table::Cell::new(vec![table::RED], fetch.uid.unwrap_or(0).to_string()),
table::Cell::new(vec![table::WHITE], String::from("!@")),
table::Cell::new(vec![table::BLUE], first_addr_from_fetch(fetch)),
table::Cell::new(vec![table::GREEN], subject_from_fetch(fetch)),
table::Cell::new(vec![table::YELLOW], date_from_fetch(fetch)),
]
})
.collect::<Vec<_>>();
table_rows.insert(0, table_head);
println!("{}", table::render(table_rows));
Ok(())
}
pub fn list_mailboxes(imap_sess: &mut ImapSession) -> imap::Result<()> {
let mboxes = imap_sess.list(Some(""), Some("*"))?;
let table_head = vec![
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("DELIM"),
),
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("NAME"),
),
table::Cell::new(
vec![table::BOLD, table::UNDERLINE, table::WHITE],
String::from("ATTRIBUTES"),
),
];
let mut table_rows = mboxes
.iter()
.map(|mbox| {
vec![
table::Cell::new(
vec![table::BLUE],
mbox.delimiter().unwrap_or("").to_string(),
),
table::Cell::new(vec![table::GREEN], mbox.name().to_string()),
table::Cell::new(
vec![table::YELLOW],
mbox.attributes()
.iter()
.map(|a| format!("{:?}", a))
.collect::<Vec<_>>()
.join(", "),
),
]
})
.collect::<Vec<_>>();
if table_rows.len() == 0 {
println!("No email found!");
} else {
table_rows.insert(0, table_head);
println!("{}", table::render(table_rows));
} }
Ok(())
} }
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Error::CreateTlsConnectorError(err)
}
}
impl From<imap::Error> for Error {
fn from(err: imap::Error) -> Error {
Error::CreateImapSession(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// pub fn list_mailboxes(imap_sess: &mut ImapSession) -> imap::Result<()> {
// let mboxes = imap_sess.list(Some(""), Some("*"))?;
// let table_head = vec![
// table::Cell::new(
// vec![table::BOLD, table::UNDERLINE, table::WHITE],
// String::from("DELIM"),
// ),
// table::Cell::new(
// vec![table::BOLD, table::UNDERLINE, table::WHITE],
// String::from("NAME"),
// ),
// table::Cell::new(
// vec![table::BOLD, table::UNDERLINE, table::WHITE],
// String::from("ATTRIBUTES"),
// ),
// ];
// let mut table_rows = mboxes
// .iter()
// .map(|mbox| {
// vec![
// table::Cell::new(
// vec![table::BLUE],
// mbox.delimiter().unwrap_or("").to_string(),
// ),
// table::Cell::new(vec![table::GREEN], mbox.name().to_string()),
// table::Cell::new(
// vec![table::YELLOW],
// mbox.attributes()
// .iter()
// .map(|a| format!("{:?}", a))
// .collect::<Vec<_>>()
// .join(", "),
// ),
// ]
// })
// .collect::<Vec<_>>();
// if table_rows.len() == 0 {
// println!("No email found!");
// } else {
// table_rows.insert(0, table_head);
// println!("{}", table::render(table_rows));
// }
// Ok(())
// }
fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mut Vec<String>) { fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mut Vec<String>) {
match part.subparts.len() { match part.subparts.len() {
0 => { 0 => {
@ -222,28 +320,28 @@ fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mu
} }
} }
pub fn read_email( // pub fn read_email(
imap_sess: &mut ImapSession, // imap_sess: &mut ImapSession,
mbox: &str, // mbox: &str,
uid: &str, // uid: &str,
mime: &str, // mime: &str,
) -> imap::Result<()> { // ) -> imap::Result<()> {
imap_sess.select(mbox)?; // imap_sess.select(mbox)?;
match imap_sess.uid_fetch(uid, "BODY[]")?.first() { // match imap_sess.uid_fetch(uid, "BODY[]")?.first() {
None => println!("No email found in mailbox {} with UID {}", mbox, uid), // None => println!("No email found in mailbox {} with UID {}", mbox, uid),
Some(email_raw) => { // Some(email_raw) => {
let email = mailparse::parse_mail(email_raw.body().unwrap_or(&[])).unwrap(); // let email = mailparse::parse_mail(email_raw.body().unwrap_or(&[])).unwrap();
let mut parts = vec![]; // let mut parts = vec![];
extract_subparts_by_mime(mime, &email, &mut parts); // extract_subparts_by_mime(mime, &email, &mut parts);
if parts.len() == 0 { // if parts.len() == 0 {
println!("No {} content found for email {}!", mime, uid); // println!("No {} content found for email {}!", mime, uid);
} else { // } else {
println!("{}", parts.join("\r\n")); // println!("{}", parts.join("\r\n"));
} // }
} // }
} // }
Ok(()) // Ok(())
} // }

View file

@ -4,10 +4,16 @@ mod smtp;
mod table; mod table;
use clap::{App, Arg, SubCommand}; use clap::{App, Arg, SubCommand};
use std::io::prelude::*;
use std::{env, fs, process};
fn nem_email_tpl() -> String { use crate::config::Config;
use crate::imap::ImapConnector;
use crate::table::DisplayTable;
fn new_email_tpl() -> String {
["To: ", "Subject: ", ""].join("\r\n")
}
fn forward_email_tpl() -> String {
["To: ", "Subject: ", ""].join("\r\n") ["To: ", "Subject: ", ""].join("\r\n")
} }
@ -28,9 +34,12 @@ fn uid_arg() -> Arg<'static, 'static> {
} }
fn main() { fn main() {
let config = config::read_file(); if let Err(err) = dispatch() {
let mut imap_sess = imap::login(&config); panic!(err);
}
}
fn dispatch() -> Result<(), imap::Error> {
let matches = App::new("Himalaya") let matches = App::new("Himalaya")
.version("0.1.0") .version("0.1.0")
.about("📫 Minimalist CLI email client") .about("📫 Minimalist CLI email client")
@ -84,11 +93,13 @@ fn main() {
) )
.get_matches(); .get_matches();
if let Some(_) = matches.subcommand_matches("list") { // if let Some(_) = matches.subcommand_matches("list") {
imap::list_mailboxes(&mut imap_sess).unwrap(); // let config = Config::new_from_file();
} // ImapConnector::new(&config.imap).list_mailboxes().unwrap();
// }
if let Some(matches) = matches.subcommand_matches("search") { if let Some(matches) = matches.subcommand_matches("search") {
let config = Config::new_from_file();
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
if let Some(matches) = matches.values_of("query") { if let Some(matches) = matches.values_of("query") {
@ -115,41 +126,74 @@ fn main() {
.1 .1
.join(" "); .join(" ");
imap::read_emails(&mut imap_sess, &mbox, &query).unwrap(); let emails = ImapConnector::new(config.imap)?
.read_emails(&mbox, &query)?
.to_table();
println!("{}", emails);
} }
} }
if let Some(matches) = matches.subcommand_matches("read") { // if let Some(matches) = matches.subcommand_matches("read") {
let mbox = matches.value_of("mailbox").unwrap(); // let mbox = matches.value_of("mailbox").unwrap();
let mime = matches.value_of("mime-type").unwrap(); // let mime = matches.value_of("mime-type").unwrap();
// let uid = matches.value_of("uid").unwrap();
if let Some(uid) = matches.value_of("uid") { // imap::read_email(&mut imap_sess, mbox, uid, mime).unwrap();
imap::read_email(&mut imap_sess, mbox, uid, mime).unwrap(); // }
}
}
if let Some(_) = matches.subcommand_matches("write") { // if let Some(_) = matches.subcommand_matches("write") {
let mut draft_path = env::temp_dir(); // let mut draft_path = env::temp_dir();
draft_path.push("himalaya-draft.mail"); // draft_path.push("himalaya-draft.mail");
fs::File::create(&draft_path) // fs::File::create(&draft_path)
.expect("Could not create draft file") // .expect("Could not create draft file")
.write(nem_email_tpl().as_bytes()) // .write(new_email_tpl().as_bytes())
.expect("Could not write into draft file"); // .expect("Could not write into draft file");
process::Command::new(env!("EDITOR")) // process::Command::new(env!("EDITOR"))
.arg(&draft_path) // .arg(&draft_path)
.status() // .status()
.expect("Could not start $EDITOR"); // .expect("Could not start $EDITOR");
let mut draft = String::new(); // let mut draft = String::new();
fs::File::open(&draft_path) // fs::File::open(&draft_path)
.expect("Could not open draft file") // .expect("Could not open draft file")
.read_to_string(&mut draft) // .read_to_string(&mut draft)
.expect("Could not read draft file"); // .expect("Could not read draft file");
fs::remove_file(&draft_path).expect("Could not remove draft file"); // fs::remove_file(&draft_path).expect("Could not remove draft file");
smtp::send(&config, &draft.as_bytes()); // smtp::send(&config, &draft.as_bytes());
} // }
// if let Some(_) = matches.subcommand_matches("forward") {
// let mbox = matches.value_of("mailbox").unwrap();
// let uid = matches.value_of("uid").unwrap();
// let mut draft_path = env::temp_dir();
// draft_path.push("himalaya-draft.mail");
// fs::File::create(&draft_path)
// .expect("Could not create draft file")
// .write(forward_email_tpl().as_bytes())
// .expect("Could not write into draft file");
// process::Command::new(env!("EDITOR"))
// .arg(&draft_path)
// .status()
// .expect("Could not start $EDITOR");
// let mut draft = String::new();
// fs::File::open(&draft_path)
// .expect("Could not open draft file")
// .read_to_string(&mut draft)
// .expect("Could not read draft file");
// fs::remove_file(&draft_path).expect("Could not remove draft file");
// smtp::send(&config, &draft.as_bytes());
// }
Ok(())
} }

View file

@ -31,19 +31,16 @@ impl fmt::Display for Style {
#[derive(Debug)] #[derive(Debug)]
pub struct Cell { pub struct Cell {
styles: Vec<Style>, pub styles: Vec<Style>,
value: String, pub value: String,
}
impl Clone for Cell {
fn clone(&self) -> Self {
Cell::new(self.styles.clone(), self.value.clone())
}
} }
impl Cell { impl Cell {
pub fn new(styles: Vec<Style>, value: String) -> Cell { pub fn new<'a>(styles: &'a [Style], value: &'a str) -> Cell {
Cell { styles, value } Cell {
styles: styles.to_vec(),
value: value.to_string(),
}
} }
pub fn printable_value_len(&self) -> usize { pub fn printable_value_len(&self) -> usize {
@ -68,50 +65,60 @@ impl Cell {
} }
} }
type Matrix<T> = Vec<Vec<T>>; pub trait DisplayCell {
fn styles(&self) -> &[Style];
fn value(&self) -> String;
pub fn transpose<T: Clone>(m: Matrix<T>) -> Matrix<T> { fn to_cell(&self) -> Cell {
let mut tm: Matrix<T> = vec![]; Cell::new(self.styles(), &self.value())
let col_size = m.iter().next().unwrap_or(&vec![]).len(); }
}
for idx in 0..col_size { pub trait DisplayRow {
let col = m fn to_row(&self) -> Vec<Cell>;
}
pub trait DisplayTable<'a, T: DisplayRow> {
fn cols() -> &'a [&'a str];
fn rows(&self) -> &Vec<T>;
fn to_table(&self) -> String {
let mut col_sizes = vec![];
let head = Self::cols()
.iter() .iter()
.map(|row| row.get(idx).unwrap().clone()) .map(|col| {
let cell = Cell::new(&[BOLD, UNDERLINE, WHITE], &col.to_uppercase());
col_sizes.push(cell.printable_value_len());
cell
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
tm.push(col) let mut body = self
} .rows()
tm
}
fn render_cols(cells: Matrix<Cell>) -> Matrix<String> {
fn render_tcols(tcells: &Vec<Cell>) -> Vec<String> {
let col_size = tcells
.iter() .iter()
.map(|cell| cell.printable_value_len()) .map(|item| {
.max() let row = item.to_row();
.unwrap(); row.iter().enumerate().for_each(|(i, cell)| {
tcells.iter().map(|tcell| tcell.render(col_size)).collect() col_sizes[i] = col_sizes[i].max(cell.printable_value_len())
}; });
row
})
.collect::<Vec<_>>();
let tcells: Matrix<String> = transpose(cells).iter().map(render_tcols).collect(); body.insert(0, head);
transpose(tcells)
}
fn render_rows(m: Matrix<String>) -> Vec<String> { body.iter().fold(String::new(), |output, row| {
m.iter() let row_str = row
.map(|row| String::from(row.join(&sep()) + "\n")) .iter()
.collect() .enumerate()
} .map(|(i, cell)| cell.render(col_sizes[i]))
.collect::<Vec<_>>()
.join(&Cell::new(&[ext(8)], "|").render(0));
pub fn render(m: Matrix<Cell>) -> String { output + &row_str + "\n"
render_rows(render_cols(m)).concat() })
} }
pub fn sep() -> String {
Cell::new(vec![ext(8)], "|".to_string()).render(0)
} }
#[allow(dead_code)] #[allow(dead_code)]