diff --git a/src/cli.rs b/src/cli.rs index 1f0826d..a1c8b7a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use crate::{ flag::command::FlagSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, - message::command::MessageSubcommand, + message::{attachment::command::AttachmentSubcommand, command::MessageSubcommand}, output::{ColorFmt, OutputFmt}, printer::Printer, }; @@ -105,6 +105,10 @@ pub enum HimalayaCommand { #[command(alias = "messages", alias = "msgs", alias = "msg")] Message(MessageSubcommand), + /// Manage attachments + #[command(subcommand)] + Attachment(AttachmentSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] #[command(alias = "manuals", alias = "mans")] @@ -124,6 +128,7 @@ impl HimalayaCommand { Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Flag(cmd) => cmd.execute(printer, config).await, Self::Message(cmd) => cmd.execute(printer, config).await, + Self::Attachment(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/message/args.rs b/src/email/message/args.rs deleted file mode 100644 index 80dbb43..0000000 --- a/src/email/message/args.rs +++ /dev/null @@ -1,356 +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, ArgAction, ArgMatches, Command}; - -use crate::template; - -const ARG_CRITERIA: &str = "criterion"; -const ARG_HEADERS: &str = "headers"; -const ARG_ID: &str = "id"; -const ARG_IDS: &str = "ids"; -const ARG_MIME_TYPE: &str = "mime-type"; -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_MESSAGE: &str = "message"; -const CMD_MOVE: &str = "move"; -const CMD_READ: &str = "read"; -const CMD_REPLY: &str = "reply"; -const CMD_SAVE: &str = "save"; -const CMD_SEND: &str = "send"; -const CMD_WRITE: &str = "write"; - -pub type All = bool; -pub type Criteria = String; -pub type Folder<'a> = &'a str; -pub type Headers<'a> = Vec<&'a str>; -pub type Id<'a> = &'a str; -pub type Ids<'a> = Vec<&'a str>; -pub type Query = String; -pub type Raw = bool; -pub type RawEmail = String; -pub type TextMime<'a> = &'a str; - -/// Represents the email commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd<'a> { - Attachments(Ids<'a>), - Copy(Ids<'a>, Folder<'a>), - Delete(Ids<'a>), - Forward( - Id<'a>, - template::args::Headers<'a>, - template::args::Body<'a>, - ), - Move(Ids<'a>, Folder<'a>), - Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>), - Reply( - Id<'a>, - All, - template::args::Headers<'a>, - template::args::Body<'a>, - ), - Save(RawEmail), - Send(RawEmail), - Write(template::args::Headers<'a>, template::args::Body<'a>), -} - -/// Email command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_MESSAGE) { - if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { - let ids = parse_ids_arg(m); - Some(Cmd::Attachments(ids)) - } else if let Some(m) = m.subcommand_matches(CMD_COPY) { - let ids = parse_ids_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); - Some(Cmd::Delete(ids)) - } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = parse_id_arg(m); - let headers = template::args::parse_headers_arg(m); - let body = template::args::parse_body_arg(m); - Some(Cmd::Forward(id, headers, body)) - } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { - let ids = parse_ids_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); - let mime = parse_mime_type_arg(m); - let raw = parse_raw_flag(m); - let headers = parse_headers_arg(m); - Some(Cmd::Read(ids, mime, raw, headers)) - } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = parse_id_arg(m); - let all = parse_reply_all_flag(m); - let headers = template::args::parse_headers_arg(m); - let body = template::args::parse_body_arg(m); - Some(Cmd::Reply(id, all, headers, body)) - } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - let email = parse_raw_arg(m); - Some(Cmd::Save(email)) - } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - let email = parse_raw_arg(m); - Some(Cmd::Send(email)) - } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { - let headers = template::args::parse_headers_arg(m); - let body = template::args::parse_body_arg(m); - Some(Cmd::Write(headers, body)) - } else { - None - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the email subcommands. -pub fn subcmd() -> Command { - Command::new(CMD_MESSAGE) - .about("Subcommand to manage messages") - .long_about("Subcommand to manage messages like read, write, reply or send") - .aliases(["msg"]) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommands([ - Command::new(CMD_ATTACHMENTS) - .about("Downloads all emails attachments") - .arg(ids_arg()), - Command::new(CMD_WRITE) - .about("Write a new email") - .aliases(["new", "n"]) - .args(template::args::args()), - Command::new(CMD_SEND) - .about("Send a raw email") - .arg(raw_arg()), - Command::new(CMD_SAVE) - .about("Save a raw email") - .arg(raw_arg()), - Command::new(CMD_READ) - .about("Read text bodies of emails") - .arg(mime_type_arg()) - .arg(raw_flag()) - .arg(headers_arg()) - .arg(ids_arg()), - Command::new(CMD_REPLY) - .about("Answer to an email") - .arg(reply_all_flag()) - .args(template::args::args()) - .arg(id_arg()), - Command::new(CMD_FORWARD) - .aliases(["fwd", "f"]) - .about("Forward an email") - .args(template::args::args()) - .arg(id_arg()), - Command::new(CMD_COPY) - .alias("cp") - .about("Copy emails to the given folder") - // .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(ids_arg()), - Command::new(CMD_DELETE) - .aliases(["remove", "rm"]) - .about("Delete emails") - .arg(ids_arg()), - ]) -} - -/// Represents the email id argument. -pub fn id_arg() -> Arg { - Arg::new(ARG_ID) - .help("Specifies the target email") - .value_name("ID") - .required(true) -} - -/// Represents the email id argument parser. -pub fn parse_id_arg(matches: &ArgMatches) -> &str { - matches.get_one::(ARG_ID).unwrap() -} - -/// Represents the email ids argument. -pub fn ids_arg() -> Arg { - Arg::new(ARG_IDS) - .help("Email ids") - .value_name("IDS") - .num_args(1..) - .required(true) -} - -/// Represents the email ids argument parser. -pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> { - matches - .get_many::(ARG_IDS) - .unwrap() - .map(String::as_str) - .collect() -} - -/// Represents the email sort criteria argument. -pub fn criteria_arg<'a>() -> Arg { - Arg::new(ARG_CRITERIA) - .help("Email sorting preferences") - .long("criterion") - .short('c') - .value_name("CRITERION:ORDER") - .action(ArgAction::Append) - .value_parser([ - "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(matches: &ArgMatches) -> String { - matches - .get_many::(ARG_CRITERIA) - .unwrap_or_default() - .map(ToOwned::to_owned) - .collect::>() - .join(" ") -} - -/// Represents the email reply all argument. -pub fn reply_all_flag() -> Arg { - Arg::new(ARG_REPLY_ALL) - .help("Include all recipients") - .long("all") - .short('A') - .action(ArgAction::SetTrue) -} - -/// Represents the email reply all argument parser. -pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool { - matches.get_flag(ARG_REPLY_ALL) -} - -/// Represents the email headers argument. -pub fn headers_arg() -> Arg { - Arg::new(ARG_HEADERS) - .help("Shows additional headers with the email") - .long("header") - .short('H') - .value_name("STRING") - .action(ArgAction::Append) -} - -/// Represents the email headers argument parser. -pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> { - m.get_many::(ARG_HEADERS) - .unwrap_or_default() - .map(String::as_str) - .collect::>() -} - -/// Represents the raw flag. -pub fn raw_flag() -> Arg { - Arg::new(ARG_RAW) - .help("Returns raw version of email") - .long("raw") - .short('r') - .action(ArgAction::SetTrue) -} - -/// Represents the raw flag parser. -pub fn parse_raw_flag(m: &ArgMatches) -> bool { - m.get_flag(ARG_RAW) -} - -/// Represents the email raw argument. -pub fn raw_arg() -> Arg { - Arg::new(ARG_RAW).raw(true) -} - -/// Represents the email raw argument parser. -pub fn parse_raw_arg(m: &ArgMatches) -> String { - m.get_one::(ARG_RAW).cloned().unwrap_or_default() -} - -/// Represents the email MIME type argument. -pub fn mime_type_arg() -> Arg { - Arg::new(ARG_MIME_TYPE) - .help("MIME type to use") - .short('t') - .long("mime-type") - .value_name("MIME") - .value_parser(["plain", "html"]) - .default_value("plain") -} - -/// Represents the email MIME type argument parser. -pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str { - matches.get_one::(ARG_MIME_TYPE).unwrap() -} - -/// Represents the email query argument. -pub fn query_arg() -> Arg { - Arg::new(ARG_QUERY) - .long_help("The query system depends on the backend, see the wiki for more details") - .value_name("QUERY") - .num_args(1..) - .required(true) -} - -/// Represents the email query argument parser. -pub fn parse_query_arg(matches: &ArgMatches) -> String { - matches - .get_many::(ARG_QUERY) - .unwrap_or_default() - .fold((false, vec![]), |(escape, mut cmds), cmd| { - match (cmd.as_str(), 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/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs new file mode 100644 index 0000000..f491e0a --- /dev/null +++ b/src/email/message/attachment/command/download.rs @@ -0,0 +1,85 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use log::info; +use std::fs; +use uuid::Uuid; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Download attachments of a message +#[derive(Debug, Parser)] +pub struct AttachmentDownloadCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl AttachmentDownloadCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing attachment download 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 ids = &self.envelopes.ids; + let emails = backend.get_messages(&folder, ids).await?; + + let mut emails_count = 0; + let mut attachments_count = 0; + + let mut ids = ids.iter(); + for email in emails.to_vec() { + let id = ids.next().unwrap(); + let attachments = email.attachments()?; + + if attachments.is_empty() { + printer.print_log(format!("No attachment found for message {id}!"))?; + continue; + } else { + emails_count += 1; + } + + printer.print_log(format!( + "{} attachment(s) found for message {id}!", + attachments.len() + ))?; + + for attachment in attachments { + let filename = attachment + .filename + .unwrap_or_else(|| Uuid::new_v4().to_string()); + let filepath = account_config.download_fpath(&filename)?; + printer.print_log(format!("Downloading {:?}…", filepath))?; + fs::write(&filepath, &attachment.body) + .with_context(|| format!("cannot save attachment at {filepath:?}"))?; + attachments_count += 1; + } + } + + match attachments_count { + 0 => printer.print("No attachment found!"), + 1 => printer.print("Downloaded 1 attachment!"), + n => printer.print(format!( + "Downloaded {} attachment(s) from {} messages(s)!", + n, emails_count, + )), + } + } +} diff --git a/src/email/message/attachment/command/mod.rs b/src/email/message/attachment/command/mod.rs new file mode 100644 index 0000000..3770ae0 --- /dev/null +++ b/src/email/message/attachment/command/mod.rs @@ -0,0 +1,24 @@ +pub mod download; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::download::AttachmentDownloadCommand; + +/// Subcommand dedicated to attachments +#[derive(Debug, Subcommand)] +pub enum AttachmentSubcommand { + /// Download all attachments of one or more messages + #[command(arg_required_else_help = true)] + Download(AttachmentDownloadCommand), +} + +impl AttachmentSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Download(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/message/attachment/mod.rs b/src/email/message/attachment/mod.rs new file mode 100644 index 0000000..9fe7961 --- /dev/null +++ b/src/email/message/attachment/mod.rs @@ -0,0 +1 @@ +pub mod command; diff --git a/src/email/message/handlers.rs b/src/email/message/handlers.rs deleted file mode 100644 index 72a99f9..0000000 --- a/src/email/message/handlers.rs +++ /dev/null @@ -1,101 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use atty::Stream; -use email::{ - account::config::AccountConfig, envelope::Id, flag::Flag, message::Message, - template::FilterParts, -}; -use log::trace; -use mail_builder::MessageBuilder; -use std::{ - fs, - io::{self, BufRead}, -}; -use url::Url; -use uuid::Uuid; - -use crate::{backend::Backend, printer::Printer, ui::editor}; - -pub async fn attachments( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, -) -> Result<()> { - let emails = backend.get_messages(&folder, &ids).await?; - let mut index = 0; - - let mut emails_count = 0; - let mut attachments_count = 0; - - let mut ids = ids.iter(); - for email in emails.to_vec() { - let id = ids.next().unwrap(); - let attachments = email.attachments()?; - - index = index + 1; - - if attachments.is_empty() { - printer.print_log(format!("No attachment found for email #{}", id))?; - continue; - } else { - emails_count = emails_count + 1; - } - - printer.print_log(format!( - "{} attachment(s) found for email #{}…", - attachments.len(), - id - ))?; - - for attachment in attachments { - let filename = attachment - .filename - .unwrap_or_else(|| Uuid::new_v4().to_string()); - let filepath = config.download_fpath(&filename)?; - printer.print_log(format!("Downloading {:?}…", filepath))?; - fs::write(&filepath, &attachment.body).context("cannot download attachment")?; - attachments_count = attachments_count + 1; - } - } - - match attachments_count { - 0 => printer.print("No attachment found!"), - 1 => printer.print("Downloaded 1 attachment!"), - n => printer.print(format!( - "Downloaded {} attachment(s) from {} email(s)!", - n, emails_count, - )), - } -} - -/// Parses and edits a message from a [mailto] URL string. -/// -/// [mailto]: https://en.wikipedia.org/wiki/Mailto -pub async fn mailto( - config: &AccountConfig, - backend: &Backend, - printer: &mut P, - url: &Url, -) -> Result<()> { - let mut builder = MessageBuilder::new().to(url.path()); - - for (key, val) in 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 = config - .generate_tpl_interpreter() - .with_show_only_headers(config.email_writing_headers()) - .build() - .from_msg_builder(builder) - .await?; - - editor::edit_tpl_with_editor(config, printer, backend, tpl).await -} diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs index 754da7b..12c67d2 100644 --- a/src/email/message/mod.rs +++ b/src/email/message/mod.rs @@ -1,6 +1,5 @@ -// pub mod args; pub mod arg; +pub mod attachment; pub mod command; pub mod config; -// pub mod handlers; // pub mod template;