From 04642859e8dd21a1ae00c6535803da1d610e1e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 16 Jan 2021 15:01:18 +0100 Subject: [PATCH] complete merge email/msg --- src/email.rs | 229 --------------------------------------------------- src/imap.rs | 50 ++++------- src/main.rs | 82 ++++++++++-------- src/msg.rs | 55 +++++++++++-- 4 files changed, 114 insertions(+), 302 deletions(-) delete mode 100644 src/email.rs diff --git a/src/email.rs b/src/email.rs deleted file mode 100644 index c08deaa..0000000 --- a/src/email.rs +++ /dev/null @@ -1,229 +0,0 @@ -use imap; -use mailparse::{self, MailHeaderMap}; -use rfc2047_decoder; - -use crate::table::{self, DisplayCell, DisplayRow, DisplayTable}; - -#[derive(Debug)] -pub struct Uid(pub 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() - } -} - -#[derive(Debug)] -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 { - // FIXME - // use imap::types::Flag::*; - - // let flags = &self.0; - // let mut flags_str = String::new(); - - // flags_str.push_str(if flags.contains(&Seen) { &" " } else { &"N" }); - // 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 - - String::new() - } -} - -#[derive(Debug)] -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() - } -} - -#[derive(Debug)] -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() - } -} - -#[derive(Debug)] -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() - } -} - -#[derive(Debug)] -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 - } -} - -// Utils - -fn extract_text_bodies_into(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(|part| extract_text_bodies_into(&mime, part, parts)); - } - } -} - -pub fn extract_text_bodies(mime: &str, email: &mailparse::ParsedMail) -> String { - let mut parts = vec![]; - extract_text_bodies_into(&mime, email, &mut parts); - parts.join("\r\n") -} diff --git a/src/imap.rs b/src/imap.rs index 7f8658d..6a4f741 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -3,7 +3,6 @@ use native_tls::{self, TlsConnector, TlsStream}; use std::{fmt, net::TcpStream, result}; use crate::config; -use crate::email::{self, Email}; use crate::mbox::Mbox; use crate::msg::Msg; @@ -114,49 +113,34 @@ impl<'a> ImapConnector<'a> { Ok(msgs) } - pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result>> { + pub fn search_msgs( + &mut self, + mbox: &str, + query: &str, + page_size: &usize, + page: &usize, + ) -> Result> { self.sess.select(mbox)?; + let begin = page * page_size; + let end = begin + (page_size - 1); let uids = self .sess - .uid_search(query)? + .search(query)? .iter() - .map(|n| n.to_string()) + .map(|seq| seq.to_string()) .collect::>(); + let range = uids[begin..end.min(uids.len())].join(","); - let emails = self + let msgs = self .sess - .uid_fetch( - uids[..20.min(uids.len())].join(","), - "(UID ENVELOPE INTERNALDATE)", - )? + .fetch(range, "(UID ENVELOPE INTERNALDATE)")? .iter() - .map(Email::from_fetch) + .rev() + .map(Msg::from) .collect::>(); - Ok(emails) - } - - pub fn read_email_body(&mut self, mbox: &str, uid: &str, mime: &str) -> Result { - self.sess.select(mbox)?; - - match self.sess.uid_fetch(uid, "BODY[]")?.first() { - None => Err(Error::ReadEmailNotFoundError(uid.to_string())), - Some(fetch) => { - let bytes = fetch.body().unwrap_or(&[]); - let email = mailparse::parse_mail(bytes)?; - let bodies = email::extract_text_bodies(&mime, &email); - - if bodies.is_empty() { - Err(Error::ReadEmailEmptyPartError( - uid.to_string(), - mime.to_string(), - )) - } else { - Ok(bodies) - } - } - } + Ok(msgs) } pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result> { diff --git a/src/main.rs b/src/main.rs index b8d2580..9b6a8b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod config; -mod email; mod imap; mod input; mod mbox; @@ -15,8 +14,8 @@ use crate::imap::ImapConnector; use crate::msg::Msg; use crate::table::DisplayTable; -const DEFAULT_PAGE_SIZE: u32 = 10; -const DEFAULT_PAGE: u32 = 0; +const DEFAULT_PAGE_SIZE: usize = 10; +const DEFAULT_PAGE: usize = 0; #[derive(Debug)] pub enum Error { @@ -91,9 +90,27 @@ fn uid_arg() -> Arg<'static, 'static> { .required(true) } +fn page_size_arg<'a>(default: &'a str) -> Arg<'a, 'a> { + Arg::with_name("size") + .help("Page size") + .short("s") + .long("size") + .value_name("INT") + .default_value(default) +} + +fn page_arg<'a>(default: &'a str) -> Arg<'a, 'a> { + Arg::with_name("page") + .help("Page number") + .short("p") + .long("page") + .value_name("INT") + .default_value(default) +} + fn run() -> Result<()> { - let default_page_size = &DEFAULT_PAGE_SIZE.to_string(); - let default_page = &DEFAULT_PAGE.to_string(); + let default_page_size_str = &DEFAULT_PAGE_SIZE.to_string(); + let default_page_str = &DEFAULT_PAGE.to_string(); let matches = App::new("Himalaya") .version("0.1.0") @@ -110,28 +127,16 @@ fn run() -> Result<()> { .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), - ), + .arg(page_size_arg(default_page_size_str)) + .arg(page_arg(default_page_str)), ) .subcommand( SubCommand::with_name("search") .aliases(&["query", "q", "s"]) .about("Lists emails matching the given IMAP query") .arg(mailbox_arg()) + .arg(page_size_arg(default_page_size_str)) + .arg(page_arg(default_page_str)) .arg( Arg::with_name("query") .help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)") @@ -205,12 +210,12 @@ fn run() -> Result<()> { .value_of("size") .unwrap() .parse() - .unwrap_or(DEFAULT_PAGE_SIZE); + .unwrap_or(DEFAULT_PAGE_SIZE as u32); let page: u32 = matches .value_of("page") .unwrap() .parse() - .unwrap_or(DEFAULT_PAGE); + .unwrap_or(DEFAULT_PAGE as u32); let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; println!("{}", msgs.to_table()); @@ -223,6 +228,16 @@ fn run() -> Result<()> { let mut imap_conn = ImapConnector::new(&config.imap)?; let mbox = matches.value_of("mailbox").unwrap(); + let page_size: usize = matches + .value_of("size") + .unwrap() + .parse() + .unwrap_or(DEFAULT_PAGE_SIZE); + let page: usize = matches + .value_of("page") + .unwrap() + .parse() + .unwrap_or(DEFAULT_PAGE); let query = matches .values_of("query") .unwrap_or_default() @@ -248,7 +263,7 @@ fn run() -> Result<()> { .1 .join(" "); - let msgs = imap_conn.read_emails(&mbox, &query)?; + let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; println!("{}", msgs.to_table()); imap_conn.close(); @@ -262,8 +277,9 @@ fn run() -> Result<()> { let uid = matches.value_of("uid").unwrap(); let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); - let body = imap_conn.read_email_body(&mbox, &uid, &mime)?; - println!("{}", body); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let text_bodies = msg.text_bodies(&mime)?; + println!("{}", text_bodies); imap_conn.close(); } @@ -275,8 +291,8 @@ fn run() -> Result<()> { 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()?; + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let parts = msg.extract_attachments()?; if parts.is_empty() { println!("No attachment found for message {}", uid); @@ -299,7 +315,7 @@ fn run() -> Result<()> { 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); input::ask_for_confirmation("Send the message?")?; @@ -318,7 +334,7 @@ fn run() -> Result<()> { 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 msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = if matches.is_present("reply-all") { msg.build_reply_all_tpl(&config)? } else { @@ -326,7 +342,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); input::ask_for_confirmation("Send the message?")?; @@ -345,10 +361,10 @@ fn run() -> Result<()> { 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 msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); 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); input::ask_for_confirmation("Send the message?")?; diff --git a/src/msg.rs b/src/msg.rs index 74bab9a..7cc790f 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -48,12 +48,22 @@ pub struct Msg { raw: Vec, } -impl From<&[u8]> for Msg { - fn from(item: &[u8]) -> Self { +impl From for Msg { + fn from(item: String) -> Self { Self { uid: 0, flags: vec![], - raw: item.to_vec(), + raw: item.as_bytes().to_vec(), + } + } +} + +impl From> for Msg { + fn from(item: Vec) -> Self { + Self { + uid: 0, + flags: vec![], + raw: item, } } } @@ -133,7 +143,38 @@ impl<'a> Msg { Ok(msg) } - fn extract_parts_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec)>) { + fn extract_text_bodies_into(part: &mailparse::ParsedMail, mime: &str, parts: &mut Vec) { + match part.subparts.len() { + 0 => { + let content_type = part + .get_headers() + .get_first_value("content-type") + .unwrap_or_default(); + + if content_type.starts_with(mime) { + parts.push(part.get_body().unwrap_or_default()) + } + } + _ => { + part.subparts + .iter() + .for_each(|part| Self::extract_text_bodies_into(part, mime, parts)); + } + } + } + + fn extract_text_bodies(&self, mime: &str) -> Result> { + let mut parts = vec![]; + Self::extract_text_bodies_into(&self.parse()?, mime, &mut parts); + Ok(parts) + } + + pub fn text_bodies(&self, mime: &str) -> Result { + let text_bodies = self.extract_text_bodies(mime)?; + Ok(text_bodies.join("\r\n")) + } + + fn extract_attachments_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec)>) { match part.subparts.len() { 0 => { let content_disp = part.get_content_disposition(); @@ -156,14 +197,14 @@ impl<'a> Msg { _ => { part.subparts .iter() - .for_each(|part| Self::extract_parts_into(part, parts)); + .for_each(|part| Self::extract_attachments_into(part, parts)); } } } - pub fn extract_parts(&self) -> Result)>> { + pub fn extract_attachments(&self) -> Result)>> { let mut parts = vec![]; - Self::extract_parts_into(&self.parse()?, &mut parts); + Self::extract_attachments_into(&self.parse()?, &mut parts); Ok(parts) }