start merging email with msg, add list msgs command

This commit is contained in:
Clément DOUIN 2021-01-16 13:16:40 +01:00
parent a803800d1c
commit 0a508f2e95
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
5 changed files with 235 additions and 84 deletions

View file

@ -4,7 +4,8 @@ use std::{fmt, net::TcpStream, result};
use crate::config;
use crate::email::{self, Email};
use crate::mailbox::Mailbox;
use crate::mbox::Mbox;
use crate::msg::Msg;
// Error wrapper
@ -79,17 +80,40 @@ impl<'a> ImapConnector<'a> {
Ok(Self { config, sess })
}
pub fn list_mboxes(&mut self) -> Result<Vec<Mailbox<'_>>> {
pub fn close(&mut self) {
match self.sess.close() {
_ => (),
}
}
pub fn list_mboxes(&mut self) -> Result<Vec<Mbox<'_>>> {
let mboxes = self
.sess
.list(Some(""), Some("*"))?
.iter()
.map(Mailbox::from_name)
.map(Mbox::from_name)
.collect::<Vec<_>>();
Ok(mboxes)
}
pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Vec<Msg>> {
let last_seq = self.sess.select(mbox)?.exists;
let begin = last_seq - (page * page_size);
let end = begin - (page_size - 1);
let range = format!("{}:{}", begin, end);
let msgs = self
.sess
.fetch(range, "(UID BODY.PEEK[])")?
.iter()
.rev()
.map(Msg::from)
.collect::<Vec<_>>();
Ok(msgs)
}
pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result<Vec<Email<'_>>> {
self.sess.select(mbox)?;

View file

