From 0a508f2e95e015955023b919f7051f381b51b2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 16 Jan 2021 13:16:40 +0100 Subject: [PATCH] start merging email with msg, add list msgs command --- src/imap.rs | 30 ++++++- src/main.rs | 173 +++++++++++++++++++++++++----------- src/{mailbox.rs => mbox.rs} | 10 +-- src/msg.rs | 104 ++++++++++++++++------ src/table.rs | 2 +- 5 files changed, 235 insertions(+), 84 deletions(-) rename src/{mailbox.rs => mbox.rs} (93%) diff --git a/src/imap.rs b/src/imap.rs index 1dcfab6..7f8658d 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -4,7 +4,8 @@ use std::{fmt, net::TcpStream, result}; use crate::config; use crate::email::{self, Email}; -use crate::mailbox::Mailbox; +use crate::mbox::Mbox; +use crate::msg::Msg; // Error wrapper @@ -79,17 +80,40 @@ impl<'a> ImapConnector<'a> { Ok(Self { config, sess }) } - pub fn list_mboxes(&mut self) -> Result>> { + pub fn close(&mut self) { + match self.sess.close() { + _ => (), + } + } + + pub fn list_mboxes(&mut self) -> Result>> { let mboxes = self .sess .list(Some(""), Some("*"))? .iter() - .map(Mailbox::from_name) + .map(Mbox::from_name) .collect::>(); Ok(mboxes) } + pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result> { + let last_seq = self.sess.select(mbox)?.exists; + let begin = last_seq - (page * page_size); + let end = begin - (page_size - 1); + let range = format!("{}:{}", begin, end); + + let msgs = self + .sess + .fetch(range, "(UID BODY.PEEK[])")? + .iter() + .rev() + .map(Msg::from) + .collect::>(); + + Ok(msgs) + } + pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result>> { self.sess.select(mbox)?; diff --git a/src/main.rs b/src/main.rs index eaeaeeb..b8d2580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod config; mod email; mod imap; mod input; -mod mailbox; +mod mbox; mod msg; mod smtp; mod table; @@ -15,6 +15,9 @@ use crate::imap::ImapConnector; use crate::msg::Msg; use crate::table::DisplayTable; +const DEFAULT_PAGE_SIZE: u32 = 10; +const DEFAULT_PAGE: u32 = 0; + #[derive(Debug)] pub enum Error { ConfigError(config::Error), @@ -89,14 +92,44 @@ fn uid_arg() -> Arg<'static, 'static> { } fn run() -> Result<()> { + let default_page_size = &DEFAULT_PAGE_SIZE.to_string(); + let default_page = &DEFAULT_PAGE.to_string(); + let matches = App::new("Himalaya") .version("0.1.0") .about("📫 Minimalist CLI email client") .author("soywod ") .setting(AppSettings::ArgRequiredElseHelp) - .subcommand(SubCommand::with_name("list").about("Lists all available mailboxes")) + .subcommand( + SubCommand::with_name("mailboxes") + .aliases(&["mboxes", "mb", "m"]) + .about("Lists all available mailboxes"), + ) + .subcommand( + SubCommand::with_name("list") + .aliases(&["lst", "l"]) + .about("Lists emails sorted by arrival date") + .arg(mailbox_arg()) + .arg( + Arg::with_name("size") + .help("Page size") + .short("s") + .long("size") + .value_name("INT") + .default_value(default_page_size), + ) + .arg( + Arg::with_name("page") + .help("Page number") + .short("p") + .long("page") + .value_name("INT") + .default_value(default_page), + ), + ) .subcommand( SubCommand::with_name("search") + .aliases(&["query", "q", "s"]) .about("Lists emails matching the given IMAP query") .arg(mailbox_arg()) .arg( @@ -109,6 +142,7 @@ fn run() -> Result<()> { ) .subcommand( SubCommand::with_name("read") + .aliases(&["r"]) .about("Reads text bodies of an email") .arg(uid_arg()) .arg(mailbox_arg()) @@ -124,6 +158,7 @@ fn run() -> Result<()> { ) .subcommand( SubCommand::with_name("attachments") + .aliases(&["attach", "a"]) .about("Downloads all attachments from an email") .arg(uid_arg()) .arg(mailbox_arg()), @@ -131,6 +166,7 @@ fn run() -> Result<()> { .subcommand(SubCommand::with_name("write").about("Writes a new email")) .subcommand( SubCommand::with_name("reply") + .aliases(&["rep", "re"]) .about("Answers to an email") .arg(uid_arg()) .arg(mailbox_arg()) @@ -143,94 +179,127 @@ fn run() -> Result<()> { ) .subcommand( SubCommand::with_name("forward") + .aliases(&["fwd", "f"]) .about("Forwards an email") .arg(uid_arg()) .arg(mailbox_arg()), ) .get_matches(); - if let Some(_) = matches.subcommand_matches("list") { + if let Some(_) = matches.subcommand_matches("mailboxes") { let config = Config::new_from_file()?; - let mboxes = ImapConnector::new(&config.imap)?.list_mboxes()?.to_table(); + let mut imap_conn = ImapConnector::new(&config.imap)?; - println!("{}", mboxes); + let mboxes = imap_conn.list_mboxes()?; + println!("{}", mboxes.to_table()); + + imap_conn.close(); + } + + if let Some(matches) = matches.subcommand_matches("list") { + let config = Config::new_from_file()?; + let mut imap_conn = ImapConnector::new(&config.imap)?; + + let mbox = matches.value_of("mailbox").unwrap(); + let page_size: u32 = matches + .value_of("size") + .unwrap() + .parse() + .unwrap_or(DEFAULT_PAGE_SIZE); + let page: u32 = matches + .value_of("page") + .unwrap() + .parse() + .unwrap_or(DEFAULT_PAGE); + + let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; + println!("{}", msgs.to_table()); + + imap_conn.close(); } if let Some(matches) = matches.subcommand_matches("search") { let config = Config::new_from_file()?; + let mut imap_conn = ImapConnector::new(&config.imap)?; + let mbox = matches.value_of("mailbox").unwrap(); - - if let Some(matches) = matches.values_of("query") { - let query = matches - .fold((false, vec![]), |(escape, mut cmds), cmd| { - match (cmd, escape) { - // Next command is an arg and needs to be escaped - ("subject", _) | ("body", _) | ("text", _) => { - cmds.push(cmd.to_string()); - (true, cmds) - } - // Escaped arg commands - (_, true) => { - cmds.push(format!("\"{}\"", cmd)); - (false, cmds) - } - // Regular commands - (_, false) => { - cmds.push(cmd.to_string()); - (false, cmds) - } + let query = matches + .values_of("query") + .unwrap_or_default() + .fold((false, vec![]), |(escape, mut cmds), cmd| { + match (cmd, escape) { + // Next command is an arg and needs to be escaped + ("subject", _) | ("body", _) | ("text", _) => { + cmds.push(cmd.to_string()); + (true, cmds) } - }) - .1 - .join(" "); + // Escaped arg commands + (_, true) => { + cmds.push(format!("\"{}\"", cmd)); + (false, cmds) + } + // Regular commands + (_, false) => { + cmds.push(cmd.to_string()); + (false, cmds) + } + } + }) + .1 + .join(" "); - let emails = ImapConnector::new(&config.imap)? - .read_emails(&mbox, &query)? - .to_table(); + let msgs = imap_conn.read_emails(&mbox, &query)?; + println!("{}", msgs.to_table()); - println!("{}", emails); - } + imap_conn.close(); } if let Some(matches) = matches.subcommand_matches("read") { let config = Config::new_from_file()?; + let mut imap_conn = ImapConnector::new(&config.imap)?; + let mbox = matches.value_of("mailbox").unwrap(); let uid = matches.value_of("uid").unwrap(); let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); - let body = ImapConnector::new(&config.imap)?.read_email_body(&mbox, &uid, &mime)?; + let body = imap_conn.read_email_body(&mbox, &uid, &mime)?; println!("{}", body); + + imap_conn.close(); } if let Some(matches) = matches.subcommand_matches("attachments") { let config = Config::new_from_file()?; - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); let mut imap_conn = ImapConnector::new(&config.imap)?; - let msg = imap_conn.read_msg(&mbox, &uid)?; - let msg = Msg::from(&msg)?; + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice()); let parts = msg.extract_parts()?; if parts.is_empty() { println!("No attachment found for message {}", uid); } else { println!("{} attachment(s) found for message {}", parts.len(), uid); - msg.extract_parts()?.iter().for_each(|(filename, bytes)| { + parts.iter().for_each(|(filename, bytes)| { let filepath = config.downloads_filepath(&filename); println!("Downloading {} …", filename); fs::write(filepath, bytes).unwrap() }); println!("Done!"); } + + imap_conn.close(); } if let Some(_) = matches.subcommand_matches("write") { let config = Config::new_from_file()?; let mut imap_conn = ImapConnector::new(&config.imap)?; + let tpl = Msg::build_new_tpl(&config)?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?; - let msg = Msg::from(content.as_bytes())?; + let msg = Msg::from(content.as_bytes()); input::ask_for_confirmation("Send the message?")?; @@ -238,17 +307,18 @@ fn run() -> Result<()> { smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?; println!("Done!"); + + imap_conn.close(); } if let Some(matches) = matches.subcommand_matches("reply") { let config = Config::new_from_file()?; - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); let mut imap_conn = ImapConnector::new(&config.imap)?; - let msg = imap_conn.read_msg(&mbox, &uid)?; - let msg = Msg::from(&msg)?; + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice()); let tpl = if matches.is_present("reply-all") { msg.build_reply_all_tpl(&config)? } else { @@ -256,7 +326,7 @@ fn run() -> Result<()> { }; let content = input::open_editor_with_tpl(&tpl.as_bytes())?; - let msg = Msg::from(content.as_bytes())?; + let msg = Msg::from(content.as_bytes()); input::ask_for_confirmation("Send the message?")?; @@ -264,20 +334,21 @@ fn run() -> Result<()> { smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?; println!("Done!"); + + imap_conn.close(); } if let Some(matches) = matches.subcommand_matches("forward") { let config = Config::new_from_file()?; - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); let mut imap_conn = ImapConnector::new(&config.imap)?; - let msg = imap_conn.read_msg(&mbox, &uid)?; - let msg = Msg::from(&msg)?; + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice()); let tpl = msg.build_forward_tpl(&config)?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?; - let msg = Msg::from(content.as_bytes())?; + let msg = Msg::from(content.as_bytes()); input::ask_for_confirmation("Send the message?")?; @@ -285,6 +356,8 @@ fn run() -> Result<()> { smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?; println!("Done!"); + + imap_conn.close(); } Ok(()) diff --git a/src/mailbox.rs b/src/mbox.rs similarity index 93% rename from src/mailbox.rs rename to src/mbox.rs index 24a6b30..dd34543 100644 --- a/src/mailbox.rs +++ b/src/mbox.rs @@ -83,13 +83,13 @@ impl DisplayCell for Attributes<'_> { } } -pub struct Mailbox<'a> { +pub struct Mbox<'a> { pub delim: Delim, pub name: Name, pub attributes: Attributes<'a>, } -impl Mailbox<'_> { +impl Mbox<'_> { pub fn from_name(name: &imap::types::Name) -> Self { Self { delim: Delim::from_name(name), @@ -99,7 +99,7 @@ impl Mailbox<'_> { } } -impl<'a> DisplayRow for Mailbox<'a> { +impl<'a> DisplayRow for Mbox<'a> { fn to_row(&self) -> Vec { vec![ self.delim.to_cell(), @@ -109,12 +109,12 @@ impl<'a> DisplayRow for Mailbox<'a> { } } -impl<'a> DisplayTable<'a, Mailbox<'a>> for Vec> { +impl<'a> DisplayTable<'a, Mbox<'a>> for Vec> { fn cols() -> &'a [&'a str] { &["delim", "name", "attributes"] } - fn rows(&self) -> &Vec> { + fn rows(&self) -> &Vec> { self } } diff --git a/src/msg.rs b/src/msg.rs index efdbedb..74bab9a 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -1,7 +1,8 @@ use lettre; use mailparse::{self, MailHeaderMap}; -use std::{fmt, ops, result}; +use std::{fmt, result}; +use crate::table::{self, DisplayRow, DisplayTable}; use crate::Config; // Error wrapper @@ -9,8 +10,7 @@ use crate::Config; #[derive(Debug)] pub enum Error { ParseMsgError(mailparse::MailParseError), - BuildEmailError(lettre::error::Error), - TryError, + BuildSendableMsgError(lettre::error::Error), } impl fmt::Display for Error { @@ -18,8 +18,7 @@ impl fmt::Display for Error { write!(f, "(msg): ")?; match self { Error::ParseMsgError(err) => err.fmt(f), - Error::BuildEmailError(err) => err.fmt(f), - Error::TryError => write!(f, "cannot parse"), + Error::BuildSendableMsgError(err) => err.fmt(f), } } } @@ -32,7 +31,7 @@ impl From for Error { impl From for Error { fn from(err: lettre::error::Error) -> Error { - Error::BuildEmailError(err) + Error::BuildSendableMsgError(err) } } @@ -40,28 +39,45 @@ impl From for Error { type Result = result::Result; -// Wrapper around mailparse::ParsedMail and lettre::Message +// Msg #[derive(Debug)] -pub struct Msg<'a>(mailparse::ParsedMail<'a>); +pub struct Msg { + pub uid: u32, + pub flags: Vec, + raw: Vec, +} -impl<'a> ops::Deref for Msg<'a> { - type Target = mailparse::ParsedMail<'a>; - - fn deref(&self) -> &Self::Target { - &self.0 +impl From<&[u8]> for Msg { + fn from(item: &[u8]) -> Self { + Self { + uid: 0, + flags: vec![], + raw: item.to_vec(), + } } } -impl<'a> Msg<'a> { - pub fn from(bytes: &'a [u8]) -> Result { - Ok(Self(mailparse::parse_mail(bytes)?)) +impl From<&imap::types::Fetch> for Msg { + fn from(fetch: &imap::types::Fetch) -> Self { + Self { + uid: fetch.uid.unwrap_or_default(), + flags: vec![], + raw: fetch.body().unwrap_or_default().to_vec(), + } + } +} + +impl<'a> Msg { + pub fn parse(&'a self) -> Result> { + Ok(mailparse::parse_mail(&self.raw)?) } pub fn to_vec(&self) -> Result> { - let headers = self.0.get_headers().get_raw_bytes().to_vec(); + let parsed = self.parse()?; + let headers = parsed.get_headers().get_raw_bytes().to_vec(); let sep = "\r\n".as_bytes().to_vec(); - let body = self.0.get_body()?.as_bytes().to_vec(); + let body = parsed.get_body()?.as_bytes().to_vec(); Ok(vec![headers, sep, body].concat()) } @@ -70,8 +86,8 @@ impl<'a> Msg<'a> { use lettre::message::header::{ContentTransferEncoding, ContentType}; use lettre::message::{Message, SinglePart}; - let msg = self - .0 + let parsed = self.parse()?; + let msg = parsed .headers .iter() .fold(Message::builder(), |msg, h| { @@ -111,7 +127,7 @@ impl<'a> Msg<'a> { SinglePart::builder() .header(ContentType("text/plain; charset=utf-8".parse().unwrap())) .header(ContentTransferEncoding::Base64) - .body(self.0.get_body_raw()?), + .body(parsed.get_body_raw()?), )?; Ok(msg) @@ -147,7 +163,7 @@ impl<'a> Msg<'a> { pub fn extract_parts(&self) -> Result)>> { let mut parts = vec![]; - Self::extract_parts_into(&self.0, &mut parts); + Self::extract_parts_into(&self.parse()?, &mut parts); Ok(parts) } @@ -167,7 +183,7 @@ impl<'a> Msg<'a> { } pub fn build_reply_tpl(&self, config: &Config) -> Result { - let msg = &self.0; + let msg = &self.parse()?; let headers = msg.get_headers(); let mut tpl = vec![]; @@ -207,7 +223,7 @@ impl<'a> Msg<'a> { } pub fn build_reply_all_tpl(&self, config: &Config) -> Result { - let msg = &self.0; + let msg = &self.parse()?; let headers = msg.get_headers(); let mut tpl = vec![]; @@ -289,7 +305,7 @@ impl<'a> Msg<'a> { } pub fn build_forward_tpl(&self, config: &Config) -> Result { - let msg = &self.0; + let msg = &self.parse()?; let headers = msg.get_headers(); let mut tpl = vec![]; @@ -313,3 +329,41 @@ impl<'a> Msg<'a> { Ok(tpl.join("\r\n")) } } + +impl DisplayRow for Msg { + fn to_row(&self) -> Vec { + match self.parse() { + Err(_) => vec![], + Ok(parsed) => { + let headers = parsed.get_headers(); + + let uid = &self.uid.to_string(); + let flags = String::new(); // TODO: render flags + let sender = headers + .get_first_value("reply-to") + .or(headers.get_first_value("from")) + .unwrap_or_default(); + let subject = headers.get_first_value("subject").unwrap_or_default(); + let date = headers.get_first_value("date").unwrap_or_default(); + + vec![ + table::Cell::new(&[table::RED], &uid), + table::Cell::new(&[table::WHITE], &flags), + table::Cell::new(&[table::BLUE], &sender), + table::Cell::new(&[table::GREEN], &subject), + table::Cell::new(&[table::YELLOW], &date), + ] + } + } + } +} + +impl<'a> DisplayTable<'a, Msg> for Vec { + fn cols() -> &'a [&'a str] { + &["uid", "flags", "sender", "subject", "date"] + } + + fn rows(&self) -> &Vec { + self + } +} diff --git a/src/table.rs b/src/table.rs index 5329580..79154fa 100644 --- a/src/table.rs +++ b/src/table.rs @@ -78,7 +78,7 @@ pub trait DisplayRow { fn to_row(&self) -> Vec; } -pub trait DisplayTable<'a, T: DisplayRow> { +pub trait DisplayTable<'a, T: DisplayRow + 'a> { fn cols() -> &'a [&'a str]; fn rows(&self) -> &Vec;