From fa2f93185f104bf53e64fa620aaea1088582a5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 28 Apr 2021 00:47:24 +0200 Subject: [PATCH] improve logging, replace log-level by RUST_LOG --- CHANGELOG.md | 17 ++- Cargo.lock | 11 ++ Cargo.toml | 2 +- src/app.rs | 32 ++++++ src/comp/cli.rs | 4 +- src/flag/cli.rs | 39 ++++--- src/imap/cli.rs | 26 ++--- src/main.rs | 52 +++++---- src/mbox/cli.rs | 30 +++--- src/mbox/model.rs | 19 +--- src/msg/cli.rs | 252 +++++++++++++++++++++++--------------------- src/msg/model.rs | 37 +------ src/output/fmt.rs | 27 +---- src/output/log.rs | 83 --------------- src/output/mod.rs | 3 +- src/output/model.rs | 67 ++++++++++++ 16 files changed, 340 insertions(+), 361 deletions(-) create mode 100644 src/app.rs delete mode 100644 src/output/log.rs create mode 100644 src/output/model.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 831d700..66341eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Replace `log-level` arg by default `RUST_LOG` env var [#130] + +### Fixed + +- IDLE mode after network interruption [#123] +- Output redirected to `stderr` [#130] +- Refactor table system [#132] +- Editon file format on Linux [#133] +- Show email address when name not available [#131] + ## [0.2.7] - 2021-04-24 ### Added @@ -24,11 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve config compatibility on Windows [#111](https://github.com/soywod/himalaya/pull/111) - Vim table containing emoji [#122] -- IDLE mode after network interruption [#123] -- Output redirected to `stderr` [#130] -- Refactor table system [#132] -- Editon file format on Linux [#133] -- Show email address when name not available [#131] ## [0.2.6] - 2021-04-17 diff --git a/Cargo.lock b/Cargo.lock index 3c0ce8f..879accc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,15 @@ dependencies = [ "memchr 2.3.4", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -165,6 +174,8 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ + "ansi_term", + "atty", "bitflags", "strsim", "textwrap", diff --git a/Cargo.toml b/Cargo.toml index 2266dc1..35aed54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] chrono = "0.4.19" -clap = {version = "2.33.3", default-features = false, features = ["suggestions"]} +clap = {version = "2.33.3", default-features = false, features = ["suggestions", "color"]} env_logger = "0.8.3" error-chain = "0.12.4" imap = "2.4.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..11259db --- /dev/null +++ b/src/app.rs @@ -0,0 +1,32 @@ +use clap; + +use crate::{ + config::model::{Account, Config}, + output::model::Output, +}; + +pub struct App<'a> { + pub config: &'a Config, + pub account: &'a Account, + pub output: &'a Output, + pub mbox: &'a str, + pub arg_matches: &'a clap::ArgMatches<'a>, +} + +impl<'a> App<'a> { + pub fn new( + config: &'a Config, + account: &'a Account, + output: &'a Output, + mbox: &'a str, + arg_matches: &'a clap::ArgMatches<'a>, + ) -> Self { + Self { + config, + account, + output, + mbox, + arg_matches, + } + } +} diff --git a/src/comp/cli.rs b/src/comp/cli.rs index b1053f6..a36bc8b 100644 --- a/src/comp/cli.rs +++ b/src/comp/cli.rs @@ -13,7 +13,7 @@ pub fn comp_subcmds<'s>() -> Vec> { .required(true)])] } -pub fn comp_matches(mut app: App, matches: &ArgMatches) -> Result { +pub fn comp_matches<'a>(app: fn() -> App<'a, 'a>, matches: &ArgMatches) -> Result { if let Some(matches) = matches.subcommand_matches("completion") { debug!("completion command matched"); let shell = match matches.value_of("shell").unwrap() { @@ -22,7 +22,7 @@ pub fn comp_matches(mut app: App, matches: &ArgMatches) -> Result { "bash" | _ => Shell::Bash, }; debug!("shell: {}", shell); - app.gen_completions_to("himalaya", shell, &mut io::stdout()); + app().gen_completions_to("himalaya", shell, &mut io::stdout()); return Ok(true); }; diff --git a/src/flag/cli.rs b/src/flag/cli.rs index c721600..370d212 100644 --- a/src/flag/cli.rs +++ b/src/flag/cli.rs @@ -1,44 +1,43 @@ -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap; use error_chain::error_chain; use log::debug; -use crate::{config::model::Account, imap::model::ImapConnector, msg::cli::uid_arg}; +use crate::{app::App, imap::model::ImapConnector, msg::cli::uid_arg}; error_chain! { links { - Config(crate::config::model::Error, crate::config::model::ErrorKind); Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); } } -fn flags_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("flags") +fn flags_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("flags") .help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11)") .value_name("FLAGS…") .multiple(true) .required(true) } -pub fn flag_subcmds<'s>() -> Vec> { - vec![SubCommand::with_name("flags") +pub fn flag_subcmds<'a>() -> Vec> { + vec![clap::SubCommand::with_name("flags") .aliases(&["flag"]) .about("Handles flags") .subcommand( - SubCommand::with_name("set") + clap::SubCommand::with_name("set") .aliases(&["s"]) .about("Replaces all message flags") .arg(uid_arg()) .arg(flags_arg()), ) .subcommand( - SubCommand::with_name("add") + clap::SubCommand::with_name("add") .aliases(&["a"]) .about("Appends flags to a message") .arg(uid_arg()) .arg(flags_arg()), ) .subcommand( - SubCommand::with_name("remove") + clap::SubCommand::with_name("remove") .aliases(&["rm", "r"]) .about("Removes flags from a message") .arg(uid_arg()) @@ -46,8 +45,8 @@ pub fn flag_subcmds<'s>() -> Vec> { )] } -pub fn flag_matches(account: &Account, mbox: &str, matches: &ArgMatches) -> Result { - if let Some(matches) = matches.subcommand_matches("set") { +pub fn flag_matches(app: &App) -> Result { + if let Some(matches) = app.arg_matches.subcommand_matches("set") { debug!("set command matched"); let uid = matches.value_of("uid").unwrap(); @@ -56,14 +55,14 @@ pub fn flag_matches(account: &Account, mbox: &str, matches: &ArgMatches) -> Resu let flags = matches.value_of("flags").unwrap(); debug!("flags: {}", flags); - let mut imap_conn = ImapConnector::new(&account)?; - imap_conn.set_flags(mbox, uid, flags)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + imap_conn.set_flags(app.mbox, uid, flags)?; imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("add") { + if let Some(matches) = app.arg_matches.subcommand_matches("add") { debug!("add command matched"); let uid = matches.value_of("uid").unwrap(); @@ -72,14 +71,14 @@ pub fn flag_matches(account: &Account, mbox: &str, matches: &ArgMatches) -> Resu let flags = matches.value_of("flags").unwrap(); debug!("flags: {}", flags); - let mut imap_conn = ImapConnector::new(&account)?; - imap_conn.add_flags(mbox, uid, flags)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + imap_conn.add_flags(app.mbox, uid, flags)?; imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("remove") { + if let Some(matches) = app.arg_matches.subcommand_matches("remove") { debug!("remove command matched"); let uid = matches.value_of("uid").unwrap(); @@ -88,8 +87,8 @@ pub fn flag_matches(account: &Account, mbox: &str, matches: &ArgMatches) -> Resu let flags = matches.value_of("flags").unwrap(); debug!("flags: {}", flags); - let mut imap_conn = ImapConnector::new(&account)?; - imap_conn.remove_flags(mbox, uid, flags)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + imap_conn.remove_flags(app.mbox, uid, flags)?; imap_conn.logout(); return Ok(true); diff --git a/src/imap/cli.rs b/src/imap/cli.rs index 88e10b5..40c84da 100644 --- a/src/imap/cli.rs +++ b/src/imap/cli.rs @@ -1,11 +1,8 @@ -use clap::{self, App, ArgMatches, SubCommand}; +use clap; use error_chain::error_chain; use log::debug; -use crate::{ - config::model::{Account, Config}, - imap::model::ImapConnector, -}; +use crate::{app::App, imap::model::ImapConnector}; error_chain! { links { @@ -14,20 +11,17 @@ error_chain! { } } -pub fn imap_subcmds<'s>() -> Vec> { - vec![SubCommand::with_name("idle").about("Spawns a blocking idle daemon")] +pub fn imap_subcmds<'a>() -> Vec> { + vec![clap::SubCommand::with_name("idle").about("Spawns a blocking idle daemon")] } -pub fn imap_matches( - config: &Config, - account: &Account, - mbox: &str, - matches: &ArgMatches, -) -> Result { - if let Some(_) = matches.subcommand_matches("idle") { +pub fn imap_matches(app: &App) -> Result { + if let Some(_) = app.arg_matches.subcommand_matches("idle") { debug!("idle command matched"); - let mut imap_conn = ImapConnector::new(&account)?; - imap_conn.idle(&config, &mbox)?; + + let mut imap_conn = ImapConnector::new(&app.account)?; + imap_conn.idle(&app.config, &app.mbox)?; + imap_conn.logout(); return Ok(true); } diff --git a/src/main.rs b/src/main.rs index c975029..1f02d5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ use clap; +use env_logger; use error_chain::error_chain; use log::{debug, error, trace}; use std::{env, path::PathBuf, process::exit}; +mod app; mod comp; mod config; mod flag; @@ -15,17 +17,14 @@ mod smtp; mod table; use crate::{ + app::App, comp::cli::{comp_matches, comp_subcmds}, config::{cli::config_args, model::Config}, flag::cli::{flag_matches, flag_subcmds}, imap::cli::{imap_matches, imap_subcmds}, mbox::cli::{mbox_matches, mbox_source_arg, mbox_subcmds}, msg::cli::{msg_matches, msg_subcmds}, - output::{ - cli::output_args, - fmt::OutputFmt, - log::{init as init_logger, LogLevel}, - }, + output::{cli::output_args, model::Output}, }; error_chain! { @@ -36,11 +35,10 @@ error_chain! { ImapCli(crate::imap::cli::Error, crate::imap::cli::ErrorKind); MboxCli(crate::mbox::cli::Error, crate::mbox::cli::ErrorKind); MsgCli(crate::msg::cli::Error, crate::msg::cli::ErrorKind); - OutputLog(crate::output::log::Error, crate::output::log::ErrorKind); } } -fn build_app<'a>() -> clap::App<'a, 'a> { +fn parse_args<'a>() -> clap::App<'a, 'a> { clap::App::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) .about(env!("CARGO_PKG_DESCRIPTION")) @@ -56,38 +54,39 @@ fn build_app<'a>() -> clap::App<'a, 'a> { } fn run() -> Result<()> { - let app = build_app(); - let matches = app.get_matches(); + env_logger::init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"), + ); - let output_fmt: OutputFmt = matches.value_of("output").unwrap().into(); - let log_level: LogLevel = matches.value_of("log-level").unwrap().into(); - init_logger(output_fmt, log_level)?; + let args = parse_args(); + let arg_matches = args.get_matches(); - // Check completion matches before the config init - if comp_matches(build_app(), &matches)? { + // Check completion before init config + if comp_matches(parse_args, &arg_matches)? { return Ok(()); } - let custom_config: Option = matches.value_of("config").map(|s| s.into()); - debug!("custom config path: {:?}", custom_config); + let output = Output::new(arg_matches.value_of("output").unwrap()); + debug!("output: {:?}", output); debug!("init config"); + let custom_config: Option = arg_matches.value_of("config").map(|s| s.into()); + debug!("custom config path: {:?}", custom_config); let config = Config::new(custom_config)?; trace!("config: {:?}", config); - let account_name = matches.value_of("account"); + let account_name = arg_matches.value_of("account"); debug!("init account: {}", account_name.unwrap_or("default")); let account = config.find_account_by_name(account_name)?; trace!("account: {:?}", account); - let mbox = matches.value_of("mailbox").unwrap(); + let mbox = arg_matches.value_of("mailbox").unwrap(); debug!("mailbox: {}", mbox); debug!("begin matching"); - let _matched = mbox_matches(&account, &matches)? - || flag_matches(&account, &mbox, &matches)? - || imap_matches(&config, &account, &mbox, &matches)? - || msg_matches(&config, &account, &mbox, &matches)?; + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let _matched = + mbox_matches(&app)? || flag_matches(&app)? || imap_matches(&app)? || msg_matches(&app)?; Ok(()) } @@ -95,13 +94,20 @@ fn run() -> Result<()> { fn main() { if let Err(ref errs) = run() { let mut errs = errs.iter(); + match errs.next() { None => (), Some(err) => { error!("{}", err); - errs.for_each(|err| error!(" ↳ {}", err)); + eprintln!("{}", err); + + errs.for_each(|err| { + error!("{}", err); + eprintln!(" ↳ {}", err); + }); } } + exit(1); } else { exit(0); diff --git a/src/mbox/cli.rs b/src/mbox/cli.rs index 50b86ab..0d06662 100644 --- a/src/mbox/cli.rs +++ b/src/mbox/cli.rs @@ -1,20 +1,17 @@ -use clap::{self, App, Arg, ArgMatches, SubCommand}; +use clap; use error_chain::error_chain; -use log::{debug, info, trace}; +use log::{debug, trace}; -use crate::{config::model::Account, imap::model::ImapConnector}; +use crate::{app::App, imap::model::ImapConnector}; error_chain! { links { - Config(crate::config::model::Error, crate::config::model::ErrorKind); Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); - MsgCli(crate::msg::cli::Error, crate::msg::cli::ErrorKind); - OutputUtils(crate::output::utils::Error, crate::output::utils::ErrorKind); } } -pub fn mbox_source_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("mailbox") +pub fn mbox_source_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("mailbox") .short("m") .long("mailbox") .help("Selects a specific mailbox") @@ -22,26 +19,27 @@ pub fn mbox_source_arg<'a>() -> Arg<'a, 'a> { .default_value("INBOX") } -pub fn mbox_target_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("target") +pub fn mbox_target_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("target") .help("Specifies the targetted mailbox") .value_name("TARGET") } -pub fn mbox_subcmds<'s>() -> Vec> { - vec![SubCommand::with_name("mailboxes") +pub fn mbox_subcmds<'a>() -> Vec> { + vec![clap::SubCommand::with_name("mailboxes") .aliases(&["mailbox", "mboxes", "mbox", "m"]) .about("Lists all mailboxes")] } -pub fn mbox_matches(account: &Account, matches: &ArgMatches) -> Result { - if let Some(_) = matches.subcommand_matches("mailboxes") { +pub fn mbox_matches(app: &App) -> Result { + if let Some(_) = app.arg_matches.subcommand_matches("mailboxes") { debug!("mailboxes command matched"); - let mut imap_conn = ImapConnector::new(&account)?; + let mut imap_conn = ImapConnector::new(&app.account)?; let mboxes = imap_conn.list_mboxes()?; - info!("{}", mboxes); + debug!("found {} mailboxes", mboxes.0.len()); trace!("mailboxes: {:?}", mboxes); + app.output.print(mboxes); imap_conn.logout(); return Ok(true); diff --git a/src/mbox/model.rs b/src/mbox/model.rs index 7e1db6d..6f5707c 100644 --- a/src/mbox/model.rs +++ b/src/mbox/model.rs @@ -2,10 +2,7 @@ use imap; use serde::Serialize; use std::fmt; -use crate::{ - output::fmt::{get_output_fmt, OutputFmt, Response}, - table::{Cell, Row, Table}, -}; +use crate::table::{Cell, Row, Table}; // Mbox @@ -42,7 +39,7 @@ impl Table for Mbox { fn row(&self) -> Row { Row::new() - .cell(Cell::new(&self.delim).red()) + .cell(Cell::new(&self.delim).white()) .cell(Cell::new(&self.name).green()) .cell(Cell::new(&self.attributes.join(", ")).shrinkable().yellow()) } @@ -55,16 +52,6 @@ pub struct Mboxes(pub Vec); impl fmt::Display for Mboxes { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - unsafe { - match get_output_fmt() { - &OutputFmt::Plain => { - writeln!(f, "\n{}", Table::render(&self.0)) - } - &OutputFmt::Json => { - let res = serde_json::to_string(&Response::new(self)).unwrap(); - write!(f, "{}", res) - } - } - } + writeln!(f, "\n{}", Table::render(&self.0)) } } diff --git a/src/msg/cli.rs b/src/msg/cli.rs index 277a215..500146d 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -1,10 +1,10 @@ -use clap::{self, App, Arg, ArgMatches, SubCommand}; +use clap; use error_chain::error_chain; -use log::{debug, error, info, trace}; +use log::{debug, error, trace}; use std::{fs, ops::Deref}; use crate::{ - config::model::{Account, Config}, + app::App, flag::model::Flag, imap::model::ImapConnector, input, @@ -15,7 +15,6 @@ use crate::{ error_chain! { links { - Config(crate::config::model::Error, crate::config::model::ErrorKind); Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); Input(crate::input::Error, crate::input::ErrorKind); MsgModel(crate::msg::model::Error, crate::msg::model::ErrorKind); @@ -26,30 +25,30 @@ error_chain! { } } -pub fn uid_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("uid") +pub fn uid_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("uid") .help("Specifies the targetted message") .value_name("UID") .required(true) } -fn reply_all_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("reply-all") +fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("reply-all") .help("Includes all recipients") .short("a") .long("all") } -fn page_size_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("page-size") +fn page_size_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("page-size") .help("Page size") .short("s") .long("size") .value_name("INT") } -fn page_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("page") +fn page_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("page") .help("Page number") .short("p") .long("page") @@ -57,8 +56,8 @@ fn page_arg<'a>() -> Arg<'a, 'a> { .default_value("0") } -fn attachment_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("attachments") +fn attachment_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("attachments") .help("Adds attachment to the message") .short("a") .long("attachment") @@ -67,41 +66,41 @@ fn attachment_arg<'a>() -> Arg<'a, 'a> { .takes_value(true) } -pub fn msg_subcmds<'s>() -> Vec> { +pub fn msg_subcmds<'a>() -> Vec> { vec![ - SubCommand::with_name("list") + clap::SubCommand::with_name("list") .aliases(&["lst", "l"]) .about("Lists all messages") .arg(page_size_arg()) .arg(page_arg()), - SubCommand::with_name("search") + clap::SubCommand::with_name("search") .aliases(&["query", "q", "s"]) .about("Lists messages matching the given IMAP query") .arg(page_size_arg()) .arg(page_arg()) .arg( - Arg::with_name("query") + clap::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::with_name("write") + clap::SubCommand::with_name("write") .aliases(&["w"]) .about("Writes a new message") .arg(attachment_arg()), - SubCommand::with_name("send") + clap::SubCommand::with_name("send") .about("Sends a raw message") - .arg(Arg::with_name("message").raw(true)), - SubCommand::with_name("save") + .arg(clap::Arg::with_name("message").raw(true)), + clap::SubCommand::with_name("save") .about("Saves a raw message") - .arg(Arg::with_name("message").raw(true)), - SubCommand::with_name("read") + .arg(clap::Arg::with_name("message").raw(true)), + clap::SubCommand::with_name("read") .aliases(&["r"]) .about("Reads text bodies of a message") .arg(uid_arg()) .arg( - Arg::with_name("mime-type") + clap::Arg::with_name("mime-type") .help("MIME type to use") .short("t") .long("mime-type") @@ -110,55 +109,55 @@ pub fn msg_subcmds<'s>() -> Vec> { .default_value("plain"), ) .arg( - Arg::with_name("raw") + clap::Arg::with_name("raw") .help("Reads raw message") .long("raw") .short("r"), ), - SubCommand::with_name("attachments") + clap::SubCommand::with_name("attachments") .aliases(&["attach", "att", "a"]) .about("Downloads all message attachments") .arg(uid_arg()), - SubCommand::with_name("reply") + clap::SubCommand::with_name("reply") .aliases(&["rep", "re"]) .about("Answers to a message") .arg(uid_arg()) .arg(reply_all_arg()), - SubCommand::with_name("forward") + clap::SubCommand::with_name("forward") .aliases(&["fwd", "f"]) .about("Forwards a message") .arg(uid_arg()), - SubCommand::with_name("copy") + clap::SubCommand::with_name("copy") .aliases(&["cp", "c"]) .about("Copies a message to the targetted mailbox") .arg(uid_arg()) .arg(mbox_target_arg()), - SubCommand::with_name("move") + clap::SubCommand::with_name("move") .aliases(&["mv", "m"]) .about("Moves a message to the targetted mailbox") .arg(uid_arg()) .arg(mbox_target_arg()), - SubCommand::with_name("delete") + clap::SubCommand::with_name("delete") .aliases(&["remove", "rm", "del", "d"]) .about("Deletes a message") .arg(uid_arg()), - SubCommand::with_name("template") + clap::SubCommand::with_name("template") .aliases(&["tpl", "t"]) .about("Generates a message template") .subcommand( - SubCommand::with_name("new") + clap::SubCommand::with_name("new") .aliases(&["n"]) .about("Generates a new message template"), ) .subcommand( - SubCommand::with_name("reply") + clap::SubCommand::with_name("reply") .aliases(&["rep", "r"]) .about("Generates a reply message template") .arg(uid_arg()) .arg(reply_all_arg()), ) .subcommand( - SubCommand::with_name("forward") + clap::SubCommand::with_name("forward") .aliases(&["fwd", "fw", "f"]) .about("Generates a forward message template") .arg(uid_arg()), @@ -166,19 +165,14 @@ pub fn msg_subcmds<'s>() -> Vec> { ] } -pub fn msg_matches( - config: &Config, - account: &Account, - mbox: &str, - matches: &ArgMatches, -) -> Result { - if let Some(matches) = matches.subcommand_matches("list") { +pub fn msg_matches(app: &App) -> Result { + if let Some(matches) = app.arg_matches.subcommand_matches("list") { debug!("list command matched"); let page_size: usize = matches .value_of("page-size") .and_then(|s| s.parse().ok()) - .unwrap_or(config.default_page_size(&account)); + .unwrap_or(app.config.default_page_size(&app.account)); debug!("page size: {}", &page_size); let page: usize = matches .value_of("page") @@ -187,23 +181,23 @@ pub fn msg_matches( .unwrap_or_default(); debug!("page: {}", &page); - let mut imap_conn = ImapConnector::new(&account)?; - let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + let msgs = imap_conn.list_msgs(&app.mbox, &page_size, &page)?; let msgs = Msgs::from(&msgs); - info!("{}", msgs); trace!("messages: {:?}", msgs); + app.output.print(msgs); imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("search") { + if let Some(matches) = app.arg_matches.subcommand_matches("search") { debug!("search command matched"); let page_size: usize = matches .value_of("page-size") .and_then(|s| s.parse().ok()) - .unwrap_or(config.default_page_size(&account)); + .unwrap_or(app.config.default_page_size(&app.account)); debug!("page size: {}", &page_size); let page: usize = matches .value_of("page") @@ -238,17 +232,17 @@ pub fn msg_matches( .join(" "); debug!("query: {}", &page); - let mut imap_conn = ImapConnector::new(&account)?; - let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + let msgs = imap_conn.search_msgs(&app.mbox, &query, &page_size, &page)?; let msgs = Msgs::from(&msgs); - info!("{}", msgs); trace!("messages: {:?}", msgs); + app.output.print(msgs); imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("read") { + if let Some(matches) = app.arg_matches.subcommand_matches("read") { debug!("read command matched"); let uid = matches.value_of("uid").unwrap(); @@ -258,30 +252,30 @@ pub fn msg_matches( let raw = matches.is_present("raw"); debug!("raw: {}", raw); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = imap_conn.read_msg(&mbox, &uid)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = imap_conn.read_msg(&app.mbox, &uid)?; if raw { let msg = String::from_utf8(msg) .chain_err(|| "Could not decode raw message as utf8 string")?; let msg = msg.trim_end_matches("\n"); - info!("{}", msg); + app.output.print(msg); } else { let msg = ReadableMsg::from_bytes(&mime, &msg)?; - info!("{}", msg); + app.output.print(msg); } imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("attachments") { + if let Some(matches) = app.arg_matches.subcommand_matches("attachments") { debug!("attachments command matched"); let uid = matches.value_of("uid").unwrap(); debug!("uid: {}", &uid); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = imap_conn.read_msg(&mbox, &uid)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = imap_conn.read_msg(&app.mbox, &uid)?; let attachments = Attachments::from_bytes(&msg)?; debug!( "{} attachment(s) found for message {}", @@ -289,30 +283,37 @@ pub fn msg_matches( &uid ); for attachment in attachments.0.iter() { - let filepath = config.downloads_filepath(&account, &attachment.filename); + let filepath = app + .config + .downloads_filepath(&app.account, &attachment.filename); debug!("downloading {}…", &attachment.filename); fs::write(&filepath, &attachment.raw) .chain_err(|| format!("Could not save attachment {:?}", filepath))?; } - info!( + + debug!( "{} attachment(s) successfully downloaded", &attachments.0.len() ); + app.output.print(format!( + "{} attachment(s) successfully downloaded", + &attachments.0.len() + )); imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("write") { + if let Some(matches) = app.arg_matches.subcommand_matches("write") { debug!("write command matched"); - let mut imap_conn = ImapConnector::new(&account)?; + let mut imap_conn = ImapConnector::new(&app.account)?; let attachments = matches .values_of("attachments") .unwrap_or_default() .map(String::from) .collect::>(); - let tpl = Msg::build_new_tpl(&config, &account)?; + let tpl = Msg::build_new_tpl(&app.config, &app.account)?; let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; let mut msg = Msg::from(content); msg.attachments = attachments; @@ -323,10 +324,10 @@ pub fn msg_matches( input::PostEditChoice::Send => { debug!("sending message…"); let msg = msg.to_sendable_msg()?; - smtp::send(&account, &msg)?; + smtp::send(&app.account, &msg)?; imap_conn.append_msg("Sent", &msg.formatted(), &[Flag::Seen])?; input::remove_draft()?; - info!("Message successfully sent"); + app.output.print("Message successfully sent"); break; } input::PostEditChoice::Edit => { @@ -338,7 +339,7 @@ pub fn msg_matches( debug!("saving to draft…"); imap_conn.append_msg("Drafts", &msg.to_vec()?, &[Flag::Seen])?; input::remove_draft()?; - info!("Message successfully saved to Drafts"); + app.output.print("Message successfully saved to Drafts"); break; } input::PostEditChoice::Discard => { @@ -353,7 +354,7 @@ pub fn msg_matches( return Ok(true); } - if let Some(matches) = matches.subcommand_matches("reply") { + if let Some(matches) = app.arg_matches.subcommand_matches("reply") { debug!("reply command matched"); let uid = matches.value_of("uid").unwrap(); @@ -366,12 +367,12 @@ pub fn msg_matches( debug!("found {} attachments", attachments.len()); trace!("attachments: {:?}", attachments); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&config, &account)? + msg.build_reply_all_tpl(&app.config, &app.account)? } else { - msg.build_reply_tpl(&config, &account)? + msg.build_reply_tpl(&app.config, &app.account)? }; let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; @@ -384,11 +385,11 @@ pub fn msg_matches( input::PostEditChoice::Send => { debug!("sending message…"); let msg = msg.to_sendable_msg()?; - smtp::send(&account, &msg)?; + smtp::send(&app.account, &msg)?; imap_conn.append_msg("Sent", &msg.formatted(), &[Flag::Seen])?; - imap_conn.add_flags(&mbox, uid, "\\Answered")?; + imap_conn.add_flags(&app.mbox, uid, "\\Answered")?; input::remove_draft()?; - info!("Message successfully sent"); + app.output.print("Message successfully sent"); break; } input::PostEditChoice::Edit => { @@ -400,7 +401,7 @@ pub fn msg_matches( debug!("saving to draft…"); imap_conn.append_msg("Drafts", &msg.to_vec()?, &[Flag::Seen])?; input::remove_draft()?; - info!("Message successfully saved to Drafts"); + app.output.print("Message successfully saved to Drafts"); break; } input::PostEditChoice::Discard => { @@ -416,7 +417,7 @@ pub fn msg_matches( return Ok(true); } - if let Some(matches) = matches.subcommand_matches("forward") { + if let Some(matches) = app.arg_matches.subcommand_matches("forward") { debug!("forward command matched"); let uid = matches.value_of("uid").unwrap(); @@ -429,9 +430,9 @@ pub fn msg_matches( debug!("found {} attachments", attachments.len()); trace!("attachments: {:?}", attachments); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = msg.build_forward_tpl(&config, &account)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); + let tpl = msg.build_forward_tpl(&app.config, &app.account)?; let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; let mut msg = Msg::from(content); msg.attachments = attachments; @@ -442,10 +443,10 @@ pub fn msg_matches( input::PostEditChoice::Send => { debug!("sending message…"); let msg = msg.to_sendable_msg()?; - smtp::send(&account, &msg)?; + smtp::send(&app.account, &msg)?; imap_conn.append_msg("Sent", &msg.formatted(), &[Flag::Seen])?; input::remove_draft()?; - info!("Message successfully sent"); + app.output.print("Message successfully sent"); break; } input::PostEditChoice::Edit => { @@ -457,7 +458,7 @@ pub fn msg_matches( debug!("saving to draft…"); imap_conn.append_msg("Drafts", &msg.to_vec()?, &[Flag::Seen])?; input::remove_draft()?; - info!("Message successfully saved to Drafts"); + app.output.print("Message successfully saved to Drafts"); break; } input::PostEditChoice::Discard => { @@ -473,14 +474,14 @@ pub fn msg_matches( return Ok(true); } - if let Some(matches) = matches.subcommand_matches("template") { + if let Some(matches) = app.arg_matches.subcommand_matches("template") { debug!("template command matched"); if let Some(_) = matches.subcommand_matches("new") { debug!("new command matched"); - let tpl = Msg::build_new_tpl(&config, &account)?; - info!("{}", tpl); + let tpl = Msg::build_new_tpl(&app.config, &app.account)?; trace!("tpl: {:?}", tpl); + app.output.print(tpl); } if let Some(matches) = matches.subcommand_matches("reply") { @@ -489,15 +490,15 @@ pub fn msg_matches( let uid = matches.value_of("uid").unwrap(); debug!("uid: {}", uid); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&config, &account)? + msg.build_reply_all_tpl(&app.config, &app.account)? } else { - msg.build_reply_tpl(&config, &account)? + msg.build_reply_tpl(&app.config, &app.account)? }; - info!("{}", tpl); trace!("tpl: {:?}", tpl); + app.output.print(tpl); imap_conn.logout(); } @@ -508,11 +509,11 @@ pub fn msg_matches( let uid = matches.value_of("uid").unwrap(); debug!("uid: {}", uid); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = msg.build_forward_tpl(&config, &account)?; - info!("{}", tpl); + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); + let tpl = msg.build_forward_tpl(&app.config, &app.account)?; trace!("tpl: {:?}", tpl); + app.output.print(tpl); imap_conn.logout(); } @@ -520,7 +521,7 @@ pub fn msg_matches( return Ok(true); } - if let Some(matches) = matches.subcommand_matches("copy") { + if let Some(matches) = app.arg_matches.subcommand_matches("copy") { debug!("copy command matched"); let uid = matches.value_of("uid").unwrap(); @@ -528,18 +529,22 @@ pub fn msg_matches( let target = matches.value_of("target").unwrap(); debug!("target: {}", &target); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); let mut flags = msg.flags.deref().to_vec(); flags.push(Flag::Seen); imap_conn.append_msg(target, &msg.raw, &flags)?; - info!("Message {} successfully copied to folder `{}`", uid, target); + debug!("message {} successfully copied to folder `{}`", uid, target); + app.output.print(format!( + "Message {} successfully copied to folder `{}`", + uid, target + )); imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("move") { + if let Some(matches) = app.arg_matches.subcommand_matches("move") { debug!("move command matched"); let uid = matches.value_of("uid").unwrap(); @@ -547,55 +552,61 @@ pub fn msg_matches( let target = matches.value_of("target").unwrap(); debug!("target: {}", &target); - let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); let mut flags = msg.flags.deref().to_vec(); flags.push(Flag::Seen); imap_conn.append_msg(target, &msg.raw, msg.flags.deref())?; - imap_conn.add_flags(&mbox, uid, "\\Seen \\Deleted")?; - info!("Message {} successfully moved to folder `{}`", uid, target); + imap_conn.add_flags(&app.mbox, uid, "\\Seen \\Deleted")?; + debug!("message {} successfully moved to folder `{}`", uid, target); + app.output.print(format!( + "Message {} successfully moved to folder `{}`", + uid, target + )); - imap_conn.expunge(&mbox)?; + imap_conn.expunge(&app.mbox)?; imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("delete") { + if let Some(matches) = app.arg_matches.subcommand_matches("delete") { debug!("delete command matched"); let uid = matches.value_of("uid").unwrap(); debug!("uid: {}", &uid); - let mut imap_conn = ImapConnector::new(&account)?; - imap_conn.add_flags(&mbox, uid, "\\Seen \\Deleted")?; - info!("Message {} successfully deleted", uid); + let mut imap_conn = ImapConnector::new(&app.account)?; + imap_conn.add_flags(&app.mbox, uid, "\\Seen \\Deleted")?; + debug!("message {} successfully deleted", uid); + app.output + .print(format!("Message {} successfully deleted", uid)); - imap_conn.expunge(&mbox)?; + imap_conn.expunge(&app.mbox)?; imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("send") { + if let Some(matches) = app.arg_matches.subcommand_matches("send") { debug!("send command matched"); - let mut imap_conn = ImapConnector::new(&account)?; + let mut imap_conn = ImapConnector::new(&app.account)?; let msg = matches.value_of("message").unwrap(); let msg = Msg::from(msg.to_string()); let msg = msg.to_sendable_msg()?; - smtp::send(&account, &msg)?; + smtp::send(&app.account, &msg)?; imap_conn.append_msg("Sent", &msg.formatted(), &[Flag::Seen])?; imap_conn.logout(); return Ok(true); } - if let Some(matches) = matches.subcommand_matches("save") { + if let Some(matches) = app.arg_matches.subcommand_matches("save") { debug!("save command matched"); - let mut imap_conn = ImapConnector::new(&account)?; + let mut imap_conn = ImapConnector::new(&app.account)?; let msg = matches.value_of("message").unwrap(); let msg = Msg::from(msg.to_string()); - imap_conn.append_msg(&mbox, &msg.to_vec()?, &[Flag::Seen])?; + imap_conn.append_msg(&app.mbox, &msg.to_vec()?, &[Flag::Seen])?; imap_conn.logout(); return Ok(true); @@ -604,10 +615,11 @@ pub fn msg_matches( { debug!("default list command matched"); - let mut imap_conn = ImapConnector::new(&account)?; - let msgs = imap_conn.list_msgs(&mbox, &config.default_page_size(&account), &0)?; + let mut imap_conn = ImapConnector::new(&app.account)?; + let msgs = + imap_conn.list_msgs(&app.mbox, &app.config.default_page_size(&app.account), &0)?; let msgs = Msgs::from(&msgs); - info!("{}", msgs); + app.output.print(msgs); imap_conn.logout(); Ok(true) diff --git a/src/msg/model.rs b/src/msg/model.rs index dd01a9c..5bc8d6c 100644 --- a/src/msg/model.rs +++ b/src/msg/model.rs @@ -15,7 +15,6 @@ use uuid::Uuid; use crate::{ config::model::{Account, Config}, flag::model::{Flag, Flags}, - output::fmt::{get_output_fmt, OutputFmt, Response}, table::{Cell, Row, Table}, }; @@ -33,17 +32,7 @@ pub struct Tpl(String); impl fmt::Display for Tpl { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - unsafe { - match get_output_fmt() { - &OutputFmt::Plain => { - write!(f, "{}", self.0) - } - &OutputFmt::Json => { - let res = serde_json::to_string(&Response::new(self)).unwrap(); - write!(f, "{}", res) - } - } - } + write!(f, "{}", self.0) } } @@ -132,17 +121,7 @@ impl Serialize for ReadableMsg { impl fmt::Display for ReadableMsg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - unsafe { - match get_output_fmt() { - &OutputFmt::Plain => { - writeln!(f, "{}", self.content) - } - &OutputFmt::Json => { - let res = serde_json::to_string(&Response::new(self)).unwrap(); - write!(f, "{}", res) - } - } - } + writeln!(f, "{}", self.content) } } @@ -677,16 +656,6 @@ impl<'m> From<&'m imap::types::ZeroCopy>> for Msgs<'m> { impl<'m> fmt::Display for Msgs<'m> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - unsafe { - match get_output_fmt() { - &OutputFmt::Plain => { - writeln!(f, "\n{}", Table::render(&self.0)) - } - &OutputFmt::Json => { - let res = serde_json::to_string(&Response::new(self)).unwrap(); - write!(f, "{}", res) - } - } - } + writeln!(f, "\n{}", Table::render(&self.0)) } } diff --git a/src/output/fmt.rs b/src/output/fmt.rs index 693af5c..fb8a9ed 100644 --- a/src/output/fmt.rs +++ b/src/output/fmt.rs @@ -1,15 +1,7 @@ use serde::Serialize; use std::fmt; -pub static mut OUTPUT_FMT: &'static OutputFmt = &OutputFmt::Plain; - -pub fn set_output_fmt(output_fmt: &'static OutputFmt) { - unsafe { OUTPUT_FMT = output_fmt } -} - -pub unsafe fn get_output_fmt() -> &'static OutputFmt { - OUTPUT_FMT -} +// Output format pub enum OutputFmt { Plain, @@ -38,6 +30,8 @@ impl fmt::Display for OutputFmt { } } +// Response helper + #[derive(Serialize)] pub struct Response { response: T, @@ -49,17 +43,4 @@ impl Response { } } -impl fmt::Display for Response { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - unsafe { - match get_output_fmt() { - &OutputFmt::Plain => { - writeln!(f, "{}", self.response) - } - &OutputFmt::Json => { - write!(f, "{}", serde_json::to_string(self).unwrap()) - } - } - } - } -} +// Print helper diff --git a/src/output/log.rs b/src/output/log.rs deleted file mode 100644 index 3dc6235..0000000 --- a/src/output/log.rs +++ /dev/null @@ -1,83 +0,0 @@ -use chrono::Local; -use env_logger; -use error_chain::error_chain; -use log::{self, debug, Level, LevelFilter}; -use std::{fmt, io, io::Write, ops::Deref}; - -use super::fmt::{set_output_fmt, OutputFmt}; - -error_chain! {} - -// Log level wrapper - -pub struct LogLevel(pub LevelFilter); - -impl From<&str> for LogLevel { - fn from(s: &str) -> Self { - match s { - "error" => Self(LevelFilter::Error), - "warn" => Self(LevelFilter::Warn), - "debug" => Self(LevelFilter::Debug), - "trace" => Self(LevelFilter::Trace), - "info" | _ => Self(LevelFilter::Info), - } - } -} - -impl fmt::Display for LogLevel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.deref()) - } -} - -impl Deref for LogLevel { - type Target = LevelFilter; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -// Init - -pub fn init(fmt: OutputFmt, filter: LogLevel) -> Result<()> { - let level_filter = filter.deref(); - let level = level_filter.to_level(); - - match fmt { - OutputFmt::Plain => { - set_output_fmt(&OutputFmt::Plain); - } - OutputFmt::Json => { - set_output_fmt(&OutputFmt::Json); - } - }; - - env_logger::Builder::new() - .target(env_logger::Target::Stdout) - .format(move |buf, record| match level { - None => Ok(()), - Some(Level::Info) => match record.metadata().level() { - Level::Info => write!(buf, "{}", record.args()), - Level::Error => writeln!(&mut io::stderr(), "{}", record.args()), - _ => writeln!(buf, "{}", record.args()), - }, - _ => { - writeln!( - buf, - "[{} {:5} {}] {}", - Local::now().format("%Y-%m-%dT%H:%M:%S"), - record.metadata().level(), - record.module_path().unwrap_or_default(), - record.args(), - ) - } - }) - .filter_level(*level_filter) - .init(); - - debug!("output format: {}", fmt); - debug!("log level: {}", filter); - - Ok(()) -} diff --git a/src/output/mod.rs b/src/output/mod.rs index ddf5f1f..7bfbe32 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,4 +1,3 @@ pub(crate) mod cli; -pub(crate) mod fmt; -pub(crate) mod log; +pub(crate) mod model; pub(crate) mod utils; diff --git a/src/output/model.rs b/src/output/model.rs new file mode 100644 index 0000000..efff04f --- /dev/null +++ b/src/output/model.rs @@ -0,0 +1,67 @@ +use serde::Serialize; +use std::fmt; + +// Output format + +#[derive(Debug)] +pub enum OutputFmt { + Plain, + Json, +} + +impl From<&str> for OutputFmt { + fn from(s: &str) -> Self { + match s { + "json" => Self::Json, + "plain" | _ => Self::Plain, + } + } +} + +impl fmt::Display for OutputFmt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let fmt = match *self { + OutputFmt::Json => "JSON", + OutputFmt::Plain => "PLAIN", + }; + + write!(f, "{}", fmt) + } +} + +// JSON output helper + +#[derive(Debug, Serialize)] +pub struct OutputJson { + response: T, +} + +impl OutputJson { + pub fn new(response: T) -> Self { + Self { response } + } +} + +// Output + +#[derive(Debug)] +pub struct Output { + fmt: OutputFmt, +} + +impl Output { + pub fn new(fmt: &str) -> Self { + Self { fmt: fmt.into() } + } + + pub fn print(&self, item: T) { + match self.fmt { + OutputFmt::Plain => { + println!("{}", item) + } + OutputFmt::Json => { + print!("{}", serde_json::to_string(&OutputJson::new(item)).unwrap()) + } + } + } +}