improve errors management

This commit is contained in:
Clément DOUIN 2021-01-11 22:38:11 +01:00
parent b023704d31
commit 2709faf30a
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
9 changed files with 365 additions and 137 deletions

115
Cargo.lock generated
View file

@ -170,6 +170,27 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if 1.0.0",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "encoding_rs"
version = "0.8.26"
@ -200,6 +221,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "funty"
version = "1.1.0"
@ -217,6 +248,17 @@ dependencies = [
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4060f4657be78b8e766215b02b18a2e862d83745545de804638e2b545e81aee6"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
]
[[package]]
name = "hermit-abi"
version = "0.1.17"
@ -236,6 +278,7 @@ dependencies = [
"mailparse",
"native-tls",
"rfc2047-decoder",
"rustyline",
"serde",
"toml",
]
@ -468,6 +511,18 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nix"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
dependencies = [
"bitflags",
"cc",
"cfg-if 1.0.0",
"libc",
]
[[package]]
name = "nom"
version = "5.1.2"
@ -568,7 +623,7 @@ dependencies = [
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall",
"redox_syscall 0.1.57",
"smallvec",
"winapi",
]
@ -638,7 +693,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom",
"getrandom 0.1.15",
"libc",
"rand_chacha",
"rand_core",
@ -661,7 +716,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
"getrandom 0.1.15",
]
[[package]]
@ -679,6 +734,25 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom 0.2.1",
"redox_syscall 0.2.4",
]
[[package]]
name = "regex"
version = "1.4.2"
@ -717,6 +791,27 @@ dependencies = [
"quoted_printable",
]
[[package]]
name = "rustyline"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8227301bfc717136f0ecbd3d064ba8199e44497a0bdd46bb01ede4387cfd2cec"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"dirs-next",
"fs2",
"libc",
"log",
"memchr",
"nix",
"scopeguard",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"winapi",
]
[[package]]
name = "ryu"
version = "1.0.5"
@ -846,7 +941,7 @@ dependencies = [
"cfg-if 0.1.10",
"libc",
"rand",
"redox_syscall",
"redox_syscall 0.1.57",
"remove_dir_all",
"winapi",
]
@ -931,6 +1026,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
[[package]]
name = "unicode-width"
version = "0.1.8"
@ -943,6 +1044,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "uuid"
version = "0.8.1"

View file

@ -12,5 +12,6 @@ lettre = "0.10.0-alpha.4"
mailparse = "0.13.1"
native-tls = "0.2"
rfc2047-decoder = "0.1.2"
rustyline = "7.1.0"
serde = { version = "1.0.118", features = ["derive"] }
toml = "0.5.8"

View file

@ -1,3 +1,4 @@
use lettre::transport::smtp::authentication::Credentials;
use serde::Deserialize;
use std::{
env, fmt,
@ -66,6 +67,10 @@ impl ServerInfo {
pub fn get_addr(&self) -> (&str, u16) {
(&self.host, self.port)
}
pub fn to_smtp_creds(&self) -> Credentials {
Credentials::new(self.login.to_owned(), self.password.to_owned())
}
}
#[derive(Debug, Deserialize)]

View file

@ -1,54 +0,0 @@
use std::env::temp_dir;
use std::fs::{remove_file, File};
use std::io::{self, Read, Write};
use std::process::Command;
use std::{fmt, result};
// Error wrapper
#[derive(Debug)]
pub enum Error {
IoError(io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::IoError(err) => err.fmt(f),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Editor utils
fn open_with_template(template: &[u8]) -> Result<String> {
// Create temporary draft
let mut draft_path = temp_dir();
draft_path.push("himalaya-draft.mail");
File::create(&draft_path)?.write(template)?;
// Open editor and save user input to draft
Command::new(env!("EDITOR")).arg(&draft_path).status()?;
// Read draft
let mut draft = String::new();
File::open(&draft_path)?.read_to_string(&mut draft)?;
remove_file(&draft_path)?;
Ok(draft)
}
pub fn open_with_new_template() -> Result<String> {
let template = ["To: ", "Subject: ", ""].join("\r\n");
open_with_template(template.as_bytes())
}

View file

@ -6,6 +6,7 @@ use std::{fmt, net::TcpStream, result};
use crate::config;
use crate::email::{self, Email};
use crate::mailbox::Mailbox;
use crate::msg::Msg;
// Error wrapper
@ -76,7 +77,7 @@ impl ImapConnector {
Ok(Self { config, sess })
}
pub fn list_mailboxes(&mut self) -> Result<Vec<Mailbox<'_>>> {
pub fn list_mboxes(&mut self) -> Result<Vec<Mailbox<'_>>> {
let mboxes = self
.sess
.list(Some(""), Some("*"))?
@ -131,4 +132,10 @@ impl ImapConnector {
}
}
}
pub fn append_msg(&mut self, mbox: &str, msg: &Msg) -> Result<()> {
use imap::types::Flag::*;
self.sess.append_with_flags(mbox, msg.to_vec(), &[Seen])?;
Ok(())
}
}

