serialize mbox to json

This commit is contained in:
Clément DOUIN 2021-01-17 22:48:50 +01:00
parent d928feefc8
commit d392d71129
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
7 changed files with 166 additions and 60 deletions

View file

@ -10,7 +10,7 @@ use std::{
};
use toml;
use crate::io::run_cmd;
use crate::output::{self, run_cmd};
// Error wrapper
@ -23,8 +23,7 @@ pub enum Error {
GetPathNotFoundError,
GetAccountNotFoundError(String),
GetAccountDefaultNotFoundError,
ParseImapPasswdUtf8Error,
ParseSmtpPasswdUtf8Error,
OutputError(output::Error),
}
impl fmt::Display for Error {
@ -39,8 +38,7 @@ impl fmt::Display for Error {
Error::GetPathNotFoundError => write!(f, "path not found"),
Error::GetAccountNotFoundError(account) => write!(f, "account {} not found", account),
Error::GetAccountDefaultNotFoundError => write!(f, "no default account found"),
Error::ParseImapPasswdUtf8Error => write!(f, "imap passwd invalid utf8"),
Error::ParseSmtpPasswdUtf8Error => write!(f, "smtp passwd invalid utf8"),
Error::OutputError(err) => err.fmt(f),
}
}
}
@ -63,6 +61,12 @@ impl From<env::VarError> for Error {
}
}
impl From<output::Error> for Error {
fn from(err: output::Error) -> Error {
Error::OutputError(err)
}
}
// Result wrapper
type Result<T> = result::Result<T, Error>;
@ -92,18 +96,14 @@ pub struct Account {
impl Account {
pub fn imap_passwd(&self) -> Result<String> {
let cmd = run_cmd(&self.imap_passwd_cmd)?;
let passwd = String::from_utf8(cmd.stdout);
let passwd = passwd.map_err(|_| Error::ParseImapPasswdUtf8Error)?;
let passwd = run_cmd(&self.imap_passwd_cmd)?;
let passwd = passwd.trim_end_matches("\n").to_owned();
Ok(passwd)
}
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let cmd = run_cmd(&self.smtp_passwd_cmd)?;
let passwd = String::from_utf8(cmd.stdout);
let passwd = passwd.map_err(|_| Error::ParseImapPasswdUtf8Error)?;
let passwd = run_cmd(&self.smtp_passwd_cmd)?;
let passwd = passwd.trim_end_matches("\n").to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))

View file

