refactor message with clap derive api (part 2)

This commit is contained in:
Clément DOUIN 2023-12-07 18:50:46 +01:00
parent a47902af7d
commit b8ef771614
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
18 changed files with 413 additions and 132 deletions

View file

@ -20,7 +20,7 @@ use email::{
add::{imap::AddFlagsImap, maildir::AddFlagsMaildir},
remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir},
set::{imap::SetFlagsImap, maildir::SetFlagsMaildir},
Flags,
Flag, Flags,
},
folder::{
add::{imap::AddFolderImap, maildir::AddFolderMaildir},
@ -800,6 +800,27 @@ impl Backend {
id_mapper.create_alias(&*id)?;
Ok(id)
}
pub async fn add_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
let backend_kind = self.toml_account_config.add_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.add_flag(folder, &ids, flag).await
}
pub async fn set_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
let backend_kind = self.toml_account_config.set_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.set_flag(folder, &ids, flag).await
}
pub async fn remove_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
let backend_kind = self.toml_account_config.remove_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.remove_flag(folder, &ids, flag).await
}
}
impl Deref for Backend {

View file

@ -80,27 +80,27 @@ pub struct Cli {
#[derive(Subcommand, Debug)]
pub enum HimalayaCommand {
/// Subcommand to manage accounts
/// Manage accounts
#[command(subcommand)]
#[command(alias = "accounts")]
Account(AccountSubcommand),
/// Subcommand to manage folders
/// Manage folders
#[command(subcommand)]
#[command(alias = "folders")]
Folder(FolderSubcommand),
/// Subcommand to manage envelopes
/// Manage envelopes
#[command(subcommand)]
#[command(alias = "envelopes")]
Envelope(EnvelopeSubcommand),
/// Subcommand to manage flags
/// Manage flags
#[command(subcommand)]
#[command(alias = "flags")]
Flag(FlagSubcommand),
/// Subcommand to manage messages
/// Manage messages
#[command(subcommand)]
#[command(alias = "messages", alias = "msgs", alias = "msg")]
Message(MessageSubcommand),

View file

@ -1,5 +1,13 @@
use clap::Parser;
/// The envelope id argument parser
#[derive(Debug, Parser)]
pub struct EnvelopeIdArg {
/// The envelope id
#[arg(value_name = "ID", required = true)]
pub id: usize,
}
/// The envelopes ids arguments parser
#[derive(Debug, Parser)]
pub struct EnvelopeIdsArgs {

View file

@ -0,0 +1,26 @@
use std::ops::Deref;
use clap::Parser;
/// The raw message body argument parser
#[derive(Debug, Parser)]
pub struct BodyRawArg {
/// Prefill the template with a custom body
#[arg(raw = true, required = false)]
#[arg(name = "body-raw", value_delimiter = ' ')]
pub raw: Vec<String>,
}
impl BodyRawArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace("\r", "").replace("\n", "\r\n")
}
}
impl Deref for BodyRawArg {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.raw
}
}

View file

@ -0,0 +1,20 @@
use clap::Parser;
/// The envelope id argument parser
#[derive(Debug, Parser)]
pub struct HeaderRawArgs {
/// Prefill the template with custom headers
///
/// A raw header should follow the pattern KEY:VAL.
#[arg(long = "header", short = 'H', required = false)]
#[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)]
pub raw: Vec<(String, String)>,
}
pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> {
if let Some((key, val)) = raw_header.split_once(":") {
Ok((key.trim().to_owned(), val.trim().to_owned()))
} else {
Err(format!("cannot parse raw header {raw_header:?}"))
}
}

View file

@ -0,0 +1,2 @@
pub mod body;
pub mod header;

View file

@ -47,6 +47,8 @@ impl MessageCopyCommand {
let ids = &self.envelopes.ids;
backend.copy_messages(from_folder, to_folder, ids).await?;
printer.print("Message(s) successfully copied from {from_folder} to {to_folder}!")
printer.print(format!(
"Message(s) successfully copied from {from_folder} to {to_folder}!"
))
}
}

View file

@ -0,0 +1,83 @@
use anyhow::{anyhow, Result};
use atty::Stream;
use clap::Parser;
use log::info;
use std::io::{self, BufRead};
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
cache::arg::disable::DisableCacheFlag,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameArg,
message::arg::{body::BodyRawArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Forward a new message
#[derive(Debug, Parser)]
pub struct MessageForwardCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Forward to all recipients
#[arg(long, short = 'A')]
pub all: bool,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: BodyRawArg,
#[command(flatten)]
pub cache: DisableCacheFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageForwardCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing message forward command");
let folder = &self.folder.name;
let account = self.account.name.as_ref().map(String::as_str);
let cache = self.cache.disable;
let (toml_account_config, account_config) =
config.clone().into_account_configs(account, cache)?;
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
let is_tty = atty::is(Stream::Stdin);
let is_json = printer.is_json();
let body = if !self.body.is_empty() && (is_tty || is_json) {
self.body.raw()
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(anyhow!("cannot find message"))?
.to_forward_tpl_builder(&account_config)
.with_headers(self.headers.raw)
.with_body(body)
.build()
.await?;
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
}
}