81
src/input.rs Normal file
View file

@ -0,0 +1,81 @@
use std::{
env::temp_dir,
fmt,
fs::{remove_file, File},
io::{self, Read, Write},
process::Command,
result,
};
use crate::config::Config;
// Error wrapper
#[derive(Debug)]
pub enum Error {
IoError(io::Error),
AskForSendingConfirmationError,
}
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::AskForSendingConfirmationError => write!(f, "action cancelled"),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Utils
fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
// Creates draft file
let mut draft_path = temp_dir();
draft_path.push("himalaya-draft.mail");
File::create(&draft_path)?.write(tpl)?;
// Opens editor and saves user input to draft file
Command::new(env!("EDITOR")).arg(&draft_path).status()?;
// Extracts draft file content
let mut draft = String::new();
File::open(&draft_path)?.read_to_string(&mut draft)?;
remove_file(&draft_path)?;
Ok(draft)
}
pub fn open_editor_with_new_tpl(config: &Config) -> Result<String> {
let from = &format!("From: {}", config.email_full());
let to = "To: ";
let subject = "Subject: ";
let headers = [from, to, subject, ""].join("\r\n");
Ok(open_editor_with_tpl(headers.as_bytes())?)
}
pub fn ask_for_confirmation(prompt: &str) -> Result<()> {
print!("{} (y/n) ", prompt);
io::stdout().flush()?;
match io::stdin()
.bytes()
.next()
.and_then(|res| res.ok())
.map(|bytes| bytes as char)
{
Some('y') | Some('Y') => Ok(()),
_ => Err(Error::AskForSendingConfirmationError),
}
}

View file

