diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c75dc7..b295969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Parse TOML config [#1] - Populate Config struct from TOML [#2] +- Set up IMAP connection [#3] +- List new emails [#6] [unreleased]: https://github.com/soywod/himalaya [#1]: https://github.com/soywod/himalaya/issues/1 [#2]: https://github.com/soywod/himalaya/issues/2 +[#3]: https://github.com/soywod/himalaya/issues/3 diff --git a/Cargo.lock b/Cargo.lock index 2f158a2..b9f0166 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.13.0" @@ -39,6 +48,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "cc" version = "1.0.66" @@ -57,6 +72,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73" +dependencies = [ + "base64 0.10.1", + "encoding_rs", +] + [[package]] name = "chrono" version = "0.4.19" @@ -86,6 +111,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "encoding_rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -118,6 +152,7 @@ version = "0.1.0" dependencies = [ "imap", "native-tls", + "rfc2047-decoder", "serde", "toml", ] @@ -128,7 +163,7 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b45e5e8d7783a68f2a0e2451bd19446412202fe21c24d34f4b282a510b91ede3" dependencies = [ - "base64", + "base64 0.13.0", "bufstream", "chrono", "imap-proto", @@ -298,6 +333,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" + [[package]] name = "rand" version = "0.7.3" @@ -372,6 +413,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "rfc2047-decoder" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ecf2ba387f446155e26796aabb727e9ae1427dd13ac9cc21773a3fbda19d77" +dependencies = [ + "base64 0.13.0", + "charset", + "quoted_printable", +] + [[package]] name = "ryu" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index c93629c..06ba880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,9 @@ version = "0.1.0" authors = ["soywod "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] imap = "2.4.0" native-tls = "0.2" +rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } toml = "0.5.8" diff --git a/src/config.rs b/src/config.rs index 91fffc9..a0d488b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,19 +15,19 @@ pub struct ServerInfo { impl ServerInfo { pub fn get_host(&self) -> &str { - &self.host[..] + &self.host } pub fn get_addr(&self) -> (&str, u16) { - (&self.host[..], self.port) + (&self.host, self.port) } pub fn get_login(&self) -> &str { - &self.login[..] + &self.login } pub fn get_password(&self) -> &str { - &self.password[..] + &self.password } } diff --git a/src/imap.rs b/src/imap.rs index 8b8f746..85cc282 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1,8 +1,10 @@ use imap; use native_tls::{TlsConnector, TlsStream}; +use rfc2047_decoder; use std::net::TcpStream; use crate::config::{Config, ServerInfo}; +use crate::table; type ImapClient = imap::Client>; type ImapSession = imap::Session>; @@ -40,6 +42,107 @@ pub fn create_imap_sess(client: ImapClient, server: &ServerInfo) -> ImapSession pub fn login(config: &Config) -> ImapSession { let tls = create_tls_connector(); let client = create_imap_client(&config.imap, &tls); - let sess = create_imap_sess(client, &config.imap); - sess + let imap_sess = create_imap_sess(client, &config.imap); + imap_sess +} + +fn subject_from_fetch(fetch: &imap::types::Fetch) -> String { + let envelope = fetch.envelope().expect("envelope is missing"); + + match &envelope.subject { + None => String::new(), + Some(bytes) => match rfc2047_decoder::decode(bytes) { + Err(_) => String::new(), + Ok(subject) => subject, + }, + } +} + +fn first_addr_from_fetch(fetch: &imap::types::Fetch) -> String { + let envelope = fetch.envelope().expect("envelope is missing"); + + 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); + + match address.name { + None => email, + Some(name) => match rfc2047_decoder::decode(name) { + Err(_) => email, + Ok(name) => name, + }, + } + } + }, + } +} + +fn date_from_fetch(fetch: &imap::types::Fetch) -> String { + let envelope = fetch.envelope().expect("envelope is missing"); + + match &envelope.date { + None => String::new(), + Some(date) => match String::from_utf8(date.to_vec()) { + Err(_) => String::new(), + Ok(date) => date, + }, + } +} + +pub fn read_new_emails(imap_sess: &mut ImapSession, mbox: &str) -> imap::Result<()> { + imap_sess.select(mbox)?; + // let mboxes = imap_sess.list(Some(""), Some("*"))?; + // println!("Mboxes {:?}", mboxes); + + let seqs = imap_sess + .search("NOT SEEN")? + .iter() + .map(|n| n.to_string()) + .collect::>() + .join(","); + + let table_head = vec![ + 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 + .fetch(seqs, "(INTERNALDATE ENVELOPE)")? + .iter() + .map(|fetch| { + vec![ + 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(()) } diff --git a/src/main.rs b/src/main.rs index 6482d51..96ab840 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,17 @@ mod config; mod imap; +mod table; + +use std::env; fn main() { + let mbox = env::args().nth(1).unwrap_or(String::from("inbox")); + let args = env::args().skip(2).collect::>().join(" ").to_owned(); let config = config::read_file(); - let sess = imap::login(&config); - println!("{:?}", sess); + let mut imap_sess = imap::login(&config); + + match args.as_str() { + "read new" => imap::read_new_emails(&mut imap_sess, &mbox).unwrap(), + _ => println!("Himalaya: command not found e"), + } }