From 18042b02b8e66285e4244ae98459034b77ec0f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 5 Jan 2021 23:54:39 +0100 Subject: [PATCH] refactor + clean table system [WIP] --- src/config.rs | 26 +-- src/imap.rs | 482 ++++++++++++++++++++++++++++++-------------------- src/main.rs | 114 ++++++++---- src/table.rs | 97 +++++----- 4 files changed, 428 insertions(+), 291 deletions(-) diff --git a/src/config.rs b/src/config.rs index 128fb5e..fc7f1b6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,21 +14,9 @@ pub struct ServerInfo { } impl ServerInfo { - pub fn get_host(&self) -> &str { - &self.host - } - pub fn get_addr(&self) -> (&str, u16) { (&self.host, self.port) } - - pub fn get_login(&self) -> &str { - &self.login - } - - pub fn get_password(&self) -> &str { - &self.password - } } #[derive(Debug, Deserialize)] @@ -40,6 +28,13 @@ pub struct 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 { format!("{} <{}>", self.name, self.email) } @@ -91,10 +86,3 @@ pub fn read_file_content() -> Result { file.read_to_string(&mut content)?; Ok(content) } - -pub fn read_file() -> Config { - match read_file_content() { - Err(err) => panic!(err), - Ok(content) => toml::from_str(&content).unwrap(), - } -} diff --git a/src/imap.rs b/src/imap.rs index cf6b7d0..e7c52ab 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1,207 +1,305 @@ use imap; use mailparse::{self, MailHeaderMap}; -use native_tls::{TlsConnector, TlsStream}; +use native_tls::{self, TlsConnector, TlsStream}; 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; -type ImapClient = imap::Client>; -type ImapSession = imap::Session>; +// Email -pub fn create_tls_connector() -> TlsConnector { - match native_tls::TlsConnector::new() { - Ok(connector) => connector, - Err(err) => { - println!("The TLS connector could not be created."); - panic!(err); - } +pub struct Uid(u32); + +impl table::DisplayCell for Uid { + fn styles(&self) -> &[table::Style] { + &[table::RED] + } + + fn value(&self) -> String { + self.0.to_string() } } -pub fn create_imap_client(server: &ServerInfo, tls: &TlsConnector) -> ImapClient { - match imap::connect(server.get_addr(), server.get_host(), &tls) { - Ok(client) => client, - Err(err) => { - println!("The IMAP socket could not be opened."); - panic!(err); - } +pub struct Flags<'a>(Vec>); + +impl table::DisplayCell for Flags<'_> { + fn styles(&self) -> &[table::Style] { + &[table::WHITE] + } + + 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 { - match client.login(server.get_login(), server.get_password()) { - Ok(sess) => sess, - Err((err, _)) => { - println!("The IMAP connection could not be established."); - panic!(err); - } +pub struct Sender(String); + +impl table::DisplayCell for Sender { + fn styles(&self) -> &[table::Style] { + &[table::BLUE] + } + + fn value(&self) -> String { + self.0.to_owned() } } -pub fn login(config: &Config) -> ImapSession { - let tls = create_tls_connector(); - let client = create_imap_client(&config.imap, &tls); - let imap_sess = create_imap_sess(client, &config.imap); - imap_sess -} +pub struct Subject(String); -fn subject_from_fetch(fetch: &imap::types::Fetch) -> String { - let envelope = fetch.envelope().expect("envelope is missing"); +impl table::DisplayCell for Subject { + fn styles(&self) -> &[table::Style] { + &[table::GREEN] + } - match &envelope.subject { - None => String::new(), - Some(bytes) => match rfc2047_decoder::decode(bytes) { - Err(_) => String::new(), - Ok(subject) => subject, - }, + fn value(&self) -> String { + self.0.to_owned() } } -fn first_addr_from_fetch(fetch: &imap::types::Fetch) -> String { - let envelope = fetch.envelope().expect("envelope is missing"); +pub struct Date(String); - match &envelope.from { - None => String::new(), - Some(addresses) => match addresses.first() { - 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); +impl table::DisplayCell for Date { + fn styles(&self) -> &[table::Style] { + &[table::YELLOW] + } - match address.name { - None => email, - Some(name) => match rfc2047_decoder::decode(name) { - Err(_) => email, - Ok(name) => name, - }, + fn value(&self) -> String { + self.0.to_owned() + } +} + +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 { + 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 { + 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 { + fetch + .internal_date() + .and_then(|date| Some(date.to_rfc3339())) + } +} + +impl<'a> table::DisplayRow for Email<'a> { + fn to_row(&self) -> Vec { + 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> { + fn cols() -> &'a [&'a str] { + &["uid", "flags", "from", "subject", "date"] + } + + fn rows(&self) -> &Vec> { + self + } +} + +// IMAP Connector + +#[derive(Debug)] +pub struct ImapConnector { + pub config: config::ServerInfo, + pub sess: imap::Session>, +} + +impl ImapConnector { + pub fn new(config: config::ServerInfo) -> Result { + 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>> { + self.sess.select(mbox)?; + + let uids = self + .sess + .uid_search(query)? + .iter() + .map(|n| n.to_string()) + .collect::>(); + + 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::>(); + + Ok(emails) } } -fn date_from_fetch(fetch: &imap::types::Fetch) -> String { - let envelope = fetch.envelope().expect("envelope is missing"); +// Error wrapper - match &envelope.date { - None => String::new(), - Some(date) => match String::from_utf8(date.to_vec()) { - Err(_) => String::new(), - Ok(date) => date, - }, +#[derive(Debug)] +pub enum Error { + CreateTlsConnectorError(native_tls::Error), + CreateImapSession(imap::Error), +} + +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<()> { - imap_sess.select(mbox)?; - - let uids = imap_sess - .uid_search(query)? - .iter() - .map(|n| n.to_string()) - .collect::>(); - - 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::>(); - - 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::>() - .join(", "), - ), - ] - }) - .collect::>(); - - if table_rows.len() == 0 { - println!("No email found!"); - } else { - table_rows.insert(0, table_head); - println!("{}", table::render(table_rows)); +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + Error::CreateTlsConnectorError(ref err) => Some(err), + Error::CreateImapSession(ref err) => Some(err), + } } - - Ok(()) } +impl From for Error { + fn from(err: native_tls::Error) -> Error { + Error::CreateTlsConnectorError(err) + } +} + +impl From for Error { + fn from(err: imap::Error) -> Error { + Error::CreateImapSession(err) + } +} + +// Result wrapper + +type Result = result::Result; + +// 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::>() +// .join(", "), +// ), +// ] +// }) +// .collect::>(); + +// 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) { match part.subparts.len() { 0 => { @@ -222,28 +320,28 @@ fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mu } } -pub fn read_email( - imap_sess: &mut ImapSession, - mbox: &str, - uid: &str, - mime: &str, -) -> imap::Result<()> { - imap_sess.select(mbox)?; +// pub fn read_email( +// imap_sess: &mut ImapSession, +// mbox: &str, +// uid: &str, +// mime: &str, +// ) -> imap::Result<()> { +// imap_sess.select(mbox)?; - match imap_sess.uid_fetch(uid, "BODY[]")?.first() { - None => println!("No email found in mailbox {} with UID {}", mbox, uid), - Some(email_raw) => { - let email = mailparse::parse_mail(email_raw.body().unwrap_or(&[])).unwrap(); - let mut parts = vec![]; - extract_subparts_by_mime(mime, &email, &mut parts); +// match imap_sess.uid_fetch(uid, "BODY[]")?.first() { +// None => println!("No email found in mailbox {} with UID {}", mbox, uid), +// Some(email_raw) => { +// let email = mailparse::parse_mail(email_raw.body().unwrap_or(&[])).unwrap(); +// let mut parts = vec![]; +// extract_subparts_by_mime(mime, &email, &mut parts); - if parts.len() == 0 { - println!("No {} content found for email {}!", mime, uid); - } else { - println!("{}", parts.join("\r\n")); - } - } - } +// if parts.len() == 0 { +// println!("No {} content found for email {}!", mime, uid); +// } else { +// println!("{}", parts.join("\r\n")); +// } +// } +// } - Ok(()) -} +// Ok(()) +// } diff --git a/src/main.rs b/src/main.rs index 39195cf..5e3d3bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,16 @@ mod smtp; mod table; 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") } @@ -28,9 +34,12 @@ fn uid_arg() -> Arg<'static, 'static> { } fn main() { - let config = config::read_file(); - let mut imap_sess = imap::login(&config); + if let Err(err) = dispatch() { + panic!(err); + } +} +fn dispatch() -> Result<(), imap::Error> { let matches = App::new("Himalaya") .version("0.1.0") .about("📫 Minimalist CLI email client") @@ -84,11 +93,13 @@ fn main() { ) .get_matches(); - if let Some(_) = matches.subcommand_matches("list") { - imap::list_mailboxes(&mut imap_sess).unwrap(); - } + // if let Some(_) = matches.subcommand_matches("list") { + // let config = Config::new_from_file(); + // ImapConnector::new(&config.imap).list_mailboxes().unwrap(); + // } if let Some(matches) = matches.subcommand_matches("search") { + let config = Config::new_from_file(); let mbox = matches.value_of("mailbox").unwrap(); if let Some(matches) = matches.values_of("query") { @@ -115,41 +126,74 @@ fn main() { .1 .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") { - let mbox = matches.value_of("mailbox").unwrap(); - let mime = matches.value_of("mime-type").unwrap(); + // if let Some(matches) = matches.subcommand_matches("read") { + // let mbox = matches.value_of("mailbox").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") { - let mut draft_path = env::temp_dir(); - draft_path.push("himalaya-draft.mail"); + // if let Some(_) = matches.subcommand_matches("write") { + // 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(nem_email_tpl().as_bytes()) - .expect("Could not write into draft file"); + // fs::File::create(&draft_path) + // .expect("Could not create draft file") + // .write(new_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"); + // 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"); + // 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"); + // 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(()) } diff --git a/src/table.rs b/src/table.rs index b1788d8..5329580 100644 --- a/src/table.rs +++ b/src/table.rs @@ -31,19 +31,16 @@ impl fmt::Display for Style { #[derive(Debug)] pub struct Cell { - styles: Vec