diff --git a/Cargo.lock b/Cargo.lock index 67c47fe..ab4bb28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,20 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.15" @@ -41,6 +56,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.10.1" @@ -179,6 +208,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + [[package]] name = "fnv" version = "1.0.7" @@ -217,6 +256,12 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + [[package]] name = "hermit-abi" version = "0.1.17" @@ -231,6 +276,7 @@ name = "himalaya" version = "0.2.0" dependencies = [ "clap", + "error-chain", "imap", "lettre", "mailparse", @@ -453,6 +499,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "native-tls" version = "0.2.6" @@ -512,6 +568,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + [[package]] name = "once_cell" version = "1.5.2" @@ -720,6 +782,12 @@ dependencies = [ "quoted_printable", ] +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + [[package]] name = "ryu" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 7636e37..25ed7bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] clap = "2.33.3" +error-chain = "0.12.4" imap = "2.4.0" lettre = "0.10.0-alpha.4" mailparse = "0.13.1" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e72e340 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,469 @@ +use clap::{self, Arg, SubCommand}; +use error_chain::error_chain; +use std::fs; + +use crate::{ + config::{self, Config}, + imap::{self, ImapConnector}, + input, + msg::{self, Attachments, Msg, ReadableMsg}, + output::{self, print}, + smtp, +}; + +error_chain! { + links { + Config(config::Error, config::ErrorKind); + Imap(imap::Error, imap::ErrorKind); + Input(input::Error, input::ErrorKind); + Message(msg::Error, msg::ErrorKind); + Output(output::Error, output::ErrorKind); + Smtp(smtp::Error, smtp::ErrorKind); + } +} + +pub struct App<'a>(pub clap::App<'a, 'a>); + +impl<'a> App<'a> { + fn mailbox_arg() -> Arg<'a, 'a> { + Arg::with_name("mailbox") + .short("m") + .long("mailbox") + .help("Name of the mailbox") + .value_name("STRING") + .default_value("INBOX") + } + + fn uid_arg() -> Arg<'a, 'a> { + Arg::with_name("uid") + .help("UID of the email") + .value_name("UID") + .required(true) + } + + fn reply_all_arg() -> Arg<'a, 'a> { + Arg::with_name("reply-all") + .help("Includes all recipients") + .short("a") + .long("all") + } + + fn page_size_arg() -> Arg<'a, 'a> { + Arg::with_name("size") + .help("Page size") + .short("s") + .long("size") + .value_name("INT") + .default_value("10") + } + + fn page_arg() -> Arg<'a, 'a> { + Arg::with_name("page") + .help("Page number") + .short("p") + .long("page") + .value_name("INT") + .default_value("0") + } + + pub fn new() -> Self { + Self(clap::App::new("Himalaya") + .version(env!("CARGO_PKG_VERSION")) + .about("📫 Minimalist CLI email client") + .author("soywod ") + .setting(clap::AppSettings::ArgRequiredElseHelp) + .arg( + Arg::with_name("output") + .long("output") + .short("o") + .help("Format of the output to print") + .value_name("STRING") + .possible_values(&["text", "json"]) + .default_value("text"), + ) + .arg( + Arg::with_name("account") + .long("account") + .short("a") + .help("Name of the account to use") + .value_name("STRING"), + ) + .subcommand( + SubCommand::with_name("mailboxes") + .aliases(&["mboxes", "mbox", "mb", "m"]) + .about("Lists all available mailboxes"), + ) + .subcommand( + SubCommand::with_name("list") + .aliases(&["lst", "l"]) + .about("Lists emails sorted by arrival date") + .arg(Self::mailbox_arg()) + .arg(Self::page_size_arg()) + .arg(Self::page_arg()), + ) + .subcommand( + SubCommand::with_name("search") + .aliases(&["query", "q", "s"]) + .about("Lists emails matching the given IMAP query") + .arg(Self::mailbox_arg()) + .arg(Self::page_size_arg()) + .arg(Self::page_arg()) + .arg( + Arg::with_name("query") + .help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)") + .value_name("QUERY") + .multiple(true) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("read") + .aliases(&["r"]) + .about("Reads text bodies of an email") + .arg(Self::uid_arg()) + .arg(Self::mailbox_arg()) + .arg( + Arg::with_name("mime-type") + .help("MIME type to use") + .short("t") + .long("mime-type") + .value_name("STRING") + .possible_values(&["plain", "html"]) + .default_value("plain"), + ), + ) + .subcommand( + SubCommand::with_name("attachments") + .aliases(&["attach", "att", "a"]) + .about("Downloads all attachments from an email") + .arg(Self::uid_arg()) + .arg(Self::mailbox_arg()), + ) + .subcommand(SubCommand::with_name("write").about("Writes a new email")) + .subcommand( + SubCommand::with_name("reply") + .aliases(&["rep", "re"]) + .about("Answers to an email") + .arg(Self::uid_arg()) + .arg(Self::mailbox_arg()) + .arg(Self::reply_all_arg()), + ) + .subcommand( + SubCommand::with_name("forward") + .aliases(&["fwd", "f"]) + .about("Forwards an email") + .arg(Self::uid_arg()) + .arg(Self::mailbox_arg()), + ) + .subcommand( + SubCommand::with_name("send") + .about("Sends a raw message") + .arg(Arg::with_name("message").raw(true)), + ) + .subcommand( + SubCommand::with_name("save") + .about("Saves a raw message in the given mailbox") + .arg(Self::mailbox_arg()) + .arg(Arg::with_name("message").raw(true)), + ) + .subcommand( + SubCommand::with_name("template") + .aliases(&["tpl", "t"]) + .about("Generates a message template") + .subcommand( + SubCommand::with_name("new") + .aliases(&["n"]) + .about("Generates a new message template") + .arg(Self::mailbox_arg()), + ) + .subcommand( + SubCommand::with_name("reply") + .aliases(&["rep", "r"]) + .about("Generates a reply message template") + .arg(Self::uid_arg()) + .arg(Self::mailbox_arg()) + .arg(Self::reply_all_arg()), + ) + .subcommand( + SubCommand::with_name("forward") + .aliases(&["fwd", "fw", "f"]) + .about("Generates a forward message template") + .arg(Self::uid_arg()) + .arg(Self::mailbox_arg()), + ), + ) + .subcommand( + SubCommand::with_name("idle") + .about("Starts the idle mode") + .arg(Self::mailbox_arg()), + )) + } + + pub fn run(self) -> Result<()> { + let matches = self.0.get_matches(); + + let account_name = matches.value_of("account"); + let output_type = matches.value_of("output").unwrap().to_owned(); + + if let Some(_) = matches.subcommand_matches("mailboxes") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let mboxes = imap_conn.list_mboxes()?; + print(&output_type, mboxes)?; + + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("list") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let mbox = matches.value_of("mailbox").unwrap(); + let page_size: u32 = matches.value_of("size").unwrap().parse().unwrap(); + let page: u32 = matches.value_of("page").unwrap().parse().unwrap(); + + let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; + print(&output_type, msgs)?; + + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("search") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + let mbox = matches.value_of("mailbox").unwrap(); + let page_size: usize = matches.value_of("size").unwrap().parse().unwrap(); + let page: usize = matches.value_of("page").unwrap().parse().unwrap(); + 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) + } + // Escaped arg commands + (_, true) => { + cmds.push(format!("\"{}\"", cmd)); + (false, cmds) + } + // Regular commands + (_, false) => { + cmds.push(cmd.to_string()); + (false, cmds) + } + } + }) + .1 + .join(" "); + + let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; + print(&output_type, msgs)?; + + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("read") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + 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 msg = imap_conn.read_msg(&mbox, &uid)?; + let msg = ReadableMsg::from_bytes(&mime, &msg)?; + + print(&output_type, msg)?; + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("attachments") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + + let msg = imap_conn.read_msg(&mbox, &uid)?; + let attachments = Attachments::from_bytes(&msg)?; + + match output_type.as_str() { + "text" => { + println!( + "{} attachment(s) found for message {}", + attachments.0.len(), + uid + ); + + attachments.0.iter().for_each(|attachment| { + let filepath = config.downloads_filepath(&account, &attachment.filename); + println!("Downloading {}…", &attachment.filename); + fs::write(filepath, &attachment.raw).unwrap() + }); + + println!("Done!"); + } + "json" => { + attachments.0.iter().for_each(|attachment| { + let filepath = config.downloads_filepath(&account, &attachment.filename); + fs::write(filepath, &attachment.raw).unwrap() + }); + + print!("{{}}"); + } + _ => (), + } + + imap_conn.logout(); + } + + if let Some(_) = matches.subcommand_matches("write") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + let tpl = Msg::build_new_tpl(&config, &account)?; + let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; + let msg = Msg::from(content); + + input::ask_for_confirmation("Send the message?")?; + + println!("Sending…"); + smtp::send(&account, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + println!("Done!"); + + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("template") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + if let Some(_) = matches.subcommand_matches("new") { + let tpl = Msg::build_new_tpl(&config, &account)?; + print(&output_type, &tpl)?; + } + + if let Some(matches) = matches.subcommand_matches("reply") { + let uid = matches.value_of("uid").unwrap(); + let mbox = matches.value_of("mailbox").unwrap(); + + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let tpl = if matches.is_present("reply-all") { + msg.build_reply_all_tpl(&config, &account)? + } else { + msg.build_reply_tpl(&config, &account)? + }; + + print(&output_type, &tpl)?; + } + + if let Some(matches) = matches.subcommand_matches("forward") { + let uid = matches.value_of("uid").unwrap(); + let mbox = matches.value_of("mailbox").unwrap(); + + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let tpl = msg.build_forward_tpl(&config, &account)?; + + print(&output_type, &tpl)?; + } + } + + if let Some(matches) = matches.subcommand_matches("reply") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let tpl = if matches.is_present("reply-all") { + msg.build_reply_all_tpl(&config, &account)? + } else { + msg.build_reply_tpl(&config, &account)? + }; + + let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; + let msg = Msg::from(content); + + input::ask_for_confirmation("Send the message?")?; + + println!("Sending…"); + smtp::send(&account, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + println!("Done!"); + + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("forward") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let tpl = msg.build_forward_tpl(&config, &account)?; + let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; + let msg = Msg::from(content); + + input::ask_for_confirmation("Send the message?")?; + + println!("Sending…"); + smtp::send(&account, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + println!("Done!"); + + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("send") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let msg = matches.value_of("message").unwrap(); + let msg = Msg::from(msg.to_string()); + + smtp::send(&account, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("save") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + + let mbox = matches.value_of("mailbox").unwrap(); + let msg = matches.value_of("message").unwrap(); + let msg = Msg::from(msg.to_string()); + + imap_conn.append_msg(mbox, &msg.to_vec()?)?; + imap_conn.logout(); + } + + if let Some(matches) = matches.subcommand_matches("idle") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(account_name)?; + let mut imap_conn = ImapConnector::new(&account)?; + let mbox = matches.value_of("mailbox").unwrap(); + imap_conn.idle(&config, &mbox)?; + } + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index cf4ae04..f86c44a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,82 +1,12 @@ +use error_chain::error_chain; use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; use serde::Deserialize; -use std::{ - collections::HashMap, - env, fmt, - fs::File, - io::{self, Read}, - path::PathBuf, - result, -}; +use std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf}; use toml; -use crate::output::{self, run_cmd}; +use crate::output::run_cmd; -// Error wrapper - -#[derive(Debug)] -pub enum Error { - IoError(io::Error), - ParseTomlError(toml::de::Error), - ParseTomlAccountsError, - GetEnvVarError(env::VarError), - GetPathNotFoundError, - GetAccountNotFoundError(String), - GetAccountDefaultNotFoundError, - OutputError(output::Error), - - // new erorrs, - RunNotifyCmdError(output::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use Error::*; - - match self { - IoError(err) => err.fmt(f), - ParseTomlError(err) => err.fmt(f), - ParseTomlAccountsError => write!(f, "no account found"), - GetEnvVarError(err) => err.fmt(f), - GetPathNotFoundError => write!(f, "path not found"), - GetAccountNotFoundError(account) => write!(f, "account {} not found", account), - GetAccountDefaultNotFoundError => write!(f, "no default account found"), - OutputError(err) => err.fmt(f), - RunNotifyCmdError(err) => { - write!(f, "run notification cmd: ")?; - err.fmt(f) - } - } - } -} - -impl From for Error { - fn from(err: io::Error) -> Error { - Error::IoError(err) - } -} - -impl From for Error { - fn from(err: toml::de::Error) -> Error { - Error::ParseTomlError(err) - } -} - -impl From for Error { - fn from(err: env::VarError) -> Error { - Error::GetEnvVarError(err) - } -} - -impl From for Error { - fn from(err: output::Error) -> Error { - Error::OutputError(err) - } -} - -// Result wrapper - -type Result = result::Result; +error_chain! {} // Account @@ -110,18 +40,32 @@ impl Account { } pub fn imap_passwd(&self) -> Result { - let passwd = run_cmd(&self.imap_passwd_cmd)?; + let passwd = run_cmd(&self.imap_passwd_cmd).chain_err(|| "Cannot run IMAP passwd cmd")?; let passwd = passwd.trim_end_matches("\n").to_owned(); Ok(passwd) } + pub fn imap_starttls(&self) -> bool { + match self.imap_starttls { + Some(true) => true, + _ => false, + } + } + pub fn smtp_creds(&self) -> Result { - let passwd = run_cmd(&self.smtp_passwd_cmd)?; + let passwd = run_cmd(&self.smtp_passwd_cmd).chain_err(|| "Cannot run SMTP passwd cmd")?; let passwd = passwd.trim_end_matches("\n").to_owned(); Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd)) } + + pub fn smtp_starttls(&self) -> bool { + match self.smtp_starttls { + Some(true) => true, + _ => false, + } + } } // Config @@ -139,7 +83,8 @@ pub struct Config { impl Config { fn path_from_xdg() -> Result { - let path = env::var("XDG_CONFIG_HOME")?; + let path = + env::var("XDG_CONFIG_HOME").chain_err(|| "Cannot find `XDG_CONFIG_HOME` env var")?; let mut path = PathBuf::from(path); path.push("himalaya"); path.push("config.toml"); @@ -147,8 +92,8 @@ impl Config { Ok(path) } - fn path_from_home() -> Result { - let path = env::var("HOME")?; + fn path_from_xdg_alt() -> Result { + let path = env::var("HOME").chain_err(|| "Cannot find `HOME` env var")?; let mut path = PathBuf::from(path); path.push(".config"); path.push("himalaya"); @@ -157,10 +102,10 @@ impl Config { Ok(path) } - fn path_from_tmp() -> Result { - let mut path = env::temp_dir(); - path.push("himalaya"); - path.push("config.toml"); + fn path_from_home() -> Result { + let path = env::var("HOME").chain_err(|| "Cannot find `HOME` env var")?; + let mut path = PathBuf::from(path); + path.push(".himalayarc"); Ok(path) } @@ -168,15 +113,17 @@ impl Config { pub fn new_from_file() -> Result { let mut file = File::open( Self::path_from_xdg() + .or_else(|_| Self::path_from_xdg_alt()) .or_else(|_| Self::path_from_home()) - .or_else(|_| Self::path_from_tmp()) - .or_else(|_| Err(Error::GetPathNotFoundError))?, - )?; + .chain_err(|| "Cannot find config path")?, + ) + .chain_err(|| "Cannot open config file")?; let mut content = vec![]; - file.read_to_end(&mut content)?; + file.read_to_end(&mut content) + .chain_err(|| "Cannot read config file")?; - Ok(toml::from_slice(&content)?) + Ok(toml::from_slice(&content).chain_err(|| "Cannot parse config file")?) } pub fn find_account_by_name(&self, name: Option<&str>) -> Result<&Account> { @@ -184,13 +131,13 @@ impl Config { Some(name) => self .accounts .get(name) - .ok_or_else(|| Error::GetAccountNotFoundError(name.to_owned())), + .ok_or_else(|| format!("Cannot find account `{}`", name).into()), None => self .accounts .iter() .find(|(_, account)| account.default.unwrap_or(false)) .map(|(_, account)| account) - .ok_or_else(|| Error::GetAccountDefaultNotFoundError), + .ok_or_else(|| "Cannot find default account".into()), } } @@ -218,7 +165,9 @@ impl Config { .as_ref() .map(|s| format!(r#"{} "{}" "{}""#, s, subject, sender)) .unwrap_or(default_cmd); - run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?; + + run_cmd(&cmd).chain_err(|| "Cannot run notify cmd")?; + Ok(()) } } diff --git a/src/imap.rs b/src/imap.rs index 6f5b9f6..9745668 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1,6 +1,7 @@ +use error_chain::error_chain; use imap; use native_tls::{self, TlsConnector, TlsStream}; -use std::{fmt, net::TcpStream, result}; +use std::net::TcpStream; use crate::{ config::{self, Account, Config}, @@ -8,78 +9,12 @@ use crate::{ msg::{Msg, Msgs}, }; -// Error wrapper - -#[derive(Debug)] -pub enum Error { - CreateTlsConnectorError(native_tls::Error), - CreateImapSession(imap::Error), - ParseEmailError(mailparse::MailParseError), - ReadEmailNotFoundError(String), - ReadEmailEmptyPartError(String, String), - ExtractAttachmentsEmptyError(String), - ConfigError(config::Error), - - // new errors - IdleError(imap::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use Error::*; - - match self { - CreateTlsConnectorError(err) => err.fmt(f), - CreateImapSession(err) => err.fmt(f), - ParseEmailError(err) => err.fmt(f), - ConfigError(err) => err.fmt(f), - ReadEmailNotFoundError(uid) => { - write!(f, "no email found for uid {}", uid) - } - ReadEmailEmptyPartError(uid, mime) => { - write!(f, "no {} content found for uid {}", mime, uid) - } - ExtractAttachmentsEmptyError(uid) => { - write!(f, "no attachment found for uid {}", uid) - } - IdleError(err) => { - write!(f, "IMAP idle mode: ")?; - err.fmt(f) - } - } +error_chain! { + links { + Config(config::Error, config::ErrorKind); } } -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) - } -} - -impl From for Error { - fn from(err: mailparse::MailParseError) -> Error { - Error::ParseEmailError(err) - } -} - -impl From for Error { - fn from(err: config::Error) -> Error { - Error::ConfigError(err) - } -} - -// Result wrapper - -type Result = result::Result; - -// Imap connector - #[derive(Debug)] pub struct ImapConnector<'a> { pub account: &'a Account, @@ -88,14 +23,18 @@ pub struct ImapConnector<'a> { impl<'a> ImapConnector<'a> { pub fn new(account: &'a Account) -> Result { - let tls = TlsConnector::new()?; - let client = match account.imap_starttls { - Some(true) => imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls), - _ => imap::connect(account.imap_addr(), &account.imap_host, &tls), + let tls = TlsConnector::new().chain_err(|| "Cannot create TLS connector")?; + let client = if account.imap_starttls() { + imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls) + .chain_err(|| "Cannot connect using STARTTLS") + } else { + imap::connect(account.imap_addr(), &account.imap_host, &tls) + .chain_err(|| "Cannot connect using TLS") }?; let sess = client .login(&account.imap_login, &account.imap_passwd()?) - .map_err(|res| res.0)?; + .map_err(|res| res.0) + .chain_err(|| "Cannot login to IMAP server")?; Ok(Self { account, sess }) } @@ -107,24 +46,32 @@ impl<'a> ImapConnector<'a> { } fn last_new_seq(&mut self) -> Result> { - Ok(self.sess.uid_search("NEW")?.into_iter().next()) + Ok(self + .sess + .uid_search("NEW") + .chain_err(|| "Cannot search new uids")? + .into_iter() + .next()) } pub fn idle(&mut self, config: &Config, mbox: &str) -> Result<()> { let mut prev_seq = 0; - self.sess.examine(mbox)?; + self.sess + .examine(mbox) + .chain_err(|| format!("Cannot examine mailbox `{}`", mbox))?; loop { self.sess .idle() .and_then(|idle| idle.wait_keepalive()) - .map_err(Error::IdleError)?; + .chain_err(|| "Cannot wait in IDLE mode")?; if let Some(seq) = self.last_new_seq()? { if prev_seq != seq { if let Some(msg) = self .sess - .uid_fetch(seq.to_string(), "(ENVELOPE)")? + .uid_fetch(seq.to_string(), "(ENVELOPE)") + .chain_err(|| "Cannot fetch enveloppe")? .iter() .next() .map(Msg::from) @@ -140,7 +87,8 @@ impl<'a> ImapConnector<'a> { pub fn list_mboxes(&mut self) -> Result { let mboxes = self .sess - .list(Some(""), Some("*"))? + .list(Some(""), Some("*")) + .chain_err(|| "Cannot list mailboxes")? .iter() .map(Mbox::from_name) .collect::>(); @@ -149,14 +97,20 @@ impl<'a> ImapConnector<'a> { } pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result { - let last_seq = self.sess.select(mbox)?.exists; + let last_seq = self + .sess + .select(mbox) + .chain_err(|| format!("Cannot select mailbox `{}`", mbox))? + .exists; + let begin = last_seq - page * page_size; let end = begin - (begin - 1).min(page_size - 1); let range = format!("{}:{}", begin, end); let msgs = self .sess - .fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)")? + .fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)") + .chain_err(|| "Cannot fetch messages")? .iter() .rev() .map(Msg::from) @@ -172,13 +126,16 @@ impl<'a> ImapConnector<'a> { page_size: &usize, page: &usize, ) -> Result { - self.sess.select(mbox)?; + self.sess + .select(mbox) + .chain_err(|| format!("Cannot select mailbox `{}`", mbox))?; let begin = page * page_size; let end = begin + (page_size - 1); let uids = self .sess - .search(query)? + .search(query) + .chain_err(|| format!("Cannot search in `{}` with query `{}`", mbox, query))? .iter() .map(|seq| seq.to_string()) .collect::>(); @@ -186,7 +143,8 @@ impl<'a> ImapConnector<'a> { let msgs = self .sess - .fetch(range, "(UID ENVELOPE INTERNALDATE)")? + .fetch(&range, "(UID ENVELOPE INTERNALDATE)") + .chain_err(|| format!("Cannot fetch range `{}`", &range))? .iter() .map(Msg::from) .collect::>(); @@ -195,17 +153,26 @@ impl<'a> ImapConnector<'a> { } pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result> { - self.sess.select(mbox)?; + self.sess + .select(mbox) + .chain_err(|| format!("Cannot select mailbox `{}`", mbox))?; - match self.sess.uid_fetch(uid, "BODY[]")?.first() { - None => Err(Error::ReadEmailNotFoundError(uid.to_string())), + match self + .sess + .uid_fetch(uid, "BODY[]") + .chain_err(|| "Cannot fetch bodies")? + .first() + { + None => Err(format!("Cannot find message `{}`", uid).into()), Some(fetch) => Ok(fetch.body().unwrap_or(&[]).to_vec()), } } pub fn append_msg(&mut self, mbox: &str, msg: &[u8]) -> Result<()> { - use imap::types::Flag::*; - self.sess.append_with_flags(mbox, msg, &[Seen])?; + self.sess + .append_with_flags(mbox, msg, &[imap::types::Flag::Seen]) + .chain_err(|| format!("Cannot append message to `{}` with \\Seen flag", mbox))?; + Ok(()) } } diff --git a/src/input.rs b/src/input.rs index e59418c..7848a53 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,72 +1,43 @@ +use error_chain::error_chain; use std::{ - env, fmt, + env, fs::{remove_file, File}, io::{self, Read, Write}, process::Command, - result, }; -// Error wrapper - -#[derive(Debug)] -pub enum Error { - IoError(io::Error), - GetEditorEnvVarNotFoundError(env::VarError), - AskForConfirmationDeniedError, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "input: ")?; - - match self { - Error::IoError(err) => err.fmt(f), - Error::GetEditorEnvVarNotFoundError(err) => err.fmt(f), - Error::AskForConfirmationDeniedError => write!(f, "action cancelled"), - } - } -} - -impl From for Error { - fn from(err: io::Error) -> Error { - Error::IoError(err) - } -} - -impl From for Error { - fn from(err: env::VarError) -> Error { - Error::GetEditorEnvVarNotFoundError(err) - } -} - -// Result wrapper - -type Result = result::Result; - -// Utils +error_chain! {} pub fn open_editor_with_tpl(tpl: &[u8]) -> Result { // Creates draft file let mut draft_path = env::temp_dir(); draft_path.push("himalaya-draft.mail"); - File::create(&draft_path)?.write(tpl)?; + File::create(&draft_path) + .chain_err(|| format!("Cannot create file `{}`", draft_path.to_string_lossy()))? + .write(tpl) + .chain_err(|| format!("Cannot write file `{}`", draft_path.to_string_lossy()))?; // Opens editor and saves user input to draft file - Command::new(env::var("EDITOR")?) + Command::new(env::var("EDITOR").chain_err(|| "Cannot find `EDITOR` env var")?) .arg(&draft_path) - .status()?; + .status() + .chain_err(|| "Cannot start editor")?; // Extracts draft file content let mut draft = String::new(); - File::open(&draft_path)?.read_to_string(&mut draft)?; - remove_file(&draft_path)?; + File::open(&draft_path) + .chain_err(|| format!("Cannot open file `{}`", draft_path.to_string_lossy()))? + .read_to_string(&mut draft) + .chain_err(|| format!("Cannot read file `{}`", draft_path.to_string_lossy()))?; + remove_file(&draft_path) + .chain_err(|| format!("Cannot remove file `{}`", draft_path.to_string_lossy()))?; Ok(draft) } pub fn ask_for_confirmation(prompt: &str) -> Result<()> { print!("{} (y/n) ", prompt); - io::stdout().flush()?; + io::stdout().flush().chain_err(|| "Cannot flush stdout")?; match io::stdin() .bytes() @@ -75,6 +46,7 @@ pub fn ask_for_confirmation(prompt: &str) -> Result<()> { .map(|bytes| bytes as char) { Some('y') | Some('Y') => Ok(()), - _ => Err(Error::AskForConfirmationDeniedError), + Some(choice) => Err(format!("Invalid choice `{}`", choice).into()), + None => Err("Empty choice".into()), } } diff --git a/src/main.rs b/src/main.rs index 5486ae0..f704053 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod app; mod config; mod imap; mod input; @@ -7,545 +8,17 @@ mod output; mod smtp; mod table; -use clap::{App, AppSettings, Arg, SubCommand}; -use std::{fmt, fs, process::exit, result}; - -use crate::config::Config; -use crate::imap::ImapConnector; -use crate::msg::{Attachments, Msg, ReadableMsg}; -use crate::output::print; - -const DEFAULT_PAGE_SIZE: usize = 10; -const DEFAULT_PAGE: usize = 0; - -#[derive(Debug)] -pub enum Error { - ConfigError(config::Error), - InputError(input::Error), - OutputError(output::Error), - MsgError(msg::Error), - ImapError(imap::Error), - SmtpError(smtp::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::ConfigError(err) => err.fmt(f), - Error::InputError(err) => err.fmt(f), - Error::OutputError(err) => err.fmt(f), - Error::MsgError(err) => err.fmt(f), - Error::ImapError(err) => err.fmt(f), - Error::SmtpError(err) => err.fmt(f), - } - } -} - -impl From for Error { - fn from(err: config::Error) -> Error { - Error::ConfigError(err) - } -} - -impl From for Error { - fn from(err: input::Error) -> Error { - Error::InputError(err) - } -} - -impl From for Error { - fn from(err: output::Error) -> Error { - Error::OutputError(err) - } -} - -impl From for Error { - fn from(err: msg::Error) -> Error { - Error::MsgError(err) - } -} - -impl From for Error { - fn from(err: imap::Error) -> Error { - Error::ImapError(err) - } -} - -impl From for Error { - fn from(err: smtp::Error) -> Error { - Error::SmtpError(err) - } -} - -// Result wrapper - -type Result = result::Result; - -// Run - -fn mailbox_arg() -> Arg<'static, 'static> { - Arg::with_name("mailbox") - .short("m") - .long("mailbox") - .help("Name of the mailbox") - .value_name("STRING") - .default_value("INBOX") -} - -fn uid_arg() -> Arg<'static, 'static> { - Arg::with_name("uid") - .help("UID of the email") - .value_name("UID") - .required(true) -} - -fn reply_all_arg() -> Arg<'static, 'static> { - Arg::with_name("reply-all") - .help("Includes all recipients") - .short("a") - .long("all") -} - -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_str = &DEFAULT_PAGE_SIZE.to_string(); - let default_page_str = &DEFAULT_PAGE.to_string(); - - let matches = App::new("Himalaya") - .version("0.2.0") - .about("📫 Minimalist CLI email client") - .author("soywod ") - .setting(AppSettings::ArgRequiredElseHelp) - .arg( - Arg::with_name("output") - .long("output") - .short("o") - .help("Format of the output to print") - .value_name("STRING") - .possible_values(&["text", "json"]) - .default_value("text"), - ) - .arg( - Arg::with_name("account") - .long("account") - .short("a") - .help("Name of the account to use") - .value_name("STRING"), - ) - .subcommand( - SubCommand::with_name("mailboxes") - .aliases(&["mboxes", "mbox", "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(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)") - .value_name("QUERY") - .multiple(true) - .required(true), - ), - ) - .subcommand( - SubCommand::with_name("read") - .aliases(&["r"]) - .about("Reads text bodies of an email") - .arg(uid_arg()) - .arg(mailbox_arg()) - .arg( - Arg::with_name("mime-type") - .help("MIME type to use") - .short("t") - .long("mime-type") - .value_name("STRING") - .possible_values(&["plain", "html"]) - .default_value("plain"), - ), - ) - .subcommand( - SubCommand::with_name("attachments") - .aliases(&["attach", "att", "a"]) - .about("Downloads all attachments from an email") - .arg(uid_arg()) - .arg(mailbox_arg()), - ) - .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()) - .arg(reply_all_arg()), - ) - .subcommand( - SubCommand::with_name("forward") - .aliases(&["fwd", "f"]) - .about("Forwards an email") - .arg(uid_arg()) - .arg(mailbox_arg()), - ) - .subcommand( - SubCommand::with_name("send") - .about("Sends a raw message") - .arg(Arg::with_name("message").raw(true)), - ) - .subcommand( - SubCommand::with_name("save") - .about("Saves a raw message in the given mailbox") - .arg(mailbox_arg()) - .arg(Arg::with_name("message").raw(true)), - ) - .subcommand( - SubCommand::with_name("template") - .aliases(&["tpl", "t"]) - .about("Generates a message template") - .subcommand( - SubCommand::with_name("new") - .aliases(&["n"]) - .about("Generates a new message template") - .arg(mailbox_arg()), - ) - .subcommand( - SubCommand::with_name("reply") - .aliases(&["rep", "r"]) - .about("Generates a reply message template") - .arg(uid_arg()) - .arg(mailbox_arg()) - .arg(reply_all_arg()), - ) - .subcommand( - SubCommand::with_name("forward") - .aliases(&["fwd", "fw", "f"]) - .about("Generates a forward message template") - .arg(uid_arg()) - .arg(mailbox_arg()), - ), - ) - .subcommand( - SubCommand::with_name("idle") - .about("Starts the idle mode") - .arg(mailbox_arg()), - ) - .get_matches(); - - let account_name = matches.value_of("account"); - let output_type = matches.value_of("output").unwrap().to_owned(); - - if let Some(_) = matches.subcommand_matches("mailboxes") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mboxes = imap_conn.list_mboxes()?; - print(&output_type, mboxes)?; - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("list") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mbox = matches.value_of("mailbox").unwrap(); - let page_size: u32 = matches - .value_of("size") - .unwrap() - .parse() - .unwrap_or(DEFAULT_PAGE_SIZE as u32); - let page: u32 = matches - .value_of("page") - .unwrap() - .parse() - .unwrap_or(DEFAULT_PAGE as u32); - - let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; - print(&output_type, msgs)?; - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("search") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - 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() - .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) - } - } - }) - .1 - .join(" "); - - let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; - print(&output_type, msgs)?; - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("read") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - 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 msg = imap_conn.read_msg(&mbox, &uid)?; - let msg = ReadableMsg::from_bytes(&mime, &msg)?; - - print(&output_type, msg)?; - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("attachments") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); - - let msg = imap_conn.read_msg(&mbox, &uid)?; - let attachments = Attachments::from_bytes(&msg)?; - - match output_type.as_str() { - "text" => { - println!( - "{} attachment(s) found for message {}", - attachments.0.len(), - uid - ); - - attachments.0.iter().for_each(|attachment| { - let filepath = config.downloads_filepath(&account, &attachment.filename); - println!("Downloading {}…", &attachment.filename); - fs::write(filepath, &attachment.raw).unwrap() - }); - - println!("Done!"); - } - "json" => { - attachments.0.iter().for_each(|attachment| { - let filepath = config.downloads_filepath(&account, &attachment.filename); - fs::write(filepath, &attachment.raw).unwrap() - }); - - print!("{{}}"); - } - _ => (), - } - - imap_conn.logout(); - } - - if let Some(_) = matches.subcommand_matches("write") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - let tpl = Msg::build_new_tpl(&config, &account)?; - let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; - let msg = Msg::from(content); - - input::ask_for_confirmation("Send the message?")?; - - println!("Sending…"); - smtp::send(&account, &msg.to_sendable_msg()?)?; - imap_conn.append_msg("Sent", &msg.to_vec()?)?; - println!("Done!"); - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("template") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - if let Some(_) = matches.subcommand_matches("new") { - let tpl = Msg::build_new_tpl(&config, &account)?; - print(&output_type, &tpl)?; - } - - if let Some(matches) = matches.subcommand_matches("reply") { - let uid = matches.value_of("uid").unwrap(); - let mbox = matches.value_of("mailbox").unwrap(); - - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&config, &account)? - } else { - msg.build_reply_tpl(&config, &account)? - }; - - print(&output_type, &tpl)?; - } - - if let Some(matches) = matches.subcommand_matches("forward") { - let uid = matches.value_of("uid").unwrap(); - let mbox = matches.value_of("mailbox").unwrap(); - - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = msg.build_forward_tpl(&config, &account)?; - - print(&output_type, &tpl)?; - } - } - - if let Some(matches) = matches.subcommand_matches("reply") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); - - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&config, &account)? - } else { - msg.build_reply_tpl(&config, &account)? - }; - - let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; - let msg = Msg::from(content); - - input::ask_for_confirmation("Send the message?")?; - - println!("Sending…"); - smtp::send(&account, &msg.to_sendable_msg()?)?; - imap_conn.append_msg("Sent", &msg.to_vec()?)?; - println!("Done!"); - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("forward") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); - - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = msg.build_forward_tpl(&config, &account)?; - let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; - let msg = Msg::from(content); - - input::ask_for_confirmation("Send the message?")?; - - println!("Sending…"); - smtp::send(&account, &msg.to_sendable_msg()?)?; - imap_conn.append_msg("Sent", &msg.to_vec()?)?; - println!("Done!"); - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("send") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let msg = matches.value_of("message").unwrap(); - let msg = Msg::from(msg.to_string()); - - smtp::send(&account, &msg.to_sendable_msg()?)?; - imap_conn.append_msg("Sent", &msg.to_vec()?)?; - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("save") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mbox = matches.value_of("mailbox").unwrap(); - let msg = matches.value_of("message").unwrap(); - let msg = Msg::from(msg.to_string()); - - imap_conn.append_msg(mbox, &msg.to_vec()?)?; - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("idle") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account_name)?; - let mut imap_conn = ImapConnector::new(&account)?; - let mbox = matches.value_of("mailbox").unwrap(); - imap_conn.idle(&config, &mbox)?; - } - - Ok(()) -} - -// Main +use crate::app::App; fn main() { - if let Err(err) = run() { - eprintln!("Error: {}", err); - exit(1); + if let Err(ref errs) = App::new().run() { + let mut errs = errs.iter(); + match errs.next() { + Some(err) => { + eprintln!("{}", err); + errs.for_each(|err| eprintln!(" ↳ {}", err)); + } + None => (), + } } } diff --git a/src/msg.rs b/src/msg.rs index b7dfbad..7104016 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -1,3 +1,4 @@ +use error_chain::error_chain; use lettre; use mailparse::{self, MailHeaderMap}; use rfc2047_decoder; @@ -11,40 +12,13 @@ use uuid::Uuid; use crate::config::{Account, Config}; use crate::table::{self, DisplayRow, DisplayTable}; -// Error wrapper - -#[derive(Debug)] -pub enum Error { - ParseMsgError(mailparse::MailParseError), - BuildSendableMsgError(lettre::error::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "(msg): ")?; - match self { - Error::ParseMsgError(err) => err.fmt(f), - Error::BuildSendableMsgError(err) => err.fmt(f), - } +error_chain! { + foreign_links { + Mailparse(mailparse::MailParseError); + Lettre(lettre::error::Error); } } -impl From for Error { - fn from(err: mailparse::MailParseError) -> Error { - Error::ParseMsgError(err) - } -} - -impl From for Error { - fn from(err: lettre::error::Error) -> Error { - Error::BuildSendableMsgError(err) - } -} - -// Result wrapper - -type Result = result::Result; - // Template #[derive(Debug)] diff --git a/src/output.rs b/src/output.rs index 7063612..14f7a14 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,69 +1,26 @@ +use error_chain::error_chain; use serde::Serialize; -use std::{ - fmt::{self, Display}, - io, - process::Command, - result, string, -}; +use std::{fmt::Display, process::Command}; -// Error wrapper - -#[derive(Debug)] -pub enum Error { - IoError(io::Error), - ParseUtf8Error(string::FromUtf8Error), - SerializeJsonError(serde_json::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "input: ")?; - - match self { - Error::IoError(err) => err.fmt(f), - Error::ParseUtf8Error(err) => err.fmt(f), - Error::SerializeJsonError(err) => err.fmt(f), - } - } -} - -impl From for Error { - fn from(err: io::Error) -> Error { - Error::IoError(err) - } -} - -impl From for Error { - fn from(err: string::FromUtf8Error) -> Error { - Error::ParseUtf8Error(err) - } -} - -impl From for Error { - fn from(err: serde_json::Error) -> Error { - Error::SerializeJsonError(err) - } -} - -// Result wrapper - -type Result = result::Result; - -// Utils +error_chain! {} pub fn run_cmd(cmd: &str) -> Result { let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output()? + Command::new("cmd").args(&["/C", cmd]).output() } else { - Command::new("sh").arg("-c").arg(cmd).output()? - }; + Command::new("sh").arg("-c").arg(cmd).output() + } + .chain_err(|| "Run command failed")?; - Ok(String::from_utf8(output.stdout)?) + Ok(String::from_utf8(output.stdout).chain_err(|| "Invalid utf8 output")?) } pub fn print(output_type: &str, item: T) -> Result<()> { match output_type { - "json" => print!("{}", serde_json::to_string(&item)?), + "json" => print!( + "{}", + serde_json::to_string(&item).chain_err(|| "Invalid JSON string")? + ), "text" | _ => println!("{}", item.to_string()), } diff --git a/src/smtp.rs b/src/smtp.rs index 8da58a0..810f0cf 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -1,53 +1,28 @@ -use lettre; -use std::{fmt, result}; +use error_chain::error_chain; +use lettre::{self, transport::smtp::SmtpTransport, Transport}; use crate::config::{self, Account}; -// Error wrapper - -#[derive(Debug)] -pub enum Error { - TransportError(lettre::transport::smtp::Error), - ConfigError(config::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "(smtp): ")?; - match self { - Error::TransportError(err) => err.fmt(f), - Error::ConfigError(err) => err.fmt(f), - } +error_chain! { + links { + Config(config::Error, config::ErrorKind); + } + foreign_links { + Smtp(lettre::transport::smtp::Error); } } -impl From for Error { - fn from(err: lettre::transport::smtp::Error) -> Error { - Error::TransportError(err) - } -} - -impl From for Error { - fn from(err: config::Error) -> Error { - Error::ConfigError(err) - } -} - -// Result wrapper - -type Result = result::Result; - -// Utils - pub fn send(account: &Account, msg: &lettre::Message) -> Result<()> { - use lettre::Transport; + let smtp_relay = if account.smtp_starttls() { + SmtpTransport::starttls_relay + } else { + SmtpTransport::relay + }; - // TODO - // lettre::transport::smtp::SmtpTransport::starttls_relay - - lettre::transport::smtp::SmtpTransport::relay(&account.smtp_host)? + smtp_relay(&account.smtp_host)? .credentials(account.smtp_creds()?) .build() - .send(msg) - .map(|_| Ok(()))? + .send(msg)?; + + Ok(()) }