diff --git a/src/app.rs b/src/app.rs index c840b55..0cc3352 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,545 +3,401 @@ use error_chain::error_chain; use std::{env, fs}; use crate::{ - config::{self, Config}, - flag::cli::{flags_matches, flags_subcommand}, - imap::{self, ImapConnector}, + config::Config, + flag::cli::{flag_matches, flag_subcmds}, + imap::ImapConnector, input, - msg::{self, Attachments, Msg, Msgs, ReadableMsg}, + mbox::cli::{mbox_arg, mbox_matches, mbox_subcmds}, + msg::cli::{msg_args, msg_matches, msg_subcmds}, output::{self, print}, smtp, }; error_chain! { links { - Config(config::Error, config::ErrorKind); - Imap(imap::Error, imap::ErrorKind); - Input(input::Error, input::ErrorKind); - Message(msg::Error, msg::ErrorKind); - Output(output::Error, output::ErrorKind); - Smtp(smtp::Error, smtp::ErrorKind); + Config(crate::config::Error, crate::config::ErrorKind); + MboxCli(crate::mbox::cli::Error, crate::mbox::cli::ErrorKind); + MsgCli(crate::msg::cli::Error, crate::msg::cli::ErrorKind); + FlagCli(crate::flag::cli::Error, crate::flag::cli::ErrorKind); } } pub struct App<'a>(pub clap::App<'a, 'a>); impl<'a> App<'a> { - fn mailbox_arg() -> Arg<'a, 'a> { - Arg::with_name("mailbox") - .short("m") - .long("mailbox") - .help("Mailbox name") - .value_name("STRING") - .default_value("INBOX") - } + // fn mailbox_arg() -> Arg<'a, 'a> { + // Arg::with_name("mailbox") + // .short("m") + // .long("mailbox") + // .help("Mailbox name") + // .value_name("STRING") + // .default_value("INBOX") + // } - fn uid_arg() -> Arg<'a, 'a> { - Arg::with_name("uid") - .help("Message UID") - .value_name("UID") - .required(true) - } + // fn uid_arg() -> Arg<'a, 'a> { + // Arg::with_name("uid") + // .help("Message UID") + // .value_name("UID") + // .required(true) + // } - fn reply_all_arg() -> Arg<'a, 'a> { - Arg::with_name("reply-all") - .help("Includes all recipients") - .short("a") - .long("all") - } + // fn reply_all_arg() -> Arg<'a, 'a> { + // Arg::with_name("reply-all") + // .help("Includes all recipients") + // .short("a") + // .long("all") + // } - fn page_size_arg() -> Arg<'a, 'a> { - Arg::with_name("size") - .help("Page size") - .short("s") - .long("size") - .value_name("INT") - .default_value("10") - } + // fn page_size_arg() -> Arg<'a, 'a> { + // Arg::with_name("size") + // .help("Page size") + // .short("s") + // .long("size") + // .value_name("INT") + // .default_value("10") + // } - fn page_arg() -> Arg<'a, 'a> { - Arg::with_name("page") - .help("Page number") - .short("p") - .long("page") - .value_name("INT") - .default_value("0") - } + // fn page_arg() -> Arg<'a, 'a> { + // Arg::with_name("page") + // .help("Page number") + // .short("p") + // .long("page") + // .value_name("INT") + // .default_value("0") + // } pub fn new() -> Self { - Self(clap::App::new(env!("CARGO_PKG_NAME")) - .version(env!("CARGO_PKG_VERSION")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .author(env!("CARGO_PKG_AUTHORS")) - .arg( - Arg::with_name("output") - .long("output") - .short("o") - .help("Formats the output") - .value_name("STRING") - .possible_values(&["text", "json"]) - .default_value("text"), - ) - .arg( - Arg::with_name("account") - .long("account") - .short("a") - .help("Selects a specific account") - .value_name("STRING"), - ) - .arg( - Arg::with_name("mailbox") - .short("m") - .long("mailbox") - .help("Selects a specific mailbox") - .value_name("STRING") - .default_value("INBOX") - ) - .subcommand( - SubCommand::with_name("mailboxes") - .aliases(&["mailbox", "mboxes", "mbox", "mb", "m"]) - .about("Lists all available mailboxes"), - ) - .subcommand(flags_subcommand()) - .subcommand( - SubCommand::with_name("list") - .aliases(&["lst", "l"]) - .about("Lists emails sorted by arrival date") - .arg(Self::mailbox_arg()) - .arg(Self::page_size_arg()) - .arg(Self::page_arg()), - ) - .subcommand( - SubCommand::with_name("search") - .aliases(&["query", "q", "s"]) - .about("Lists emails matching the given IMAP query") - .arg(Self::mailbox_arg()) - .arg(Self::page_size_arg()) - .arg(Self::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( - SubCommand::with_name("read") - .aliases(&["r"]) - .about("Reads text bodies of an email") - .arg(Self::uid_arg()) - .arg(Self::mailbox_arg()) - .arg( - Arg::with_name("mime-type") - .help("MIME type to use") - .short("t") - .long("mime-type") - .value_name("STRING") - .possible_values(&["plain", "html"]) - .default_value("plain"), - ), - ) - .subcommand( - SubCommand::with_name("attachments") - .aliases(&["attach", "att", "a"]) - .about("Downloads all attachments from an email") - .arg(Self::uid_arg()) - .arg(Self::mailbox_arg()), - ) - .subcommand(SubCommand::with_name("write").about("Writes a new email")) - .subcommand( - SubCommand::with_name("reply") - .aliases(&["rep", "re"]) - .about("Answers to an email") - .arg(Self::uid_arg()) - .arg(Self::mailbox_arg()) - .arg(Self::reply_all_arg()), - ) - .subcommand( - SubCommand::with_name("forward") - .aliases(&["fwd", "f"]) - .about("Forwards an email") - .arg(Self::uid_arg()) - .arg(Self::mailbox_arg()), - ) - .subcommand( - SubCommand::with_name("send") - .about("Sends a raw message") - .arg(Arg::with_name("message").raw(true)), - ) - .subcommand( - SubCommand::with_name("save") - .about("Saves a raw message in the given mailbox") - .arg(Self::mailbox_arg()) - .arg(Arg::with_name("message").raw(true)), - ) - .subcommand( - SubCommand::with_name("template") - .aliases(&["tpl", "t"]) - .about("Generates a message template") - .subcommand( - SubCommand::with_name("new") - .aliases(&["n"]) - .about("Generates a new message template") - .arg(Self::mailbox_arg()), - ) - .subcommand( - SubCommand::with_name("reply") - .aliases(&["rep", "r"]) - .about("Generates a reply message template") - .arg(Self::uid_arg()) - .arg(Self::mailbox_arg()) - .arg(Self::reply_all_arg()), - ) - .subcommand( - SubCommand::with_name("forward") - .aliases(&["fwd", "fw", "f"]) - .about("Generates a forward message template") - .arg(Self::uid_arg()) - .arg(Self::mailbox_arg()), - ), - ) - .subcommand( - SubCommand::with_name("idle") - .about("Starts the idle mode") - .arg(Self::mailbox_arg()), - )) + let app = clap::App::new(env!("CARGO_PKG_NAME")) + .version(env!("CARGO_PKG_VERSION")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .author(env!("CARGO_PKG_AUTHORS")) + .arg( + Arg::with_name("output") + .long("output") + .short("o") + .help("Defines the output format") + .value_name("STRING") + .possible_values(&["plain", "json"]) + .default_value("plain"), + ) + .arg( + Arg::with_name("account") + .long("account") + .short("a") + .help("Selects a specific account") + .value_name("STRING"), + ) + .arg(mbox_arg()); + + let app = app.subcommands(mbox_subcmds()); + let app = app.subcommands(msg_subcmds()); + let app = app.subcommands(flag_subcmds()); + let app = app.args(&msg_args()); + + Self(app) } pub fn run(self) -> Result<()> { let matches = self.0.get_matches(); - let output_type = matches.value_of("output").unwrap().to_owned(); - let account = matches.value_of("account"); - let mbox = matches.value_of("mailbox").unwrap(); + let matched = mbox_matches(&matches)?; + let matched = flag_matches(matched, &matches)?; + let matched = msg_matches(matched, &matches)?; - if let Some(_) = matches.subcommand_matches("mailboxes") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // if let Some(matches) = matches.subcommand_matches("search") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // let page_size: usize = matches.value_of("size").unwrap().parse().unwrap(); + // let page: usize = matches.value_of("page").unwrap().parse().unwrap(); + // 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(" "); - let mboxes = imap_conn.list_mboxes()?; - print(&output_type, mboxes)?; + // let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; + // let msgs = Msgs::from(&msgs); - imap_conn.logout(); - } + // print(&output_type, msgs)?; - if let Some(matches) = matches.subcommand_matches("flags") { - flags_matches(account, &mbox, &matches) - .chain_err(|| "Could not handle flags arg matches")?; - } + // imap_conn.logout(); + // } - if let Some(matches) = matches.subcommand_matches("list") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // if let Some(matches) = matches.subcommand_matches("read") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - let mbox = matches.value_of("mailbox").unwrap(); - let page_size: u32 = matches.value_of("size").unwrap().parse().unwrap(); - let page: u32 = matches.value_of("page").unwrap().parse().unwrap(); + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); + // let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); - let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; - let msgs = Msgs::from(&msgs); + // let msg = imap_conn.read_msg(&mbox, &uid)?; + // let msg = ReadableMsg::from_bytes(&mime, &msg)?; - print(&output_type, msgs)?; + // print(&output_type, msg)?; + // imap_conn.logout(); + // } - imap_conn.logout(); - } + // if let Some(matches) = matches.subcommand_matches("attachments") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - if let Some(matches) = matches.subcommand_matches("search") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; - let mbox = matches.value_of("mailbox").unwrap(); - let page_size: usize = matches.value_of("size").unwrap().parse().unwrap(); - let page: usize = matches.value_of("page").unwrap().parse().unwrap(); - 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(" "); + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); - let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; - let msgs = Msgs::from(&msgs); + // let msg = imap_conn.read_msg(&mbox, &uid)?; + // let attachments = Attachments::from_bytes(&msg)?; - print(&output_type, msgs)?; + // match output_type.as_str() { + // "text" => { + // println!( + // "{} attachment(s) found for message {}", + // attachments.0.len(), + // uid + // ); - imap_conn.logout(); - } + // attachments.0.iter().for_each(|attachment| { + // let filepath = config.downloads_filepath(&account, &attachment.filename); + // println!("Downloading {}…", &attachment.filename); + // fs::write(filepath, &attachment.raw).unwrap() + // }); - if let Some(matches) = matches.subcommand_matches("read") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // println!("Done!"); + // } + // "json" => { + // attachments.0.iter().for_each(|attachment| { + // let filepath = config.downloads_filepath(&account, &attachment.filename); + // fs::write(filepath, &attachment.raw).unwrap() + // }); - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); - let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); + // print!("{{}}"); + // } + // _ => (), + // } - let msg = imap_conn.read_msg(&mbox, &uid)?; - let msg = ReadableMsg::from_bytes(&mime, &msg)?; + // imap_conn.logout(); + // } - print(&output_type, msg)?; - imap_conn.logout(); - } + // if let Some(_) = matches.subcommand_matches("write") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + // let tpl = Msg::build_new_tpl(&config, &account)?; + // let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; + // let mut msg = Msg::from(content); - if let Some(matches) = matches.subcommand_matches("attachments") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // loop { + // match input::post_edit_choice() { + // Ok(choice) => match choice { + // input::Choice::Send => { + // println!("Sending…"); + // let msg = msg.to_sendable_msg()?; + // smtp::send(&account, &msg)?; + // imap_conn.append_msg("Sent", &msg.formatted())?; + // println!("Done!"); + // break; + // } + // input::Choice::Draft => { + // println!("Saving to draft…"); + // imap_conn.append_msg("Drafts", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Edit => { + // let content = input::open_editor_with_draft()?; + // msg = Msg::from(content); + // } + // input::Choice::Quit => break, + // }, + // Err(err) => eprintln!("{}", err), + // } + // } - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); + // imap_conn.logout(); + // } - let msg = imap_conn.read_msg(&mbox, &uid)?; - let attachments = Attachments::from_bytes(&msg)?; + // if let Some(matches) = matches.subcommand_matches("template") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - match output_type.as_str() { - "text" => { - println!( - "{} attachment(s) found for message {}", - attachments.0.len(), - uid - ); + // if let Some(_) = matches.subcommand_matches("new") { + // let tpl = Msg::build_new_tpl(&config, &account)?; + // print(&output_type, &tpl)?; + // } - attachments.0.iter().for_each(|attachment| { - let filepath = config.downloads_filepath(&account, &attachment.filename); - println!("Downloading {}…", &attachment.filename); - fs::write(filepath, &attachment.raw).unwrap() - }); + // if let Some(matches) = matches.subcommand_matches("reply") { + // let uid = matches.value_of("uid").unwrap(); + // let mbox = matches.value_of("mailbox").unwrap(); - println!("Done!"); - } - "json" => { - attachments.0.iter().for_each(|attachment| { - let filepath = config.downloads_filepath(&account, &attachment.filename); - fs::write(filepath, &attachment.raw).unwrap() - }); + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = if matches.is_present("reply-all") { + // msg.build_reply_all_tpl(&config, &account)? + // } else { + // msg.build_reply_tpl(&config, &account)? + // }; - print!("{{}}"); - } - _ => (), - } + // print(&output_type, &tpl)?; + // } - imap_conn.logout(); - } + // if let Some(matches) = matches.subcommand_matches("forward") { + // let uid = matches.value_of("uid").unwrap(); + // let mbox = matches.value_of("mailbox").unwrap(); - if let Some(_) = matches.subcommand_matches("write") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; - let tpl = Msg::build_new_tpl(&config, &account)?; - let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; - let mut msg = Msg::from(content); + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = msg.build_forward_tpl(&config, &account)?; - loop { - match input::post_edit_choice() { - Ok(choice) => match choice { - input::Choice::Send => { - println!("Sending…"); - let msg = msg.to_sendable_msg()?; - smtp::send(&account, &msg)?; - imap_conn.append_msg("Sent", &msg.formatted())?; - println!("Done!"); - break; - } - input::Choice::Draft => { - println!("Saving to draft…"); - imap_conn.append_msg("Drafts", &msg.to_vec()?)?; - println!("Done!"); - break; - } - input::Choice::Edit => { - let content = input::open_editor_with_draft()?; - msg = Msg::from(content); - } - input::Choice::Quit => break, - }, - Err(err) => eprintln!("{}", err), - } - } + // print(&output_type, &tpl)?; + // } + // } - imap_conn.logout(); - } + // if let Some(matches) = matches.subcommand_matches("reply") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - if let Some(matches) = matches.subcommand_matches("template") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); - if let Some(_) = matches.subcommand_matches("new") { - let tpl = Msg::build_new_tpl(&config, &account)?; - print(&output_type, &tpl)?; - } + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = if matches.is_present("reply-all") { + // msg.build_reply_all_tpl(&config, &account)? + // } else { + // msg.build_reply_tpl(&config, &account)? + // }; - if let Some(matches) = matches.subcommand_matches("reply") { - let uid = matches.value_of("uid").unwrap(); - let mbox = matches.value_of("mailbox").unwrap(); + // let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; + // let mut msg = Msg::from(content); - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&config, &account)? - } else { - msg.build_reply_tpl(&config, &account)? - }; + // loop { + // match input::post_edit_choice() { + // Ok(choice) => match choice { + // input::Choice::Send => { + // println!("Sending…"); + // smtp::send(&account, &msg.to_sendable_msg()?)?; + // imap_conn.append_msg("Sent", &msg.to_vec()?)?; + // imap_conn.add_flags(mbox, uid, "\\Answered")?; + // println!("Done!"); + // break; + // } + // input::Choice::Draft => { + // println!("Saving to draft…"); + // imap_conn.append_msg("Drafts", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Edit => { + // let content = input::open_editor_with_draft()?; + // msg = Msg::from(content); + // } + // input::Choice::Quit => break, + // }, + // Err(err) => eprintln!("{}", err), + // } + // } - print(&output_type, &tpl)?; - } + // imap_conn.logout(); + // } - if let Some(matches) = matches.subcommand_matches("forward") { - let uid = matches.value_of("uid").unwrap(); - let mbox = matches.value_of("mailbox").unwrap(); + // if let Some(matches) = matches.subcommand_matches("forward") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = msg.build_forward_tpl(&config, &account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); - print(&output_type, &tpl)?; - } - } + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = msg.build_forward_tpl(&config, &account)?; + // let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; + // let mut msg = Msg::from(content); - if let Some(matches) = matches.subcommand_matches("reply") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // loop { + // match input::post_edit_choice() { + // Ok(choice) => match choice { + // input::Choice::Send => { + // println!("Sending…"); + // smtp::send(&account, &msg.to_sendable_msg()?)?; + // imap_conn.append_msg("Sent", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Draft => { + // println!("Saving to draft…"); + // imap_conn.append_msg("Drafts", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Edit => { + // let content = input::open_editor_with_draft()?; + // msg = Msg::from(content); + // } + // input::Choice::Quit => break, + // }, + // Err(err) => eprintln!("{}", err), + // } + // } - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); + // imap_conn.logout(); + // } - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&config, &account)? - } else { - msg.build_reply_tpl(&config, &account)? - }; + // if let Some(matches) = matches.subcommand_matches("send") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; - let mut msg = Msg::from(content); + // let msg = matches.value_of("message").unwrap(); + // let msg = Msg::from(msg.to_string()); + // let msg = msg.to_sendable_msg()?; - loop { - match input::post_edit_choice() { - Ok(choice) => match choice { - input::Choice::Send => { - println!("Sending…"); - smtp::send(&account, &msg.to_sendable_msg()?)?; - imap_conn.append_msg("Sent", &msg.to_vec()?)?; - imap_conn.add_flags(mbox, uid, "\\Answered")?; - println!("Done!"); - break; - } - input::Choice::Draft => { - println!("Saving to draft…"); - imap_conn.append_msg("Drafts", &msg.to_vec()?)?; - println!("Done!"); - break; - } - input::Choice::Edit => { - let content = input::open_editor_with_draft()?; - msg = Msg::from(content); - } - input::Choice::Quit => break, - }, - Err(err) => eprintln!("{}", err), - } - } + // smtp::send(&account, &msg)?; + // imap_conn.append_msg("Sent", &msg.formatted())?; + // imap_conn.logout(); + // } - imap_conn.logout(); - } + // if let Some(matches) = matches.subcommand_matches("save") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; - if let Some(matches) = matches.subcommand_matches("forward") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // let msg = matches.value_of("message").unwrap(); + // let msg = Msg::from(msg.to_string()); - let mbox = matches.value_of("mailbox").unwrap(); - let uid = matches.value_of("uid").unwrap(); + // imap_conn.append_msg(mbox, &msg.to_vec()?)?; + // imap_conn.logout(); + // } - let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); - let tpl = msg.build_forward_tpl(&config, &account)?; - let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; - let mut msg = Msg::from(content); - - loop { - match input::post_edit_choice() { - Ok(choice) => match choice { - input::Choice::Send => { - println!("Sending…"); - smtp::send(&account, &msg.to_sendable_msg()?)?; - imap_conn.append_msg("Sent", &msg.to_vec()?)?; - println!("Done!"); - break; - } - input::Choice::Draft => { - println!("Saving to draft…"); - imap_conn.append_msg("Drafts", &msg.to_vec()?)?; - println!("Done!"); - break; - } - input::Choice::Edit => { - let content = input::open_editor_with_draft()?; - msg = Msg::from(content); - } - input::Choice::Quit => break, - }, - Err(err) => eprintln!("{}", err), - } - } - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("send") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let msg = matches.value_of("message").unwrap(); - let msg = Msg::from(msg.to_string()); - let msg = msg.to_sendable_msg()?; - - smtp::send(&account, &msg)?; - imap_conn.append_msg("Sent", &msg.formatted())?; - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("save") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; - - let mbox = matches.value_of("mailbox").unwrap(); - let msg = matches.value_of("message").unwrap(); - let msg = Msg::from(msg.to_string()); - - imap_conn.append_msg(mbox, &msg.to_vec()?)?; - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("idle") { - let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; - let mut imap_conn = ImapConnector::new(&account)?; - let mbox = matches.value_of("mailbox").unwrap(); - imap_conn.idle(&config, &mbox)?; - } + // if let Some(matches) = matches.subcommand_matches("idle") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // imap_conn.idle(&config, &mbox)?; + // } Ok(()) } diff --git a/src/flag/cli.rs b/src/flag/cli.rs index 57553c5..24ffa67 100644 --- a/src/flag/cli.rs +++ b/src/flag/cli.rs @@ -23,15 +23,15 @@ fn uid_arg<'a, 'b>() -> Arg<'a, 'b> { fn flags_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name("flags") .help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11)") - .value_name("FLAGS") + .value_name("FLAGS…") .multiple(true) .required(true) } -pub fn flags_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("flags") - .aliases(&["flag", "fg"]) - .about("Manages flags") +pub fn flag_subcmds<'a>() -> Vec> { + vec![SubCommand::with_name("flags") + .aliases(&["flag"]) + .about("Handles flags") .subcommand( SubCommand::with_name("set") .aliases(&["s"]) @@ -52,31 +52,40 @@ pub fn flags_subcommand<'a, 'b>() -> App<'a, 'b> { .about("Removes flags from a message") .arg(uid_arg()) .arg(flags_arg()), - ) + )] } -pub fn flags_matches(account: Option<&str>, mbox: &str, matches: &ArgMatches) -> Result<()> { +pub fn flag_matches(matched: bool, matches: &ArgMatches) -> Result { + if matched { + return Ok(true); + } + let config = Config::new_from_file()?; - let account = config.find_account_by_name(account)?; + let account = config.find_account_by_name(matches.value_of("account"))?; + let output_fmt = matches.value_of("output").unwrap(); + let mbox = matches.value_of("mailbox").unwrap(); let mut imap_conn = ImapConnector::new(&account)?; if let Some(matches) = matches.subcommand_matches("set") { let uid = matches.value_of("uid").unwrap(); let flags = matches.value_of("flags").unwrap(); imap_conn.set_flags(mbox, uid, flags)?; + return Ok(true); } if let Some(matches) = matches.subcommand_matches("add") { let uid = matches.value_of("uid").unwrap(); let flags = matches.value_of("flags").unwrap(); imap_conn.add_flags(mbox, uid, flags)?; + return Ok(true); } if let Some(matches) = matches.subcommand_matches("remove") { let uid = matches.value_of("uid").unwrap(); let flags = matches.value_of("flags").unwrap(); imap_conn.remove_flags(mbox, uid, flags)?; + return Ok(true); } - Ok(()) + Ok(false) } diff --git a/src/imap.rs b/src/imap.rs index d8a6686..a7ff851 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -5,8 +5,8 @@ use std::net::TcpStream; use crate::{ config::{self, Account, Config}, - mbox::{Mbox, Mboxes}, - msg::Msg, + mbox::model::{Mbox, Mboxes}, + msg::model::Msg, }; error_chain! { diff --git a/src/main.rs b/src/main.rs index 2db212c..d9802a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,6 @@ mod app; mod config; mod imap; mod input; -mod mbox; -mod msg; mod output; mod smtp; mod table; @@ -11,6 +9,14 @@ mod flag { pub(crate) mod cli; pub(crate) mod model; } +mod msg { + pub(crate) mod cli; + pub(crate) mod model; +} +mod mbox { + pub(crate) mod cli; + pub(crate) mod model; +} use crate::app::App; @@ -18,11 +24,11 @@ fn main() { if let Err(ref errs) = App::new().run() { let mut errs = errs.iter(); match errs.next() { + None => (), Some(err) => { eprintln!("{}", err); errs.for_each(|err| eprintln!(" ↳ {}", err)); } - None => (), } } } diff --git a/src/mbox/cli.rs b/src/mbox/cli.rs new file mode 100644 index 0000000..8b73417 --- /dev/null +++ b/src/mbox/cli.rs @@ -0,0 +1,44 @@ +use clap::{self, App, Arg, ArgMatches, SubCommand}; +use error_chain::error_chain; + +use crate::{config::Config, imap::ImapConnector, output::print}; + +error_chain! { + links { + Config(crate::config::Error, crate::config::ErrorKind); + Imap(crate::imap::Error, crate::imap::ErrorKind); + MsgCli(crate::msg::cli::Error, crate::msg::cli::ErrorKind); + Output(crate::output::Error, crate::output::ErrorKind); + } +} + +pub fn mbox_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("mailbox") + .short("m") + .long("mailbox") + .help("Selects a specific mailbox") + .value_name("STRING") + .default_value("INBOX") +} + +pub fn mbox_subcmds<'a>() -> Vec> { + vec![SubCommand::with_name("mailboxes") + .aliases(&["mailbox", "mboxes", "mbox", "m"]) + .about("Lists all mailboxes")] +} + +pub fn mbox_matches(matches: &ArgMatches) -> Result { + if let Some(_) = matches.subcommand_matches("mailboxes") { + let config = Config::new_from_file()?; + let account = config.find_account_by_name(matches.value_of("account"))?; + let output_fmt = matches.value_of("output").unwrap(); + let mut imap_conn = ImapConnector::new(&account)?; + let mboxes = imap_conn.list_mboxes()?; + print(&output_fmt, mboxes)?; + imap_conn.logout(); + + return Ok(true); + } + + Ok(false) +} diff --git a/src/mbox/model.rs b/src/mbox/model.rs new file mode 100644 index 0000000..6ad92d5 --- /dev/null +++ b/src/mbox/model.rs @@ -0,0 +1,63 @@ +use imap; +use serde::Serialize; +use std::fmt; + +use crate::table::{self, DisplayRow, DisplayTable}; + +// Mbox + +#[derive(Debug, Serialize)] +pub struct Mbox { + pub delim: String, + pub name: String, + pub attributes: Vec, +} + +impl Mbox { + pub fn from_name(name: &imap::types::Name) -> Self { + Self { + delim: name.delimiter().unwrap_or_default().to_owned(), + name: name.name().to_owned(), + attributes: vec![], // TODO: set attributes + } + } +} + +impl DisplayRow for Mbox { + fn to_row(&self) -> Vec { + use crate::table::*; + + vec![ + Cell::new(&[BLUE], &self.delim), + Cell::new(&[GREEN], &self.name), + FlexCell::new(&[YELLOW], &self.attributes.join(", ")), + ] + } +} + +// Mboxes + +#[derive(Debug, Serialize)] +pub struct Mboxes(pub Vec); + +impl<'a> DisplayTable<'a, Mbox> for Mboxes { + fn header_row() -> Vec { + use crate::table::*; + + vec![ + Cell::new(&[BOLD, UNDERLINE, WHITE], "DELIM"), + Cell::new(&[BOLD, UNDERLINE, WHITE], "NAME"), + FlexCell::new(&[BOLD, UNDERLINE, WHITE], "ATTRIBUTES"), + ] + } + + fn rows(&self) -> &Vec { + &self.0 + } +} + +impl fmt::Display for Mboxes { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\n{}", self.to_table()) + } +} diff --git a/src/msg/cli.rs b/src/msg/cli.rs new file mode 100644 index 0000000..42eb0dc --- /dev/null +++ b/src/msg/cli.rs @@ -0,0 +1,536 @@ +use clap::{self, App, Arg, ArgMatches, SubCommand}; +use error_chain::error_chain; +use std::{env, fs}; + +use crate::{ + config::{self, Account, Config}, + imap::{self, ImapConnector}, + input, + // msg::{self, Attachments, Msg, Msgs, ReadableMsg}, + msg::model::Msgs, + output::{self, print}, + smtp, +}; + +error_chain! { + links { + Config(config::Error, config::ErrorKind); + Imap(imap::Error, imap::ErrorKind); + Input(input::Error, input::ErrorKind); + Output(output::Error, output::ErrorKind); + Smtp(smtp::Error, smtp::ErrorKind); + } +} + +fn mailbox_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("mailbox") + .short("m") + .long("mailbox") + .help("Mailbox name") + .value_name("STRING") + .default_value("INBOX") +} + +fn uid_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("uid") + .help("Message UID") + .value_name("UID") + .required(true) +} + +fn reply_all_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("reply-all") + .help("Includes all recipients") + .short("a") + .long("all") +} + +fn page_size_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("size") + .help("Page size") + .short("s") + .long("size") + .value_name("INT") + .default_value("10") +} + +fn page_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("page") + .help("Page number") + .short("p") + .long("page") + .value_name("INT") + .default_value("0") +} + +pub fn msg_args<'a>() -> Vec> { + vec![ + Arg::with_name("page") + .help("Page number") + .short("p") + .long("page") + .value_name("INT") + .default_value("0"), + Arg::with_name("size") + .help("Page size") + .short("s") + .long("size") + .value_name("INT") + .default_value("10"), + ] +} + +pub fn msg_subcmds<'a>() -> Vec> { + vec![SubCommand::with_name("list") + .aliases(&["lst", "l"]) + .about("Lists messages sorted by UID") + .arg(page_size_arg()) + .arg(page_arg())] + // SubCommand::with_name("messages") + // .aliases(&["messages", "message", "msgs", "msg"]) + // .about("Handles message actions") + // .arg(mailbox_arg()) + // .subcommand( + // SubCommand::with_name("list") + // .aliases(&["lst", "l"]) + // .about("Lists messages sorted by UID") + // .arg(page_size_arg()) + // .arg(page_arg()), + // ) + // .subcommand( + // SubCommand::with_name("search") + // .aliases(&["query", "q", "s"]) + // .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(SubCommand::with_name("write").about("Writes a new message")) + // .subcommand( + // SubCommand::with_name("send") + // .about("Sends a raw message") + // .arg(Arg::with_name("message").raw(true)), + // ) + // .subcommand( + // SubCommand::with_name("save") + // .about("Saves a raw message") + // .arg(Arg::with_name("message").raw(true)), + // ) + // .subcommand( + // SubCommand::with_name("read") + // .aliases(&["r"]) + // .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("STRING") + // .possible_values(&["plain", "html"]) + // .default_value("plain"), + // ), + // ) + // .subcommand( + // SubCommand::with_name("attachments") + // .aliases(&["attach", "att", "a"]) + // .about("Downloads all attachments from an email") + // .arg(uid_arg()), + // ) + // .subcommand( + // SubCommand::with_name("reply") + // .aliases(&["rep", "re"]) + // .about("Answers to an email") + // .arg(uid_arg()) + // .arg(reply_all_arg()), + // ) + // .subcommand( + // SubCommand::with_name("forward") + // .aliases(&["fwd", "f"]) + // .about("Forwards an email") + // .arg(uid_arg()), + // ) + // .subcommand( + // SubCommand::with_name("template") + // .aliases(&["tpl", "t"]) + // .about("Generates a message template") + // .subcommand( + // SubCommand::with_name("new") + // .aliases(&["n"]) + // .about("Generates a new message template"), + // ) + // .subcommand( + // SubCommand::with_name("reply") + // .aliases(&["rep", "r"]) + // .about("Generates a reply message template") + // .arg(uid_arg()) + // .arg(reply_all_arg()), + // ) + // .subcommand( + // SubCommand::with_name("forward") + // .aliases(&["fwd", "fw", "f"]) + // .about("Generates a forward message template") + // .arg(uid_arg()), + // ), + // ) +} + +pub fn msg_matches(matched: bool, matches: &ArgMatches) -> Result { + if matched { + return Ok(true); + } + + let config = Config::new_from_file()?; + let account = config.find_account_by_name(matches.value_of("account"))?; + let output_fmt = matches.value_of("output").unwrap(); + let mbox = matches.value_of("mailbox").unwrap(); + let mut imap_conn = ImapConnector::new(&account)?; + + let page_size: u32 = matches.value_of("size").unwrap().parse().unwrap(); + let page: u32 = matches.value_of("page").unwrap().parse().unwrap(); + + let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; + let msgs = Msgs::from(&msgs); + + print(&output_fmt, msgs)?; + imap_conn.logout(); + + Ok(false) + // let output_type = matches.value_of("output").unwrap().to_owned(); + // let account = matches.value_of("account"); + // let mbox = matches.value_of("mailbox").unwrap(); + + // if let Some(_) = matches.subcommand_matches("mailboxes") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mboxes = imap_conn.list_mboxes()?; + // print(&output_type, mboxes)?; + + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("flags") { + // flags_matches(account, &mbox, &matches) + // .chain_err(|| "Could not handle flags arg matches")?; + // } + + // if let Some(matches) = matches.subcommand_matches("list") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mbox = matches.value_of("mailbox").unwrap(); + // let page_size: u32 = matches.value_of("size").unwrap().parse().unwrap(); + // let page: u32 = matches.value_of("page").unwrap().parse().unwrap(); + + // let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; + // let msgs = Msgs::from(&msgs); + + // print(&output_type, msgs)?; + + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("search") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // let page_size: usize = matches.value_of("size").unwrap().parse().unwrap(); + // let page: usize = matches.value_of("page").unwrap().parse().unwrap(); + // 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(" "); + + // let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; + // let msgs = Msgs::from(&msgs); + + // print(&output_type, msgs)?; + + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("read") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); + // let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); + + // let msg = imap_conn.read_msg(&mbox, &uid)?; + // let msg = ReadableMsg::from_bytes(&mime, &msg)?; + + // print(&output_type, msg)?; + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("attachments") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); + + // let msg = imap_conn.read_msg(&mbox, &uid)?; + // let attachments = Attachments::from_bytes(&msg)?; + + // match output_type.as_str() { + // "text" => { + // println!( + // "{} attachment(s) found for message {}", + // attachments.0.len(), + // uid + // ); + + // attachments.0.iter().for_each(|attachment| { + // let filepath = config.downloads_filepath(&account, &attachment.filename); + // println!("Downloading {}…", &attachment.filename); + // fs::write(filepath, &attachment.raw).unwrap() + // }); + + // println!("Done!"); + // } + // "json" => { + // attachments.0.iter().for_each(|attachment| { + // let filepath = config.downloads_filepath(&account, &attachment.filename); + // fs::write(filepath, &attachment.raw).unwrap() + // }); + + // print!("{{}}"); + // } + // _ => (), + // } + + // imap_conn.logout(); + // } + + // if let Some(_) = matches.subcommand_matches("write") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + // let tpl = Msg::build_new_tpl(&config, &account)?; + // let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; + // let mut msg = Msg::from(content); + + // loop { + // match input::post_edit_choice() { + // Ok(choice) => match choice { + // input::Choice::Send => { + // println!("Sending…"); + // let msg = msg.to_sendable_msg()?; + // smtp::send(&account, &msg)?; + // imap_conn.append_msg("Sent", &msg.formatted())?; + // println!("Done!"); + // break; + // } + // input::Choice::Draft => { + // println!("Saving to draft…"); + // imap_conn.append_msg("Drafts", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Edit => { + // let content = input::open_editor_with_draft()?; + // msg = Msg::from(content); + // } + // input::Choice::Quit => break, + // }, + // Err(err) => eprintln!("{}", err), + // } + // } + + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("template") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // if let Some(_) = matches.subcommand_matches("new") { + // let tpl = Msg::build_new_tpl(&config, &account)?; + // print(&output_type, &tpl)?; + // } + + // if let Some(matches) = matches.subcommand_matches("reply") { + // let uid = matches.value_of("uid").unwrap(); + // let mbox = matches.value_of("mailbox").unwrap(); + + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = if matches.is_present("reply-all") { + // msg.build_reply_all_tpl(&config, &account)? + // } else { + // msg.build_reply_tpl(&config, &account)? + // }; + + // print(&output_type, &tpl)?; + // } + + // if let Some(matches) = matches.subcommand_matches("forward") { + // let uid = matches.value_of("uid").unwrap(); + // let mbox = matches.value_of("mailbox").unwrap(); + + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = msg.build_forward_tpl(&config, &account)?; + + // print(&output_type, &tpl)?; + // } + // } + + // if let Some(matches) = matches.subcommand_matches("reply") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); + + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = if matches.is_present("reply-all") { + // msg.build_reply_all_tpl(&config, &account)? + // } else { + // msg.build_reply_tpl(&config, &account)? + // }; + + // let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; + // let mut msg = Msg::from(content); + + // loop { + // match input::post_edit_choice() { + // Ok(choice) => match choice { + // input::Choice::Send => { + // println!("Sending…"); + // smtp::send(&account, &msg.to_sendable_msg()?)?; + // imap_conn.append_msg("Sent", &msg.to_vec()?)?; + // imap_conn.add_flags(mbox, uid, "\\Answered")?; + // println!("Done!"); + // break; + // } + // input::Choice::Draft => { + // println!("Saving to draft…"); + // imap_conn.append_msg("Drafts", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Edit => { + // let content = input::open_editor_with_draft()?; + // msg = Msg::from(content); + // } + // input::Choice::Quit => break, + // }, + // Err(err) => eprintln!("{}", err), + // } + // } + + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("forward") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mbox = matches.value_of("mailbox").unwrap(); + // let uid = matches.value_of("uid").unwrap(); + + // let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); + // let tpl = msg.build_forward_tpl(&config, &account)?; + // let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?; + // let mut msg = Msg::from(content); + + // loop { + // match input::post_edit_choice() { + // Ok(choice) => match choice { + // input::Choice::Send => { + // println!("Sending…"); + // smtp::send(&account, &msg.to_sendable_msg()?)?; + // imap_conn.append_msg("Sent", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Draft => { + // println!("Saving to draft…"); + // imap_conn.append_msg("Drafts", &msg.to_vec()?)?; + // println!("Done!"); + // break; + // } + // input::Choice::Edit => { + // let content = input::open_editor_with_draft()?; + // msg = Msg::from(content); + // } + // input::Choice::Quit => break, + // }, + // Err(err) => eprintln!("{}", err), + // } + // } + + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("send") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let msg = matches.value_of("message").unwrap(); + // let msg = Msg::from(msg.to_string()); + // let msg = msg.to_sendable_msg()?; + + // smtp::send(&account, &msg)?; + // imap_conn.append_msg("Sent", &msg.formatted())?; + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("save") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + + // let mbox = matches.value_of("mailbox").unwrap(); + // let msg = matches.value_of("message").unwrap(); + // let msg = Msg::from(msg.to_string()); + + // imap_conn.append_msg(mbox, &msg.to_vec()?)?; + // imap_conn.logout(); + // } + + // if let Some(matches) = matches.subcommand_matches("idle") { + // let config = Config::new_from_file()?; + // let account = config.find_account_by_name(account)?; + // let mut imap_conn = ImapConnector::new(&account)?; + // let mbox = matches.value_of("mailbox").unwrap(); + // imap_conn.idle(&config, &mbox)?; + // } +} diff --git a/src/msg/model.rs b/src/msg/model.rs new file mode 100644 index 0000000..28e76fa --- /dev/null +++ b/src/msg/model.rs @@ -0,0 +1,578 @@ +use error_chain::error_chain; +use lettre; +use mailparse::{self, MailHeaderMap}; +use rfc2047_decoder; +use serde::{ + ser::{self, SerializeStruct}, + Serialize, +}; +use std::{fmt, result}; +use uuid::Uuid; + +use crate::config::{Account, Config}; +use crate::flag::model::{Flag, Flags}; +use crate::table::{self, DisplayRow, DisplayTable}; + +error_chain! { + foreign_links { + Mailparse(mailparse::MailParseError); + Lettre(lettre::error::Error); + } +} + +// Template + +#[derive(Debug)] +pub struct Tpl(String); + +impl fmt::Display for Tpl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Serialize for Tpl { + fn serialize(&self, serializer: S) -> result::Result + where + S: ser::Serializer, + { + let mut state = serializer.serialize_struct("Tpl", 1)?; + state.serialize_field("template", &self.0)?; + state.end() + } +} + +// Attachments + +#[derive(Debug)] +pub struct Attachment { + pub filename: String, + pub raw: Vec, +} + +impl<'a> Attachment { + // TODO: put in common with ReadableMsg + pub fn from_part(part: &'a mailparse::ParsedMail) -> Self { + Self { + filename: part + .get_content_disposition() + .params + .get("filename") + .unwrap_or(&Uuid::new_v4().to_simple().to_string()) + .to_owned(), + raw: part.get_body_raw().unwrap_or_default(), + } + } +} + +#[derive(Debug)] +pub struct Attachments(pub Vec); + +impl<'a> Attachments { + fn extract_from_part(&'a mut self, part: &'a mailparse::ParsedMail) { + if part.subparts.is_empty() { + let ctype = part + .get_headers() + .get_first_value("content-type") + .unwrap_or_default(); + + if !ctype.starts_with("text") { + self.0.push(Attachment::from_part(part)); + } + } else { + part.subparts + .iter() + .for_each(|part| self.extract_from_part(part)); + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let msg = mailparse::parse_mail(bytes)?; + let mut attachments = Self(vec![]); + attachments.extract_from_part(&msg); + Ok(attachments) + } +} + +// Readable message + +#[derive(Debug)] +pub struct ReadableMsg { + pub content: String, + pub has_attachment: bool, +} + +impl Serialize for ReadableMsg { + fn serialize(&self, serializer: S) -> result::Result + where + S: ser::Serializer, + { + let mut state = serializer.serialize_struct("ReadableMsg", 2)?; + state.serialize_field("content", &self.content)?; + state.serialize_field("hasAttachment", if self.has_attachment { &1 } else { &0 })?; + state.end() + } +} + +impl fmt::Display for ReadableMsg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.content) + } +} + +impl<'a> ReadableMsg { + fn flatten_parts(part: &'a mailparse::ParsedMail) -> Vec<&'a mailparse::ParsedMail<'a>> { + if part.subparts.is_empty() { + vec![part] + } else { + part.subparts + .iter() + .flat_map(Self::flatten_parts) + .collect::>() + } + } + + pub fn from_bytes(mime: &str, bytes: &[u8]) -> Result { + let msg = mailparse::parse_mail(bytes)?; + let (text_part, html_part, has_attachment) = Self::flatten_parts(&msg).into_iter().fold( + (None, None, false), + |(mut text_part, mut html_part, mut has_attachment), part| { + let ctype = part + .get_headers() + .get_first_value("content-type") + .unwrap_or_default(); + + if text_part.is_none() && ctype.starts_with("text/plain") { + text_part = part.get_body().ok(); + } else { + if html_part.is_none() && ctype.starts_with("text/html") { + html_part = part.get_body().ok(); + } else { + has_attachment = true + }; + }; + + (text_part, html_part, has_attachment) + }, + ); + + let content = if mime == "text/plain" { + text_part.or(html_part).unwrap_or_default() + } else { + html_part.or(text_part).unwrap_or_default() + }; + + Ok(Self { + content, + has_attachment, + }) + } +} + +// Message + +// #[derive(Debug, Serialize, PartialEq)] +// #[serde(rename_all = "lowercase")] +// pub enum Flag { +// Seen, +// Answered, +// Flagged, +// } + +// impl Flag { +// fn from_imap_flag(flag: &imap::types::Flag<'_>) -> Option { +// match flag { +// imap::types::Flag::Seen => Some(Self::Seen), +// imap::types::Flag::Answered => Some(Self::Answered), +// imap::types::Flag::Flagged => Some(Self::Flagged), +// _ => None, +// } +// } +// } + +#[derive(Debug, Serialize)] +pub struct Msg<'m> { + pub uid: u32, + pub flags: Flags<'m>, + pub subject: String, + pub sender: String, + pub date: String, + + #[serde(skip_serializing)] + raw: Vec, +} + +impl<'m> From> for Msg<'m> { + fn from(raw: Vec) -> Self { + Self { + uid: 0, + flags: Flags::new(&[]), + subject: String::from(""), + sender: String::from(""), + date: String::from(""), + raw, + } + } +} + +impl<'m> From for Msg<'m> { + fn from(raw: String) -> Self { + Self::from(raw.as_bytes().to_vec()) + } +} + +impl<'m> From<&'m imap::types::Fetch> for Msg<'m> { + fn from(fetch: &'m imap::types::Fetch) -> Self { + match fetch.envelope() { + None => Self::from(fetch.body().unwrap_or_default().to_vec()), + Some(envelope) => Self { + uid: fetch.uid.unwrap_or_default(), + flags: Flags::new(fetch.flags()), + subject: envelope + .subject + .and_then(|subj| rfc2047_decoder::decode(subj).ok()) + .unwrap_or_default(), + sender: envelope + .from + .as_ref() + .and_then(|addrs| addrs.first()?.name) + .and_then(|name| rfc2047_decoder::decode(name).ok()) + .unwrap_or_default(), + date: fetch + .internal_date() + .map(|date| date.naive_local().to_string()) + .unwrap_or_default(), + raw: fetch.body().unwrap_or_default().to_vec(), + }, + } + } +} + +impl<'m> Msg<'m> { + pub fn parse(&'m self) -> Result> { + Ok(mailparse::parse_mail(&self.raw)?) + } + + pub fn to_vec(&self) -> Result> { + let parsed = self.parse()?; + let headers = parsed.get_headers().get_raw_bytes().to_vec(); + let sep = "\r\n".as_bytes().to_vec(); + let body = parsed.get_body()?.as_bytes().to_vec(); + + Ok(vec![headers, sep, body].concat()) + } + + pub fn to_sendable_msg(&self) -> Result { + use lettre::message::header::{ContentTransferEncoding, ContentType}; + use lettre::message::{Message, SinglePart}; + + let parsed = self.parse()?; + let msg = parsed + .headers + .iter() + .fold(Message::builder(), |msg, h| { + let value = String::from_utf8(h.get_value_raw().to_vec()) + .unwrap() + .replace("\r", ""); + + match h.get_key().to_lowercase().as_str() { + "in-reply-to" => msg.in_reply_to(value.parse().unwrap()), + "from" => match value.parse() { + Ok(addr) => msg.from(addr), + Err(_) => msg, + }, + "to" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.to(addr), + Err(_) => msg, + }), + "cc" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.cc(addr), + Err(_) => msg, + }), + "bcc" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.bcc(addr), + Err(_) => msg, + }), + "subject" => msg.subject(value), + _ => msg, + } + }) + .singlepart( + SinglePart::builder() + .header(ContentType("text/plain; charset=utf-8".parse().unwrap())) + .header(ContentTransferEncoding::Base64) + .body(parsed.get_body_raw()?), + )?; + + Ok(msg) + } + + fn extract_text_bodies_into(part: &mailparse::ParsedMail, mime: &str, parts: &mut Vec) { + match part.subparts.len() { + 0 => { + let content_type = part + .get_headers() + .get_first_value("content-type") + .unwrap_or_default(); + + if content_type.starts_with(mime) { + parts.push(part.get_body().unwrap_or_default()) + } + } + _ => { + part.subparts + .iter() + .for_each(|part| Self::extract_text_bodies_into(part, mime, parts)); + } + } + } + + fn extract_text_bodies(&self, mime: &str) -> Result> { + let mut parts = vec![]; + Self::extract_text_bodies_into(&self.parse()?, mime, &mut parts); + Ok(parts) + } + + pub fn text_bodies(&self, mime: &str) -> Result { + let text_bodies = self.extract_text_bodies(mime)?; + Ok(text_bodies.join("\r\n")) + } + + pub fn build_new_tpl(config: &Config, account: &Account) -> Result { + let mut tpl = vec![]; + + // "Content" headers + tpl.push("Content-Type: text/plain; charset=utf-8".to_string()); + tpl.push("Content-Transfer-Encoding: 8bit".to_string()); + + // "From" header + tpl.push(format!("From: {}", config.address(account))); + + // "To" header + tpl.push("To: ".to_string()); + + // "Subject" header + tpl.push("Subject: ".to_string()); + + Ok(Tpl(tpl.join("\r\n"))) + } + + pub fn build_reply_tpl(&self, config: &Config, account: &Account) -> Result { + let msg = &self.parse()?; + let headers = msg.get_headers(); + let mut tpl = vec![]; + + // "Content" headers + tpl.push("Content-Type: text/plain; charset=utf-8".to_string()); + tpl.push("Content-Transfer-Encoding: 8bit".to_string()); + + // "From" header + tpl.push(format!("From: {}", config.address(account))); + + // "In-Reply-To" header + if let Some(msg_id) = headers.get_first_value("message-id") { + tpl.push(format!("In-Reply-To: {}", msg_id)); + } + + // "To" header + let to = headers + .get_first_value("reply-to") + .or(headers.get_first_value("from")) + .unwrap_or(String::new()); + tpl.push(format!("To: {}", to)); + + // "Subject" header + let subject = headers.get_first_value("subject").unwrap_or(String::new()); + tpl.push(format!("Subject: Re: {}", subject)); + + // Separator between headers and body + tpl.push(String::new()); + + // Original msg prepend with ">" + let thread = self + .text_bodies("text/plain")? + .replace("\r", "") + .split("\n") + .map(|line| format!(">{}", line)) + .collect::>() + .join("\r\n"); + tpl.push(thread); + + Ok(Tpl(tpl.join("\r\n"))) + } + + pub fn build_reply_all_tpl(&self, config: &Config, account: &Account) -> Result { + let msg = &self.parse()?; + let headers = msg.get_headers(); + let mut tpl = vec![]; + + // "Content" headers + tpl.push("Content-Type: text/plain; charset=utf-8".to_string()); + tpl.push("Content-Transfer-Encoding: 8bit".to_string()); + + // "From" header + tpl.push(format!("From: {}", config.address(account))); + + // "In-Reply-To" header + if let Some(msg_id) = headers.get_first_value("message-id") { + tpl.push(format!("In-Reply-To: {}", msg_id)); + } + + // "To" header + // All addresses coming from original "To" … + let email: lettre::Address = account.email.parse().unwrap(); + let to = headers + .get_all_values("to") + .iter() + .flat_map(|addrs| addrs.split(",")) + .fold(vec![], |mut mboxes, addr| { + match addr.trim().parse::() { + Err(_) => mboxes, + Ok(mbox) => { + // … except current user's one (from config) … + if mbox.email != email { + mboxes.push(mbox.to_string()); + } + mboxes + } + } + }); + // … and the ones coming from either "Reply-To" or "From" + let reply_to = headers + .get_all_values("reply-to") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>(); + let reply_to = if reply_to.is_empty() { + headers + .get_all_values("from") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>() + } else { + reply_to + }; + tpl.push(format!("To: {}", vec![reply_to, to].concat().join(", "))); + + // "Cc" header + let cc = headers + .get_all_values("cc") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>(); + if !cc.is_empty() { + tpl.push(format!("Cc: {}", cc.join(", "))); + } + + // "Subject" header + let subject = headers.get_first_value("subject").unwrap_or(String::new()); + tpl.push(format!("Subject: Re: {}", subject)); + + // Separator between headers and body + tpl.push(String::new()); + + // Original msg prepend with ">" + let thread = self + .text_bodies("text/plain")? + .split("\r\n") + .map(|line| format!(">{}", line)) + .collect::>() + .join("\r\n"); + tpl.push(thread); + + Ok(Tpl(tpl.join("\r\n"))) + } + + pub fn build_forward_tpl(&self, config: &Config, account: &Account) -> Result { + let msg = &self.parse()?; + let headers = msg.get_headers(); + let mut tpl = vec![]; + + // "Content" headers + tpl.push("Content-Type: text/plain; charset=utf-8".to_string()); + tpl.push("Content-Transfer-Encoding: 8bit".to_string()); + + // "From" header + tpl.push(format!("From: {}", config.address(account))); + + // "To" header + tpl.push("To: ".to_string()); + + // "Subject" header + let subject = headers.get_first_value("subject").unwrap_or(String::new()); + tpl.push(format!("Subject: Fwd: {}", subject)); + + // Separator between headers and body + tpl.push(String::new()); + + // Original msg + tpl.push("-------- Forwarded Message --------".to_string()); + tpl.push(self.text_bodies("text/plain")?); + + Ok(Tpl(tpl.join("\r\n"))) + } +} + +impl<'m> DisplayRow for Msg<'m> { + fn to_row(&self) -> Vec { + use crate::table::*; + + let unseen = if self.flags.contains(&Flag::Seen) { + RESET + } else { + BOLD + }; + + vec![ + Cell::new(&[unseen.to_owned(), RED], &self.uid.to_string()), + Cell::new(&[unseen.to_owned(), WHITE], &self.flags.to_string()), + FlexCell::new(&[unseen.to_owned(), GREEN], &self.subject), + Cell::new(&[unseen.to_owned(), BLUE], &self.sender), + Cell::new(&[unseen.to_owned(), YELLOW], &self.date), + ] + } +} + +// Msgs + +#[derive(Debug, Serialize)] +pub struct Msgs<'m>(pub Vec>); + +impl<'m> DisplayTable<'m, Msg<'m>> for Msgs<'m> { + fn header_row() -> Vec { + use crate::table::*; + + vec![ + Cell::new(&[BOLD, UNDERLINE, WHITE], "UID"), + Cell::new(&[BOLD, UNDERLINE, WHITE], "FLAGS"), + FlexCell::new(&[BOLD, UNDERLINE, WHITE], "SUBJECT"), + Cell::new(&[BOLD, UNDERLINE, WHITE], "SENDER"), + Cell::new(&[BOLD, UNDERLINE, WHITE], "DATE"), + ] + } + + fn rows(&self) -> &Vec> { + &self.0 + } +} + +impl<'m> From<&'m imap::types::ZeroCopy>> for Msgs<'m> { + fn from(fetches: &'m imap::types::ZeroCopy>) -> Self { + Self(fetches.iter().map(Msg::from).collect::>()) + } +} + +impl<'m> fmt::Display for Msgs<'m> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\n{}", self.to_table()) + } +}