mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 17:15:12 +00:00
serialize mbox to json
This commit is contained in:
parent
d928feefc8
commit
d392d71129
|
@ -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))
|
||||
|
|
16
src/imap.rs
16
src/imap.rs
|
@ -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>> {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
67
src/main.rs
67
src/main.rs
|
@ -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!");
|
||||
|
|
20
src/mbox.rs
20
src/mbox.rs
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
20
src/msg.rs
20
src/msg.rs
|
@ -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
71
src/output.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in a new issue