View file

@ -0,0 +1,46 @@
use anyhow::Result;
use clap::Parser;
use log::info;
use mail_builder::MessageBuilder;
use url::Url;
use crate::{backend::Backend, config::TomlConfig, printer::Printer, ui::editor};
/// Parse and edit a message from a mailto URL string
#[derive(Debug, Parser)]
pub struct MessageMailtoCommand {
/// The mailto url
#[arg()]
pub url: Url,
}
impl MessageMailtoCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing message mailto command");
let (toml_account_config, account_config) =
config.clone().into_account_configs(None, false)?;
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
let mut builder = MessageBuilder::new().to(self.url.path());
for (key, val) in self.url.query_pairs() {
match key.to_lowercase().as_bytes() {
b"cc" => builder = builder.cc(val.to_string()),
b"bcc" => builder = builder.bcc(val.to_string()),
b"subject" => builder = builder.subject(val),
b"body" => builder = builder.text_body(val),
_ => (),
}
}
let tpl = account_config
.generate_tpl_interpreter()
.with_show_only_headers(account_config.email_writing_headers())
.build()
.from_msg_builder(builder)
.await?;
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
}
}

View file

@ -1,9 +1,13 @@
pub mod copy;
pub mod delete;
pub mod forward;
pub mod mailto;
pub mod move_;
pub mod read;
pub mod reply;
pub mod save;
pub mod send;
pub mod write;
use anyhow::Result;
use clap::Subcommand;
@ -11,8 +15,10 @@ use clap::Subcommand;
use crate::{config::TomlConfig, printer::Printer};
use self::{
copy::MessageCopyCommand, delete::MessageDeleteCommand, move_::MessageMoveCommand,
read::MessageReadCommand, save::MessageSaveCommand, send::MessageSendCommand,
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
mailto::MessageMailtoCommand, move_::MessageMoveCommand, read::MessageReadCommand,
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
write::MessageWriteCommand,
};
/// Subcommand to manage messages
@ -22,6 +28,22 @@ pub enum MessageSubcommand {
#[command(arg_required_else_help = true)]
Read(MessageReadCommand),
/// Write a new message
#[command(alias = "new", alias = "compose")]
Write(MessageWriteCommand),
/// Reply to a message
#[command()]
Reply(MessageReplyCommand),
/// Forward a message
#[command(alias = "fwd")]
Forward(MessageForwardCommand),
/// Parse and edit a message from a mailto URL string
#[command()]
Mailto(MessageMailtoCommand),
/// Save a message to a folder
#[command(arg_required_else_help = true)]
#[command(alias = "add", alias = "create")]
@ -48,6 +70,10 @@ impl MessageSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Read(cmd) => cmd.execute(printer, config).await,
Self::Write(cmd) => cmd.execute(printer, config).await,
Self::Reply(cmd) => cmd.execute(printer, config).await,
Self::Forward(cmd) => cmd.execute(printer, config).await,
Self::Mailto(cmd) => cmd.execute(printer, config).await,
Self::Save(cmd) => cmd.execute(printer, config).await,
Self::Send(cmd) => cmd.execute(printer, config).await,
Self::Copy(cmd) => cmd.execute(printer, config).await,

View file

@ -47,6 +47,8 @@ impl MessageMoveCommand {
let ids = &self.envelopes.ids;
backend.move_messages(from_folder, to_folder, ids).await?;
printer.print("Message(s) successfully moved from {from_folder} to {to_folder}!")
printer.print(format!(
"Message(s) successfully moved from {from_folder} to {to_folder}!"
))
}
}

View file

@ -0,0 +1,86 @@
use anyhow::{anyhow, Result};
use atty::Stream;
use clap::Parser;
use email::flag::Flag;
use log::info;
use std::io::{self, BufRead};
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
cache::arg::disable::DisableCacheFlag,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameArg,
message::arg::{body::BodyRawArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Reply a new message
#[derive(Debug, Parser)]
pub struct MessageReplyCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Reply to all recipients
#[arg(long, short = 'A')]
pub all: bool,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: BodyRawArg,
#[command(flatten)]
pub cache: DisableCacheFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageReplyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing message reply command");
let folder = &self.folder.name;
let account = self.account.name.as_ref().map(String::as_str);
let cache = self.cache.disable;
let (toml_account_config, account_config) =
config.clone().into_account_configs(account, cache)?;
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
let is_tty = atty::is(Stream::Stdin);
let is_json = printer.is_json();
let body = if !self.body.is_empty() && (is_tty || is_json) {
self.body.raw()
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(anyhow!("cannot find message"))?
.to_reply_tpl_builder(&account_config)
.with_headers(self.headers.raw)
.with_body(body)
.with_reply_all(self.all)
.build()
.await?;
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await?;
backend.add_flag(&folder, &[id], Flag::Answered).await
}
}