@ -3,8 +3,8 @@ use native_tls::{self, TlsConnector, TlsStream};
use std::{fmt, net::TcpStream, result};
use crate::config::{self, Account};
use crate::mbox::Mbox;
use crate::msg::Msg;
use crate::mbox::{Mbox, Mboxes};
use crate::msg::{Msg, Msgs};
// Error wrapper
@ -94,7 +94,7 @@ impl<'a> ImapConnector<'a> {
}
}
pub fn list_mboxes(&mut self) -> Result<Vec<Mbox>> {
pub fn list_mboxes(&mut self) -> Result<Mboxes> {
let mboxes = self
.sess
.list(Some(""), Some("*"))?
@ -102,10 +102,10 @@ impl<'a> ImapConnector<'a> {
.map(Mbox::from_name)
.collect::<Vec<_>>();
Ok(mboxes)
Ok(Mboxes(mboxes))
}
pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Vec<Msg>> {
pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Msgs> {
let last_seq = self.sess.select(mbox)?.exists;
let begin = last_seq - (page * page_size);
let end = begin - (page_size - 1);
@ -119,7 +119,7 @@ impl<'a> ImapConnector<'a> {
.map(Msg::from)
.collect::<Vec<_>>();
Ok(msgs)
Ok(Msgs(msgs))
}
pub fn search_msgs(
@ -128,7 +128,7 @@ impl<'a> ImapConnector<'a> {
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Vec<Msg>> {
) -> Result<Msgs> {
self.sess.select(mbox)?;
let begin = page * page_size;
@ -149,7 +149,7 @@ impl<'a> ImapConnector<'a> {
.map(Msg::from)
.collect::<Vec<_>>();
Ok(msgs)
Ok(Msgs(msgs))
}
pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {

View file

@ -2,7 +2,7 @@ use std::{
env, fmt,
fs::{remove_file, File},
io::{self, Read, Write},
process::{Command, Output},
process::Command,
result,
};
@ -78,11 +78,3 @@ pub fn ask_for_confirmation(prompt: &str) -> Result<()> {
_ => Err(Error::AskForConfirmationDeniedError),
}
}
pub fn run_cmd(cmd: &str) -> io::Result<Output> {
if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", cmd]).output()
} else {
Command::new("sh").arg("-c").arg(cmd).output()
}
}

View file

@ -1,8 +1,9 @@
mod config;
mod imap;
mod io;
mod input;
mod mbox;
mod msg;
mod output;
mod smtp;
mod table;
@ -12,7 +13,7 @@ use std::{fmt, fs, process::exit, result};
use crate::config::Config;
use crate::imap::ImapConnector;
use crate::msg::Msg;
use crate::table::DisplayTable;
use crate::output::print;
const DEFAULT_PAGE_SIZE: usize = 10;
const DEFAULT_PAGE: usize = 0;
@ -20,7 +21,8 @@ const DEFAULT_PAGE: usize = 0;
#[derive(Debug)]
pub enum Error {
ConfigError(config::Error),
IoError(io::Error),
InputError(input::Error),
OutputError(output::Error),
MsgError(msg::Error),
ImapError(imap::Error),
SmtpError(smtp::Error),
@ -30,7 +32,8 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::ConfigError(err) => err.fmt(f),
Error::IoError(err) => err.fmt(f),
Error::InputError(err) => err.fmt(f),
Error::OutputError(err) => err.fmt(f),
Error::MsgError(err) => err.fmt(f),
Error::ImapError(err) => err.fmt(f),
Error::SmtpError(err) => err.fmt(f),
@ -44,9 +47,15 @@ impl From<config::Error> for Error {
}
}
impl From<crate::io::Error> for Error {
fn from(err: crate::io::Error) -> Error {
Error::IoError(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)
}
}
@ -117,6 +126,15 @@ fn run() -> Result<()> {
.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")
@ -199,6 +217,7 @@ fn run() -> Result<()> {
.get_matches();
let account_name = matches.value_of("account");
let output_type = matches.value_of("output").unwrap().to_owned();
if let Some(_) = matches.subcommand_matches("mailboxes") {
let config = Config::new_from_file()?;
@ -206,7 +225,7 @@ fn run() -> Result<()> {
let mut imap_conn = ImapConnector::new(&account)?;
let mboxes = imap_conn.list_mboxes()?;
println!("{}", mboxes.to_table());
print(&output_type, mboxes)?;
imap_conn.close();
}
@ -215,7 +234,6 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?;
let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap();
let page_size: u32 = matches
.value_of("size")
@ -229,7 +247,7 @@ fn run() -> Result<()> {
.unwrap_or(DEFAULT_PAGE as u32);
let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?;
println!("{}", msgs.to_table());
print(&output_type, msgs)?;
imap_conn.close();
}
@ -238,7 +256,6 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?;
let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap();
let page_size: usize = matches
.value_of("size")
@ -276,7 +293,7 @@ fn run() -> Result<()> {
.join(" ");
let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?;
println!("{}", msgs.to_table());
print(&output_type, msgs)?;
imap_conn.close();
}
@ -285,12 +302,11 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?;
let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mime = format!("text/{}", matches.value_of("mime-type").unwrap());
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let text_bodies = msg.text_bodies(&mime)?;
println!("{}", text_bodies);
@ -301,10 +317,8 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?;
let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let parts = msg.extract_attachments()?;
@ -314,7 +328,7 @@ fn run() -> Result<()> {
println!("{} attachment(s) found for message {}", parts.len(), uid);
parts.iter().for_each(|(filename, bytes)| {
let filepath = config.downloads_filepath(&account, &filename);
println!("Downloading {} ", filename);
println!("Downloading {}", filename);
fs::write(filepath, bytes).unwrap()
});
println!("Done!");
@ -327,14 +341,13 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?;
let account = config.get_account(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?;
let tpl = Msg::build_new_tpl(&config, &account)?;
let content = io::open_editor_with_tpl(&tpl.as_bytes())?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content);
io::ask_for_confirmation("Send the message?")?;
input::ask_for_confirmation("Send the message?")?;
println!("Sending ");
println!("Sending");
smtp::send(&account, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!");
@ -357,12 +370,12 @@ fn run() -> Result<()> {
msg.build_reply_tpl(&config, &account)?
};
let content = io::open_editor_with_tpl(&tpl.as_bytes())?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content);
io::ask_for_confirmation("Send the message?")?;
input::ask_for_confirmation("Send the message?")?;
println!("Sending ");
println!("Sending");
smtp::send(&account, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!");
@ -380,12 +393,12 @@ fn run() -> Result<()> {
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let tpl = msg.build_forward_tpl(&config, &account)?;
let content = io::open_editor_with_tpl(&tpl.as_bytes())?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content);
io::ask_for_confirmation("Send the message?")?;
input::ask_for_confirmation("Send the message?")?;
println!("Sending ");
println!("Sending");
smtp::send(&account, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!");

View file

@ -1,7 +1,12 @@
use imap;
use serde::Serialize;
use std::fmt;
use crate::table::{self, DisplayRow, DisplayTable};
// Mbox
#[derive(Debug, Serialize)]
pub struct Mbox {
pub delim: String,
pub name: String,
@ -30,7 +35,12 @@ impl DisplayRow for Mbox {
}
}
impl<'a> DisplayTable<'a, Mbox> for Vec<Mbox> {
// Mboxes
#[derive(Debug, Serialize)]
pub struct Mboxes(pub Vec<Mbox>);
impl<'a> DisplayTable<'a, Mbox> for Mboxes {
fn header_row() -> Vec<table::Cell> {
use crate::table::*;
@ -42,6 +52,12 @@ impl<'a> DisplayTable<'a, Mbox> for Vec<Mbox> {
}
fn rows(&self) -> &Vec<Mbox> {
self
&self.0
}
}
impl fmt::Display for Mboxes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_table())
}
}

View file

@ -1,5 +1,6 @@
use lettre;
use mailparse::{self, MailHeaderMap};
use serde::Serialize;
use std::{fmt, result};
use crate::config::{Account, Config};
@ -41,10 +42,12 @@ type Result<T> = result::Result<T, Error>;
// Msg
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub struct Msg {
pub uid: u32,
pub flags: Vec<String>,
#[serde(skip_serializing)]
raw: Vec<u8>,
}
@ -406,7 +409,12 @@ impl DisplayRow for Msg {
}
}
impl<'a> DisplayTable<'a, Msg> for Vec<Msg> {
// Msgs
#[derive(Debug, Serialize)]
pub struct Msgs(pub Vec<Msg>);
impl<'a> DisplayTable<'a, Msg> for Msgs {
fn header_row() -> Vec<table::Cell> {
use crate::table::*;
@ -420,6 +428,12 @@ impl<'a> DisplayTable<'a, Msg> for Vec<Msg> {
}
fn rows(&self) -> &Vec<Msg> {
self
&self.0
}
}
impl fmt::Display for Msgs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_table())
}
}

71
src/output.rs Normal file
View file

@ -0,0 +1,71 @@
use serde::Serialize;
use std::{
fmt::{self, Display},
io,
process::Command,
result, string,
};
// Error wrapper
#[derive(Debug)]
pub enum Error {
IoError(io::Error),
ParseUtf8Error(string::FromUtf8Error),
SerializeJsonError(serde_json::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "input: ")?;
match self {
Error::IoError(err) => err.fmt(f),
Error::ParseUtf8Error(err) => err.fmt(f),
Error::SerializeJsonError(err) => err.fmt(f),
}
}
}
impl From<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> {
let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", cmd]).output()?
} else {
Command::new("sh").arg("-c").arg(cmd).output()?
};
Ok(String::from_utf8(output.stdout)?)
}
pub fn print<T: Display + Serialize>(output_type: &str, item: T) -> Result<()> {
match output_type {
"json" => print!("{}", serde_json::to_string(&item)?),
"text" | _ => println!("{}", item.to_string()),
}
Ok(())
}