mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-22 02:50:19 +00:00
refactor envelope with clap derive api
This commit is contained in:
parent
4a77253c1d
commit
2c33dd2f9f
14 changed files with 304 additions and 642 deletions
|
@ -6,6 +6,7 @@ use crate::{
|
|||
account::command::AccountSubcommand,
|
||||
completion::command::CompletionGenerateCommand,
|
||||
config::{self, TomlConfig},
|
||||
envelope::command::EnvelopeSubcommand,
|
||||
folder::command::FolderSubcommand,
|
||||
manual::command::ManualGenerateCommand,
|
||||
output::{ColorFmt, OutputFmt},
|
||||
|
@ -96,6 +97,10 @@ pub enum HimalayaCommand {
|
|||
#[command(subcommand)]
|
||||
Folder(FolderSubcommand),
|
||||
|
||||
/// Subcommand to manage envelopes
|
||||
#[command(subcommand)]
|
||||
Envelope(EnvelopeSubcommand),
|
||||
|
||||
/// Generate manual pages to a directory
|
||||
#[command(arg_required_else_help = true)]
|
||||
Manual(ManualGenerateCommand),
|
||||
|
@ -110,6 +115,7 @@ impl HimalayaCommand {
|
|||
match self {
|
||||
Self::Account(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Folder(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Envelope(cmd) => cmd.execute(printer, config).await,
|
||||
Self::Manual(cmd) => cmd.execute(printer).await,
|
||||
Self::Completion(cmd) => cmd.execute(printer).await,
|
||||
}
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
//! Email CLI module.
|
||||
//!
|
||||
//! This module contains the command matcher, the subcommands and the
|
||||
//! arguments related to the email domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_PAGE: &str = "page";
|
||||
const ARG_PAGE_SIZE: &str = "page-size";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_ENVELOPE: &str = "envelope";
|
||||
|
||||
pub type Page = usize;
|
||||
pub type PageSize = usize;
|
||||
|
||||
/// Represents the email commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
}
|
||||
|
||||
/// Email command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ENVELOPE) {
|
||||
if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
Some(Cmd::List(max_table_width, page_size, page))
|
||||
} else {
|
||||
Some(Cmd::List(None, None, 0))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the envelope subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_ENVELOPE)
|
||||
.about("Subcommand to manage envelopes")
|
||||
.long_about("Subcommand to manage envelopes like list")
|
||||
.subcommands([Command::new(CMD_LIST)
|
||||
.alias("lst")
|
||||
.about("List envelopes")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())])
|
||||
}
|
||||
|
||||
/// Represents the page size argument.
|
||||
fn page_size_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE_SIZE)
|
||||
.help("Page size")
|
||||
.long("page-size")
|
||||
.short('s')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the page size argument parser.
|
||||
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE_SIZE)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Represents the page argument.
|
||||
fn page_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE)
|
||||
.help("Page number")
|
||||
.short('p')
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
/// Represents the page argument parser.
|
||||
fn parse_page_arg(matches: &ArgMatches) -> usize {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default()
|
||||
}
|
68
src/email/envelope/command/list.rs
Normal file
68
src/email/envelope/command/list.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
account::arg::name::AccountNameFlag,
|
||||
backend::Backend,
|
||||
cache::arg::disable::DisableCacheFlag,
|
||||
config::TomlConfig,
|
||||
folder::arg::name::FolderNameOptionalArg,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::arg::max_width::MaxTableWidthFlag,
|
||||
};
|
||||
|
||||
/// List all envelopes from a folder
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct EnvelopeListCommand {
|
||||
#[command(flatten)]
|
||||
pub folder: FolderNameOptionalArg,
|
||||
|
||||
/// The page number
|
||||
#[arg(long, short, value_name = "NUMBER", default_value = "1")]
|
||||
pub page: usize,
|
||||
|
||||
/// The page size
|
||||
#[arg(long, short = 's', value_name = "NUMBER")]
|
||||
pub page_size: Option<usize>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub table: MaxTableWidthFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub account: AccountNameFlag,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cache: DisableCacheFlag,
|
||||
}
|
||||
|
||||
impl EnvelopeListCommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
info!("executing envelope list command");
|
||||
|
||||
let folder = &self.folder.name;
|
||||
|
||||
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||
let (toml_account_config, account_config) = config
|
||||
.clone()
|
||||
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||
|
||||
let page_size = self
|
||||
.page_size
|
||||
.unwrap_or(account_config.email_listing_page_size());
|
||||
let page = 1.max(self.page) - 1;
|
||||
|
||||
let envelopes = backend.list_envelopes(folder, page_size, page).await?;
|
||||
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &account_config.email_reading_format,
|
||||
max_width: self.table.max_width,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
24
src/email/envelope/command/mod.rs
Normal file
24
src/email/envelope/command/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
pub mod list;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::{config::TomlConfig, printer::Printer};
|
||||
|
||||
use self::list::EnvelopeListCommand;
|
||||
|
||||
/// Subcommand to manage envelopes
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum EnvelopeSubcommand {
|
||||
/// List all envelopes from a folder
|
||||
#[command(alias = "lst")]
|
||||
List(EnvelopeListCommand),
|
||||
}
|
||||
|
||||
impl EnvelopeSubcommand {
|
||||
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||
match self {
|
||||
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use email::account::config::AccountConfig;
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
};
|
||||
|
||||
pub async fn list<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &Backend,
|
||||
folder: &str,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
|
||||
let envelopes = backend.list_envelopes(&folder, page_size, page).await?;
|
||||
trace!("envelopes: {:?}", envelopes);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(envelopes),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
pub mod args;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod flag;
|
||||
pub mod handlers;
|
||||
|
||||
use anyhow::Result;
|
||||
use email::account::config::AccountConfig;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
use anyhow::Result;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
|
||||
use crate::{folder, template};
|
||||
use crate::template;
|
||||
|
||||
const ARG_CRITERIA: &str = "criterion";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
|
@ -71,7 +71,7 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
Some(Cmd::Attachments(ids))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
let folder = "INBOX";
|
||||
Some(Cmd::Copy(ids, folder))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
|
@ -83,7 +83,7 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
Some(Cmd::Forward(id, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
let folder = "INBOX";
|
||||
Some(Cmd::Move(ids, folder))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
|
||||
let ids = parse_ids_arg(m);
|
||||
|
@ -158,12 +158,12 @@ pub fn subcmd() -> Command {
|
|||
Command::new(CMD_COPY)
|
||||
.alias("cp")
|
||||
.about("Copy emails to the given folder")
|
||||
.arg(folder::args::target_arg())
|
||||
// .arg(folder::args::target_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_MOVE)
|
||||
.alias("mv")
|
||||
.about("Move emails to the given folder")
|
||||
.arg(folder::args::target_arg())
|
||||
// .arg(folder::args::target_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_DELETE)
|
||||
.aliases(["remove", "rm"])
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use clap::Parser;
|
||||
use email::account::config::DEFAULT_INBOX_FOLDER;
|
||||
|
||||
/// The folder name argument parser
|
||||
#[derive(Debug, Parser)]
|
||||
|
@ -7,3 +8,11 @@ pub struct FolderNameArg {
|
|||
#[arg(name = "folder-name", value_name = "FOLDER")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The optional folder name argument parser
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FolderNameOptionalArg {
|
||||
/// The name of the folder
|
||||
#[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)]
|
||||
pub name: String,
|
||||
}
|
||||
|
|
|
@ -1,241 +0,0 @@
|
|||
//! Folder CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher
|
||||
//! related to the folder domain.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, Arg, ArgAction, ArgMatches, Command};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_ALL: &str = "all";
|
||||
const ARG_EXCLUDE: &str = "exclude";
|
||||
const ARG_INCLUDE: &str = "include";
|
||||
const ARG_GLOBAL_SOURCE: &str = "global-source";
|
||||
const ARG_SOURCE: &str = "source";
|
||||
const ARG_TARGET: &str = "target";
|
||||
const CMD_CREATE: &str = "create";
|
||||
const CMD_DELETE: &str = "delete";
|
||||
const CMD_EXPUNGE: &str = "expunge";
|
||||
const CMD_FOLDER: &str = "folder";
|
||||
const CMD_LIST: &str = "list";
|
||||
|
||||
/// Represents the folder commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
Create,
|
||||
List(table::args::MaxTableWidth),
|
||||
Expunge,
|
||||
Delete,
|
||||
}
|
||||
|
||||
/// Represents the folder command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) {
|
||||
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
|
||||
info!("expunge folder subcommand matched");
|
||||
Some(Cmd::Expunge)
|
||||
} else if let Some(_) = m.subcommand_matches(CMD_CREATE) {
|
||||
debug!("create folder command matched");
|
||||
Some(Cmd::Create)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
debug!("list folders command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else if let Some(_) = m.subcommand_matches(CMD_DELETE) {
|
||||
debug!("delete folder command matched");
|
||||
Some(Cmd::Delete)
|
||||
} else {
|
||||
info!("no folder subcommand matched, falling back to subcommand list");
|
||||
Some(Cmd::List(None))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the folder subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_FOLDER)
|
||||
.about("Subcommand to manage folders")
|
||||
.long_about("Subcommand to manage folders like list, expunge or delete")
|
||||
.subcommands([
|
||||
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
|
||||
Command::new(CMD_CREATE)
|
||||
.aliases(["add", "new"])
|
||||
.about("Create a new folder"),
|
||||
Command::new(CMD_LIST)
|
||||
.about("List folders")
|
||||
.arg(table::args::max_width()),
|
||||
Command::new(CMD_DELETE)
|
||||
.aliases(["remove", "rm"])
|
||||
.about("Delete a folder with all its emails"),
|
||||
])
|
||||
}
|
||||
|
||||
/// Represents the source folder argument.
|
||||
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||
[Arg::new(ARG_GLOBAL_SOURCE)
|
||||
.help("Override the default INBOX folder")
|
||||
.long_help(
|
||||
"Override the default INBOX folder.
|
||||
|
||||
The given folder will be used by default for all other commands (when
|
||||
applicable).",
|
||||
)
|
||||
.long("folder")
|
||||
.short('f')
|
||||
.global(true)
|
||||
.value_name("name")]
|
||||
}
|
||||
|
||||
pub fn parse_global_source_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches
|
||||
.get_one::<String>(ARG_GLOBAL_SOURCE)
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn source_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_SOURCE).help(help).value_name("name")
|
||||
}
|
||||
|
||||
pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the all folders argument.
|
||||
pub fn all_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_ALL)
|
||||
.help(help)
|
||||
.long("all-folders")
|
||||
.alias("all")
|
||||
.short('A')
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with(ARG_SOURCE)
|
||||
.conflicts_with(ARG_INCLUDE)
|
||||
.conflicts_with(ARG_EXCLUDE)
|
||||
}
|
||||
|
||||
/// Represents the folders to include argument.
|
||||
pub fn include_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_INCLUDE)
|
||||
.help(help)
|
||||
.long("include-folder")
|
||||
.alias("only")
|
||||
.short('F')
|
||||
.value_name("FOLDER")
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.conflicts_with(ARG_SOURCE)
|
||||
.conflicts_with(ARG_ALL)
|
||||
.conflicts_with(ARG_EXCLUDE)
|
||||
}
|
||||
|
||||
/// Represents the folders to exclude argument.
|
||||
pub fn exclude_arg(help: &'static str) -> Arg {
|
||||
Arg::new(ARG_EXCLUDE)
|
||||
.help(help)
|
||||
.long("exclude-folder")
|
||||
.alias("except")
|
||||
.short('x')
|
||||
.value_name("FOLDER")
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.conflicts_with(ARG_SOURCE)
|
||||
.conflicts_with(ARG_ALL)
|
||||
.conflicts_with(ARG_INCLUDE)
|
||||
}
|
||||
|
||||
/// Represents the folders to exclude argument parser.
|
||||
pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet<String> {
|
||||
m.get_many::<String>(ARG_EXCLUDE)
|
||||
.unwrap_or_default()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the target folder argument.
|
||||
pub fn target_arg() -> Arg {
|
||||
Arg::new(ARG_TARGET)
|
||||
.help("Specifies the target folder")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the target folder argument parser.
|
||||
pub fn parse_target_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{error::ErrorKind, Command};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds() {
|
||||
let arg = Command::new("himalaya")
|
||||
.subcommand(subcmd())
|
||||
.get_matches_from(&["himalaya", "folders"]);
|
||||
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
|
||||
|
||||
let arg = Command::new("himalaya")
|
||||
.subcommand(subcmd())
|
||||
.get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]);
|
||||
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_source_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
Command::new("himalaya")
|
||||
.arg(source_arg())
|
||||
.get_matches_from(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(None, app.get_one::<String>(ARG_SOURCE).map(String::as_str));
|
||||
|
||||
let app = get_matches_from!["-f", "SOURCE"];
|
||||
assert_eq!(
|
||||
Some("SOURCE"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
|
||||
let app = get_matches_from!["--folder", "SOURCE"];
|
||||
assert_eq!(
|
||||
Some("SOURCE"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_target_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
Command::new("himalaya")
|
||||
.arg(target_arg())
|
||||
.try_get_matches_from_mut(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind());
|
||||
|
||||
let app = get_matches_from!["TARGET"];
|
||||
assert_eq!(
|
||||
Some("TARGET"),
|
||||
app.unwrap()
|
||||
.get_one::<String>(ARG_TARGET)
|
||||
.map(String::as_str)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -49,3 +49,194 @@ impl FolderSubcommand {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_trait::async_trait;
|
||||
use email::{
|
||||
account::config::AccountConfig,
|
||||
backend::Backend,
|
||||
envelope::{Envelope, Envelopes},
|
||||
flag::Flags,
|
||||
folder::{Folder, Folders},
|
||||
message::Messages,
|
||||
};
|
||||
use std::{any::Any, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::printer::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_should_list_mboxes() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.content = String::default();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> anyhow::Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestBackend;
|
||||
|
||||
#[async_trait]
|
||||
impl Backend for TestBackend {
|
||||
fn name(&self) -> String {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn add_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn list_folders(&mut self) -> email::Result<Folders> {
|
||||
Ok(Folders::from_iter([
|
||||
Folder {
|
||||
name: "INBOX".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
Folder {
|
||||
name: "Sent".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
]))
|
||||
}
|
||||
async fn expunge_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn purge_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn delete_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result<Envelope> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn list_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> email::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn search_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> email::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn remove_flags(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
_: &Flags,
|
||||
) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let account_config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
|
||||
assert!(list(&account_config, &mut printer, &mut backend, None)
|
||||
.await
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │DESC \n",
|
||||
"INBOX │desc \n",
|
||||
"Sent │desc \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,247 +0,0 @@
|
|||
//! Folder handling module.
|
||||
//!
|
||||
//! This module gathers all folder actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use dialoguer::Confirm;
|
||||
use email::account::config::AccountConfig;
|
||||
use std::process;
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
};
|
||||
|
||||
use super::Folders;
|
||||
|
||||
pub async fn expunge<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
|
||||
backend.expunge_folder(folder).await?;
|
||||
printer.print(format!("Folder {folder} successfully expunged!"))
|
||||
}
|
||||
|
||||
pub async fn list<P: Printer>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &Backend,
|
||||
max_width: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let folders: Folders = backend.list_folders().await?.into();
|
||||
printer.print_table(
|
||||
// TODO: remove Box
|
||||
Box::new(folders),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn create<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
|
||||
backend.add_folder(folder).await?;
|
||||
printer.print("Folder successfully created!")
|
||||
}
|
||||
|
||||
pub async fn delete<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
|
||||
if let Some(false) | None = Confirm::new()
|
||||
.with_prompt(format!("Confirm deletion of folder {folder}?"))
|
||||
.default(false)
|
||||
.report(false)
|
||||
.interact_opt()?
|
||||
{
|
||||
process::exit(0);
|
||||
};
|
||||
|
||||
backend.delete_folder(folder).await?;
|
||||
printer.print("Folder successfully deleted!")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_trait::async_trait;
|
||||
use email::{
|
||||
account::config::AccountConfig,
|
||||
backend::Backend,
|
||||
envelope::{Envelope, Envelopes},
|
||||
flag::Flags,
|
||||
folder::{Folder, Folders},
|
||||
message::Messages,
|
||||
};
|
||||
use std::{any::Any, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::printer::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_should_list_mboxes() {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct StringWriter {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl io::Write for StringWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.content
|
||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.content = String::default();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for StringWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteColor for StringWriter {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PrinterServiceTest {
|
||||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
opts: PrintTableOpts,
|
||||
) -> anyhow::Result<()> {
|
||||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestBackend;
|
||||
|
||||
#[async_trait]
|
||||
impl Backend for TestBackend {
|
||||
fn name(&self) -> String {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn add_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn list_folders(&mut self) -> email::Result<Folders> {
|
||||
Ok(Folders::from_iter([
|
||||
Folder {
|
||||
name: "INBOX".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
Folder {
|
||||
name: "Sent".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
]))
|
||||
}
|
||||
async fn expunge_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn purge_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn delete_folder(&mut self, _: &str) -> email::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result<Envelope> {
|
||||
unimplemented!();
|
||||
}
|
||||
async fn list_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> email::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn search_envelopes(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> email::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn remove_flags(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
_: &Flags,
|
||||
) -> email::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let account_config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
|
||||
assert!(list(&account_config, &mut printer, &mut backend, None)
|
||||
.await
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
||||
"NAME │DESC \n",
|
||||
"INBOX │desc \n",
|
||||
"Sent │desc \n",
|
||||
"\n"
|
||||
],
|
||||
printer.writer.content
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
pub mod arg;
|
||||
pub mod args;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
use clap::{Arg, ArgMatches};
|
||||
|
||||
const ARG_MAX_TABLE_WIDTH: &str = "max-table-width";
|
||||
|
||||
pub(crate) type MaxTableWidth = Option<usize>;
|
||||
|
||||
/// Represents the max table width argument.
|
||||
pub fn max_width() -> Arg {
|
||||
Arg::new(ARG_MAX_TABLE_WIDTH)
|
||||
.help("Defines a maximum width for the table")
|
||||
.long("max-width")
|
||||
.short('w')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the max table width argument parser.
|
||||
pub fn parse_max_width(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.get_one::<String>(ARG_MAX_TABLE_WIDTH)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
pub mod arg;
|
||||
pub mod args;
|
||||
pub mod table;
|
||||
|
||||
pub use table::*;
|
||||
|
|
Loading…
Reference in a new issue