diff --git a/README.md b/README.md index 15e6f6d..3292c8e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ for all the options.* ## Features -- Mailbox listing +- Folder listing - Email listing and searching - Email composition based on `$EDITOR` - Email manipulation (copy/move/delete) diff --git a/src/domain/email/args.rs b/src/domain/email/args.rs index 1e69682..7e3d160 100644 --- a/src/domain/email/args.rs +++ b/src/domain/email/args.rs @@ -1,316 +1,352 @@ -//! Module related to message CLI. +//! Module related to email CLI. //! -//! This module provides subcommands, arguments and a command matcher related to message. +//! This module provides subcommands, arguments and a command matcher related to email. use anyhow::Result; use clap::{self, App, Arg, ArgMatches, SubCommand}; use himalaya_lib::email::TplOverride; -use log::{debug, info, trace}; +use log::{debug, trace}; use crate::{email, flag, folder, tpl, ui::table}; -type Seq<'a> = &'a str; -type PageSize = usize; -type Page = usize; -type Mbox<'a> = &'a str; -type TextMime<'a> = &'a str; -type Raw = bool; -type All = bool; -type RawMsg<'a> = &'a str; -type Query = String; -type AttachmentPaths<'a> = Vec<&'a str>; -type MaxTableWidth = Option; -type Encrypt = bool; -type Criteria = String; -type Headers<'a> = Vec<&'a str>; +const ARG_ATTACHMENTS: &str = "attachment"; +const ARG_CRITERIA: &str = "criterion"; +const ARG_ENCRYPT: &str = "encrypt"; +const ARG_HEADERS: &str = "header"; +const ARG_ID: &str = "id"; +const ARG_IDS: &str = "ids"; +const ARG_MIME_TYPE: &str = "mime-type"; +const ARG_PAGE: &str = "page"; +const ARG_PAGE_SIZE: &str = "page-size"; +const ARG_QUERY: &str = "query"; +const ARG_RAW: &str = "raw"; +const ARG_REPLY_ALL: &str = "reply-all"; +const CMD_ATTACHMENTS: &str = "attachments"; +const CMD_COPY: &str = "copy"; +const CMD_DELETE: &str = "delete"; +const CMD_FORWARD: &str = "forward"; +const CMD_LIST: &str = "list"; +const CMD_MOVE: &str = "move"; +const CMD_READ: &str = "read"; +const CMD_REPLY: &str = "reply"; +const CMD_SAVE: &str = "save"; +const CMD_SEARCH: &str = "search"; +const CMD_SEND: &str = "send"; +const CMD_SORT: &str = "sort"; +const CMD_WRITE: &str = "write"; -/// Message commands. +type Criteria = String; +type Encrypt = bool; +type Folder<'a> = &'a str; +type Page = usize; +type PageSize = usize; +type Query = String; +type Raw = bool; +type RawEmail<'a> = &'a str; +type TextMime<'a> = &'a str; + +pub(crate) type All = bool; +pub(crate) type Attachments<'a> = Vec<&'a str>; +pub(crate) type Headers<'a> = Vec<&'a str>; +pub(crate) type Id<'a> = &'a str; +pub(crate) type Ids<'a> = &'a str; + +/// Represents the email commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Attachments(Seq<'a>), - Copy(Seq<'a>, Mbox<'a>), - Delete(Seq<'a>), - Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt), - List(MaxTableWidth, Option, Page), - Move(Seq<'a>, Mbox<'a>), - Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>), - Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt), - Save(RawMsg<'a>), - Search(Query, MaxTableWidth, Option, Page), - Sort(Criteria, Query, MaxTableWidth, Option, Page), - Send(RawMsg<'a>), - Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt), + Attachments(Id<'a>), + Copy(Id<'a>, Folder<'a>), + Delete(Id<'a>), + Forward(Id<'a>, Attachments<'a>, Encrypt), + List(table::args::MaxTableWidth, Option, Page), + Move(Id<'a>, Folder<'a>), + Read(Id<'a>, TextMime<'a>, Raw, Headers<'a>), + Reply(Id<'a>, All, Attachments<'a>, Encrypt), + Save(RawEmail<'a>), + Search(Query, table::args::MaxTableWidth, Option, Page), + Send(RawEmail<'a>), + Sort( + Criteria, + Query, + table::args::MaxTableWidth, + Option, + Page, + ), + Write(TplOverride<'a>, Attachments<'a>, Encrypt), Flag(Option>), Tpl(Option>), } -/// Message command matcher. +/// Email command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - info!("entering message command matcher"); + trace!("matches: {:?}", m); - if let Some(m) = m.subcommand_matches("attachments") { - info!("attachments command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - return Ok(Some(Cmd::Attachments(seq))); - } + let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { + debug!("attachments command matched"); + let id = parse_id_arg(m); + Cmd::Attachments(id) + } else if let Some(m) = m.subcommand_matches(CMD_COPY) { + debug!("copy command matched"); + let id = parse_id_arg(m); + let folder = folder::args::parse_target_arg(m); + Cmd::Copy(id, folder) + } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { + debug!("delete command matched"); + let id = parse_id_arg(m); + Cmd::Delete(id) + } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { + debug!("forward command matched"); + let id = parse_id_arg(m); + let attachments = parse_attachments_arg(m); + let encrypt = parse_encrypt_flag(m); + Cmd::Forward(id, attachments, encrypt) + } else if let Some(m) = m.subcommand_matches(CMD_LIST) { + debug!("list command matched"); + let max_table_width = table::args::parse_max_width(m); + let page_size = parse_page_size_arg(m); + let page = parse_page_arg(m); + Cmd::List(max_table_width, page_size, page) + } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { + debug!("move command matched"); + let id = parse_id_arg(m); + let folder = folder::args::parse_target_arg(m); + Cmd::Move(id, folder) + } else if let Some(m) = m.subcommand_matches(CMD_READ) { + debug!("read command matched"); + let id = parse_id_arg(m); + let mime = parse_mime_type_arg(m); + let raw = parse_raw_flag(m); + let headers = parse_headers_arg(m); + Cmd::Read(id, mime, raw, headers) + } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { + debug!("reply command matched"); + let id = parse_id_arg(m); + let all = parse_reply_all_flag(m); + let attachments = parse_attachments_arg(m); + let encrypt = parse_encrypt_flag(m); + Cmd::Reply(id, all, attachments, encrypt) + } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { + debug!("save command matched"); + let email = parse_raw_arg(m); + Cmd::Save(email) + } else if let Some(m) = m.subcommand_matches(CMD_SEARCH) { + debug!("search command matched"); + let max_table_width = table::args::parse_max_width(m); + let page_size = parse_page_size_arg(m); + let page = parse_page_arg(m); + let query = parse_query_arg(m); + Cmd::Search(query, max_table_width, page_size, page) + } else if let Some(m) = m.subcommand_matches(CMD_SORT) { + debug!("sort command matched"); + let max_table_width = table::args::parse_max_width(m); + let page_size = parse_page_size_arg(m); + let page = parse_page_arg(m); + let criteria = parse_criteria_arg(m); + let query = parse_query_arg(m); + Cmd::Sort(criteria, query, max_table_width, page_size, page) + } else if let Some(m) = m.subcommand_matches(CMD_SEND) { + debug!("send command matched"); + let email = parse_raw_arg(m); + Cmd::Send(email) + } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { + debug!("write command matched"); + let attachments = parse_attachments_arg(m); + let encrypt = parse_encrypt_flag(m); + let tpl = tpl::args::parse_override_arg(m); + Cmd::Write(tpl, attachments, encrypt) + } else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) { + Cmd::Tpl(tpl::args::matches(m)?) + } else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) { + Cmd::Flag(flag::args::matches(m)?) + } else { + debug!("default list command matched"); + Cmd::List(None, None, 0) + }; - if let Some(m) = m.subcommand_matches("copy") { - info!("copy command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - let mbox = m.value_of("folder-target").unwrap(); - debug!(r#"target mailbox: "{:?}""#, mbox); - return Ok(Some(Cmd::Copy(seq, mbox))); - } - - if let Some(m) = m.subcommand_matches("delete") { - info!("copy command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - return Ok(Some(Cmd::Delete(seq))); - } - - if let Some(m) = m.subcommand_matches("forward") { - info!("forward command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); - debug!("attachments paths: {:?}", paths); - let encrypt = m.is_present("encrypt"); - debug!("encrypt: {}", encrypt); - return Ok(Some(Cmd::Forward(seq, paths, encrypt))); - } - - if let Some(m) = m.subcommand_matches("list") { - info!("list command matched"); - let max_table_width = m - .value_of("max-table-width") - .and_then(|width| width.parse::().ok()); - debug!("max table width: {:?}", max_table_width); - let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); - debug!("page size: {:?}", page_size); - let page = m - .value_of("page") - .unwrap_or("1") - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default(); - debug!("page: {}", page); - return Ok(Some(Cmd::List(max_table_width, page_size, page))); - } - - if let Some(m) = m.subcommand_matches("move") { - info!("move command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - let mbox = m.value_of("folder-target").unwrap(); - debug!("target mailbox: {:?}", mbox); - return Ok(Some(Cmd::Move(seq, mbox))); - } - - if let Some(m) = m.subcommand_matches("read") { - info!("read command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - let mime = m.value_of("mime-type").unwrap(); - debug!("text mime: {}", mime); - let raw = m.is_present("raw"); - debug!("raw: {}", raw); - let headers: Vec<&str> = m.values_of("headers").unwrap_or_default().collect(); - debug!("headers: {:?}", headers); - return Ok(Some(Cmd::Read(seq, mime, raw, headers))); - } - - if let Some(m) = m.subcommand_matches("reply") { - info!("reply command matched"); - let seq = m.value_of("seq").unwrap(); - debug!("seq: {}", seq); - let all = m.is_present("reply-all"); - debug!("reply all: {}", all); - let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); - debug!("attachments paths: {:?}", paths); - let encrypt = m.is_present("encrypt"); - debug!("encrypt: {}", encrypt); - - return Ok(Some(Cmd::Reply(seq, all, paths, encrypt))); - } - - if let Some(m) = m.subcommand_matches("save") { - info!("save command matched"); - let msg = m.value_of("message").unwrap_or_default(); - trace!("message: {}", msg); - return Ok(Some(Cmd::Save(msg))); - } - - if let Some(m) = m.subcommand_matches("search") { - info!("search command matched"); - let max_table_width = m - .value_of("max-table-width") - .and_then(|width| width.parse::().ok()); - debug!("max table width: {:?}", max_table_width); - let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); - debug!("page size: {:?}", page_size); - let page = m - .value_of("page") - .unwrap() - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default(); - debug!("page: {}", page); - let query = m - .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) - } - // Escaped arg commands - (_, true) => { - cmds.push(format!("\"{}\"", cmd)); - (false, cmds) - } - // Regular commands - (_, false) => { - cmds.push(cmd.to_string()); - (false, cmds) - } - } - }) - .1 - .join(" "); - debug!("query: {}", query); - return Ok(Some(Cmd::Search(query, max_table_width, page_size, page))); - } - - if let Some(m) = m.subcommand_matches("sort") { - info!("sort command matched"); - let max_table_width = m - .value_of("max-table-width") - .and_then(|width| width.parse::().ok()); - debug!("max table width: {:?}", max_table_width); - let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); - debug!("page size: {:?}", page_size); - let page = m - .value_of("page") - .unwrap() - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default(); - debug!("page: {:?}", page); - let criteria = m - .values_of("criterion") - .unwrap_or_default() - .collect::>() - .join(" "); - debug!("criteria: {:?}", criteria); - let query = m - .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) - } - // Escaped arg commands - (_, true) => { - cmds.push(format!("\"{}\"", cmd)); - (false, cmds) - } - // Regular commands - (_, false) => { - cmds.push(cmd.to_string()); - (false, cmds) - } - } - }) - .1 - .join(" "); - debug!("query: {:?}", query); - return Ok(Some(Cmd::Sort( - criteria, - query, - max_table_width, - page_size, - page, - ))); - } - - if let Some(m) = m.subcommand_matches("send") { - info!("send command matched"); - let msg = m.value_of("message").unwrap_or_default(); - trace!("message: {}", msg); - return Ok(Some(Cmd::Send(msg))); - } - - if let Some(m) = m.subcommand_matches("write") { - info!("write command matched"); - let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); - debug!("attachments paths: {:?}", attachment_paths); - let encrypt = m.is_present("encrypt"); - debug!("encrypt: {}", encrypt); - let tpl = tpl::args::from_args(m); - return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt))); - } - - if let Some(m) = m.subcommand_matches("template") { - return Ok(Some(Cmd::Tpl(tpl::args::matches(m)?))); - } - - if let Some(m) = m.subcommand_matches("flag") { - return Ok(Some(Cmd::Flag(flag::args::matches(m)?))); - } - - info!("default list command matched"); - Ok(Some(Cmd::List(None, None, 0))) + Ok(Some(cmd)) } -/// Message sequence number argument. -pub fn seq_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("seq") - .help("Specifies the targetted message") - .value_name("SEQ") +/// Represents the email subcommands. +pub fn subcmds<'a>() -> Vec> { + vec![ + flag::args::subcmds(), + tpl::args::subcmds(), + vec![ + SubCommand::with_name(CMD_ATTACHMENTS) + .aliases(&["attachment", "attach", "att", "at", "a"]) + .about("Downloads all attachments of the targeted email") + .arg(email::args::id_arg()), + SubCommand::with_name(CMD_LIST) + .aliases(&["lst", "l"]) + .about("Lists all emails") + .arg(page_size_arg()) + .arg(page_arg()) + .arg(table::args::max_width()), + SubCommand::with_name(CMD_SEARCH) + .aliases(&["s", "query", "q"]) + .about("Lists emails matching the given IMAP query") + .arg(page_size_arg()) + .arg(page_arg()) + .arg(table::args::max_width()) + .arg(query_arg()), + SubCommand::with_name(CMD_SORT) + .about("Sorts emails by the given criteria and matching the given IMAP query") + .arg(page_size_arg()) + .arg(page_arg()) + .arg(table::args::max_width()) + .arg(criteria_arg()) + .arg(query_arg()), + SubCommand::with_name(CMD_WRITE) + .about("Writes a new email") + .args(&tpl::args::args()) + .arg(attachments_arg()) + .arg(encrypt_flag()), + SubCommand::with_name(CMD_SEND) + .about("Sends a raw email") + .arg(raw_arg()), + SubCommand::with_name(CMD_SAVE) + .about("Saves a raw email") + .arg(raw_arg()), + SubCommand::with_name(CMD_READ) + .about("Reads text bodies of a email") + .arg(id_arg()) + .arg(mime_type_arg()) + .arg(raw_flag()) + .arg(headers_arg()), + SubCommand::with_name(CMD_REPLY) + .aliases(&["rep", "r"]) + .about("Answers to an email") + .arg(id_arg()) + .arg(reply_all_flag()) + .arg(attachments_arg()) + .arg(encrypt_flag()), + SubCommand::with_name(CMD_FORWARD) + .aliases(&["fwd", "f"]) + .about("Forwards an email") + .arg(id_arg()) + .arg(attachments_arg()) + .arg(encrypt_flag()), + SubCommand::with_name(CMD_COPY) + .aliases(&["cp", "c"]) + .about("Copies an email to the targeted folder") + .arg(id_arg()) + .arg(folder::args::target_arg()), + SubCommand::with_name(CMD_MOVE) + .aliases(&["mv"]) + .about("Moves an email to the targeted folder") + .arg(id_arg()) + .arg(folder::args::target_arg()), + SubCommand::with_name(CMD_DELETE) + .aliases(&["del", "d", "remove", "rm"]) + .about("Deletes an email") + .arg(id_arg()), + ], + ] + .concat() +} + +/// Represents the email id argument. +pub fn id_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_ID) + .help("Specifies the targeted email") + .value_name("ID") .required(true) } -/// Message sequence range argument. -pub fn seq_range_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("seq-range") - .help("Specifies targetted message(s)") - .long_help("Specifies a range of targetted messages. The range follows the [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501#section-9) format: `1:5` matches messages with sequence number between 1 and 5, `1,5` matches messages with sequence number 1 or 5, * matches all messages.") - .value_name("SEQ") +/// Represents the email id argument parser. +pub fn parse_id_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(ARG_ID).unwrap() +} + +/// Represents the email sort criteria argument. +pub fn criteria_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_CRITERIA) + .long("criterion") + .short("c") + .help("Email sorting preferences") + .value_name("CRITERION:ORDER") + .takes_value(true) + .multiple(true) + .required(true) + .possible_values(&[ + "arrival", + "arrival:asc", + "arrival:desc", + "cc", + "cc:asc", + "cc:desc", + "date", + "date:asc", + "date:desc", + "from", + "from:asc", + "from:desc", + "size", + "size:asc", + "size:desc", + "subject", + "subject:asc", + "subject:desc", + "to", + "to:asc", + "to:desc", + ]) +} + +/// Represents the email sort criteria argument parser. +pub fn parse_criteria_arg<'a>(matches: &'a ArgMatches<'a>) -> String { + matches + .values_of(ARG_CRITERIA) + .unwrap_or_default() + .collect::>() + .join(" ") +} + +/// Represents the email ids argument. +pub fn id_range_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_IDS) + .help("Specifies targeted email(s)") + .long_help("Specifies a range of targeted emails. The range follows the RFC3501 format.") + .value_name("RANGE") .required(true) } -/// Message reply all argument. -pub fn reply_all_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("reply-all") +/// Represents the email ids argument parser. +pub fn parse_ids_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(email::args::ARG_IDS).unwrap() +} + +/// Represents the email reply all argument. +pub fn reply_all_flag<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_REPLY_ALL) .help("Includes all recipients") .short("A") .long("all") } -/// Message page size argument. +/// Represents the email reply all argument parser. +pub fn parse_reply_all_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { + matches.is_present(ARG_REPLY_ALL) +} + +/// Represents the page size argument. fn page_size_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("page-size") + Arg::with_name(ARG_PAGE_SIZE) .help("Page size") .short("s") .long("size") .value_name("INT") } -/// Message page argument. +/// Represents the page size argument parser. +fn parse_page_size_arg<'a>(matches: &'a ArgMatches<'a>) -> Option { + matches.value_of(ARG_PAGE_SIZE).and_then(|s| s.parse().ok()) +} + +/// Represents the page argument. fn page_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("page") + Arg::with_name(ARG_PAGE) .help("Page number") .short("p") .long("page") @@ -318,154 +354,136 @@ fn page_arg<'a>() -> Arg<'a, 'a> { .default_value("0") } -/// Message attachment argument. +/// Represents the page argument parser. +fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize { + matches + .value_of(ARG_PAGE) + .unwrap_or("1") + .parse() + .ok() + .map(|page| 1.max(page) - 1) + .unwrap_or_default() +} + +/// Represents the email attachments argument. pub fn attachments_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("attachments") - .help("Adds attachment to the message") + Arg::with_name(ARG_ATTACHMENTS) + .help("Adds attachment to the email") .short("a") .long("attachment") .value_name("PATH") .multiple(true) } -/// Represents the message headers argument. +/// Represents the email attachments argument parser. +pub fn parse_attachments_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> { + matches + .values_of(ARG_ATTACHMENTS) + .unwrap_or_default() + .collect() +} + +/// Represents the email headers argument. pub fn headers_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("headers") - .help("Shows additional headers with the message") + Arg::with_name(ARG_HEADERS) + .help("Shows additional headers with the email") .short("h") .long("header") - .value_name("STR") + .value_name("STRING") .multiple(true) } -/// Message encrypt argument. -pub fn encrypt_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("encrypt") - .help("Encrypts the message") +/// Represents the email headers argument parser. +pub fn parse_headers_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> { + matches.values_of(ARG_HEADERS).unwrap_or_default().collect() +} + +/// Represents the raw flag. +pub fn raw_flag<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_RAW) + .help("Reads a raw email") + .long("raw") + .short("r") +} + +/// Represents the raw flag parser. +pub fn parse_raw_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { + matches.is_present(ARG_RAW) +} + +/// Represents the email raw argument. +pub fn raw_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_RAW).raw(true) +} + +/// Represents the email raw argument parser. +pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(ARG_RAW).unwrap_or_default() +} + +/// Represents the email encrypt flag. +pub fn encrypt_flag<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_ENCRYPT) + .help("Encrypts the email") .short("e") .long("encrypt") } -/// Message subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![ - flag::args::subcmds(), - tpl::args::subcmds(), - vec![ - SubCommand::with_name("attachments") - .aliases(&["attachment", "att", "a"]) - .about("Downloads all message attachments") - .arg(email::args::seq_arg()), - SubCommand::with_name("list") - .aliases(&["lst", "l"]) - .about("Lists all messages") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()), - SubCommand::with_name("search") - .aliases(&["s", "query", "q"]) - .about("Lists messages matching the given IMAP query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()) - .arg( - Arg::with_name("query") - .help("IMAP query") - .long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.") - .value_name("QUERY") - .multiple(true) - .required(true), - ), - SubCommand::with_name("sort") - .about("Sorts messages by the given criteria and matching the given IMAP query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()) - .arg( - Arg::with_name("criterion") - .long("criterion") - .short("c") - .help("Defines the message sorting preferences") - .value_name("CRITERION:ORDER") - .takes_value(true) - .multiple(true) - .required(true) - .possible_values(&[ - "arrival", "arrival:asc", "arrival:desc", - "cc", "cc:asc", "cc:desc", - "date", "date:asc", "date:desc", - "from", "from:asc", "from:desc", - "size", "size:asc", "size:desc", - "subject", "subject:asc", "subject:desc", - "to", "to:asc", "to:desc", - ]), - ) - .arg( - Arg::with_name("query") - .help("IMAP query") - .long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.") - .value_name("QUERY") - .default_value("ALL") - .raw(true), - ), - SubCommand::with_name("write") - .about("Writes a new message") - .args(&tpl::args::tpl_args()) - .arg(attachments_arg()) - .arg(encrypt_arg()), - SubCommand::with_name("send") - .about("Sends a raw message") - .arg(Arg::with_name("message").raw(true)), - SubCommand::with_name("save") - .about("Saves a raw message") - .arg(Arg::with_name("message").raw(true)), - SubCommand::with_name("read") - .about("Reads text bodies of a message") - .arg(seq_arg()) - .arg( - Arg::with_name("mime-type") - .help("MIME type to use") - .short("t") - .long("mime-type") - .value_name("MIME") - .possible_values(&["plain", "html"]) - .default_value("plain"), - ) - .arg( - Arg::with_name("raw") - .help("Reads raw message") - .long("raw") - .short("r"), - ) - .arg(headers_arg()), - SubCommand::with_name("reply") - .aliases(&["rep", "r"]) - .about("Answers to a message") - .arg(seq_arg()) - .arg(reply_all_arg()) - .arg(attachments_arg()) - .arg(encrypt_arg()), - SubCommand::with_name("forward") - .aliases(&["fwd", "f"]) - .about("Forwards a message") - .arg(seq_arg()) - .arg(attachments_arg()) - .arg(encrypt_arg()), - SubCommand::with_name("copy") - .aliases(&["cp", "c"]) - .about("Copies a message to the targetted mailbox") - .arg(seq_arg()) - .arg(folder::args::target_arg()), - SubCommand::with_name("move") - .aliases(&["mv"]) - .about("Moves a message to the targetted mailbox") - .arg(seq_arg()) - .arg(folder::args::target_arg()), - SubCommand::with_name("delete") - .aliases(&["del", "d", "remove", "rm"]) - .about("Deletes a message") - .arg(seq_arg()), - ], - ] - .concat() +/// Represents the email encrypt flag parser. +pub fn parse_encrypt_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { + matches.is_present(ARG_ENCRYPT) +} + +/// Represents the email MIME type argument. +pub fn mime_type_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_MIME_TYPE) + .help("MIME type to use") + .short("t") + .long("mime-type") + .value_name("MIME") + .possible_values(&["plain", "html"]) + .default_value("plain") +} + +/// Represents the email MIME type argument parser. +pub fn parse_mime_type_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(ARG_MIME_TYPE).unwrap() +} + +/// Represents the email query argument. +pub fn query_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_QUERY) + .help("IMAP query") + .long_help("The IMAP query format follows the RFC3501. The query is case-insensitive.") + .value_name("QUERY") + .multiple(true) + .required(true) +} + +/// Represents the email query argument parser. +pub fn parse_query_arg<'a>(matches: &'a ArgMatches<'a>) -> String { + matches + .values_of(ARG_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) + } + // Escaped arg commands + (_, true) => { + cmds.push(format!("\"{}\"", cmd)); + (false, cmds) + } + // Regular commands + (_, false) => { + cmds.push(cmd.to_string()); + (false, cmds) + } + } + }) + .1 + .join(" ") } diff --git a/src/domain/email/handlers.rs b/src/domain/email/handlers.rs index 12f8599..b632907 100644 --- a/src/domain/email/handlers.rs +++ b/src/domain/email/handlers.rs @@ -51,7 +51,7 @@ pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>( printer.print_struct("Done!") } -/// Copy a message from a mailbox to another. +/// Copy a message from a folder to another. pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, mbox_src: &str, @@ -77,7 +77,7 @@ pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>( printer.print_struct(format!("Message(s) {} successfully deleted", seq)) } -/// Forward the given message UID from the selected mailbox. +/// Forward the given message UID from the selected folder. pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( seq: &str, attachments_paths: Vec<&str>, @@ -104,7 +104,7 @@ pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( Ok(()) } -/// List paginated messages from the selected mailbox. +/// List paginated messages from the selected folder. pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( max_width: Option, page_size: Option, @@ -195,7 +195,7 @@ pub fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( Ok(()) } -/// Move a message from a mailbox to another. +/// Move a message from a folder to another. pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, mbox_src: &str, @@ -260,16 +260,14 @@ pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( Ok(()) } -/// Saves a raw message to the targetted mailbox. +/// Saves a raw message to the targetted folder. pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( mbox: &str, raw_msg: &str, printer: &mut P, backend: &mut B, ) -> Result<()> { - info!("entering save message handler"); - - debug!("mailbox: {}", mbox); + debug!("folder: {}", mbox); let is_tty = atty::is(Stream::Stdin); debug!("is tty: {}", is_tty); @@ -290,7 +288,8 @@ pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( Ok(()) } -/// Paginate messages from the selected mailbox matching the specified query. +/// Paginate messages from the selected folder matching the specified +/// query. pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>( query: String, max_width: Option, @@ -314,7 +313,8 @@ pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>( ) } -/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria. +/// Paginates messages from the selected folder matching the specified +/// query, sorted by the given criteria. pub fn sort<'a, P: Printer, B: Backend<'a> + ?Sized>( sort: String, query: String, diff --git a/src/domain/flag/args.rs b/src/domain/flag/args.rs index 78abaf8..c4098aa 100644 --- a/src/domain/flag/args.rs +++ b/src/domain/flag/args.rs @@ -1,7 +1,7 @@ -//! Message flag CLI module. +//! Email flag CLI module. //! -//! This module provides subcommands, arguments and a command matcher related to the message flag -//! domain. +//! This module provides subcommands, arguments and a command matcher +//! related to the email flag domain. use anyhow::Result; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -9,101 +9,91 @@ use log::{debug, info}; use crate::email; -type SeqRange<'a> = &'a str; +const ARG_FLAGS: &str = "flag"; + +const CMD_ADD: &str = "add"; +const CMD_DEL: &str = "remove"; +const CMD_SET: &str = "set"; + +pub(crate) const CMD_FLAG: &str = "flag"; + type Flags = String; /// Represents the flag commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - /// Represents the add flags command. - Add(SeqRange<'a>, Flags), - /// Represents the set flags command. - Set(SeqRange<'a>, Flags), - /// Represents the remove flags command. - Remove(SeqRange<'a>, Flags), + Add(email::args::Ids<'a>, Flags), + Set(email::args::Ids<'a>, Flags), + Del(email::args::Ids<'a>, Flags), } -/// Defines the flag command matcher. +/// Represents the flag command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - info!("entering message flag command matcher"); - - if let Some(m) = m.subcommand_matches("add") { - info!("add subcommand matched"); - let seq_range = m.value_of("seq-range").unwrap(); - debug!("seq range: {}", seq_range); - let flags: String = m - .values_of("flags") - .unwrap_or_default() - .collect::>() - .join(" "); - debug!("flags: {:?}", flags); - return Ok(Some(Cmd::Add(seq_range, flags))); - } - - if let Some(m) = m.subcommand_matches("set") { - info!("set subcommand matched"); - let seq_range = m.value_of("seq-range").unwrap(); - debug!("seq range: {}", seq_range); - let flags: String = m - .values_of("flags") - .unwrap_or_default() - .collect::>() - .join(" "); - debug!("flags: {:?}", flags); - return Ok(Some(Cmd::Set(seq_range, flags))); - } - - if let Some(m) = m.subcommand_matches("remove") { + let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) { + debug!("add subcommand matched"); + let ids = email::args::parse_ids_arg(m); + let flags: String = parse_flags_arg(m); + Some(Cmd::Add(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_SET) { + debug!("set subcommand matched"); + let ids = email::args::parse_ids_arg(m); + let flags: String = parse_flags_arg(m); + Some(Cmd::Set(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_DEL) { info!("remove subcommand matched"); - let seq_range = m.value_of("seq-range").unwrap(); - debug!("seq range: {}", seq_range); - let flags: String = m - .values_of("flags") - .unwrap_or_default() - .collect::>() - .join(" "); - debug!("flags: {:?}", flags); - return Ok(Some(Cmd::Remove(seq_range, flags))); - } + let ids = email::args::parse_ids_arg(m); + let flags: String = parse_flags_arg(m); + Some(Cmd::Del(ids, flags)) + } else { + None + }; - Ok(None) + Ok(cmd) } -/// Defines the flags argument. -fn flags_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name("flags") - .help("IMAP flags") - .long_help("IMAP flags. Flags are case-insensitive, and they do not need to be prefixed with `\\`.") +/// Represents the flag subcommands. +pub fn subcmds<'a>() -> Vec> { + vec![SubCommand::with_name(CMD_FLAG) + .aliases(&["flags", "flg"]) + .about("Handles email flags") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name(CMD_ADD) + .aliases(&["a"]) + .about("Adds email flags") + .arg(email::args::id_range_arg()) + .arg(flags_arg()), + ) + .subcommand( + SubCommand::with_name(CMD_SET) + .aliases(&["s", "change", "c"]) + .about("Sets email flags") + .arg(email::args::id_range_arg()) + .arg(flags_arg()), + ) + .subcommand( + SubCommand::with_name(CMD_DEL) + .aliases(&["rem", "rm", "r", "delete", "del", "d"]) + .about("Removes email flags") + .arg(email::args::id_range_arg()) + .arg(flags_arg()), + )] +} + +/// Represents the flags argument. +pub fn flags_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_FLAGS) + .long_help("Flags are case-insensitive, and they do not need to be prefixed with `\\`.") .value_name("FLAGSā€¦") .multiple(true) .required(true) } -/// Contains flag subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name("flag") - .aliases(&["flags", "flg"]) - .about("Handles flags") - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - SubCommand::with_name("add") - .aliases(&["a"]) - .about("Adds flags to a message") - .arg(email::args::seq_range_arg()) - .arg(flags_arg()), - ) - .subcommand( - SubCommand::with_name("set") - .aliases(&["s", "change", "c"]) - .about("Replaces all message flags") - .arg(email::args::seq_range_arg()) - .arg(flags_arg()), - ) - .subcommand( - SubCommand::with_name("remove") - .aliases(&["rem", "rm", "r", "delete", "del", "d"]) - .about("Removes flags from a message") - .arg(email::args::seq_range_arg()) - .arg(flags_arg()), - )] +/// Represents the flags argument parser. +pub fn parse_flags_arg<'a>(matches: &'a ArgMatches<'a>) -> String { + matches + .values_of(ARG_FLAGS) + .unwrap_or_default() + .collect::>() + .join(" ") } diff --git a/src/domain/folder/args.rs b/src/domain/folder/args.rs index cf4a898..972f4c3 100644 --- a/src/domain/folder/args.rs +++ b/src/domain/folder/args.rs @@ -1,80 +1,98 @@ -//! Mailbox CLI module. +//! Folder CLI module. //! -//! This module provides subcommands, arguments and a command matcher related to the mailbox -//! domain. +//! This module provides subcommands, arguments and a command matcher +//! related to the folder domain. use anyhow::Result; -use clap; -use log::{debug, info}; +use clap::{self, App, Arg, ArgMatches, SubCommand}; +use log::debug; use crate::ui::table; -type MaxTableWidth = Option; +const ARG_SOURCE: &str = "source"; +const ARG_TARGET: &str = "target"; +const CMD_FOLDERS: &str = "folders"; -/// Represents the mailbox commands. +/// Represents the folder commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd { - /// Represents the list mailboxes command. - List(MaxTableWidth), + List(table::args::MaxTableWidth), } -/// Defines the mailbox command matcher. -pub fn matches(m: &clap::ArgMatches) -> Result> { - info!("entering mailbox command matcher"); +/// Represents the folder command matcher. +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) { + debug!("folders command matched"); + let max_table_width = table::args::parse_max_width(m); + Some(Cmd::List(max_table_width)) + } else { + None + }; - if let Some(m) = m.subcommand_matches("mailboxes") { - info!("mailboxes command matched"); - let max_table_width = m - .value_of("max-table-width") - .and_then(|width| width.parse::().ok()); - debug!("max table width: {:?}", max_table_width); - return Ok(Some(Cmd::List(max_table_width))); - } - - Ok(None) + Ok(cmd) } -/// Contains mailbox subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![clap::SubCommand::with_name("mailboxes") +/// Represents folder subcommands. +pub fn subcmds<'a>() -> Vec> { + vec![SubCommand::with_name(CMD_FOLDERS) .aliases(&[ - "mailbox", "mboxes", "mbox", "mb", "m", "folders", "fold", "fo", + "folder", + "fold", + "fo", + "mailboxes", + "mailbox", + "mboxes", + "mbox", + "mb", + "m", ]) .about("Lists folders") .arg(table::args::max_width())] } -/// Defines the source mailbox argument. -pub fn source_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("folder-source") +/// Represents the source folder argument. +pub fn source_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_SOURCE) .short("f") .long("folder") .help("Specifies the folder source") .value_name("SOURCE") } -/// Defines the target mailbox argument. -pub fn target_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("folder-target") +/// Represents the source folder argument parser. +pub fn parse_source_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(ARG_SOURCE).unwrap() +} + +/// Represents the target folder argument. +pub fn target_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name(ARG_TARGET) .help("Specifies the folder target") .value_name("TARGET") .required(true) } +/// Represents the target folder argument parser. +pub fn parse_target_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(ARG_TARGET).unwrap() +} + #[cfg(test)] mod tests { + use clap::{App, ErrorKind}; + use super::*; #[test] fn it_should_match_cmds() { - let arg = clap::App::new("himalaya") + let arg = App::new("himalaya") .subcommands(subcmds()) - .get_matches_from(&["himalaya", "mailboxes"]); + .get_matches_from(&["himalaya", "folders"]); assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap()); - let arg = clap::App::new("himalaya") + let arg = App::new("himalaya") .subcommands(subcmds()) - .get_matches_from(&["himalaya", "mailboxes", "--max-width", "20"]); + .get_matches_from(&["himalaya", "folders", "--max-width", "20"]); assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap()); } @@ -82,57 +100,53 @@ mod tests { fn it_should_match_aliases() { macro_rules! get_matches_from { ($alias:expr) => { - clap::App::new("himalaya") + App::new("himalaya") .subcommands(subcmds()) .get_matches_from(&["himalaya", $alias]) .subcommand_name() }; } - assert_eq!(Some("mailboxes"), get_matches_from!["mailboxes"]); - assert_eq!(Some("mailboxes"), get_matches_from!["mboxes"]); - assert_eq!(Some("mailboxes"), get_matches_from!["mbox"]); - assert_eq!(Some("mailboxes"), get_matches_from!["mb"]); - assert_eq!(Some("mailboxes"), get_matches_from!["m"]); + assert_eq!(Some("folders"), get_matches_from!["folders"]); + assert_eq!(Some("folders"), get_matches_from!["folder"]); + assert_eq!(Some("folders"), get_matches_from!["fold"]); + assert_eq!(Some("folders"), get_matches_from!["fo"]); } #[test] fn it_should_match_source_arg() { macro_rules! get_matches_from { ($($arg:expr),*) => { - clap::App::new("himalaya") + App::new("himalaya") .arg(source_arg()) .get_matches_from(&["himalaya", $($arg,)*]) }; } let app = get_matches_from![]; - assert_eq!(None, app.value_of("folder-source")); + assert_eq!(None, app.value_of("source")); - let app = get_matches_from!["-m", "SOURCE"]; - assert_eq!(Some("SOURCE"), app.value_of("folder-source")); + let app = get_matches_from!["-f", "SOURCE"]; + assert_eq!(Some("SOURCE"), app.value_of("source")); - let app = get_matches_from!["--mailbox", "SOURCE"]; - assert_eq!(Some("SOURCE"), app.value_of("folder-source")); + let app = get_matches_from!["--folder", "SOURCE"]; + assert_eq!(Some("SOURCE"), app.value_of("source")); } #[test] fn it_should_match_target_arg() { macro_rules! get_matches_from { ($($arg:expr),*) => { - clap::App::new("himalaya") + App::new("himalaya") .arg(target_arg()) .get_matches_from_safe(&["himalaya", $($arg,)*]) }; } let app = get_matches_from![]; - assert_eq!( - clap::ErrorKind::MissingRequiredArgument, - app.unwrap_err().kind - ); + assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind); let app = get_matches_from!["TARGET"]; - assert_eq!(Some("TARGET"), app.unwrap().value_of("folder-target")); + assert_eq!(Some("TARGET"), app.unwrap().value_of("target")); } } diff --git a/src/domain/folder/handlers.rs b/src/domain/folder/handlers.rs index 125d4bd..c5d932c 100644 --- a/src/domain/folder/handlers.rs +++ b/src/domain/folder/handlers.rs @@ -1,26 +1,25 @@ -//! Mailbox handling module. +//! Folder handling module. //! -//! This module gathers all mailbox actions triggered by the CLI. +//! This module gathers all folder actions triggered by the CLI. use anyhow::Result; use himalaya_lib::{AccountConfig, Backend}; -use log::{info, trace}; +use log::trace; use crate::printer::{PrintTableOpts, Printer}; -/// Lists all mailboxes. +/// Lists all folders. pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( max_width: Option, config: &AccountConfig, printer: &mut P, backend: &mut B, ) -> Result<()> { - info!("entering list mailbox handler"); - let mboxes = backend.folder_list()?; - trace!("mailboxes: {:?}", mboxes); + let folders = backend.folder_list()?; + trace!("folders: {:?}", folders); printer.print_table( // TODO: remove Box - Box::new(mboxes), + Box::new(folders), PrintTableOpts { format: &config.email_reading_format, max_width, @@ -109,20 +108,18 @@ mod tests { unimplemented!(); } fn folder_list(&mut self) -> backend::Result { - Ok(Folders { - folders: vec![ - Folder { - delim: "/".into(), - name: "INBOX".into(), - desc: "desc".into(), - }, - Folder { - delim: "/".into(), - name: "Sent".into(), - desc: "desc".into(), - }, - ], - }) + Ok(Folders(vec![ + Folder { + delim: "/".into(), + name: "INBOX".into(), + desc: "desc".into(), + }, + Folder { + delim: "/".into(), + name: "Sent".into(), + desc: "desc".into(), + }, + ])) } fn folder_delete(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); @@ -143,9 +140,6 @@ mod tests { fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result { unimplemented!() } - fn email_list(&mut self, _: &str, _: &str) -> backend::Result { - unimplemented!() - } fn email_get(&mut self, _: &str, _: &str) -> backend::Result { unimplemented!() } @@ -167,6 +161,9 @@ mod tests { fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } + fn as_any(&self) -> &(dyn std::any::Any + 'a) { + self + } } let account_config = AccountConfig::default(); diff --git a/src/domain/imap/args.rs b/src/domain/imap/args.rs index 6e8eab4..abfd932 100644 --- a/src/domain/imap/args.rs +++ b/src/domain/imap/args.rs @@ -42,7 +42,7 @@ pub fn matches(m: &ArgMatches) -> Result> { pub fn subcmds<'a>() -> Vec> { vec![ clap::SubCommand::with_name("notify") - .about("Notifies when new messages arrive in the given mailbox") + .about("Notifies when new messages arrive in the given folder") .aliases(&["idle"]) .arg( clap::Arg::with_name("keepalive") diff --git a/src/domain/tpl/args.rs b/src/domain/tpl/args.rs index 081ca3a..f09423a 100644 --- a/src/domain/tpl/args.rs +++ b/src/domain/tpl/args.rs @@ -1,138 +1,163 @@ -//! Module related to message template CLI. +//! Module related to email template CLI. //! -//! This module provides subcommands, arguments and a command matcher related to message template. +//! This module provides subcommands, arguments and a command matcher +//! related to email templating. use anyhow::Result; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; use himalaya_lib::email::TplOverride; -use log::{debug, info, trace}; +use log::debug; use crate::email; -type Seq<'a> = &'a str; -type ReplyAll = bool; -type AttachmentPaths<'a> = Vec<&'a str>; +const ARG_BCC: &str = "bcc"; +const ARG_BODY: &str = "body"; +const ARG_CC: &str = "cc"; +const ARG_FROM: &str = "from"; +const ARG_HEADERS: &str = "header"; +const ARG_SIGNATURE: &str = "signature"; +const ARG_SUBJECT: &str = "subject"; +const ARG_TO: &str = "to"; +const ARG_TPL: &str = "template"; +const CMD_FORWARD: &str = "forward"; +const CMD_NEW: &str = "new"; +const CMD_REPLY: &str = "reply"; +const CMD_SAVE: &str = "save"; +const CMD_SEND: &str = "send"; + +pub(crate) const CMD_TPL: &str = "template"; + type Tpl<'a> = &'a str; -pub fn from_args<'a>(matches: &'a ArgMatches<'a>) -> TplOverride { - TplOverride { - subject: matches.value_of("subject"), - from: matches.values_of("from").map(|v| v.collect()), - to: matches.values_of("to").map(|v| v.collect()), - cc: matches.values_of("cc").map(|v| v.collect()), - bcc: matches.values_of("bcc").map(|v| v.collect()), - headers: matches.values_of("headers").map(|v| v.collect()), - body: matches.value_of("body"), - sig: matches.value_of("signature"), - } -} - -/// Message template commands. +/// Represents the template commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { + Forward(email::args::Id<'a>, TplOverride<'a>), New(TplOverride<'a>), - Reply(Seq<'a>, ReplyAll, TplOverride<'a>), - Forward(Seq<'a>, TplOverride<'a>), - Save(AttachmentPaths<'a>, Tpl<'a>), - Send(AttachmentPaths<'a>, Tpl<'a>), + Reply(email::args::Id<'a>, email::args::All, TplOverride<'a>), + Save(email::args::Attachments<'a>, Tpl<'a>), + Send(email::args::Attachments<'a>, Tpl<'a>), } -/// Message template command matcher. +/// Represents the template command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - info!("entering message template command matcher"); + let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) { + debug!("forward subcommand matched"); + let id = email::args::parse_id_arg(m); + let tpl = parse_override_arg(m); + Some(Cmd::Forward(id, tpl)) + } else if let Some(m) = m.subcommand_matches(CMD_NEW) { + debug!("new subcommand matched"); + let tpl = parse_override_arg(m); + Some(Cmd::New(tpl)) + } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { + debug!("reply subcommand matched"); + let id = email::args::parse_id_arg(m); + let all = email::args::parse_reply_all_flag(m); + let tpl = parse_override_arg(m); + Some(Cmd::Reply(id, all, tpl)) + } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { + debug!("save subcommand matched"); + let attachments = email::args::parse_attachments_arg(m); + let tpl = parse_raw_arg(m); + Some(Cmd::Save(attachments, tpl)) + } else if let Some(m) = m.subcommand_matches(CMD_SEND) { + debug!("send subcommand matched"); + let attachments = email::args::parse_attachments_arg(m); + let tpl = parse_raw_arg(m); + Some(Cmd::Send(attachments, tpl)) + } else { + None + }; - if let Some(m) = m.subcommand_matches("new") { - info!("new subcommand matched"); - let tpl = from_args(m); - trace!("template override: {:?}", tpl); - return Ok(Some(Cmd::New(tpl))); - } - - if let Some(m) = m.subcommand_matches("reply") { - info!("reply subcommand matched"); - let seq = m.value_of("seq").unwrap(); - debug!("sequence: {}", seq); - let all = m.is_present("reply-all"); - debug!("reply all: {}", all); - let tpl = from_args(m); - trace!("template override: {:?}", tpl); - return Ok(Some(Cmd::Reply(seq, all, tpl))); - } - - if let Some(m) = m.subcommand_matches("forward") { - info!("forward subcommand matched"); - let seq = m.value_of("seq").unwrap(); - debug!("sequence: {}", seq); - let tpl = from_args(m); - trace!("template args: {:?}", tpl); - return Ok(Some(Cmd::Forward(seq, tpl))); - } - - if let Some(m) = m.subcommand_matches("save") { - info!("save subcommand matched"); - let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); - trace!("attachments paths: {:?}", attachment_paths); - let tpl = m.value_of("template").unwrap_or_default(); - trace!("template: {}", tpl); - return Ok(Some(Cmd::Save(attachment_paths, tpl))); - } - - if let Some(m) = m.subcommand_matches("send") { - info!("send subcommand matched"); - let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); - trace!("attachments paths: {:?}", attachment_paths); - let tpl = m.value_of("template").unwrap_or_default(); - trace!("template: {}", tpl); - return Ok(Some(Cmd::Send(attachment_paths, tpl))); - } - - Ok(None) + Ok(cmd) } -/// Message template args. -pub fn tpl_args<'a>() -> Vec> { +/// Represents the template subcommands. +pub fn subcmds<'a>() -> Vec> { + vec![SubCommand::with_name(CMD_TPL) + .aliases(&["tpl"]) + .about("Handles email templates") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name(CMD_NEW) + .aliases(&["n"]) + .about("Generates a template for a new email") + .args(&args()), + ) + .subcommand( + SubCommand::with_name(CMD_REPLY) + .aliases(&["rep", "re", "r"]) + .about("Generates a template for replying to an email") + .arg(email::args::id_arg()) + .arg(email::args::reply_all_flag()) + .args(&args()), + ) + .subcommand( + SubCommand::with_name(CMD_FORWARD) + .aliases(&["fwd", "fw", "f"]) + .about("Generates a template for forwarding an email") + .arg(email::args::id_arg()) + .args(&args()), + ) + .subcommand( + SubCommand::with_name(CMD_SAVE) + .about("Saves an email based on the given template") + .arg(&email::args::attachments_arg()) + .arg(Arg::with_name(ARG_TPL).raw(true)), + ) + .subcommand( + SubCommand::with_name(CMD_SEND) + .about("Sends an email based on the given template") + .arg(&email::args::attachments_arg()) + .arg(Arg::with_name(ARG_TPL).raw(true)), + )] +} + +/// Represents the template arguments. +pub fn args<'a>() -> Vec> { vec![ - Arg::with_name("subject") + Arg::with_name(ARG_SUBJECT) .help("Overrides the Subject header") .short("s") .long("subject") .value_name("STRING"), - Arg::with_name("from") + Arg::with_name(ARG_FROM) .help("Overrides the From header") .short("f") .long("from") .value_name("ADDR") .multiple(true), - Arg::with_name("to") + Arg::with_name(ARG_TO) .help("Overrides the To header") .short("t") .long("to") .value_name("ADDR") .multiple(true), - Arg::with_name("cc") + Arg::with_name(ARG_CC) .help("Overrides the Cc header") .short("c") .long("cc") .value_name("ADDR") .multiple(true), - Arg::with_name("bcc") + Arg::with_name(ARG_BCC) .help("Overrides the Bcc header") .short("b") .long("bcc") .value_name("ADDR") .multiple(true), - Arg::with_name("header") + Arg::with_name(ARG_HEADERS) .help("Overrides a specific header") .short("h") .long("header") - .value_name("KEY: VAL") + .value_name("KEY:VAL") .multiple(true), - Arg::with_name("body") + Arg::with_name(ARG_BODY) .help("Overrides the body") .short("B") .long("body") .value_name("STRING"), - Arg::with_name("signature") + Arg::with_name(ARG_SIGNATURE) .help("Overrides the signature") .short("S") .long("signature") @@ -140,43 +165,21 @@ pub fn tpl_args<'a>() -> Vec> { ] } -/// Message template subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name("template") - .aliases(&["tpl"]) - .about("Generates a message template") - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - SubCommand::with_name("new") - .aliases(&["n"]) - .about("Generates a new message template") - .args(&tpl_args()), - ) - .subcommand( - SubCommand::with_name("reply") - .aliases(&["rep", "re", "r"]) - .about("Generates a reply message template") - .arg(email::args::seq_arg()) - .arg(email::args::reply_all_arg()) - .args(&tpl_args()), - ) - .subcommand( - SubCommand::with_name("forward") - .aliases(&["fwd", "fw", "f"]) - .about("Generates a forward message template") - .arg(email::args::seq_arg()) - .args(&tpl_args()), - ) - .subcommand( - SubCommand::with_name("save") - .about("Saves a message based on the given template") - .arg(&email::args::attachments_arg()) - .arg(Arg::with_name("template").raw(true)), - ) - .subcommand( - SubCommand::with_name("send") - .about("Sends a message based on the given template") - .arg(&email::args::attachments_arg()) - .arg(Arg::with_name("template").raw(true)), - )] +/// Represents the template override argument parser. +pub fn parse_override_arg<'a>(matches: &'a ArgMatches<'a>) -> TplOverride { + TplOverride { + subject: matches.value_of(ARG_SUBJECT), + from: matches.values_of(ARG_FROM).map(Iterator::collect), + to: matches.values_of(ARG_TO).map(Iterator::collect), + cc: matches.values_of(ARG_CC).map(Iterator::collect), + bcc: matches.values_of(ARG_BCC).map(Iterator::collect), + headers: matches.values_of(ARG_HEADERS).map(Iterator::collect), + body: matches.value_of(ARG_BODY), + signature: matches.value_of(ARG_SIGNATURE), + } +} + +/// Represents the raw template argument parser. +pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { + matches.value_of(ARG_TPL).unwrap_or_default() } diff --git a/src/main.rs b/src/main.rs index e94abef..c9e16ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,7 +106,7 @@ fn main() -> Result<()> { _ => (), } - // Check mailbox commands. + // Check folder commands. match folder::args::matches(&m)? { Some(folder::args::Cmd::List(max_width)) => { return folder::handlers::list( @@ -254,7 +254,7 @@ fn main() -> Result<()> { backend.as_mut(), ); } - Some(flag::args::Cmd::Remove(seq_range, ref flags)) => { + Some(flag::args::Cmd::Del(seq_range, ref flags)) => { return flag::handlers::remove( seq_range, flags, diff --git a/src/ui/table/args.rs b/src/ui/table/args.rs index 29ec40b..e9da52b 100644 --- a/src/ui/table/args.rs +++ b/src/ui/table/args.rs @@ -1,10 +1,21 @@ -use clap::Arg; +use clap::{Arg, ArgMatches}; -/// Defines the max table width argument. +const ARG_MAX_TABLE_WIDTH: &str = "max-table-width"; + +pub(crate) type MaxTableWidth = Option; + +/// Represents the max table width argument. pub fn max_width<'a>() -> Arg<'a, 'a> { - Arg::with_name("max-table-width") + Arg::with_name(ARG_MAX_TABLE_WIDTH) .help("Defines a maximum width for the table") .short("w") .long("max-width") .value_name("INT") } + +/// Represents the max table width argument parser. +pub fn parse_max_width<'a>(matches: &'a ArgMatches<'a>) -> Option { + matches + .value_of(ARG_MAX_TABLE_WIDTH) + .and_then(|width| width.parse::().ok()) +}