@ -1,8 +1,9 @@
mod config;
mod editor;
mod email;
mod imap;
mod input;
mod mailbox;
mod msg;
mod smtp;
mod table;
@ -11,21 +12,26 @@ use std::{fmt, process::exit, result};
use crate::config::Config;
use crate::imap::ImapConnector;
use crate::msg::Msg;
use crate::table::DisplayTable;
#[derive(Debug)]
pub enum Error {
ConfigError(config::Error),
InputError(input::Error),
MsgError(msg::Error),
ImapError(imap::Error),
EditorError(editor::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::MsgError(err) => err.fmt(f),
Error::ImapError(err) => err.fmt(f),
Error::EditorError(err) => err.fmt(f),
Error::SmtpError(err) => err.fmt(f),
}
}
}
@ -36,15 +42,27 @@ impl From<config::Error> for Error {
}
}
impl From<input::Error> for Error {
fn from(err: input::Error) -> Error {
Error::InputError(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<editor::Error> for Error {
fn from(err: editor::Error) -> Error {
Error::EditorError(err)
impl From<smtp::Error> for Error {
fn from(err: smtp::Error) -> Error {
Error::SmtpError(err)
}
}
@ -60,7 +78,6 @@ fn mailbox_arg() -> Arg<'static, 'static> {
.long("mailbox")
.help("Name of the targeted mailbox")
.value_name("STRING")
.default_value("INBOX")
}
fn uid_arg() -> Arg<'static, 'static> {
@ -80,7 +97,7 @@ fn run() -> Result<()> {
.subcommand(
SubCommand::with_name("search")
.about("Lists emails matching the given IMAP query")
.arg(mailbox_arg())
.arg(mailbox_arg().default_value("INBOX"))
.arg(
Arg::with_name("query")
.help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)")
@ -93,7 +110,7 @@ fn run() -> Result<()> {
SubCommand::with_name("read")
.about("Reads an email by its UID")
.arg(uid_arg())
.arg(mailbox_arg())
.arg(mailbox_arg().default_value("INBOX"))
.arg(
Arg::with_name("mime-type")
.help("MIME type to use")
@ -104,16 +121,12 @@ fn run() -> Result<()> {
.default_value("text/plain"),
),
)
.subcommand(
SubCommand::with_name("write")
.about("Writes a new email")
.arg(mailbox_arg()),
)
.subcommand(SubCommand::with_name("write").about("Writes a new email"))
.subcommand(
SubCommand::with_name("reply")
.about("Replies to an email by its UID")
.arg(uid_arg())
.arg(mailbox_arg())
.arg(mailbox_arg().default_value("INBOX"))
.arg(
Arg::with_name("reply all")
.help("Replies to all recipients")
@ -125,20 +138,13 @@ fn run() -> Result<()> {
SubCommand::with_name("forward")
.about("Forwards an email by its UID")
.arg(uid_arg())
.arg(mailbox_arg()),
)
.subcommand(
SubCommand::with_name("send")
.about("Send a draft by its UID")
.arg(uid_arg()),
.arg(mailbox_arg().default_value("INBOX")),
)
.get_matches();
if let Some(_) = matches.subcommand_matches("list") {
let config = Config::new_from_file()?;
let mboxes = ImapConnector::new(config.imap)?
.list_mailboxes()?
.to_table();
let mboxes = ImapConnector::new(config.imap)?.list_mboxes()?.to_table();
println!("{}", mboxes);
}
@ -191,12 +197,15 @@ fn run() -> Result<()> {
if let Some(_) = matches.subcommand_matches("write") {
let config = Config::new_from_file()?;
let draft = editor::open_with_new_template()?;
let content = input::open_editor_with_new_tpl(&config)?;
let msg = Msg::from_raw(content.as_bytes())?;
// TODO: save as draft instead (IMAP)
println!("Sending ...");
smtp::send(&config, draft.as_bytes());
println!("Done!");
input::ask_for_confirmation("Would you like to send this email?")?;
println!("Sending …");
smtp::send(&config.smtp, &msg)?;
ImapConnector::new(config.imap)?.append_msg("Sent", &msg)?;
println!("Sent!");
}
if let Some(_) = matches.subcommand_matches("reply") {
@ -207,10 +216,6 @@ fn run() -> Result<()> {
// TODO
}
if let Some(_) = matches.subcommand_matches("send") {
// TODO
}
Ok(())
}

85
src/msg.rs Normal file
View file

@ -0,0 +1,85 @@
use lettre;
use mailparse;
use std::{fmt, result};
// Error wrapper
#[derive(Debug)]
pub enum Error {
ParseMsgError(mailparse::MailParseError),
BuildEmailError(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::BuildEmailError(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::BuildEmailError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Wrapper around mailparse::ParsedMail and lettre::Message
pub struct Msg(lettre::Message);
impl Msg {
pub fn from_raw(bytes: &[u8]) -> Result<Msg> {
use lettre::message::header::{ContentTransferEncoding, ContentType};
use lettre::message::{Message, SinglePart};
let parsed_msg = mailparse::parse_mail(bytes)?;
let built_msg = parsed_msg
.headers
.iter()
.fold(Message::builder(), |msg, h| {
match h.get_key().to_lowercase().as_str() {
"from" => msg.from(h.get_value().parse().unwrap()),
"to" => msg.to(h.get_value().parse().unwrap()),
"cc" => match h.get_value().parse() {
Err(_) => msg,
Ok(addr) => msg.cc(addr),
},
"bcc" => match h.get_value().parse() {
Err(_) => msg,
Ok(addr) => msg.bcc(addr),
},
"subject" => msg.subject(h.get_value()),
_ => msg,
}
})
.singlepart(
SinglePart::builder()
.header(ContentType("text/plain; charset=utf-8".parse().unwrap()))
.header(ContentTransferEncoding::Base64)
.body(parsed_msg.get_body_raw()?),
)?;
Ok(Msg(built_msg))
}
pub fn as_sendable_msg(&self) -> &lettre::Message {
&self.0
}
pub fn to_vec(&self) -> Vec<u8> {
self.0.formatted()
}
}

View file

@ -1,52 +1,43 @@
use lettre::{
message::{header, Message, SinglePart},
transport::smtp::{authentication::Credentials, SmtpTransport},
Transport,
};
use mailparse;
use lettre;
use std::{fmt, result};
use crate::config;
use crate::msg::Msg;
// TODO: improve error management
pub fn send(config: &config::Config, bytes: &[u8]) {
let email_origin = mailparse::parse_mail(bytes).unwrap();
let email = email_origin
.headers
.iter()
.fold(Message::builder(), |msg, h| {
match h.get_key().to_lowercase().as_str() {
"to" => msg.to(h.get_value().parse().unwrap()),
"cc" => match h.get_value().parse() {
Err(_) => msg,
Ok(addr) => msg.cc(addr),
},
"bcc" => match h.get_value().parse() {
Err(_) => msg,
Ok(addr) => msg.bcc(addr),
},
"subject" => msg.subject(h.get_value()),
_ => msg,
}
})
.from(config.email_full().parse().unwrap())
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf-8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Base64)
.body(email_origin.get_body_raw().unwrap()),
)
.unwrap();
// Error wrapper
let creds = Credentials::new(config.smtp.login.clone(), config.smtp.password.clone());
let mailer = SmtpTransport::relay(&config.smtp.host)
.unwrap()
.credentials(creds)
.build();
#[derive(Debug)]
pub enum Error {
TransportError(lettre::transport::smtp::Error),
}
match mailer.send(&email) {
Ok(_) => (),
Err(e) => panic!("Could not send email: {:?}", e),
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),
}
}
}
impl From<lettre::transport::smtp::Error> for Error {
fn from(err: lettre::transport::smtp::Error) -> Error {
Error::TransportError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
// Utils
pub fn send(config: &config::ServerInfo, msg: &Msg) -> Result<()> {
use lettre::Transport;
lettre::transport::smtp::SmtpTransport::relay(&config.host)?
.credentials(config.to_smtp_creds())
.build()
.send(msg.as_sendable_msg())
.map(|_| Ok(()))?
}