From 80837044b2a746d6566df6af96e2ac205e6d56f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 6 Jan 2021 14:42:19 +0100 Subject: [PATCH] refactor list mailbox command --- src/email.rs | 191 ++++++++++++++++++++++++++++++++++++ src/imap.rs | 261 ++++++------------------------------------------- src/mailbox.rs | 120 +++++++++++++++++++++++ src/main.rs | 26 +++-- 4 files changed, 358 insertions(+), 240 deletions(-) create mode 100644 src/email.rs create mode 100644 src/mailbox.rs diff --git a/src/email.rs b/src/email.rs new file mode 100644 index 0000000..62834a1 --- /dev/null +++ b/src/email.rs @@ -0,0 +1,191 @@ +use imap; +use rfc2047_decoder; + +use crate::table::{self, DisplayCell, DisplayRow, DisplayTable}; + +pub struct Uid(u32); + +impl Uid { + pub fn from_fetch(fetch: &imap::types::Fetch) -> Self { + Self(fetch.uid.unwrap()) + } +} + +impl DisplayCell for Uid { + fn styles(&self) -> &[table::Style] { + &[table::RED] + } + + fn value(&self) -> String { + self.0.to_string() + } +} + +pub struct Flags<'a>(Vec>); + +impl Flags<'_> { + pub fn from_fetch(fetch: &imap::types::Fetch) -> Self { + 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 + }); + + Self(flags) + } +} + +impl 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 struct Sender(String); + +impl Sender { + fn try_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)) + }) + } + + pub fn from_fetch(fetch: &imap::types::Fetch) -> Self { + Self(Self::try_from_fetch(fetch).unwrap_or(String::new())) + } +} + +impl DisplayCell for Sender { + fn styles(&self) -> &[table::Style] { + &[table::BLUE] + } + + fn value(&self) -> String { + self.0.to_owned() + } +} + +pub struct Subject(String); + +impl Subject { + fn try_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", ""))) + } + + pub fn from_fetch(fetch: &imap::types::Fetch) -> Self { + Self(Self::try_from_fetch(fetch).unwrap_or(String::new())) + } +} + +impl DisplayCell for Subject { + fn styles(&self) -> &[table::Style] { + &[table::GREEN] + } + + fn value(&self) -> String { + self.0.to_owned() + } +} + +pub struct Date(String); + +impl Date { + fn try_from_fetch(fetch: &imap::types::Fetch) -> Option { + fetch + .internal_date() + .and_then(|date| Some(date.to_rfc3339())) + } + + pub fn from_fetch(fetch: &imap::types::Fetch) -> Self { + Self(Self::try_from_fetch(fetch).unwrap_or(String::new())) + } +} + +impl DisplayCell for Date { + fn styles(&self) -> &[table::Style] { + &[table::YELLOW] + } + + fn value(&self) -> String { + self.0.to_owned() + } +} + +pub struct Email<'a> { + pub uid: Uid, + pub flags: Flags<'a>, + pub from: Sender, + pub subject: Subject, + pub date: Date, +} + +impl Email<'_> { + pub fn from_fetch(fetch: &imap::types::Fetch) -> Self { + Self { + uid: Uid::from_fetch(fetch), + from: Sender::from_fetch(fetch), + subject: Subject::from_fetch(fetch), + date: Date::from_fetch(fetch), + flags: Flags::from_fetch(fetch), + } + } +} + +impl<'a> DisplayRow for Email<'a> { + fn to_row(&self) -> Vec { + vec![ + self.uid.to_cell(), + self.flags.to_cell(), + self.from.to_cell(), + self.subject.to_cell(), + self.date.to_cell(), + ] + } +} + +impl<'a> DisplayTable<'a, Email<'a>> for Vec> { + fn cols() -> &'a [&'a str] { + &["uid", "flags", "from", "subject", "date"] + } + + fn rows(&self) -> &Vec> { + self + } +} diff --git a/src/imap.rs b/src/imap.rs index e7c52ab..ce8dd88 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1,150 +1,11 @@ use imap; use mailparse::{self, MailHeaderMap}; use native_tls::{self, TlsConnector, TlsStream}; -use rfc2047_decoder; use std::{error, fmt, net::TcpStream, result}; use crate::config; -use crate::table; - -// Email - -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 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 struct Sender(String); - -impl table::DisplayCell for Sender { - fn styles(&self) -> &[table::Style] { - &[table::BLUE] - } - - fn value(&self) -> String { - self.0.to_owned() - } -} - -pub struct Subject(String); - -impl table::DisplayCell for Subject { - fn styles(&self) -> &[table::Style] { - &[table::GREEN] - } - - fn value(&self) -> String { - self.0.to_owned() - } -} - -pub struct Date(String); - -impl table::DisplayCell for Date { - fn styles(&self) -> &[table::Style] { - &[table::YELLOW] - } - - 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 +use crate::email::Email; +use crate::mailbox::Mailbox; #[derive(Debug)] pub struct ImapConnector { @@ -163,6 +24,17 @@ impl ImapConnector { Ok(Self { config, sess }) } + pub fn list_mailboxes(&mut self) -> Result>> { + let mboxes = self + .sess + .list(Some(""), Some("*"))? + .iter() + .map(Mailbox::from_name) + .collect::>(); + + Ok(mboxes) + } + pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result>> { self.sess.select(mbox)?; @@ -180,29 +52,7 @@ impl ImapConnector { "(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), - } - }) + .map(Email::from_fetch) .collect::>(); Ok(emails) @@ -251,75 +101,26 @@ impl From for Error { 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)); +// fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mut Vec) { +// match part.subparts.len() { +// 0 => { +// if part +// .get_headers() +// .get_first_value("content-type") +// .and_then(|v| if v.starts_with(mime) { Some(()) } else { None }) +// .is_some() +// { +// parts.push(part.get_body().unwrap_or(String::new())) +// } +// } +// _ => { +// part.subparts +// .iter() +// .for_each(|p| extract_subparts_by_mime(mime, p, parts)); +// } // } - -// Ok(()) // } -fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mut Vec) { - match part.subparts.len() { - 0 => { - if part - .get_headers() - .get_first_value("content-type") - .and_then(|v| if v.starts_with(mime) { Some(()) } else { None }) - .is_some() - { - parts.push(part.get_body().unwrap_or(String::new())) - } - } - _ => { - part.subparts - .iter() - .for_each(|p| extract_subparts_by_mime(mime, p, parts)); - } - } -} - // pub fn read_email( // imap_sess: &mut ImapSession, // mbox: &str, diff --git a/src/mailbox.rs b/src/mailbox.rs new file mode 100644 index 0000000..24a6b30 --- /dev/null +++ b/src/mailbox.rs @@ -0,0 +1,120 @@ +use imap; + +use crate::table::{self, DisplayCell, DisplayRow, DisplayTable}; + +pub struct Delim(String); + +impl Delim { + pub fn from_name(name: &imap::types::Name) -> Self { + Self(name.delimiter().unwrap_or("/").to_string()) + } +} + +impl DisplayCell for Delim { + fn styles(&self) -> &[table::Style] { + &[table::BLUE] + } + + fn value(&self) -> String { + self.0.to_owned() + } +} + +pub struct Name(String); + +impl Name { + pub fn from_name(name: &imap::types::Name) -> Self { + Self(name.name().to_string()) + } +} + +impl DisplayCell for Name { + fn styles(&self) -> &[table::Style] { + &[table::GREEN] + } + + fn value(&self) -> String { + self.0.to_owned() + } +} + +pub struct Attributes<'a>(Vec>); + +impl Attributes<'_> { + pub fn from_name(name: &imap::types::Name) -> Self { + let attrs = name.attributes().iter().fold(vec![], |mut attrs, attr| { + use imap::types::NameAttribute::*; + + match attr { + NoInferiors => attrs.push(NoInferiors), + NoSelect => attrs.push(NoSelect), + Marked => attrs.push(Marked), + Unmarked => attrs.push(Unmarked), + _ => (), + }; + + attrs + }); + + Self(attrs) + } +} + +impl DisplayCell for Attributes<'_> { + fn styles(&self) -> &[table::Style] { + &[table::YELLOW] + } + + fn value(&self) -> String { + use imap::types::NameAttribute::*; + + self.0 + .iter() + .map(|attr| match attr { + NoInferiors => vec!["no inferiors"], + NoSelect => vec!["no select"], + Marked => vec!["marked"], + Unmarked => vec!["unmarked"], + _ => vec![], + }) + .collect::>() + .concat() + .join(", ") + } +} + +pub struct Mailbox<'a> { + pub delim: Delim, + pub name: Name, + pub attributes: Attributes<'a>, +} + +impl Mailbox<'_> { + pub fn from_name(name: &imap::types::Name) -> Self { + Self { + delim: Delim::from_name(name), + name: Name::from_name(name), + attributes: Attributes::from_name(name), + } + } +} + +impl<'a> DisplayRow for Mailbox<'a> { + fn to_row(&self) -> Vec { + vec![ + self.delim.to_cell(), + self.name.to_cell(), + self.attributes.to_cell(), + ] + } +} + +impl<'a> DisplayTable<'a, Mailbox<'a>> for Vec> { + fn cols() -> &'a [&'a str] { + &["delim", "name", "attributes"] + } + + fn rows(&self) -> &Vec> { + self + } +} diff --git a/src/main.rs b/src/main.rs index 5e3d3bf..69eac7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ mod config; +mod email; mod imap; +mod mailbox; mod smtp; mod table; @@ -9,13 +11,13 @@ use crate::config::Config; use crate::imap::ImapConnector; use crate::table::DisplayTable; -fn new_email_tpl() -> String { - ["To: ", "Subject: ", ""].join("\r\n") -} +// fn new_email_tpl() -> String { +// ["To: ", "Subject: ", ""].join("\r\n") +// } -fn forward_email_tpl() -> String { - ["To: ", "Subject: ", ""].join("\r\n") -} +// fn forward_email_tpl() -> String { +// ["To: ", "Subject: ", ""].join("\r\n") +// } fn mailbox_arg() -> Arg<'static, 'static> { Arg::with_name("mailbox") @@ -93,10 +95,14 @@ fn dispatch() -> Result<(), imap::Error> { ) .get_matches(); - // if let Some(_) = matches.subcommand_matches("list") { - // let config = Config::new_from_file(); - // ImapConnector::new(&config.imap).list_mailboxes().unwrap(); - // } + if let Some(_) = matches.subcommand_matches("list") { + let config = Config::new_from_file(); + let mboxes = ImapConnector::new(config.imap)? + .list_mailboxes()? + .to_table(); + + println!("{}", mboxes); + } if let Some(matches) = matches.subcommand_matches("search") { let config = Config::new_from_file();