@ -2,7 +2,7 @@ mod config;
mod email;
mod imap;
mod input;
mod mailbox;
mod mbox;
mod msg;
mod smtp;
mod table;
@ -15,6 +15,9 @@ use crate::imap::ImapConnector;
use crate::msg::Msg;
use crate::table::DisplayTable;
const DEFAULT_PAGE_SIZE: u32 = 10;
const DEFAULT_PAGE: u32 = 0;
#[derive(Debug)]
pub enum Error {
ConfigError(config::Error),
@ -89,14 +92,44 @@ fn uid_arg() -> Arg<'static, 'static> {
}
fn run() -> Result<()> {
let default_page_size = &DEFAULT_PAGE_SIZE.to_string();
let default_page = &DEFAULT_PAGE.to_string();
let matches = App::new("Himalaya")
.version("0.1.0")
.about("📫 Minimalist CLI email client")
.author("soywod <clement.douin@posteo.net>")
.setting(AppSettings::ArgRequiredElseHelp)
.subcommand(SubCommand::with_name("list").about("Lists all available mailboxes"))
.subcommand(
SubCommand::with_name("mailboxes")
.aliases(&["mboxes", "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(
Arg::with_name("size")
.help("Page size")
.short("s")
.long("size")
.value_name("INT")
.default_value(default_page_size),
)
.arg(
Arg::with_name("page")
.help("Page number")
.short("p")
.long("page")
.value_name("INT")
.default_value(default_page),
),
)
.subcommand(
SubCommand::with_name("search")
.aliases(&["query", "q", "s"])
.about("Lists emails matching the given IMAP query")
.arg(mailbox_arg())
.arg(
@ -109,6 +142,7 @@ fn run() -> Result<()> {
)
.subcommand(
SubCommand::with_name("read")
.aliases(&["r"])
.about("Reads text bodies of an email")
.arg(uid_arg())
.arg(mailbox_arg())
@ -124,6 +158,7 @@ fn run() -> Result<()> {
)
.subcommand(
SubCommand::with_name("attachments")
.aliases(&["attach", "a"])
.about("Downloads all attachments from an email")
.arg(uid_arg())
.arg(mailbox_arg()),
@ -131,6 +166,7 @@ fn run() -> Result<()> {
.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())
@ -143,94 +179,127 @@ fn run() -> Result<()> {
)
.subcommand(
SubCommand::with_name("forward")
.aliases(&["fwd", "f"])
.about("Forwards an email")
.arg(uid_arg())
.arg(mailbox_arg()),
)
.get_matches();
if let Some(_) = matches.subcommand_matches("list") {
if let Some(_) = matches.subcommand_matches("mailboxes") {
let config = Config::new_from_file()?;
let mboxes = ImapConnector::new(&config.imap)?.list_mboxes()?.to_table();
let mut imap_conn = ImapConnector::new(&config.imap)?;
println!("{}", mboxes);
let mboxes = imap_conn.list_mboxes()?;
println!("{}", mboxes.to_table());
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("list") {
let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
let mbox = matches.value_of("mailbox").unwrap();
let page_size: u32 = matches
.value_of("size")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE_SIZE);
let page: u32 = matches
.value_of("page")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE);
let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?;
println!("{}", msgs.to_table());
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("search") {
let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
let mbox = matches.value_of("mailbox").unwrap();
if let Some(matches) = matches.values_of("query") {
let query = matches
.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)
}
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)
}
})
.1
.join(" ");
// Escaped arg commands
(_, true) => {
cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
}
})
.1
.join(" ");
let emails = ImapConnector::new(&config.imap)?
.read_emails(&mbox, &query)?
.to_table();
let msgs = imap_conn.read_emails(&mbox, &query)?;
println!("{}", msgs.to_table());
println!("{}", emails);
}
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("read") {
let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
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 body = ImapConnector::new(&config.imap)?.read_email_body(&mbox, &uid, &mime)?;
let body = imap_conn.read_email_body(&mbox, &uid, &mime)?;
println!("{}", body);
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("attachments") {
let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mut imap_conn = ImapConnector::new(&config.imap)?;
let msg = imap_conn.read_msg(&mbox, &uid)?;
let msg = Msg::from(&msg)?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let parts = msg.extract_parts()?;
if parts.is_empty() {
println!("No attachment found for message {}", uid);
} else {
println!("{} attachment(s) found for message {}", parts.len(), uid);
msg.extract_parts()?.iter().for_each(|(filename, bytes)| {
parts.iter().for_each(|(filename, bytes)| {
let filepath = config.downloads_filepath(&filename);
println!("Downloading {}", filename);
fs::write(filepath, bytes).unwrap()
});
println!("Done!");
}
imap_conn.close();
}
if let Some(_) = matches.subcommand_matches("write") {
let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
let tpl = Msg::build_new_tpl(&config)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes())?;
let msg = Msg::from(content.as_bytes());
input::ask_for_confirmation("Send the message?")?;
@ -238,17 +307,18 @@ fn run() -> Result<()> {
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!");
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("reply") {
let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mut imap_conn = ImapConnector::new(&config.imap)?;
let msg = imap_conn.read_msg(&mbox, &uid)?;
let msg = Msg::from(&msg)?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let tpl = if matches.is_present("reply-all") {
msg.build_reply_all_tpl(&config)?
} else {
@ -256,7 +326,7 @@ fn run() -> Result<()> {
};
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes())?;
let msg = Msg::from(content.as_bytes());
input::ask_for_confirmation("Send the message?")?;
@ -264,20 +334,21 @@ fn run() -> Result<()> {
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!");
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("forward") {
let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mut imap_conn = ImapConnector::new(&config.imap)?;
let msg = imap_conn.read_msg(&mbox, &uid)?;
let msg = Msg::from(&msg)?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let tpl = msg.build_forward_tpl(&config)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes())?;
let msg = Msg::from(content.as_bytes());
input::ask_for_confirmation("Send the message?")?;
@ -285,6 +356,8 @@ fn run() -> Result<()> {
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!");
imap_conn.close();
}
Ok(())

View file

@ -83,13 +83,13 @@ impl DisplayCell for Attributes<'_> {
}
}
pub struct Mailbox<'a> {
pub struct Mbox<'a> {
pub delim: Delim,
pub name: Name,
pub attributes: Attributes<'a>,
}
impl Mailbox<'_> {
impl Mbox<'_> {
pub fn from_name(name: &imap::types::Name) -> Self {
Self {
delim: Delim::from_name(name),
@ -99,7 +99,7 @@ impl Mailbox<'_> {
}
}
impl<'a> DisplayRow for Mailbox<'a> {
impl<'a> DisplayRow for Mbox<'a> {
fn to_row(&self) -> Vec<table::Cell> {
vec![
self.delim.to_cell(),
@ -109,12 +109,12 @@ impl<'a> DisplayRow for Mailbox<'a> {
}
}
impl<'a> DisplayTable<'a, Mailbox<'a>> for Vec<Mailbox<'a>> {
impl<'a> DisplayTable<'a, Mbox<'a>> for Vec<Mbox<'a>> {
fn cols() -> &'a [&'a str] {
&["delim", "name", "attributes"]
}
fn rows(&self) -> &Vec<Mailbox<'a>> {
fn rows(&self) -> &Vec<Mbox<'a>> {
self
}
}

View file

@ -1,7 +1,8 @@
use lettre;
use mailparse::{self, MailHeaderMap};
use std::{fmt, ops, result};
use std::{fmt, result};
use crate::table::{self, DisplayRow, DisplayTable};
use crate::Config;
// Error wrapper
@ -9,8 +10,7 @@ use crate::Config;
#[derive(Debug)]
pub enum Error {
ParseMsgError(mailparse::MailParseError),
BuildEmailError(lettre::error::Error),
TryError,
BuildSendableMsgError(lettre::error::Error),
}
impl fmt::Display for Error {
@ -18,8 +18,7 @@ impl fmt::Display for Error {
write!(f, "(msg): ")?;
match self {
Error::ParseMsgError(err) => err.fmt(f),
Error::BuildEmailError(err) => err.fmt(f),
Error::TryError => write!(f, "cannot parse"),
Error::BuildSendableMsgError(err) => err.fmt(f),
}
}
}
@ -32,7 +31,7 @@ impl From<mailparse::MailParseError> for Error {
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
Error::BuildEmailError(err)
Error::BuildSendableMsgError(err)
}
}
@ -40,28 +39,45 @@ impl From<lettre::error::Error> for Error {
type Result<T> = result::Result<T, Error>;
// Wrapper around mailparse::ParsedMail and lettre::Message
// Msg
#[derive(Debug)]
pub struct Msg<'a>(mailparse::ParsedMail<'a>);
pub struct Msg {
pub uid: u32,
pub flags: Vec<String>,
raw: Vec<u8>,
}
impl<'a> ops::Deref for Msg<'a> {
type Target = mailparse::ParsedMail<'a>;
fn deref(&self) -> &Self::Target {
&self.0
impl From<&[u8]> for Msg {
fn from(item: &[u8]) -> Self {
Self {
uid: 0,
flags: vec![],
raw: item.to_vec(),
}
}
}
impl<'a> Msg<'a> {
pub fn from(bytes: &'a [u8]) -> Result<Self> {
Ok(Self(mailparse::parse_mail(bytes)?))
impl From<&imap::types::Fetch> for Msg {
fn from(fetch: &imap::types::Fetch) -> Self {
Self {
uid: fetch.uid.unwrap_or_default(),
flags: vec![],
raw: fetch.body().unwrap_or_default().to_vec(),
}
}
}
impl<'a> Msg {
pub fn parse(&'a self) -> Result<mailparse::ParsedMail<'a>> {
Ok(mailparse::parse_mail(&self.raw)?)
}
pub fn to_vec(&self) -> Result<Vec<u8>> {
let headers = self.0.get_headers().get_raw_bytes().to_vec();
let parsed = self.parse()?;
let headers = parsed.get_headers().get_raw_bytes().to_vec();
let sep = "\r\n".as_bytes().to_vec();
let body = self.0.get_body()?.as_bytes().to_vec();
let body = parsed.get_body()?.as_bytes().to_vec();
Ok(vec![headers, sep, body].concat())
}
@ -70,8 +86,8 @@ impl<'a> Msg<'a> {
use lettre::message::header::{ContentTransferEncoding, ContentType};
use lettre::message::{Message, SinglePart};
let msg = self
.0
let parsed = self.parse()?;
let msg = parsed
.headers
.iter()
.fold(Message::builder(), |msg, h| {
@ -111,7 +127,7 @@ impl<'a> Msg<'a> {
SinglePart::builder()
.header(ContentType("text/plain; charset=utf-8".parse().unwrap()))
.header(ContentTransferEncoding::Base64)
.body(self.0.get_body_raw()?),
.body(parsed.get_body_raw()?),
)?;
Ok(msg)
@ -147,7 +163,7 @@ impl<'a> Msg<'a> {
pub fn extract_parts(&self) -> Result<Vec<(String, Vec<u8>)>> {
let mut parts = vec![];
Self::extract_parts_into(&self.0, &mut parts);
Self::extract_parts_into(&self.parse()?, &mut parts);
Ok(parts)
}
@ -167,7 +183,7 @@ impl<'a> Msg<'a> {
}
pub fn build_reply_tpl(&self, config: &Config) -> Result<String> {
let msg = &self.0;
let msg = &self.parse()?;
let headers = msg.get_headers();
let mut tpl = vec![];
@ -207,7 +223,7 @@ impl<'a> Msg<'a> {
}
pub fn build_reply_all_tpl(&self, config: &Config) -> Result<String> {
let msg = &self.0;
let msg = &self.parse()?;
let headers = msg.get_headers();
let mut tpl = vec![];
@ -289,7 +305,7 @@ impl<'a> Msg<'a> {
}
pub fn build_forward_tpl(&self, config: &Config) -> Result<String> {
let msg = &self.0;
let msg = &self.parse()?;
let headers = msg.get_headers();
let mut tpl = vec![];
@ -313,3 +329,41 @@ impl<'a> Msg<'a> {
Ok(tpl.join("\r\n"))
}
}
impl DisplayRow for Msg {
fn to_row(&self) -> Vec<table::Cell> {
match self.parse() {
Err(_) => vec![],
Ok(parsed) => {
let headers = parsed.get_headers();
let uid = &self.uid.to_string();
let flags = String::new(); // TODO: render flags
let sender = headers
.get_first_value("reply-to")
.or(headers.get_first_value("from"))
.unwrap_or_default();
let subject = headers.get_first_value("subject").unwrap_or_default();
let date = headers.get_first_value("date").unwrap_or_default();
vec![
table::Cell::new(&[table::RED], &uid),
table::Cell::new(&[table::WHITE], &flags),
table::Cell::new(&[table::BLUE], &sender),
table::Cell::new(&[table::GREEN], &subject),
table::Cell::new(&[table::YELLOW], &date),
]
}
}
}
}
impl<'a> DisplayTable<'a, Msg> for Vec<Msg> {
fn cols() -> &'a [&'a str] {
&["uid", "flags", "sender", "subject", "date"]
}
fn rows(&self) -> &Vec<Msg> {
self
}
}

View file

@ -78,7 +78,7 @@ pub trait DisplayRow {
fn to_row(&self) -> Vec<Cell>;
}
pub trait DisplayTable<'a, T: DisplayRow> {
pub trait DisplayTable<'a, T: DisplayRow + 'a> {
fn cols() -> &'a [&'a str];
fn rows(&self) -> &Vec<T>;