diff --git a/src/domain/imap/service.rs b/src/domain/imap/service.rs index 5afb494b..d8df6a4b 100644 --- a/src/domain/imap/service.rs +++ b/src/domain/imap/service.rs @@ -8,11 +8,11 @@ use log::{debug, trace}; use native_tls::{self, TlsConnector, TlsStream}; use std::{collections::HashSet, convert::TryFrom, iter::FromIterator, net::TcpStream}; -use crate::{ - domain::{ - account::entity::Account, config::entity::Config, mbox::entity::Mbox, msg::entity::Msg, - }, - flag::model::Flags, +use crate::domain::{ + account::entity::Account, + config::entity::Config, + mbox::entity::Mbox, + msg::{entity::Msg, flag::entity::Flags}, }; type ImapSession = imap::Session>; diff --git a/src/domain/msg/arg.rs b/src/domain/msg/arg.rs index 8d177c30..f06fee23 100644 --- a/src/domain/msg/arg.rs +++ b/src/domain/msg/arg.rs @@ -1,191 +1,218 @@ -use anyhow::{anyhow, Context, Result}; -use atty::Stream; -use clap; -use imap::types::Flag; -use lettre::message::header::ContentTransferEncoding; -use log::{debug, error, trace}; -use std::{ - borrow::Cow, - collections::HashMap, - convert::TryFrom, - fs, - io::{self, BufRead}, -}; -use url::Url; +use anyhow::Result; +use clap::{self, App, Arg, ArgMatches, SubCommand}; +use log::debug; -use super::{ - body::Body, - entity::{Msg, MsgSerialized, Msgs}, - headers::Headers, -}; -use crate::{ - domain::{ - account::entity::Account, - imap::service::ImapServiceInterface, - mbox::{arg::target_arg, entity::Mbox}, - smtp::service::SmtpServiceInterface, - }, - flag::model::Flags, - input, - output::service::{OutputService, OutputServiceInterface}, -}; +use crate::domain::{mbox, msg}; -pub fn subcmds<'a>() -> Vec> { - vec![ - clap::SubCommand::with_name("list") - .aliases(&["lst"]) - .about("Lists all messages") - .arg(page_size_arg()) - .arg(page_arg()), - clap::SubCommand::with_name("search") - .aliases(&["query", "q"]) - .about("Lists messages matching the given IMAP query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg( - clap::Arg::with_name("query") - .help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)") - .value_name("QUERY") - .multiple(true) - .required(true), - ), - clap::SubCommand::with_name("write") - .about("Writes a new message") - .arg(attachment_arg()), - clap::SubCommand::with_name("send") - .about("Sends a raw message") - .arg(clap::Arg::with_name("message").raw(true).last(true)), - clap::SubCommand::with_name("save") - .about("Saves a raw message") - .arg(clap::Arg::with_name("message").raw(true)), - clap::SubCommand::with_name("read") - .about("Reads text bodies of a message") - .arg(uid_arg()) - .arg( - clap::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( - clap::Arg::with_name("raw") - .help("Reads raw message") - .long("raw") - .short("r"), - ), - clap::SubCommand::with_name("attachments") - .about("Downloads all message attachments") - .arg(uid_arg()), - clap::SubCommand::with_name("reply") - .about("Answers to a message") - .arg(uid_arg()) - .arg(reply_all_arg()) - .arg(attachment_arg()), - clap::SubCommand::with_name("forward") - .aliases(&["fwd"]) - .about("Forwards a message") - .arg(uid_arg()) - .arg(attachment_arg()), - clap::SubCommand::with_name("copy") - .aliases(&["cp"]) - .about("Copies a message to the targetted mailbox") - .arg(uid_arg()) - .arg(target_arg()), - clap::SubCommand::with_name("move") - .aliases(&["mv"]) - .about("Moves a message to the targetted mailbox") - .arg(uid_arg()) - .arg(target_arg()), - clap::SubCommand::with_name("delete") - .aliases(&["remove", "rm"]) - .about("Deletes a message") - .arg(uid_arg()), - clap::SubCommand::with_name("template") - .aliases(&["tpl"]) - .about("Generates a message template") - .subcommand( - clap::SubCommand::with_name("new") - .aliases(&["n"]) - .about("Generates a new message template") - .args(&tpl_args()), - ) - .subcommand( - clap::SubCommand::with_name("reply") - .aliases(&["rep", "r"]) - .about("Generates a reply message template") - .arg(uid_arg()) - .arg(reply_all_arg()) - .args(&tpl_args()), - ) - .subcommand( - clap::SubCommand::with_name("forward") - .aliases(&["fwd", "fw", "f"]) - .about("Generates a forward message template") - .arg(uid_arg()) - .args(&tpl_args()), - ), - ] +type Uid<'a> = &'a str; +type PageSize = usize; +type Page = usize; +type TargetMbox<'a> = Option<&'a str>; +type Mime = String; +type Raw = bool; +type All = bool; +type RawMsg<'a> = &'a str; +type Query = String; +type AttachmentsPaths<'a> = Vec<&'a str>; + +/// Enumeration of all possible matches. +pub enum Match<'a> { + Attachments(Uid<'a>), + Copy(Uid<'a>, TargetMbox<'a>), + Delete(Uid<'a>), + Forward(Uid<'a>, AttachmentsPaths<'a>), + List(Option, Page), + Move(Uid<'a>, TargetMbox<'a>), + Read(Uid<'a>, Mime, Raw), + Reply(Uid<'a>, All, AttachmentsPaths<'a>), + Save(TargetMbox<'a>, RawMsg<'a>), + Search(Query, Option, Page), + Send(RawMsg<'a>), + Write(AttachmentsPaths<'a>), + + Flag(msg::flag::arg::Match<'a>), + Tpl(msg::tpl::arg::Match<'a>), } -pub fn matches( - arg_matches: &clap::ArgMatches, - mbox: &Mbox, - account: &Account, - output: &OutputService, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result { - match arg_matches.subcommand() { - ("attachments", Some(matches)) => { - msg_matches_attachments(&output, &account, &matches, imap) - } - ("copy", Some(matches)) => msg_matches_copy(&output, &matches, imap), - ("delete", Some(matches)) => msg_matches_delete(&output, &matches, imap), - ("forward", Some(matches)) => msg_matches_forward(&output, &account, &matches, imap, smtp), - ("move", Some(matches)) => msg_matches_move(&output, &matches, imap), - ("read", Some(matches)) => msg_matches_read(&output, &matches, imap), - ("reply", Some(matches)) => msg_matches_reply(&output, &account, &matches, imap, smtp), - ("save", Some(matches)) => msg_matches_save(&mbox, matches, imap), - ("search", Some(matches)) => msg_matches_search(&output, &account, &matches, imap), - ("send", Some(matches)) => msg_matches_send(&output, &matches, imap, smtp), - ("write", Some(matches)) => msg_matches_write(&output, &account, &matches, imap, smtp), - - ("template", Some(matches)) => Ok(msg_matches_tpl(&output, &account, &matches, imap)?), - ("list", opt_matches) => msg_matches_list(&output, &account, opt_matches, imap), - (_other, opt_matches) => msg_matches_list(&output, &account, opt_matches, imap), +pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { + if let Some(m) = m.subcommand_matches("attachments") { + debug!("attachments command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", &uid); + return Ok(Some(Match::Attachments(uid))); } + + if let Some(m) = m.subcommand_matches("copy") { + debug!("copy command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", &uid); + let target = m.value_of("target"); + debug!("target mailbox: `{:?}`", target); + return Ok(Some(Match::Copy(uid, target))); + } + + if let Some(m) = m.subcommand_matches("delete") { + debug!("copy command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", &uid); + return Ok(Some(Match::Delete(uid))); + } + + if let Some(m) = msg::flag::arg::matches(&m)? { + return Ok(Some(Match::Flag(m))); + } + + if let Some(m) = m.subcommand_matches("forward") { + debug!("forward command matched"); + let uid = m.value_of("uid").unwrap(); + let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); + debug!("attachments paths: {:?}", paths); + debug!("uid: {}", &uid); + return Ok(Some(Match::Forward(uid, paths))); + } + + if let Some(m) = m.subcommand_matches("list") { + debug!("list command matched"); + 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(Match::List(page_size, page))); + } + + if let Some(m) = m.subcommand_matches("move") { + debug!("move command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", &uid); + let target = m.value_of("target"); + debug!("target mailbox: `{:?}`", target); + return Ok(Some(Match::Move(uid, target))); + } + + if let Some(m) = m.subcommand_matches("read") { + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + let mime = format!("text/{}", m.value_of("mime-type").unwrap()); + debug!("mime: {}", mime); + let raw = m.is_present("raw"); + debug!("raw: {}", raw); + return Ok(Some(Match::Read(uid, mime, raw))); + } + + if let Some(m) = m.subcommand_matches("reply") { + debug!("reply command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + 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); + return Ok(Some(Match::Reply(uid, all, paths))); + } + + if let Some(m) = m.subcommand_matches("save") { + debug!("save command matched"); + let msg = m.value_of("message").unwrap(); + debug!("message: {}", &msg); + let target = m.value_of("target"); + debug!("target mailbox: `{:?}`", target); + return Ok(Some(Match::Save(target, msg))); + } + + if let Some(m) = m.subcommand_matches("search") { + debug!("search command matched"); + 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(" "); + return Ok(Some(Match::Search(query, page_size, page))); + } + + if let Some(m) = m.subcommand_matches("send") { + debug!("send command matched"); + let msg = m.value_of("message").unwrap_or_default(); + debug!("message: {}", msg); + return Ok(Some(Match::Send(msg))); + } + + if let Some(m) = msg::tpl::arg::matches(&m)? { + return Ok(Some(Match::Tpl(m))); + } + + if let Some(m) = m.subcommand_matches("write") { + debug!("write command matched"); + let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); + debug!("attachments paths: {:?}", attachment_paths); + return Ok(Some(Match::Write(attachment_paths))); + } + + debug!("default list command matched"); + Ok(Some(Match::List(None, 0))) } // == Argument Functions == /// Returns an Clap-Argument to be able to use `` in the commandline like /// for the `himalaya read` subcommand. -pub(crate) fn uid_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("uid") +pub(crate) fn uid_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("uid") .help("Specifies the targetted message") .value_name("UID") .required(true) } -fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("reply-all") +pub(crate) fn reply_all_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("reply-all") .help("Includes all recipients") .short("A") .long("all") } -fn page_size_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("page-size") +fn page_size_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("page-size") .help("Page size") .short("s") .long("size") .value_name("INT") } -fn page_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("page") +fn page_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("page") .help("Page number") .short("p") .long("page") @@ -193,8 +220,9 @@ fn page_arg<'a>() -> clap::Arg<'a, 'a> { .default_value("0") } -fn attachment_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("attachments") +/// TODO: move to attachment folder +fn attachment_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("attachments") .help("Adds attachment to the message") .short("a") .long("attachment") @@ -202,746 +230,84 @@ fn attachment_arg<'a>() -> clap::Arg<'a, 'a> { .multiple(true) } -fn tpl_args<'a>() -> Vec> { +/// Messages subcommands. +pub fn subcmds<'a>() -> Vec> { vec![ - clap::Arg::with_name("subject") - .help("Overrides the Subject header") - .short("s") - .long("subject") - .value_name("STRING"), - clap::Arg::with_name("from") - .help("Overrides the From header") - .short("f") - .long("from") - .value_name("ADDR") - .multiple(true), - clap::Arg::with_name("to") - .help("Overrides the To header") - .short("t") - .long("to") - .value_name("ADDR") - .multiple(true), - clap::Arg::with_name("cc") - .help("Overrides the Cc header") - .short("c") - .long("cc") - .value_name("ADDR") - .multiple(true), - clap::Arg::with_name("bcc") - .help("Overrides the Bcc header") - .short("b") - .long("bcc") - .value_name("ADDR") - .multiple(true), - clap::Arg::with_name("header") - .help("Overrides a specific header") - .short("h") - .long("header") - .value_name("KEY: VAL") - .multiple(true), - clap::Arg::with_name("body") - .help("Overrides the body") - .short("B") - .long("body") - .value_name("STRING"), - clap::Arg::with_name("signature") - .help("Overrides the signature") - .short("S") - .long("signature") - .value_name("STRING"), + msg::flag::arg::subcmds(), + msg::tpl::arg::subcmds(), + vec![ + SubCommand::with_name("list") + .aliases(&["lst", "l"]) + .about("Lists all messages") + .arg(page_size_arg()) + .arg(page_arg()), + SubCommand::with_name("search") + .aliases(&["s", "query", "q"]) + .about("Lists messages matching the given IMAP query") + .arg(page_size_arg()) + .arg(page_arg()) + .arg( + Arg::with_name("query") + .help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)") + .value_name("QUERY") + .multiple(true) + .required(true), + ), + SubCommand::with_name("write") + .about("Writes a new message") + .arg(attachment_arg()), + SubCommand::with_name("send") + .about("Sends a raw message") + .arg(Arg::with_name("message").raw(true).last(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(uid_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"), + ), + SubCommand::with_name("attachments") + .about("Downloads all message attachments") + .arg(uid_arg()), + SubCommand::with_name("reply") + .about("Answers to a message") + .arg(uid_arg()) + .arg(reply_all_arg()) + .arg(attachment_arg()), + SubCommand::with_name("forward") + .aliases(&["fwd"]) + .about("Forwards a message") + .arg(uid_arg()) + .arg(attachment_arg()), + SubCommand::with_name("copy") + .aliases(&["cp"]) + .about("Copies a message to the targetted mailbox") + .arg(uid_arg()) + .arg(mbox::arg::target_arg()), + SubCommand::with_name("move") + .aliases(&["mv"]) + .about("Moves a message to the targetted mailbox") + .arg(uid_arg()) + .arg(mbox::arg::target_arg()), + SubCommand::with_name("delete") + .aliases(&["remove", "rm"]) + .about("Deletes a message") + .arg(uid_arg()), + ], ] -} - -fn msg_matches_list( - output: &OutputService, - account: &Account, - opt_matches: Option<&clap::ArgMatches>, - imap: &mut ImapService, -) -> Result { - debug!("list command matched"); - - let page_size: usize = opt_matches - .and_then(|matches| matches.value_of("page-size").and_then(|s| s.parse().ok())) - .unwrap_or(account.default_page_size); - debug!("page size: {:?}", page_size); - let page: usize = opt_matches - .and_then(|matches| matches.value_of("page").unwrap().parse().ok()) - .map(|page| 1.max(page) - 1) - .unwrap_or_default(); - debug!("page: {}", &page); - - let msgs = imap.list_msgs(&page_size, &page)?; - let msgs = if let Some(ref fetches) = msgs { - Msgs::try_from(fetches)? - } else { - Msgs::new() - }; - - trace!("messages: {:?}", msgs); - - output.print(msgs)?; - - imap.logout()?; - Ok(true) -} - -fn msg_matches_search( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("search command matched"); - - let page_size: usize = matches - .value_of("page-size") - .and_then(|s| s.parse().ok()) - .unwrap_or(account.default_page_size); - debug!("page size: {}", &page_size); - let page: usize = matches - .value_of("page") - .unwrap() - .parse() - .map(|page| 1.max(page) - 1) - .unwrap_or(1); - debug!("page: {}", &page); - - let query = matches - .values_of("query") - .unwrap_or_default() - .fold((false, vec![]), |(escape, mut cmds), cmd| { - match (cmd, escape) { - // Next command is an arg and needs to be escaped - ("subject", _) | ("body", _) | ("text", _) => { - cmds.push(cmd.to_string()); - (true, cmds) - } - // Escaped arg commands - (_, true) => { - cmds.push(format!("\"{}\"", cmd)); - (false, cmds) - } - // Regular commands - (_, false) => { - cmds.push(cmd.to_string()); - (false, cmds) - } - } - }) - .1 - .join(" "); - debug!("query: {}", &page); - - let msgs = imap.search_msgs(&query, &page_size, &page)?; - let msgs = if let Some(ref fetches) = msgs { - Msgs::try_from(fetches)? - } else { - Msgs::new() - }; - trace!("messages: {:?}", msgs); - output.print(msgs)?; - - imap.logout()?; - Ok(true) -} - -fn msg_matches_read( - output: &OutputService, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("read command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); - debug!("mime: {}", mime); - let raw = matches.is_present("raw"); - debug!("raw: {}", raw); - - let msg = imap.get_msg(&uid)?; - if raw { - output.print(msg.get_raw_as_string()?)?; - } else { - output.print(MsgSerialized::try_from(&msg)?)?; - } - imap.logout()?; - Ok(true) -} - -fn msg_matches_attachments( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("attachments command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", &uid); - - // get the msg and than it's attachments - let msg = imap.get_msg(&uid)?; - let attachments = msg.attachments.clone(); - - debug!( - "{} attachment(s) found for message {}", - &attachments.len(), - &uid - ); - - // Iterate through all attachments and download them to the download - // directory of the account. - for attachment in &attachments { - let filepath = account.downloads_dir.join(&attachment.filename); - debug!("downloading {}…", &attachment.filename); - fs::write(&filepath, &attachment.body_raw) - .with_context(|| format!("cannot save attachment {:?}", filepath))?; - } - - debug!( - "{} attachment(s) successfully downloaded", - &attachments.len() - ); - - output.print(format!( - "{} attachment(s) successfully downloaded", - &attachments.len() - ))?; - - imap.logout()?; - Ok(true) -} - -fn msg_matches_write( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result { - debug!("write command matched"); - - // create the new msg - // TODO: Make the header starting customizeable like from template - let mut msg = Msg::new_with_headers( - &account, - Headers { - subject: Some(String::new()), - to: Vec::new(), - ..Headers::default() - }, - ); - - // take care of the attachments - let attachment_paths: Vec<&str> = matches - .values_of("attachments") - .unwrap_or_default() - .collect(); - - attachment_paths - .iter() - .for_each(|path| msg.add_attachment(path)); - - msg_interaction(output, &mut msg, imap, smtp)?; - - imap.logout()?; - - Ok(true) -} - -fn msg_matches_reply( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result { - debug!("reply command matched"); - - // -- Preparations -- - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - let mut msg = imap.get_msg(&uid)?; - - // Change the msg to a reply-msg. - msg.change_to_reply(&account, matches.is_present("reply-all"))?; - - // Apply the given attachments to the reply-msg. - let attachments: Vec<&str> = matches - .values_of("attachments") - .unwrap_or_default() - .collect(); - - attachments.iter().for_each(|path| msg.add_attachment(path)); - - debug!("found {} attachments", attachments.len()); - trace!("attachments: {:?}", attachments); - - msg_interaction(output, &mut msg, imap, smtp)?; - - imap.logout()?; - Ok(true) -} - -pub fn msg_matches_mailto( - output: &OutputService, - account: &Account, - url: &Url, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result<()> { - debug!("mailto command matched"); - - let mut cc = Vec::new(); - let mut bcc = Vec::new(); - let mut subject = Cow::default(); - let mut body = Cow::default(); - - for (key, val) in url.query_pairs() { - match key.as_bytes() { - b"cc" => { - cc.push(val.into()); - } - b"bcc" => { - bcc.push(val.into()); - } - b"subject" => { - subject = val; - } - b"body" => { - body = val; - } - _ => (), - } - } - - let headers = Headers { - from: vec![account.address()], - to: vec![url.path().to_string()], - encoding: ContentTransferEncoding::Base64, - bcc: Some(bcc), - cc: Some(cc), - signature: Some(account.signature.to_owned()), - subject: Some(subject.into()), - ..Headers::default() - }; - - let mut msg = Msg::new_with_headers(&account, headers); - msg.body = Body::new_with_text(body); - msg_interaction(output, &mut msg, imap, smtp)?; - - imap.logout()?; - Ok(()) -} - -fn msg_matches_forward( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result { - debug!("forward command matched"); - - // fetch the msg - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - - let mut msg = imap.get_msg(&uid)?; - // prepare to forward it - msg.change_to_forwarding(&account); - - let attachments: Vec<&str> = matches - .values_of("attachments") - .unwrap_or_default() - .collect(); - - attachments.iter().for_each(|path| msg.add_attachment(path)); - - debug!("found {} attachments", attachments.len()); - trace!("attachments: {:?}", attachments); - - // apply changes - msg_interaction(output, &mut msg, imap, smtp)?; - - imap.logout()?; - - Ok(true) -} - -fn msg_matches_copy( - output: &OutputService, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("copy command matched"); - - // fetch the message to be copyied - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", &uid); - let target = Mbox::try_from(matches.value_of("target"))?; - - let mut msg = imap.get_msg(&uid)?; - // the message, which will be in the new mailbox doesn't need to be seen - msg.flags.insert(Flag::Seen); - - imap.append_msg(&target, &mut msg)?; - - debug!("message {} successfully copied to folder `{}`", uid, target); - - output.print(format!( - "Message {} successfully copied to folder `{}`", - uid, target - ))?; - - imap.logout()?; - Ok(true) -} - -fn msg_matches_move( - output: &OutputService, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("move command matched"); - - // fetch the msg which should be moved - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", &uid); - let target = Mbox::try_from(matches.value_of("target"))?; - - let mut msg = imap.get_msg(&uid)?; - // create the msg in the target-msgbox - msg.flags.insert(Flag::Seen); - imap.append_msg(&target, &mut msg)?; - - debug!("message {} successfully moved to folder `{}`", uid, target); - output.print(format!( - "Message {} successfully moved to folder `{}`", - uid, target - ))?; - - // delete the msg in the old mailbox - let flags = vec![Flag::Seen, Flag::Deleted]; - imap.add_flags(uid, Flags::from(flags))?; - imap.expunge()?; - imap.logout()?; - Ok(true) -} - -fn msg_matches_delete( - output: &OutputService, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("delete command matched"); - - // remove the message according to its UID - let uid = matches.value_of("uid").unwrap(); - let flags = vec![Flag::Seen, Flag::Deleted]; - imap.add_flags(uid, Flags::from(flags))?; - imap.expunge()?; - - debug!("message {} successfully deleted", uid); - output.print(format!("Message {} successfully deleted", uid))?; - - imap.logout()?; - Ok(true) -} - -fn msg_matches_send( - output: &OutputService, - matches: &clap::ArgMatches, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result { - debug!("send command matched"); - - let msg = if atty::is(Stream::Stdin) || output.is_json() { - matches - .value_of("message") - .unwrap_or_default() - .replace("\r", "") - .replace("\n", "\r\n") - } else { - io::stdin() - .lock() - .lines() - .filter_map(|ln| ln.ok()) - .map(|ln| ln.to_string()) - .collect::>() - .join("\r\n") - }; - - let mut msg = Msg::try_from(msg.as_str())?; - - // send the message/msg - let sendable = msg.to_sendable_msg()?; - smtp.send(&sendable)?; - debug!("message sent!"); - - // add the message/msg to the Sent-Mailbox of the user - msg.flags.insert(Flag::Seen); - let mbox = Mbox::from("Sent"); - imap.append_msg(&mbox, &mut msg)?; - - imap.logout()?; - - Ok(true) -} - -fn msg_matches_save( - mbox: &Mbox, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("save command matched"); - let msg: &str = matches.value_of("message").unwrap(); - let mut msg = Msg::try_from(msg)?; - msg.flags.insert(Flag::Seen); - imap.append_msg(&mbox, &mut msg)?; - imap.logout()?; - Ok(true) -} - -pub fn msg_matches_tpl( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - match matches.subcommand() { - ("new", Some(matches)) => tpl_matches_new(&output, &account, matches), - ("reply", Some(matches)) => tpl_matches_reply(&output, &account, matches, imap), - ("forward", Some(matches)) => tpl_matches_forward(&output, &account, matches, imap), - - // TODO: find a way to show the help message for template subcommand - _ => Err(anyhow!("Subcommand not found")), - } -} - -// == Helper functions == -// -- Template Subcommands -- -// These functions are more used for the "template" subcommand -fn override_msg_with_args(msg: &mut Msg, matches: &clap::ArgMatches) { - // -- Collecting credentials -- - let from: Vec = match matches.values_of("from") { - Some(from) => from.map(|arg| arg.to_string()).collect(), - None => msg.headers.from.clone(), - }; - - let to: Vec = match matches.values_of("to") { - Some(to) => to.map(|arg| arg.to_string()).collect(), - None => Vec::new(), - }; - - let subject = matches - .value_of("subject") - .and_then(|subject| Some(subject.to_string())); - - let cc: Option> = matches - .values_of("cc") - .and_then(|cc| Some(cc.map(|arg| arg.to_string()).collect())); - - let bcc: Option> = matches - .values_of("bcc") - .and_then(|bcc| Some(bcc.map(|arg| arg.to_string()).collect())); - - let signature = matches - .value_of("signature") - .and_then(|signature| Some(signature.to_string())) - .or(msg.headers.signature.clone()); - - let custom_headers: Option>> = { - if let Some(matched_headers) = matches.values_of("header") { - let mut custom_headers: HashMap> = HashMap::new(); - - // collect the custom headers - for header in matched_headers { - let mut header = header.split(":"); - let key = header.next().unwrap_or_default(); - let val = header.next().unwrap_or_default().trim_start(); - - debug!("overriden header: {}={}", key, val); - - custom_headers.insert(key.to_string(), vec![val.to_string()]); - } - - Some(custom_headers) - } else { - None - } - }; - - let body = { - if atty::isnt(Stream::Stdin) { - let body = io::stdin() - .lock() - .lines() - .filter_map(|line| line.ok()) - .map(|line| line.to_string()) - .collect::>() - .join("\n"); - debug!("overriden body from stdin: {:?}", body); - body - } else if let Some(body) = matches.value_of("body") { - debug!("overriden body: {:?}", body); - body.to_string() - } else { - String::new() - } - }; - - let body = Body::new_with_text(body); - - // -- Creating and printing -- - let headers = Headers { - from, - subject, - to, - cc, - bcc, - signature, - custom_headers, - ..msg.headers.clone() - }; - - msg.headers = headers; - msg.body = body; -} - -fn tpl_matches_new( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, -) -> Result { - debug!("new command matched"); - let mut msg = Msg::new(&account); - override_msg_with_args(&mut msg, &matches); - trace!("Message: {:?}", msg); - output.print(MsgSerialized::try_from(&msg)?)?; - Ok(true) -} - -fn tpl_matches_reply( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("reply command matched"); - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - let mut msg = imap.get_msg(&uid)?; - msg.change_to_reply(&account, matches.is_present("reply-all"))?; - override_msg_with_args(&mut msg, &matches); - trace!("Message: {:?}", msg); - output.print(MsgSerialized::try_from(&msg)?)?; - Ok(true) -} - -fn tpl_matches_forward( - output: &OutputService, - account: &Account, - matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - debug!("forward command matched"); - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - let mut msg = imap.get_msg(&uid)?; - msg.change_to_forwarding(&account); - override_msg_with_args(&mut msg, &matches); - trace!("Message: {:?}", msg); - output.print(MsgSerialized::try_from(&msg)?)?; - Ok(true) -} - -/// This function opens the prompt to do some actions to the msg like sending, editing it again and -/// so on. -fn msg_interaction( - output: &OutputService, - msg: &mut Msg, - imap: &mut ImapService, - smtp: &mut SmtpService, -) -> Result { - // let the user change the body a little bit first, before opening the prompt - msg.edit_body()?; - - loop { - match input::post_edit_choice() { - Ok(choice) => match choice { - input::PostEditChoice::Send => { - debug!("sending message…"); - - // prepare the msg to be send - let sendable = match msg.to_sendable_msg() { - Ok(sendable) => sendable, - // In general if an error occured, then this is normally - // due to a missing value of a header. So let's give the - // user another try and give him/her the chance to fix - // that :) - Err(err) => { - println!("{}", err); - println!("Please reedit your msg to make it to a sendable message!"); - continue; - } - }; - smtp.send(&sendable)?; - - // TODO: Gmail sent mailboxes are called `[Gmail]/Sent` - // which creates a conflict, fix this! - - // let the server know, that the user sent a msg - msg.flags.insert(Flag::Seen); - let mbox = Mbox::from("Sent"); - imap.append_msg(&mbox, msg)?; - - // remove the draft, since we sent it - input::remove_draft()?; - output.print("Message successfully sent")?; - break; - } - // edit the body of the msg - input::PostEditChoice::Edit => { - // Did something goes wrong when the user changed the - // content? - if let Err(err) = msg.edit_body() { - println!("[ERROR] {}", err); - println!(concat!( - "Please try to fix the problem by editing", - "the msg again." - )); - } - } - input::PostEditChoice::LocalDraft => break, - input::PostEditChoice::RemoteDraft => { - debug!("saving to draft…"); - - msg.flags.insert(Flag::Seen); - - let mbox = Mbox::from("Drafts"); - match imap.append_msg(&mbox, msg) { - Ok(_) => { - input::remove_draft()?; - output.print("Message successfully saved to Drafts")?; - } - Err(err) => { - output.print("Cannot save draft to the server")?; - return Err(err.into()); - } - }; - break; - } - input::PostEditChoice::Discard => { - input::remove_draft()?; - break; - } - }, - Err(err) => error!("{}", err), - } - } - - Ok(true) + .concat() } diff --git a/src/domain/msg/entity.rs b/src/domain/msg/entity.rs index 4f801230..32511f70 100644 --- a/src/domain/msg/entity.rs +++ b/src/domain/msg/entity.rs @@ -5,8 +5,7 @@ use mailparse; use super::{attachment::Attachment, body::Body, headers::Headers}; use crate::{ - domain::account::entity::Account, - flag::model::Flags, + domain::{account::entity::Account, msg::flag::entity::Flags}, ui::table::{Cell, Row, Table}, }; diff --git a/src/domain/msg/flag/arg.rs b/src/domain/msg/flag/arg.rs new file mode 100644 index 00000000..4f9fc565 --- /dev/null +++ b/src/domain/msg/flag/arg.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use clap::{self, App, Arg, ArgMatches, SubCommand}; +use log::debug; + +use crate::domain::msg; + +type Uid<'a> = &'a str; +type Flags<'a> = &'a str; + +/// Enumeration of all possible matches. +pub enum Match<'a> { + Set(Uid<'a>, Flags<'a>), + Add(Uid<'a>, Flags<'a>), + Remove(Uid<'a>, Flags<'a>), +} + +/// Message flag arg matcher. +pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { + if let Some(m) = m.subcommand_matches("set") { + debug!("set command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + let flags = m.value_of("flags").unwrap(); + debug!("flags: {}", flags); + return Ok(Some(Match::Set(uid, flags))); + } + + if let Some(m) = m.subcommand_matches("add") { + debug!("add command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + let flags = m.value_of("flags").unwrap(); + debug!("flags: {}", flags); + return Ok(Some(Match::Add(uid, flags))); + } + + if let Some(m) = m.subcommand_matches("remove") { + debug!("remove command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + let flags = m.value_of("flags").unwrap(); + debug!("flags: {}", flags); + return Ok(Some(Match::Remove(uid, flags))); + } + + Ok(None) +} + +/// Message flag arg. +fn flags_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("flags") + .help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11). Just write the flag name without the backslash. Example: --flags \"Seen Answered\"") + .value_name("FLAGS…") + .multiple(true) + .required(true) +} + +/// Message flag subcommands. +pub fn subcmds<'a>() -> Vec> { + vec![SubCommand::with_name("flags") + .about("Handles flags") + .subcommand( + SubCommand::with_name("set") + .about("Replaces all message flags") + .arg(msg::arg::uid_arg()) + .arg(flags_arg()), + ) + .subcommand( + SubCommand::with_name("add") + .about("Appends flags to a message") + .arg(msg::arg::uid_arg()) + .arg(flags_arg()), + ) + .subcommand( + SubCommand::with_name("remove") + .aliases(&["rm"]) + .about("Removes flags from a message") + .arg(msg::arg::uid_arg()) + .arg(flags_arg()), + )] +} diff --git a/src/flag/model.rs b/src/domain/msg/flag/entity.rs similarity index 99% rename from src/flag/model.rs rename to src/domain/msg/flag/entity.rs index 51393f53..281634dc 100644 --- a/src/flag/model.rs +++ b/src/domain/msg/flag/entity.rs @@ -205,8 +205,7 @@ fn convert_to_static<'func>(flag: &'func Flag) -> Result, ()> { #[cfg(test)] mod tests { - - use crate::flag::model::Flags; + use crate::domain::msg::flag::entity::Flags; use imap::types::Flag; use std::collections::HashSet; diff --git a/src/domain/msg/flag/handler.rs b/src/domain/msg/flag/handler.rs new file mode 100644 index 00000000..36280ef0 --- /dev/null +++ b/src/domain/msg/flag/handler.rs @@ -0,0 +1,36 @@ +use anyhow::Result; + +use crate::domain::{imap::service::ImapServiceInterface, msg::flag::entity::Flags}; + +pub fn set( + uid: &str, + flags: &str, + imap: &mut ImapService, +) -> Result<()> { + let flags = Flags::from(flags); + imap.set_flags(uid, flags)?; + imap.logout()?; + Ok(()) +} + +pub fn add( + uid: &str, + flags: &str, + imap: &mut ImapService, +) -> Result<()> { + let flags = Flags::from(flags); + imap.add_flags(uid, flags)?; + imap.logout()?; + Ok(()) +} + +pub fn remove( + uid: &str, + flags: &str, + imap: &mut ImapService, +) -> Result<()> { + let flags = Flags::from(flags); + imap.remove_flags(uid, flags)?; + imap.logout()?; + Ok(()) +} diff --git a/src/domain/msg/flag/mod.rs b/src/domain/msg/flag/mod.rs new file mode 100644 index 00000000..d0da5b40 --- /dev/null +++ b/src/domain/msg/flag/mod.rs @@ -0,0 +1,5 @@ +//! Module related to messages flag. + +pub mod arg; +pub mod entity; +pub mod handler; diff --git a/src/domain/msg/handler.rs b/src/domain/msg/handler.rs new file mode 100644 index 00000000..0fbfc3ec --- /dev/null +++ b/src/domain/msg/handler.rs @@ -0,0 +1,430 @@ +use anyhow::{Context, Result}; +use atty::Stream; +use imap::types::Flag; +use lettre::message::header::ContentTransferEncoding; +use log::{debug, error, trace}; +use std::{ + borrow::Cow, + convert::TryFrom, + fs, + io::{self, BufRead}, +}; +use url::Url; + +use crate::{ + domain::{ + account::entity::Account, + imap::service::ImapServiceInterface, + mbox::entity::Mbox, + msg::{ + body::Body, + entity::{Msg, Msgs}, + }, + smtp::service::SmtpServiceInterface, + }, + input, + output::service::{OutputService, OutputServiceInterface}, +}; + +use super::{entity::MsgSerialized, flag::entity::Flags, headers::Headers}; + +// TODO: move this function to the right folder +fn msg_interaction( + output: &OutputService, + msg: &mut Msg, + imap: &mut ImapService, + smtp: &mut SmtpService, +) -> Result { + // let the user change the body a little bit first, before opening the prompt + msg.edit_body()?; + + loop { + match input::post_edit_choice() { + Ok(choice) => match choice { + input::PostEditChoice::Send => { + debug!("sending message…"); + + // prepare the msg to be send + let sendable = match msg.to_sendable_msg() { + Ok(sendable) => sendable, + // In general if an error occured, then this is normally + // due to a missing value of a header. So let's give the + // user another try and give him/her the chance to fix + // that :) + Err(err) => { + println!("{}", err); + println!("Please reedit your msg to make it to a sendable message!"); + continue; + } + }; + smtp.send(&sendable)?; + + // TODO: Gmail sent mailboxes are called `[Gmail]/Sent` + // which creates a conflict, fix this! + + // let the server know, that the user sent a msg + msg.flags.insert(Flag::Seen); + let mbox = Mbox::from("Sent"); + imap.append_msg(&mbox, msg)?; + + // remove the draft, since we sent it + input::remove_draft()?; + output.print("Message successfully sent")?; + break; + } + // edit the body of the msg + input::PostEditChoice::Edit => { + // Did something goes wrong when the user changed the + // content? + if let Err(err) = msg.edit_body() { + println!("[ERROR] {}", err); + println!(concat!( + "Please try to fix the problem by editing", + "the msg again." + )); + } + } + input::PostEditChoice::LocalDraft => break, + input::PostEditChoice::RemoteDraft => { + debug!("saving to draft…"); + + msg.flags.insert(Flag::Seen); + + let mbox = Mbox::from("Drafts"); + match imap.append_msg(&mbox, msg) { + Ok(_) => { + input::remove_draft()?; + output.print("Message successfully saved to Drafts")?; + } + Err(err) => { + output.print("Cannot save draft to the server")?; + return Err(err.into()); + } + }; + break; + } + input::PostEditChoice::Discard => { + input::remove_draft()?; + break; + } + }, + Err(err) => error!("{}", err), + } + } + + Ok(true) +} + +pub fn attachments( + uid: &str, + account: &Account, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let msg = imap.get_msg(&uid)?; + let attachments = msg.attachments.clone(); + + debug!( + "{} attachment(s) found for message {}", + &attachments.len(), + &uid + ); + + // Iterate through all attachments and download them to the download + // directory of the account. + for attachment in &attachments { + let filepath = account.downloads_dir.join(&attachment.filename); + debug!("downloading {}…", &attachment.filename); + fs::write(&filepath, &attachment.body_raw) + .context(format!("cannot save attachment {:?}", filepath))?; + } + + debug!( + "{} attachment(s) successfully downloaded", + &attachments.len() + ); + + output.print(format!( + "{} attachment(s) successfully downloaded", + &attachments.len() + ))?; + + imap.logout()?; + Ok(()) +} + +pub fn copy( + uid: &str, + mbox: Option<&str>, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let target = Mbox::try_from(mbox)?; + let mut msg = imap.get_msg(&uid)?; + // the message, which will be in the new mailbox doesn't need to be seen + msg.flags.insert(Flag::Seen); + imap.append_msg(&target, &mut msg)?; + debug!("message {} successfully copied to folder `{}`", uid, target); + output.print(format!( + "Message {} successfully copied to folder `{}`", + uid, target + ))?; + imap.logout()?; + Ok(()) +} + +pub fn delete( + uid: &str, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let flags = vec![Flag::Seen, Flag::Deleted]; + imap.add_flags(uid, Flags::from(flags))?; + imap.expunge()?; + debug!("message {} successfully deleted", uid); + output.print(format!("Message {} successfully deleted", uid))?; + imap.logout()?; + Ok(()) +} + +pub fn forward( + uid: &str, + attachments_paths: Vec<&str>, + account: &Account, + output: &OutputService, + imap: &mut ImapService, + smtp: &mut SmtpService, +) -> Result<()> { + let mut msg = imap.get_msg(&uid)?; + // prepare to forward it + msg.change_to_forwarding(&account); + attachments_paths + .iter() + .for_each(|path| msg.add_attachment(path)); + debug!("found {} attachments", attachments_paths.len()); + trace!("attachments: {:?}", attachments_paths); + // apply changes + msg_interaction(output, &mut msg, imap, smtp)?; + imap.logout()?; + Ok(()) +} + +pub fn list( + page_size: Option, + page: usize, + account: &Account, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let page_size = page_size.unwrap_or(account.default_page_size); + let msgs = imap.list_msgs(&page_size, &page)?; + let msgs = if let Some(ref fetches) = msgs { + Msgs::try_from(fetches)? + } else { + Msgs::new() + }; + trace!("messages: {:#?}", msgs); + output.print(msgs)?; + imap.logout()?; + Ok(()) +} + +pub fn mailto( + url: &Url, + account: &Account, + output: &OutputService, + imap: &mut ImapService, + smtp: &mut SmtpService, +) -> Result<()> { + let mut cc = Vec::new(); + let mut bcc = Vec::new(); + let mut subject = Cow::default(); + let mut body = Cow::default(); + + for (key, val) in url.query_pairs() { + match key.as_bytes() { + b"cc" => { + cc.push(val.into()); + } + b"bcc" => { + bcc.push(val.into()); + } + b"subject" => { + subject = val; + } + b"body" => { + body = val; + } + _ => (), + } + } + + let headers = Headers { + from: vec![account.address()], + to: vec![url.path().to_string()], + encoding: ContentTransferEncoding::Base64, + bcc: Some(bcc), + cc: Some(cc), + signature: Some(account.signature.to_owned()), + subject: Some(subject.into()), + ..Headers::default() + }; + + let mut msg = Msg::new_with_headers(&account, headers); + msg.body = Body::new_with_text(body); + msg_interaction(output, &mut msg, imap, smtp)?; + imap.logout()?; + Ok(()) +} + +pub fn move_( + uid: &str, + mbox: Option<&str>, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let target = Mbox::try_from(mbox)?; + let mut msg = imap.get_msg(&uid)?; + // create the msg in the target-msgbox + msg.flags.insert(Flag::Seen); + imap.append_msg(&target, &mut msg)?; + debug!("message {} successfully moved to folder `{}`", uid, target); + output.print(format!( + "Message {} successfully moved to folder `{}`", + uid, target + ))?; + // delete the msg in the old mailbox + let flags = vec![Flag::Seen, Flag::Deleted]; + imap.add_flags(uid, Flags::from(flags))?; + imap.expunge()?; + imap.logout()?; + Ok(()) +} + +pub fn read( + uid: &str, + // TODO: use the mime to select the right body + _mime: String, + raw: bool, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let msg = imap.get_msg(&uid)?; + if raw { + output.print(msg.get_raw_as_string()?)?; + } else { + output.print(MsgSerialized::try_from(&msg)?)?; + } + imap.logout()?; + Ok(()) +} + +pub fn reply( + uid: &str, + all: bool, + attachments_paths: Vec<&str>, + account: &Account, + output: &OutputService, + imap: &mut ImapService, + smtp: &mut SmtpService, +) -> Result<()> { + let mut msg = imap.get_msg(&uid)?; + // Change the msg to a reply-msg. + msg.change_to_reply(&account, all)?; + // Apply the given attachments to the reply-msg. + attachments_paths + .iter() + .for_each(|path| msg.add_attachment(path)); + debug!("found {} attachments", attachments_paths.len()); + trace!("attachments: {:#?}", attachments_paths); + msg_interaction(output, &mut msg, imap, smtp)?; + imap.logout()?; + Ok(()) +} + +pub fn save( + mbox: Option<&str>, + msg: &str, + imap: &mut ImapService, +) -> Result<()> { + let mbox = Mbox::try_from(mbox)?; + let mut msg = Msg::try_from(msg)?; + msg.flags.insert(Flag::Seen); + imap.append_msg(&mbox, &mut msg)?; + imap.logout()?; + Ok(()) +} + +pub fn search( + page_size: Option, + page: usize, + query: String, + account: &Account, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let page_size = page_size.unwrap_or(account.default_page_size); + let msgs = imap.search_msgs(&query, &page_size, &page)?; + let msgs = if let Some(ref fetches) = msgs { + Msgs::try_from(fetches)? + } else { + Msgs::new() + }; + trace!("messages: {:?}", msgs); + output.print(msgs)?; + imap.logout()?; + Ok(()) +} + +pub fn send( + msg: &str, + output: &OutputService, + imap: &mut ImapService, + smtp: &mut SmtpService, +) -> Result<()> { + let msg = if atty::is(Stream::Stdin) || output.is_json() { + msg.replace("\r", "").replace("\n", "\r\n") + } else { + io::stdin() + .lock() + .lines() + .filter_map(|ln| ln.ok()) + .map(|ln| ln.to_string()) + .collect::>() + .join("\r\n") + }; + let mut msg = Msg::try_from(msg.as_str())?; + // send the message/msg + let sendable = msg.to_sendable_msg()?; + smtp.send(&sendable)?; + debug!("message sent!"); + // add the message/msg to the Sent-Mailbox of the user + msg.flags.insert(Flag::Seen); + let mbox = Mbox::from("Sent"); + imap.append_msg(&mbox, &mut msg)?; + imap.logout()?; + Ok(()) +} + +pub fn write( + attachments_paths: Vec<&str>, + account: &Account, + output: &OutputService, + imap: &mut ImapService, + smtp: &mut SmtpService, +) -> Result<()> { + let mut msg = Msg::new_with_headers( + &account, + Headers { + subject: Some(String::new()), + to: Vec::new(), + ..Headers::default() + }, + ); + attachments_paths + .iter() + .for_each(|path| msg.add_attachment(path)); + msg_interaction(output, &mut msg, imap, smtp)?; + imap.logout()?; + Ok(()) +} diff --git a/src/domain/msg/mod.rs b/src/domain/msg/mod.rs index eb92a3d9..a71efd91 100644 --- a/src/domain/msg/mod.rs +++ b/src/domain/msg/mod.rs @@ -35,3 +35,6 @@ pub mod headers; /// This module is used in the `Msg` struct, which should represent the body of /// a msg; The part where you're writing some text like `Dear Mr. LMAO`. pub mod body; +pub mod flag; +pub mod handler; +pub mod tpl; diff --git a/src/domain/msg/tpl/arg.rs b/src/domain/msg/tpl/arg.rs new file mode 100644 index 00000000..efcc25bc --- /dev/null +++ b/src/domain/msg/tpl/arg.rs @@ -0,0 +1,120 @@ +use anyhow::Result; +use clap::{self, App, Arg, ArgMatches, SubCommand}; +use log::debug; + +use crate::domain::msg::{self, arg::uid_arg}; + +type Uid<'a> = &'a str; +type All = bool; + +/// Enumeration of all possible matches. +pub enum Match<'a> { + New, + Reply(Uid<'a>, All), + Forward(Uid<'a>), +} + +/// Message template arg matcher. +pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { + if let Some(_) = m.subcommand_matches("new") { + debug!("new command matched"); + return Ok(Some(Match::New)); + } + + if let Some(m) = m.subcommand_matches("reply") { + debug!("reply command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + let all = m.is_present("reply-all"); + debug!("reply all: {}", all); + return Ok(Some(Match::Reply(uid, all))); + } + + if let Some(m) = m.subcommand_matches("forward") { + debug!("forward command matched"); + let uid = m.value_of("uid").unwrap(); + debug!("uid: {}", uid); + return Ok(Some(Match::Forward(uid))); + } + + Ok(None) +} + +/// Message template args. +pub fn tpl_args<'a>() -> Vec> { + vec![ + Arg::with_name("subject") + .help("Overrides the Subject header") + .short("s") + .long("subject") + .value_name("STRING"), + Arg::with_name("from") + .help("Overrides the From header") + .short("f") + .long("from") + .value_name("ADDR") + .multiple(true), + Arg::with_name("to") + .help("Overrides the To header") + .short("t") + .long("to") + .value_name("ADDR") + .multiple(true), + Arg::with_name("cc") + .help("Overrides the Cc header") + .short("c") + .long("cc") + .value_name("ADDR") + .multiple(true), + Arg::with_name("bcc") + .help("Overrides the Bcc header") + .short("b") + .long("bcc") + .value_name("ADDR") + .multiple(true), + Arg::with_name("header") + .help("Overrides a specific header") + .short("h") + .long("header") + .value_name("KEY: VAL") + .multiple(true), + Arg::with_name("body") + .help("Overrides the body") + .short("B") + .long("body") + .value_name("STRING"), + Arg::with_name("signature") + .help("Overrides the signature") + .short("S") + .long("signature") + .value_name("STRING"), + ] +} + +/// Message template subcommands. +pub fn subcmds<'a>() -> Vec> { + vec![SubCommand::with_name("template") + .aliases(&["tpl"]) + .about("Generates a message template") + .subcommand( + SubCommand::with_name("new") + .aliases(&["n"]) + .about("Generates a new message template") + .args(&tpl_args()), + ) + .subcommand( + SubCommand::with_name("reply") + .aliases(&["rep", "r"]) + .about("Generates a reply message template") + .arg(uid_arg()) + .arg(msg::arg::reply_all_arg()) + .args(&tpl_args()), + ) + .subcommand( + SubCommand::with_name("forward") + .aliases(&["fwd", "fw", "f"]) + .about("Generates a forward message template") + .arg(uid_arg()) + .args(&tpl_args()), + )] +} diff --git a/src/domain/msg/tpl/handler.rs b/src/domain/msg/tpl/handler.rs new file mode 100644 index 00000000..23261bbc --- /dev/null +++ b/src/domain/msg/tpl/handler.rs @@ -0,0 +1,150 @@ +use std::convert::TryFrom; + +use anyhow::Result; +use log::trace; + +use crate::{ + domain::{ + account::entity::Account, + imap::service::ImapServiceInterface, + msg::entity::{Msg, MsgSerialized}, + }, + output::service::OutputServiceInterface, +}; + +pub fn new( + account: &Account, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let msg = Msg::new(&account); + // FIXME + // override_msg_with_args(&mut msg, &matches); + trace!("message: {:#?}", msg); + output.print(MsgSerialized::try_from(&msg)?)?; + imap.logout()?; + Ok(()) +} + +pub fn reply( + uid: &str, + all: bool, + account: &Account, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let mut msg = imap.get_msg(&uid)?; + msg.change_to_reply(&account, all)?; + // FIXME + // override_msg_with_args(&mut msg, &matches); + trace!("Message: {:?}", msg); + output.print(MsgSerialized::try_from(&msg)?)?; + imap.logout()?; + Ok(()) +} + +pub fn forward( + uid: &str, + account: &Account, + output: &OutputService, + imap: &mut ImapService, +) -> Result<()> { + let mut msg = imap.get_msg(&uid)?; + msg.change_to_forwarding(&account); + // FIXME + // override_msg_with_args(&mut msg, &matches); + trace!("Message: {:?}", msg); + output.print(MsgSerialized::try_from(&msg)?)?; + imap.logout()?; + Ok(()) +} + +// == Helper functions == +// -- Template Subcommands -- +// These functions are more used for the "template" subcommand +// fn override_msg_with_args(msg: &mut Msg) { +// // -- Collecting credentials -- +// let from: Vec = match matches.values_of("from") { +// Some(from) => from.map(|arg| arg.to_string()).collect(), +// None => msg.headers.from.clone(), +// }; + +// let to: Vec = match matches.values_of("to") { +// Some(to) => to.map(|arg| arg.to_string()).collect(), +// None => Vec::new(), +// }; + +// let subject = matches +// .value_of("subject") +// .and_then(|subject| Some(subject.to_string())); + +// let cc: Option> = matches +// .values_of("cc") +// .and_then(|cc| Some(cc.map(|arg| arg.to_string()).collect())); + +// let bcc: Option> = matches +// .values_of("bcc") +// .and_then(|bcc| Some(bcc.map(|arg| arg.to_string()).collect())); + +// let signature = matches +// .value_of("signature") +// .and_then(|signature| Some(signature.to_string())) +// .or(msg.headers.signature.clone()); + +// let custom_headers: Option>> = { +// if let Some(matched_headers) = matches.values_of("header") { +// let mut custom_headers: HashMap> = HashMap::new(); + +// // collect the custom headers +// for header in matched_headers { +// let mut header = header.split(":"); +// let key = header.next().unwrap_or_default(); +// let val = header.next().unwrap_or_default().trim_start(); + +// debug!("overriden header: {}={}", key, val); + +// custom_headers.insert(key.to_string(), vec![val.to_string()]); +// } + +// Some(custom_headers) +// } else { +// None +// } +// }; + +// let body = { +// if atty::isnt(Stream::Stdin) { +// let body = io::stdin() +// .lock() +// .lines() +// .filter_map(|line| line.ok()) +// .map(|line| line.to_string()) +// .collect::>() +// .join("\n"); +// debug!("overriden body from stdin: {:?}", body); +// body +// } else if let Some(body) = matches.value_of("body") { +// debug!("overriden body: {:?}", body); +// body.to_string() +// } else { +// String::new() +// } +// }; + +// let body = Body::new_with_text(body); + +// // -- Creating and printing -- +// let headers = Headers { +// from, +// subject, +// to, +// cc, +// bcc, +// signature, +// custom_headers, +// ..msg.headers.clone() +// }; + +// msg.headers = headers; +// msg.body = body; +// } diff --git a/src/domain/msg/tpl/mod.rs b/src/domain/msg/tpl/mod.rs new file mode 100644 index 00000000..bf7874f2 --- /dev/null +++ b/src/domain/msg/tpl/mod.rs @@ -0,0 +1,4 @@ +//! Module related to messages template. + +pub mod arg; +pub mod handler; diff --git a/src/flag/arg.rs b/src/flag/arg.rs deleted file mode 100644 index 326b9b78..00000000 --- a/src/flag/arg.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use clap; -use log::debug; - -use crate::{ - domain::{imap::service::ImapServiceInterface, msg::arg::uid_arg}, - flag::model::Flags, -}; - -fn flags_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("flags") - .help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11). Just write the flag name without the backslash. Example: --flags \"Seen Answered\"") - .value_name("FLAGS…") - .multiple(true) - .required(true) -} - -pub fn subcmds<'a>() -> Vec> { - vec![clap::SubCommand::with_name("flags") - .about("Handles flags") - .subcommand( - clap::SubCommand::with_name("set") - .about("Replaces all message flags") - .arg(uid_arg()) - .arg(flags_arg()), - ) - .subcommand( - clap::SubCommand::with_name("add") - .about("Appends flags to a message") - .arg(uid_arg()) - .arg(flags_arg()), - ) - .subcommand( - clap::SubCommand::with_name("remove") - .aliases(&["rm"]) - .about("Removes flags from a message") - .arg(uid_arg()) - .arg(flags_arg()), - )] -} - -pub fn matches( - arg_matches: &clap::ArgMatches, - imap: &mut ImapService, -) -> Result { - if let Some(matches) = arg_matches.subcommand_matches("set") { - debug!("set command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - - let flags = matches.value_of("flags").unwrap(); - debug!("flags: {}", flags); - let flags = Flags::from(flags); - - imap.set_flags(uid, flags)?; - imap.logout()?; - return Ok(true); - } - - if let Some(matches) = arg_matches.subcommand_matches("add") { - debug!("add command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - - let flags = matches.value_of("flags").unwrap(); - debug!("flags: {}", flags); - let flags = Flags::from(flags); - - imap.add_flags(uid, flags)?; - imap.logout()?; - return Ok(true); - } - - if let Some(matches) = arg_matches.subcommand_matches("remove") { - debug!("remove command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - - let flags = matches.value_of("flags").unwrap(); - debug!("flags: {}", flags); - let flags = Flags::from(flags); - - imap.remove_flags(uid, flags)?; - imap.logout()?; - return Ok(true); - } - - debug!("nothing matched"); - Ok(false) -} diff --git a/src/flag/mod.rs b/src/flag/mod.rs deleted file mode 100644 index 550ac90a..00000000 --- a/src/flag/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod arg; -pub mod model; diff --git a/src/lib.rs b/src/lib.rs index 43da26b8..cdfb6443 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,10 +17,6 @@ pub mod compl; /// Everything which is related to the config files. For example the structure of your config file. pub mod config; -/// A wrapper for representing a flag of a message or mailbox. For example the delete-flag or -/// read-flag. -pub mod flag; - /// Handles the input-interaction with the user. For example if you want to edit the body of your /// message, his module takes care of the draft and calls your ~(neo)vim~ your favourite editor. pub mod input; diff --git a/src/main.rs b/src/main.rs index fa44fef4..64b7ff12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap; use env_logger; use std::{convert::TryFrom, env}; +use url::Url; use himalaya::{ compl, @@ -14,7 +15,6 @@ use himalaya::{ msg, smtp::service::SmtpService, }, - flag, output::{cli::output_args, service::OutputService}, }; @@ -26,11 +26,10 @@ fn create_app<'a>() -> clap::App<'a, 'a> { .args(&output_args()) .args(&config_args()) .arg(mbox::arg::source_arg()) - .subcommands(flag::arg::subcmds()) + .subcommands(compl::arg::subcmds()) .subcommands(imap::arg::subcmds()) .subcommands(mbox::arg::subcmds()) .subcommands(msg::arg::subcmds()) - .subcommands(compl::arg::subcmds()) } fn main() -> Result<()> { @@ -39,19 +38,18 @@ fn main() -> Result<()> { env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"), ); - // TODO: put in a `mailto` module - // let raw_args: Vec = env::args().collect(); - // if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - // let config = Config::new(None)?; - // let account = config.find_account_by_name(None)?.clone(); - // let output = Output::new("plain"); - // let mbox = "INBOX"; - // let arg_matches = ArgMatches::default(); - // let app = Ctx::new(config, output, mbox, arg_matches); - // let url = Url::parse(&raw_args[1])?; - // let smtp = domain::smtp::service::SMTPService::new(&app.account); - // return Ok(msg_matches_mailto(&app, &url, smtp)?); - // } + // Check mailto match BEFORE app initialization. + let raw_args: Vec = env::args().collect(); + if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { + let mbox = Mbox::from("INBOX"); + let config = Config::try_from(None)?; + let account = Account::try_from((&config, None))?; + let output = OutputService::from("plain"); + let url = Url::parse(&raw_args[1])?; + let mut imap = ImapService::from((&account, &mbox)); + let mut smtp = SmtpService::from(&account); + return msg::handler::mailto(&url, &account, &output, &mut imap, &mut smtp); + } let app = create_app(); let m = app.get_matches(); @@ -91,9 +89,69 @@ fn main() -> Result<()> { _ => (), } - // TODO: use same system as compl - let _matched = flag::arg::matches(&m, &mut imap)? - || msg::arg::matches(&m, &mbox, &account, &output, &mut imap, &mut smtp)?; + // Check message matches. + match msg::arg::matches(&m)? { + Some(msg::arg::Match::Attachments(uid)) => { + return msg::handler::attachments(uid, &account, &output, &mut imap); + } + Some(msg::arg::Match::Copy(uid, mbox)) => { + return msg::handler::copy(uid, mbox, &output, &mut imap); + } + Some(msg::arg::Match::Delete(uid)) => { + return msg::handler::delete(uid, &output, &mut imap); + } + Some(msg::arg::Match::Forward(uid, paths)) => { + return msg::handler::forward(uid, paths, &account, &output, &mut imap, &mut smtp); + } + Some(msg::arg::Match::List(page_size, page)) => { + return msg::handler::list(page_size, page, &account, &output, &mut imap); + } + Some(msg::arg::Match::Move(uid, mbox)) => { + return msg::handler::move_(uid, mbox, &output, &mut imap); + } + Some(msg::arg::Match::Read(uid, mime, raw)) => { + return msg::handler::read(uid, mime, raw, &output, &mut imap); + } + Some(msg::arg::Match::Reply(uid, all, paths)) => { + return msg::handler::reply(uid, all, paths, &account, &output, &mut imap, &mut smtp); + } + Some(msg::arg::Match::Save(mbox, msg)) => { + return msg::handler::save(mbox, msg, &mut imap); + } + Some(msg::arg::Match::Search(query, page_size, page)) => { + return msg::handler::search(page_size, page, query, &account, &output, &mut imap); + } + Some(msg::arg::Match::Send(msg)) => { + return msg::handler::send(msg, &output, &mut imap, &mut smtp); + } + Some(msg::arg::Match::Write(paths)) => { + return msg::handler::write(paths, &account, &output, &mut imap, &mut smtp); + } + + Some(msg::arg::Match::Flag(m)) => match m { + msg::flag::arg::Match::Set(uid, flags) => { + return msg::flag::handler::set(uid, flags, &mut imap); + } + msg::flag::arg::Match::Add(uid, flags) => { + return msg::flag::handler::add(uid, flags, &mut imap); + } + msg::flag::arg::Match::Remove(uid, flags) => { + return msg::flag::handler::remove(uid, flags, &mut imap); + } + }, + Some(msg::arg::Match::Tpl(m)) => match m { + msg::tpl::arg::Match::New => { + return msg::tpl::handler::new(&account, &output, &mut imap); + } + msg::tpl::arg::Match::Reply(uid, all) => { + return msg::tpl::handler::reply(uid, all, &account, &output, &mut imap); + } + msg::tpl::arg::Match::Forward(uid) => { + return msg::tpl::handler::forward(uid, &account, &output, &mut imap); + } + }, + _ => (), + } Ok(()) } diff --git a/src/output/service.rs b/src/output/service.rs index b05c3716..8f203687 100644 --- a/src/output/service.rs +++ b/src/output/service.rs @@ -12,6 +12,15 @@ pub enum OutputFmt { Json, } +impl From<&str> for OutputFmt { + fn from(fmt: &str) -> Self { + match fmt { + slice if slice.eq_ignore_ascii_case("json") => Self::Json, + _ => Self::Plain, + } + } +} + impl TryFrom> for OutputFmt { type Error = Error; @@ -99,6 +108,15 @@ impl Default for OutputService { } } +impl From<&str> for OutputService { + fn from(fmt: &str) -> Self { + debug!("init output service"); + debug!("output: `{:?}`", fmt); + let fmt = fmt.into(); + Self { fmt } + } +} + impl TryFrom> for OutputService { type Error = Error;