improve errors management

This commit is contained in:
Clément DOUIN 2021-03-12 22:59:08 +01:00
parent 781c4a2722
commit 1e5cce0205
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
10 changed files with 699 additions and 894 deletions

68
Cargo.lock generated
View file

@ -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"

View file

@ -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"

469
src/app.rs Normal file
View file

@ -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 <clement.douin@posteo.net>")
.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(())
}
}

View file

@ -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<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
impl From<toml::de::Error> for Error {
fn from(err: toml::de::Error) -> Error {
Error::ParseTomlError(err)
}
}
impl From<env::VarError> for Error {
fn from(err: env::VarError) -> Error {
Error::GetEnvVarError(err)
}
}
impl From<output::Error> for Error {
fn from(err: output::Error) -> Error {
Error::OutputError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
error_chain! {}
// Account
@ -110,18 +40,32 @@ impl Account {
}
pub fn imap_passwd(&self) -> Result<String> {
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<SmtpCredentials> {
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<PathBuf> {
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<PathBuf> {
let path = env::var("HOME")?;
fn path_from_xdg_alt() -> Result<PathBuf> {
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<PathBuf> {
let mut path = env::temp_dir();
path.push("himalaya");
path.push("config.toml");
fn path_from_home() -> Result<PathBuf> {
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<Self> {
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(())
}
}

View file

@ -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<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Error::CreateTlsConnectorError(err)
}
}
impl From<imap::Error> for Error {
fn from(err: imap::Error) -> Error {
Error::CreateImapSession(err)
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::ParseEmailError(err)
}
}
impl From<config::Error> for Error {
fn from(err: config::Error) -> Error {
Error::ConfigError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// 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<Self> {
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<Option<u32>> {
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<Mboxes> {
let mboxes = self
.sess
.list(Some(""), Some("*"))?
.list(Some(""), Some("*"))
.chain_err(|| "Cannot list mailboxes")?
.iter()
.map(Mbox::from_name)
.collect::<Vec<_>>();
@ -149,14 +97,20 @@ impl<'a> ImapConnector<'a> {
}
pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Msgs> {
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<Msgs> {
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::<Vec<_>>();
@ -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::<Vec<_>>();
@ -195,17 +153,26 @@ impl<'a> ImapConnector<'a> {
}
pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {
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(())
}
}

View file

@ -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<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
impl From<env::VarError> for Error {
fn from(err: env::VarError) -> Error {
Error::GetEditorEnvVarNotFoundError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Utils
error_chain! {}
pub fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
// 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()),
}
}

View file

@ -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<config::Error> for Error {
fn from(err: config::Error) -> Error {
Error::ConfigError(err)
}
}
impl From<input::Error> for Error {
fn from(err: input::Error) -> Error {
Error::InputError(err)
}
}
impl From<output::Error> for Error {
fn from(err: output::Error) -> Error {
Error::OutputError(err)
}
}
impl From<msg::Error> for Error {
fn from(err: msg::Error) -> Error {
Error::MsgError(err)
}
}
impl From<imap::Error> for Error {
fn from(err: imap::Error) -> Error {
Error::ImapError(err)
}
}
impl From<smtp::Error> for Error {
fn from(err: smtp::Error) -> Error {
Error::SmtpError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// 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 <clement.douin@posteo.net>")
.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 => (),
}
}
}

View file

@ -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<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::ParseMsgError(err)
}
}
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
Error::BuildSendableMsgError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Template
#[derive(Debug)]

View file

@ -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<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
impl From<string::FromUtf8Error> for Error {
fn from(err: string::FromUtf8Error) -> Error {
Error::ParseUtf8Error(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::SerializeJsonError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Utils
error_chain! {}
pub fn run_cmd(cmd: &str) -> Result<String> {
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<T: Display + Serialize>(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()),
}

View file

@ -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<lettre::transport::smtp::Error> for Error {
fn from(err: lettre::transport::smtp::Error) -> Error {
Error::TransportError(err)
}
}
impl From<config::Error> for Error {
fn from(err: config::Error) -> Error {
Error::ConfigError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// 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(())
}