View file

@ -56,6 +56,6 @@ impl MessageSaveCommand {
.add_raw_message(folder, raw_email.as_bytes())
.await?;
printer.print("Message successfully saved to {folder}!")
printer.print(format!("Message successfully saved to {folder}!"))
}
}

View file

@ -57,7 +57,7 @@ impl MessageSendCommand {
.add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen)
.await?;
printer.print("Message successfully sent and saved to {folder}!")
printer.print(format!("Message successfully sent and saved to {folder}!"))
} else {
printer.print("Message successfully sent!")
}

View file

@ -0,0 +1,66 @@
use anyhow::Result;
use atty::Stream;
use clap::Parser;
use email::message::Message;
use log::info;
use std::io::{self, BufRead};
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
cache::arg::disable::DisableCacheFlag,
config::TomlConfig,
message::arg::{body::BodyRawArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Write a new message
#[derive(Debug, Parser)]
pub struct MessageWriteCommand {
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: BodyRawArg,
#[command(flatten)]
pub cache: DisableCacheFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing message write command");
let account = self.account.name.as_ref().map(String::as_str);
let cache = self.cache.disable;
let (toml_account_config, account_config) =
config.clone().into_account_configs(account, cache)?;
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
let is_tty = atty::is(Stream::Stdin);
let is_json = printer.is_json();
let body = if !self.body.is_empty() && (is_tty || is_json) {
self.body.raw()
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let tpl = Message::new_tpl_builder(&account_config)
.with_headers(self.headers.raw)
.with_body(body)
.build()
.await?;
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
}
}

View file

@ -69,30 +69,6 @@ pub async fn attachments<P: Printer>(
}
}
pub async fn forward<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
folder: &str,
id: &str,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let tpl = backend
.get_messages(&folder, &[id])
.await?
.first()
.ok_or_else(|| anyhow!("cannot find email {}", id))?
.to_forward_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.build()
.await?;
trace!("initial template: {tpl}");
editor::edit_tpl_with_editor(config, printer, backend, tpl).await?;
Ok(())
}
/// Parses and edits a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
@ -123,49 +99,3 @@ pub async fn mailto<P: Printer>(
editor::edit_tpl_with_editor(config, printer, backend, tpl).await
}
pub async fn reply<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
folder: &str,
id: &str,
all: bool,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or_else(|| anyhow!("cannot find email {}", id))?
.to_reply_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.with_reply_all(all)
.build()
.await?;
trace!("initial template: {tpl}");
editor::edit_tpl_with_editor(config, printer, backend, tpl).await?;
backend
.add_flag(&folder, &Id::single(id), Flag::Answered)
.await?;
Ok(())
}
pub async fn write<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let tpl = Message::new_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.build()
.await?;
trace!("initial template: {tpl}");
editor::edit_tpl_with_editor(config, printer, backend, tpl).await?;
Ok(())
}

View file

@ -1,4 +1,5 @@
// pub mod args;
pub mod arg;
pub mod command;
pub mod config;
// pub mod handlers;

View file

@ -3,6 +3,7 @@ use clap::Parser;
use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV};
use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter};
use log::{debug, warn};
use std::env;
#[tokio::main]
async fn main() -> Result<()> {
@ -17,6 +18,17 @@ async fn main() -> Result<()> {
.format_timestamp(None)
.init();
let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
// TODO
// let cmd = MessageMailtoCommand::command()
// .no_binary_name(true)
// .try_get_matches_from([&raw_args[1]]);
// match cmd {
// Ok(m) => m.exec
// }
}
let cli = Cli::parse();
let mut printer = StdoutPrinter::new(cli.output, cli.color);
@ -61,56 +73,6 @@ async fn main() -> Result<()> {
// )
// .await;
// }
// Some(message::args::Cmd::Forward(id, headers, body)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return message::handlers::forward(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// id,
// headers,
// body,
// )
// .await;
// }
// Some(message::args::Cmd::Reply(id, all, headers, body)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return message::handlers::reply(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// id,
// all,
// headers,
// body,
// )
// .await;
// }
// Some(message::args::Cmd::Save(raw_email)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return message::handlers::save(&mut printer, &backend, &folder, raw_email).await;
// }
// Some(message::args::Cmd::Send(raw_email)) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return message::handlers::send(&account_config, &mut printer, &backend, raw_email)
// .await;
// }
// Some(message::args::Cmd::Write(headers, body)) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return message::handlers::write(
// &account_config,
// &mut printer,
// &backend,
// headers,
// body,
// )
// .await;
// }
// _ => (),
// }