mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-20 07:01:12 +00:00
improve errors management
This commit is contained in:
parent
781c4a2722
commit
1e5cce0205
68
Cargo.lock
generated
68
Cargo.lock
generated
|
@ -1,5 +1,20 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# 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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.15"
|
version = "0.7.15"
|
||||||
|
@ -41,6 +56,20 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
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]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
@ -179,6 +208,16 @@ dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -217,6 +256,12 @@ dependencies = [
|
||||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
"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]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.17"
|
version = "0.1.17"
|
||||||
|
@ -231,6 +276,7 @@ name = "himalaya"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"error-chain",
|
||||||
"imap",
|
"imap",
|
||||||
"lettre",
|
"lettre",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
|
@ -453,6 +499,16 @@ version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
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]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
@ -512,6 +568,12 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
|
@ -720,6 +782,12 @@ dependencies = [
|
||||||
"quoted_printable",
|
"quoted_printable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
|
|
@ -7,6 +7,7 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
|
error-chain = "0.12.4"
|
||||||
imap = "2.4.0"
|
imap = "2.4.0"
|
||||||
lettre = "0.10.0-alpha.4"
|
lettre = "0.10.0-alpha.4"
|
||||||
mailparse = "0.13.1"
|
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 lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf};
|
||||||
collections::HashMap,
|
|
||||||
env, fmt,
|
|
||||||
fs::File,
|
|
||||||
io::{self, Read},
|
|
||||||
path::PathBuf,
|
|
||||||
result,
|
|
||||||
};
|
|
||||||
use toml;
|
use toml;
|
||||||
|
|
||||||
use crate::output::{self, run_cmd};
|
use crate::output::run_cmd;
|
||||||
|
|
||||||
// Error wrapper
|
error_chain! {}
|
||||||
|
|
||||||
#[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>;
|
|
||||||
|
|
||||||
// Account
|
// Account
|
||||||
|
|
||||||
|
@ -110,18 +40,32 @@ impl Account {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn imap_passwd(&self) -> Result<String> {
|
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();
|
let passwd = passwd.trim_end_matches("\n").to_owned();
|
||||||
|
|
||||||
Ok(passwd)
|
Ok(passwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn imap_starttls(&self) -> bool {
|
||||||
|
match self.imap_starttls {
|
||||||
|
Some(true) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
|
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();
|
let passwd = passwd.trim_end_matches("\n").to_owned();
|
||||||
|
|
||||||
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
|
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn smtp_starttls(&self) -> bool {
|
||||||
|
match self.smtp_starttls {
|
||||||
|
Some(true) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
|
@ -139,7 +83,8 @@ pub struct Config {
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
fn path_from_xdg() -> Result<PathBuf> {
|
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);
|
let mut path = PathBuf::from(path);
|
||||||
path.push("himalaya");
|
path.push("himalaya");
|
||||||
path.push("config.toml");
|
path.push("config.toml");
|
||||||
|
@ -147,8 +92,8 @@ impl Config {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_from_home() -> Result<PathBuf> {
|
fn path_from_xdg_alt() -> Result<PathBuf> {
|
||||||
let path = env::var("HOME")?;
|
let path = env::var("HOME").chain_err(|| "Cannot find `HOME` env var")?;
|
||||||
let mut path = PathBuf::from(path);
|
let mut path = PathBuf::from(path);
|
||||||
path.push(".config");
|
path.push(".config");
|
||||||
path.push("himalaya");
|
path.push("himalaya");
|
||||||
|
@ -157,10 +102,10 @@ impl Config {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_from_tmp() -> Result<PathBuf> {
|
fn path_from_home() -> Result<PathBuf> {
|
||||||
let mut path = env::temp_dir();
|
let path = env::var("HOME").chain_err(|| "Cannot find `HOME` env var")?;
|
||||||
path.push("himalaya");
|
let mut path = PathBuf::from(path);
|
||||||
path.push("config.toml");
|
path.push(".himalayarc");
|
||||||
|
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
@ -168,15 +113,17 @@ impl Config {
|
||||||
pub fn new_from_file() -> Result<Self> {
|
pub fn new_from_file() -> Result<Self> {
|
||||||
let mut file = File::open(
|
let mut file = File::open(
|
||||||
Self::path_from_xdg()
|
Self::path_from_xdg()
|
||||||
|
.or_else(|_| Self::path_from_xdg_alt())
|
||||||
.or_else(|_| Self::path_from_home())
|
.or_else(|_| Self::path_from_home())
|
||||||
.or_else(|_| Self::path_from_tmp())
|
.chain_err(|| "Cannot find config path")?,
|
||||||
.or_else(|_| Err(Error::GetPathNotFoundError))?,
|
)
|
||||||
)?;
|
.chain_err(|| "Cannot open config file")?;
|
||||||
|
|
||||||
let mut content = vec![];
|
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> {
|
pub fn find_account_by_name(&self, name: Option<&str>) -> Result<&Account> {
|
||||||
|
@ -184,13 +131,13 @@ impl Config {
|
||||||
Some(name) => self
|
Some(name) => self
|
||||||
.accounts
|
.accounts
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| Error::GetAccountNotFoundError(name.to_owned())),
|
.ok_or_else(|| format!("Cannot find account `{}`", name).into()),
|
||||||
None => self
|
None => self
|
||||||
.accounts
|
.accounts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, account)| account.default.unwrap_or(false))
|
.find(|(_, account)| account.default.unwrap_or(false))
|
||||||
.map(|(_, account)| account)
|
.map(|(_, account)| account)
|
||||||
.ok_or_else(|| Error::GetAccountDefaultNotFoundError),
|
.ok_or_else(|| "Cannot find default account".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +165,9 @@ impl Config {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| format!(r#"{} "{}" "{}""#, s, subject, sender))
|
.map(|s| format!(r#"{} "{}" "{}""#, s, subject, sender))
|
||||||
.unwrap_or(default_cmd);
|
.unwrap_or(default_cmd);
|
||||||
run_cmd(&cmd).map_err(Error::RunNotifyCmdError)?;
|
|
||||||
|
run_cmd(&cmd).chain_err(|| "Cannot run notify cmd")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
147
src/imap.rs
147
src/imap.rs
|
@ -1,6 +1,7 @@
|
||||||
|
use error_chain::error_chain;
|
||||||
use imap;
|
use imap;
|
||||||
use native_tls::{self, TlsConnector, TlsStream};
|
use native_tls::{self, TlsConnector, TlsStream};
|
||||||
use std::{fmt, net::TcpStream, result};
|
use std::net::TcpStream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{self, Account, Config},
|
config::{self, Account, Config},
|
||||||
|
@ -8,77 +9,11 @@ use crate::{
|
||||||
msg::{Msg, Msgs},
|
msg::{Msg, Msgs},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Error wrapper
|
error_chain! {
|
||||||
|
links {
|
||||||
#[derive(Debug)]
|
Config(config::Error, config::ErrorKind);
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[derive(Debug)]
|
||||||
pub struct ImapConnector<'a> {
|
pub struct ImapConnector<'a> {
|
||||||
|
@ -88,14 +23,18 @@ pub struct ImapConnector<'a> {
|
||||||
|
|
||||||
impl<'a> ImapConnector<'a> {
|
impl<'a> ImapConnector<'a> {
|
||||||
pub fn new(account: &'a Account) -> Result<Self> {
|
pub fn new(account: &'a Account) -> Result<Self> {
|
||||||
let tls = TlsConnector::new()?;
|
let tls = TlsConnector::new().chain_err(|| "Cannot create TLS connector")?;
|
||||||
let client = match account.imap_starttls {
|
let client = if account.imap_starttls() {
|
||||||
Some(true) => imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls),
|
imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls)
|
||||||
_ => imap::connect(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
|
let sess = client
|
||||||
.login(&account.imap_login, &account.imap_passwd()?)
|
.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 })
|
Ok(Self { account, sess })
|
||||||
}
|
}
|
||||||
|
@ -107,24 +46,32 @@ impl<'a> ImapConnector<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_new_seq(&mut self) -> Result<Option<u32>> {
|
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<()> {
|
pub fn idle(&mut self, config: &Config, mbox: &str) -> Result<()> {
|
||||||
let mut prev_seq = 0;
|
let mut prev_seq = 0;
|
||||||
self.sess.examine(mbox)?;
|
self.sess
|
||||||
|
.examine(mbox)
|
||||||
|
.chain_err(|| format!("Cannot examine mailbox `{}`", mbox))?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
self.sess
|
self.sess
|
||||||
.idle()
|
.idle()
|
||||||
.and_then(|idle| idle.wait_keepalive())
|
.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 let Some(seq) = self.last_new_seq()? {
|
||||||
if prev_seq != seq {
|
if prev_seq != seq {
|
||||||
if let Some(msg) = self
|
if let Some(msg) = self
|
||||||
.sess
|
.sess
|
||||||
.uid_fetch(seq.to_string(), "(ENVELOPE)")?
|
.uid_fetch(seq.to_string(), "(ENVELOPE)")
|
||||||
|
.chain_err(|| "Cannot fetch enveloppe")?
|
||||||
.iter()
|
.iter()
|
||||||
.next()
|
.next()
|
||||||
.map(Msg::from)
|
.map(Msg::from)
|
||||||
|
@ -140,7 +87,8 @@ impl<'a> ImapConnector<'a> {
|
||||||
pub fn list_mboxes(&mut self) -> Result<Mboxes> {
|
pub fn list_mboxes(&mut self) -> Result<Mboxes> {
|
||||||
let mboxes = self
|
let mboxes = self
|
||||||
.sess
|
.sess
|
||||||
.list(Some(""), Some("*"))?
|
.list(Some(""), Some("*"))
|
||||||
|
.chain_err(|| "Cannot list mailboxes")?
|
||||||
.iter()
|
.iter()
|
||||||
.map(Mbox::from_name)
|
.map(Mbox::from_name)
|
||||||
.collect::<Vec<_>>();
|
.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> {
|
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 begin = last_seq - page * page_size;
|
||||||
let end = begin - (begin - 1).min(page_size - 1);
|
let end = begin - (begin - 1).min(page_size - 1);
|
||||||
let range = format!("{}:{}", begin, end);
|
let range = format!("{}:{}", begin, end);
|
||||||
|
|
||||||
let msgs = self
|
let msgs = self
|
||||||
.sess
|
.sess
|
||||||
.fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)")?
|
.fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)")
|
||||||
|
.chain_err(|| "Cannot fetch messages")?
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.map(Msg::from)
|
.map(Msg::from)
|
||||||
|
@ -172,13 +126,16 @@ impl<'a> ImapConnector<'a> {
|
||||||
page_size: &usize,
|
page_size: &usize,
|
||||||
page: &usize,
|
page: &usize,
|
||||||
) -> Result<Msgs> {
|
) -> Result<Msgs> {
|
||||||
self.sess.select(mbox)?;
|
self.sess
|
||||||
|
.select(mbox)
|
||||||
|
.chain_err(|| format!("Cannot select mailbox `{}`", mbox))?;
|
||||||
|
|
||||||
let begin = page * page_size;
|
let begin = page * page_size;
|
||||||
let end = begin + (page_size - 1);
|
let end = begin + (page_size - 1);
|
||||||
let uids = self
|
let uids = self
|
||||||
.sess
|
.sess
|
||||||
.search(query)?
|
.search(query)
|
||||||
|
.chain_err(|| format!("Cannot search in `{}` with query `{}`", mbox, query))?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|seq| seq.to_string())
|
.map(|seq| seq.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -186,7 +143,8 @@ impl<'a> ImapConnector<'a> {
|
||||||
|
|
||||||
let msgs = self
|
let msgs = self
|
||||||
.sess
|
.sess
|
||||||
.fetch(range, "(UID ENVELOPE INTERNALDATE)")?
|
.fetch(&range, "(UID ENVELOPE INTERNALDATE)")
|
||||||
|
.chain_err(|| format!("Cannot fetch range `{}`", &range))?
|
||||||
.iter()
|
.iter()
|
||||||
.map(Msg::from)
|
.map(Msg::from)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -195,17 +153,26 @@ impl<'a> ImapConnector<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {
|
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() {
|
match self
|
||||||
None => Err(Error::ReadEmailNotFoundError(uid.to_string())),
|
.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()),
|
Some(fetch) => Ok(fetch.body().unwrap_or(&[]).to_vec()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_msg(&mut self, mbox: &str, msg: &[u8]) -> Result<()> {
|
pub fn append_msg(&mut self, mbox: &str, msg: &[u8]) -> Result<()> {
|
||||||
use imap::types::Flag::*;
|
self.sess
|
||||||
self.sess.append_with_flags(mbox, msg, &[Seen])?;
|
.append_with_flags(mbox, msg, &[imap::types::Flag::Seen])
|
||||||
|
.chain_err(|| format!("Cannot append message to `{}` with \\Seen flag", mbox))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
66
src/input.rs
66
src/input.rs
|
@ -1,72 +1,43 @@
|
||||||
|
use error_chain::error_chain;
|
||||||
use std::{
|
use std::{
|
||||||
env, fmt,
|
env,
|
||||||
fs::{remove_file, File},
|
fs::{remove_file, File},
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
process::Command,
|
process::Command,
|
||||||
result,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Error wrapper
|
error_chain! {}
|
||||||
|
|
||||||
#[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
|
|
||||||
|
|
||||||
pub fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
|
pub fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
|
||||||
// Creates draft file
|
// Creates draft file
|
||||||
let mut draft_path = env::temp_dir();
|
let mut draft_path = env::temp_dir();
|
||||||
draft_path.push("himalaya-draft.mail");
|
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
|
// 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)
|
.arg(&draft_path)
|
||||||
.status()?;
|
.status()
|
||||||
|
.chain_err(|| "Cannot start editor")?;
|
||||||
|
|
||||||
// Extracts draft file content
|
// Extracts draft file content
|
||||||
let mut draft = String::new();
|
let mut draft = String::new();
|
||||||
File::open(&draft_path)?.read_to_string(&mut draft)?;
|
File::open(&draft_path)
|
||||||
remove_file(&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)
|
Ok(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ask_for_confirmation(prompt: &str) -> Result<()> {
|
pub fn ask_for_confirmation(prompt: &str) -> Result<()> {
|
||||||
print!("{} (y/n) ", prompt);
|
print!("{} (y/n) ", prompt);
|
||||||
io::stdout().flush()?;
|
io::stdout().flush().chain_err(|| "Cannot flush stdout")?;
|
||||||
|
|
||||||
match io::stdin()
|
match io::stdin()
|
||||||
.bytes()
|
.bytes()
|
||||||
|
@ -75,6 +46,7 @@ pub fn ask_for_confirmation(prompt: &str) -> Result<()> {
|
||||||
.map(|bytes| bytes as char)
|
.map(|bytes| bytes as char)
|
||||||
{
|
{
|
||||||
Some('y') | Some('Y') => Ok(()),
|
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 config;
|
||||||
mod imap;
|
mod imap;
|
||||||
mod input;
|
mod input;
|
||||||
|
@ -7,545 +8,17 @@ mod output;
|
||||||
mod smtp;
|
mod smtp;
|
||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
use clap::{App, AppSettings, Arg, SubCommand};
|
use crate::app::App;
|
||||||
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
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(err) = run() {
|
if let Err(ref errs) = App::new().run() {
|
||||||
eprintln!("Error: {}", err);
|
let mut errs = errs.iter();
|
||||||
exit(1);
|
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 lettre;
|
||||||
use mailparse::{self, MailHeaderMap};
|
use mailparse::{self, MailHeaderMap};
|
||||||
use rfc2047_decoder;
|
use rfc2047_decoder;
|
||||||
|
@ -11,39 +12,12 @@ use uuid::Uuid;
|
||||||
use crate::config::{Account, Config};
|
use crate::config::{Account, Config};
|
||||||
use crate::table::{self, DisplayRow, DisplayTable};
|
use crate::table::{self, DisplayRow, DisplayTable};
|
||||||
|
|
||||||
// Error wrapper
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
#[derive(Debug)]
|
Mailparse(mailparse::MailParseError);
|
||||||
pub enum Error {
|
Lettre(lettre::error::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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// Template
|
||||||
|
|
||||||
|
|
|
@ -1,69 +1,26 @@
|
||||||
|
use error_chain::error_chain;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{
|
use std::{fmt::Display, process::Command};
|
||||||
fmt::{self, Display},
|
|
||||||
io,
|
|
||||||
process::Command,
|
|
||||||
result, string,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Error wrapper
|
error_chain! {}
|
||||||
|
|
||||||
#[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
|
|
||||||
|
|
||||||
pub fn run_cmd(cmd: &str) -> Result<String> {
|
pub fn run_cmd(cmd: &str) -> Result<String> {
|
||||||
let output = if cfg!(target_os = "windows") {
|
let output = if cfg!(target_os = "windows") {
|
||||||
Command::new("cmd").args(&["/C", cmd]).output()?
|
Command::new("cmd").args(&["/C", cmd]).output()
|
||||||
} else {
|
} 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<()> {
|
pub fn print<T: Display + Serialize>(output_type: &str, item: T) -> Result<()> {
|
||||||
match output_type {
|
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()),
|
"text" | _ => println!("{}", item.to_string()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
57
src/smtp.rs
57
src/smtp.rs
|
@ -1,53 +1,28 @@
|
||||||
use lettre;
|
use error_chain::error_chain;
|
||||||
use std::{fmt, result};
|
use lettre::{self, transport::smtp::SmtpTransport, Transport};
|
||||||
|
|
||||||
use crate::config::{self, Account};
|
use crate::config::{self, Account};
|
||||||
|
|
||||||
// Error wrapper
|
error_chain! {
|
||||||
|
links {
|
||||||
#[derive(Debug)]
|
Config(config::Error, config::ErrorKind);
|
||||||
pub enum Error {
|
|
||||||
TransportError(lettre::transport::smtp::Error),
|
|
||||||
ConfigError(config::Error),
|
|
||||||
}
|
}
|
||||||
|
foreign_links {
|
||||||
impl fmt::Display for Error {
|
Smtp(lettre::transport::smtp::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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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<()> {
|
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
|
smtp_relay(&account.smtp_host)?
|
||||||
// lettre::transport::smtp::SmtpTransport::starttls_relay
|
|
||||||
|
|
||||||
lettre::transport::smtp::SmtpTransport::relay(&account.smtp_host)?
|
|
||||||
.credentials(account.smtp_creds()?)
|
.credentials(account.smtp_creds()?)
|
||||||
.build()
|
.build()
|
||||||
.send(msg)
|
.send(msg)?;
|
||||||
.map(|_| Ok(()))?
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue