mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-22 02:50:19 +00:00
improve errors management
This commit is contained in:
parent
781c4a2722
commit
1e5cce0205
10 changed files with 699 additions and 894 deletions
68
Cargo.lock
generated
68
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
469
src/app.rs
Normal 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(())
|
||||
}
|
||||
}
|
131
src/config.rs
131
src/config.rs
|
@ -1,82 +1,12 @@
|
|||
use error_chain::error_chain;
|
||||
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env, fmt,
|
||||
fs::File,
|
||||
io::{self, Read},
|
||||
path::PathBuf,
|
||||
result,
|
||||
};
|
||||
use std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf};
|
||||
use toml;
|
||||
|
||||
use crate::output::{self, run_cmd};
|
||||
use crate::output::run_cmd;
|
||||
|
||||
// Error wrapper
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
IoError(io::Error),
|
||||
ParseTomlError(toml::de::Error),
|
||||
ParseTomlAccountsError,
|
||||
GetEnvVarError(env::VarError),
|
||||
GetPathNotFoundError,
|
||||
GetAccountNotFoundError(String),
|
||||
GetAccountDefaultNotFoundError,
|
||||
OutputError(output::Error),
|
||||
|
||||
// new erorrs,
|
||||
RunNotifyCmdError(output::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
IoError(err) => err.fmt(f),
|
||||
ParseTomlError(err) => err.fmt(f),
|
||||
ParseTomlAccountsError => write!(f, "no account found"),
|
||||
GetEnvVarError(err) => err.fmt(f),
|
||||
GetPathNotFoundError => write!(f, "path not found"),
|
||||
GetAccountNotFoundError(account) => write!(f, "account {} not found", account),
|
||||
GetAccountDefaultNotFoundError => write!(f, "no default account found"),
|
||||
OutputError(err) => err.fmt(f),
|
||||
RunNotifyCmdError(err) => {
|
||||
write!(f, "run notification cmd: ")?;
|
||||
err.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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(())
|
||||
}
|
||||
}
|
||||
|
|
147
src/imap.rs
147
src/imap.rs
|
@ -1,6 +1,7 @@
|
|||
use error_chain::error_chain;
|
||||
use imap;
|
||||
use native_tls::{self, TlsConnector, TlsStream};
|
||||
use std::{fmt, net::TcpStream, result};
|
||||
use std::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
config::{self, Account, Config},
|
||||
|
@ -8,78 +9,12 @@ use crate::{
|
|||
msg::{Msg, Msgs},
|
||||
};
|
||||
|
||||
// Error wrapper
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
CreateTlsConnectorError(native_tls::Error),
|
||||
CreateImapSession(imap::Error),
|
||||
ParseEmailError(mailparse::MailParseError),
|
||||
ReadEmailNotFoundError(String),
|
||||
ReadEmailEmptyPartError(String, String),
|
||||
ExtractAttachmentsEmptyError(String),
|
||||
ConfigError(config::Error),
|
||||
|
||||
// new errors
|
||||
IdleError(imap::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
CreateTlsConnectorError(err) => err.fmt(f),
|
||||
CreateImapSession(err) => err.fmt(f),
|
||||
ParseEmailError(err) => err.fmt(f),
|
||||
ConfigError(err) => err.fmt(f),
|
||||
ReadEmailNotFoundError(uid) => {
|
||||
write!(f, "no email found for uid {}", uid)
|
||||
}
|
||||
ReadEmailEmptyPartError(uid, mime) => {
|
||||
write!(f, "no {} content found for uid {}", mime, uid)
|
||||
}
|
||||
ExtractAttachmentsEmptyError(uid) => {
|
||||
write!(f, "no attachment found for uid {}", uid)
|
||||
}
|
||||
IdleError(err) => {
|
||||
write!(f, "IMAP idle mode: ")?;
|
||||
err.fmt(f)
|
||||
}
|
||||
}
|
||||
error_chain! {
|
||||
links {
|
||||
Config(config::Error, config::ErrorKind);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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(())
|
||||
}
|
||||
}
|
||||
|
|
66
src/input.rs
66
src/input.rs
|
@ -1,72 +1,43 @@
|
|||
use error_chain::error_chain;
|
||||
use std::{
|
||||
env, fmt,
|
||||
env,
|
||||
fs::{remove_file, File},
|
||||
io::{self, Read, Write},
|
||||
process::Command,
|
||||
result,
|
||||
};
|
||||
|
||||
// Error wrapper
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
IoError(io::Error),
|
||||
GetEditorEnvVarNotFoundError(env::VarError),
|
||||
AskForConfirmationDeniedError,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "input: ")?;
|
||||
|
||||
match self {
|
||||
Error::IoError(err) => err.fmt(f),
|
||||
Error::GetEditorEnvVarNotFoundError(err) => err.fmt(f),
|
||||
Error::AskForConfirmationDeniedError => write!(f, "action cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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()),
|
||||
}
|
||||
}
|
||||
|
|
549
src/main.rs
549
src/main.rs
|
@ -1,3 +1,4 @@
|
|||
mod app;
|
||||
mod config;
|
||||
mod imap;
|
||||
mod input;
|
||||
|
@ -7,545 +8,17 @@ mod output;
|
|||
mod smtp;
|
||||
mod table;
|
||||
|
||||
use clap::{App, AppSettings, Arg, SubCommand};
|
||||
use std::{fmt, fs, process::exit, result};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::imap::ImapConnector;
|
||||
use crate::msg::{Attachments, Msg, ReadableMsg};
|
||||
use crate::output::print;
|
||||
|
||||
const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
const DEFAULT_PAGE: usize = 0;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
ConfigError(config::Error),
|
||||
InputError(input::Error),
|
||||
OutputError(output::Error),
|
||||
MsgError(msg::Error),
|
||||
ImapError(imap::Error),
|
||||
SmtpError(smtp::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::ConfigError(err) => err.fmt(f),
|
||||
Error::InputError(err) => err.fmt(f),
|
||||
Error::OutputError(err) => err.fmt(f),
|
||||
Error::MsgError(err) => err.fmt(f),
|
||||
Error::ImapError(err) => err.fmt(f),
|
||||
Error::SmtpError(err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
36
src/msg.rs
36
src/msg.rs
|
@ -1,3 +1,4 @@
|
|||
use error_chain::error_chain;
|
||||
use lettre;
|
||||
use mailparse::{self, MailHeaderMap};
|
||||
use rfc2047_decoder;
|
||||
|
@ -11,40 +12,13 @@ use uuid::Uuid;
|
|||
use crate::config::{Account, Config};
|
||||
use crate::table::{self, DisplayRow, DisplayTable};
|
||||
|
||||
// Error wrapper
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
ParseMsgError(mailparse::MailParseError),
|
||||
BuildSendableMsgError(lettre::error::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "(msg): ")?;
|
||||
match self {
|
||||
Error::ParseMsgError(err) => err.fmt(f),
|
||||
Error::BuildSendableMsgError(err) => err.fmt(f),
|
||||
}
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Mailparse(mailparse::MailParseError);
|
||||
Lettre(lettre::error::Error);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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)]
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
|
||||
|
|
59
src/smtp.rs
59
src/smtp.rs
|
@ -1,53 +1,28 @@
|
|||
use lettre;
|
||||
use std::{fmt, result};
|
||||
use error_chain::error_chain;
|
||||
use lettre::{self, transport::smtp::SmtpTransport, Transport};
|
||||
|
||||
use crate::config::{self, Account};
|
||||
|
||||
// Error wrapper
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
TransportError(lettre::transport::smtp::Error),
|
||||
ConfigError(config::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "(smtp): ")?;
|
||||
match self {
|
||||
Error::TransportError(err) => err.fmt(f),
|
||||
Error::ConfigError(err) => err.fmt(f),
|
||||
}
|
||||
error_chain! {
|
||||
links {
|
||||
Config(config::Error, config::ErrorKind);
|
||||
}
|
||||
foreign_links {
|
||||
Smtp(lettre::transport::smtp::Error);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<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(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue