diff --git a/src/config.rs b/src/config.rs index b8915d8..e733828 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use std::{ }; use toml; -use crate::io::run_cmd; +use crate::output::{self, run_cmd}; // Error wrapper @@ -23,8 +23,7 @@ pub enum Error { GetPathNotFoundError, GetAccountNotFoundError(String), GetAccountDefaultNotFoundError, - ParseImapPasswdUtf8Error, - ParseSmtpPasswdUtf8Error, + OutputError(output::Error), } impl fmt::Display for Error { @@ -39,8 +38,7 @@ impl fmt::Display for Error { Error::GetPathNotFoundError => write!(f, "path not found"), Error::GetAccountNotFoundError(account) => write!(f, "account {} not found", account), Error::GetAccountDefaultNotFoundError => write!(f, "no default account found"), - Error::ParseImapPasswdUtf8Error => write!(f, "imap passwd invalid utf8"), - Error::ParseSmtpPasswdUtf8Error => write!(f, "smtp passwd invalid utf8"), + Error::OutputError(err) => err.fmt(f), } } } @@ -63,6 +61,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: output::Error) -> Error { + Error::OutputError(err) + } +} + // Result wrapper type Result = result::Result; @@ -92,18 +96,14 @@ pub struct Account { impl Account { pub fn imap_passwd(&self) -> Result { - let cmd = run_cmd(&self.imap_passwd_cmd)?; - let passwd = String::from_utf8(cmd.stdout); - let passwd = passwd.map_err(|_| Error::ParseImapPasswdUtf8Error)?; + let passwd = run_cmd(&self.imap_passwd_cmd)?; let passwd = passwd.trim_end_matches("\n").to_owned(); Ok(passwd) } pub fn smtp_creds(&self) -> Result { - let cmd = run_cmd(&self.smtp_passwd_cmd)?; - let passwd = String::from_utf8(cmd.stdout); - let passwd = passwd.map_err(|_| Error::ParseImapPasswdUtf8Error)?; + let passwd = run_cmd(&self.smtp_passwd_cmd)?; let passwd = passwd.trim_end_matches("\n").to_owned(); Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd)) diff --git a/src/imap.rs b/src/imap.rs index 2b99c40..550245c 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -3,8 +3,8 @@ use native_tls::{self, TlsConnector, TlsStream}; use std::{fmt, net::TcpStream, result}; use crate::config::{self, Account}; -use crate::mbox::Mbox; -use crate::msg::Msg; +use crate::mbox::{Mbox, Mboxes}; +use crate::msg::{Msg, Msgs}; // Error wrapper @@ -94,7 +94,7 @@ impl<'a> ImapConnector<'a> { } } - pub fn list_mboxes(&mut self) -> Result> { + pub fn list_mboxes(&mut self) -> Result { let mboxes = self .sess .list(Some(""), Some("*"))? @@ -102,10 +102,10 @@ impl<'a> ImapConnector<'a> { .map(Mbox::from_name) .collect::>(); - Ok(mboxes) + Ok(Mboxes(mboxes)) } - pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result> { + pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result { let last_seq = self.sess.select(mbox)?.exists; let begin = last_seq - (page * page_size); let end = begin - (page_size - 1); @@ -119,7 +119,7 @@ impl<'a> ImapConnector<'a> { .map(Msg::from) .collect::>(); - Ok(msgs) + Ok(Msgs(msgs)) } pub fn search_msgs( @@ -128,7 +128,7 @@ impl<'a> ImapConnector<'a> { query: &str, page_size: &usize, page: &usize, - ) -> Result> { + ) -> Result { self.sess.select(mbox)?; let begin = page * page_size; @@ -149,7 +149,7 @@ impl<'a> ImapConnector<'a> { .map(Msg::from) .collect::>(); - Ok(msgs) + Ok(Msgs(msgs)) } pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result> { diff --git a/src/io.rs b/src/input.rs similarity index 87% rename from src/io.rs rename to src/input.rs index 80de57d..e59418c 100644 --- a/src/io.rs +++ b/src/input.rs @@ -2,7 +2,7 @@ use std::{ env, fmt, fs::{remove_file, File}, io::{self, Read, Write}, - process::{Command, Output}, + process::Command, result, }; @@ -78,11 +78,3 @@ pub fn ask_for_confirmation(prompt: &str) -> Result<()> { _ => Err(Error::AskForConfirmationDeniedError), } } - -pub fn run_cmd(cmd: &str) -> io::Result { - if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output() - } else { - Command::new("sh").arg("-c").arg(cmd).output() - } -} diff --git a/src/main.rs b/src/main.rs index ea89734..8aad980 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ mod config; mod imap; -mod io; +mod input; mod mbox; mod msg; +mod output; mod smtp; mod table; @@ -12,7 +13,7 @@ use std::{fmt, fs, process::exit, result}; use crate::config::Config; use crate::imap::ImapConnector; use crate::msg::Msg; -use crate::table::DisplayTable; +use crate::output::print; const DEFAULT_PAGE_SIZE: usize = 10; const DEFAULT_PAGE: usize = 0; @@ -20,7 +21,8 @@ const DEFAULT_PAGE: usize = 0; #[derive(Debug)] pub enum Error { ConfigError(config::Error), - IoError(io::Error), + InputError(input::Error), + OutputError(output::Error), MsgError(msg::Error), ImapError(imap::Error), SmtpError(smtp::Error), @@ -30,7 +32,8 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Error::ConfigError(err) => err.fmt(f), - Error::IoError(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), @@ -44,9 +47,15 @@ impl From for Error { } } -impl From for Error { - fn from(err: crate::io::Error) -> Error { - Error::IoError(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) } } @@ -117,6 +126,15 @@ fn run() -> Result<()> { .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") @@ -199,6 +217,7 @@ fn run() -> Result<()> { .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()?; @@ -206,7 +225,7 @@ fn run() -> Result<()> { let mut imap_conn = ImapConnector::new(&account)?; let mboxes = imap_conn.list_mboxes()?; - println!("{}", mboxes.to_table()); + print(&output_type, mboxes)?; imap_conn.close(); } @@ -215,7 +234,6 @@ fn run() -> Result<()> { let config = Config::new_from_file()?; let account = config.get_account(account_name)?; let mut imap_conn = ImapConnector::new(&account)?; - let mbox = matches.value_of("mailbox").unwrap(); let page_size: u32 = matches .value_of("size") @@ -229,7 +247,7 @@ fn run() -> Result<()> { .unwrap_or(DEFAULT_PAGE as u32); let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; - println!("{}", msgs.to_table()); + print(&output_type, msgs)?; imap_conn.close(); } @@ -238,7 +256,6 @@ fn run() -> Result<()> { let config = Config::new_from_file()?; let account = config.get_account(account_name)?; let mut imap_conn = ImapConnector::new(&account)?; - let mbox = matches.value_of("mailbox").unwrap(); let page_size: usize = matches .value_of("size") @@ -276,7 +293,7 @@ fn run() -> Result<()> { .join(" "); let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; - println!("{}", msgs.to_table()); + print(&output_type, msgs)?; imap_conn.close(); } @@ -285,12 +302,11 @@ fn run() -> Result<()> { let config = Config::new_from_file()?; let account = config.get_account(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 = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + let text_bodies = msg.text_bodies(&mime)?; println!("{}", text_bodies); @@ -301,10 +317,8 @@ fn run() -> Result<()> { let config = Config::new_from_file()?; let account = config.get_account(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 parts = msg.extract_attachments()?; @@ -314,7 +328,7 @@ fn run() -> Result<()> { println!("{} attachment(s) found for message {}", parts.len(), uid); parts.iter().for_each(|(filename, bytes)| { let filepath = config.downloads_filepath(&account, &filename); - println!("Downloading {} …", filename); + println!("Downloading {}…", filename); fs::write(filepath, bytes).unwrap() }); println!("Done!"); @@ -327,14 +341,13 @@ fn run() -> Result<()> { let config = Config::new_from_file()?; let account = config.get_account(account_name)?; let mut imap_conn = ImapConnector::new(&account)?; - let tpl = Msg::build_new_tpl(&config, &account)?; - let content = io::open_editor_with_tpl(&tpl.as_bytes())?; + let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let msg = Msg::from(content); - io::ask_for_confirmation("Send the message?")?; + input::ask_for_confirmation("Send the message?")?; - println!("Sending …"); + println!("Sending…"); smtp::send(&account, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?; println!("Done!"); @@ -357,12 +370,12 @@ fn run() -> Result<()> { msg.build_reply_tpl(&config, &account)? }; - let content = io::open_editor_with_tpl(&tpl.as_bytes())?; + let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let msg = Msg::from(content); - io::ask_for_confirmation("Send the message?")?; + input::ask_for_confirmation("Send the message?")?; - println!("Sending …"); + println!("Sending…"); smtp::send(&account, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?; println!("Done!"); @@ -380,12 +393,12 @@ fn run() -> Result<()> { let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = msg.build_forward_tpl(&config, &account)?; - let content = io::open_editor_with_tpl(&tpl.as_bytes())?; + let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let msg = Msg::from(content); - io::ask_for_confirmation("Send the message?")?; + input::ask_for_confirmation("Send the message?")?; - println!("Sending …"); + println!("Sending…"); smtp::send(&account, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?; println!("Done!"); diff --git a/src/mbox.rs b/src/mbox.rs index 8f6290e..2cb894e 100644 --- a/src/mbox.rs +++ b/src/mbox.rs @@ -1,7 +1,12 @@ use imap; +use serde::Serialize; +use std::fmt; use crate::table::{self, DisplayRow, DisplayTable}; +// Mbox + +#[derive(Debug, Serialize)] pub struct Mbox { pub delim: String, pub name: String, @@ -30,7 +35,12 @@ impl DisplayRow for Mbox { } } -impl<'a> DisplayTable<'a, Mbox> for Vec { +// Mboxes + +#[derive(Debug, Serialize)] +pub struct Mboxes(pub Vec); + +impl<'a> DisplayTable<'a, Mbox> for Mboxes { fn header_row() -> Vec { use crate::table::*; @@ -42,6 +52,12 @@ impl<'a> DisplayTable<'a, Mbox> for Vec { } fn rows(&self) -> &Vec { - self + &self.0 + } +} + +impl fmt::Display for Mboxes { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_table()) } } diff --git a/src/msg.rs b/src/msg.rs index 8a43a12..8d23e6a 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -1,5 +1,6 @@ use lettre; use mailparse::{self, MailHeaderMap}; +use serde::Serialize; use std::{fmt, result}; use crate::config::{Account, Config}; @@ -41,10 +42,12 @@ type Result = result::Result; // Msg -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct Msg { pub uid: u32, pub flags: Vec, + + #[serde(skip_serializing)] raw: Vec, } @@ -406,7 +409,12 @@ impl DisplayRow for Msg { } } -impl<'a> DisplayTable<'a, Msg> for Vec { +// Msgs + +#[derive(Debug, Serialize)] +pub struct Msgs(pub Vec); + +impl<'a> DisplayTable<'a, Msg> for Msgs { fn header_row() -> Vec { use crate::table::*; @@ -420,6 +428,12 @@ impl<'a> DisplayTable<'a, Msg> for Vec { } fn rows(&self) -> &Vec { - self + &self.0 + } +} + +impl fmt::Display for Msgs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_table()) } } diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..7063612 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,71 @@ +use serde::Serialize; +use std::{ + fmt::{self, Display}, + io, + process::Command, + result, string, +}; + +// 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 + +pub fn run_cmd(cmd: &str) -> Result { + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(&["/C", cmd]).output()? + } else { + Command::new("sh").arg("-c").arg(cmd).output()? + }; + + Ok(String::from_utf8(output.stdout)?) +} + +pub fn print(output_type: &str, item: T) -> Result<()> { + match output_type { + "json" => print!("{}", serde_json::to_string(&item)?), + "text" | _ => println!("{}", item.to_string()), + } + + Ok(()) +}