From 2acd5d71d349144dabcbfcffbd3f60f85c8711c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 4 Aug 2021 21:42:59 +0200 Subject: [PATCH] add basic support of xdg-email (#162) --- Cargo.lock | 29 ++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 22 +++++++++++++++++--- src/msg/cli.rs | 45 +++++++++++++++++++++++++++++++++++++++++ src/msg/tpl/model.rs | 48 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c21625..deeb217 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,6 +263,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "funty" version = "1.1.0" @@ -322,6 +332,7 @@ dependencies = [ "toml", "tree_magic", "unicode-width", + "url", "uuid", ] @@ -674,6 +685,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "petgraph" version = "0.5.1" @@ -1069,6 +1086,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "uuid" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 2c81e04..01d81bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,5 @@ terminal_size = "0.1.15" toml = "0.5.8" tree_magic = "0.2.3" unicode-width = "0.1.7" +url = "2.2.2" uuid = {version = "0.8", features = ["v4"]} diff --git a/src/main.rs b/src/main.rs index 9f0afd6..8cb1252 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,18 @@ -use clap; +use clap::{self, ArgMatches}; use env_logger; use error_chain::error_chain; use log::{debug, error, trace}; use std::{env, path::PathBuf, process::exit}; +use url::{self, Url}; use himalaya::{ - ctx::Ctx, comp::cli::{comp_matches, comp_subcmds}, config::{cli::config_args, model::Config}, + ctx::Ctx, flag::cli::{flag_matches, flag_subcmds}, imap::cli::{imap_matches, imap_subcmds}, mbox::cli::{mbox_matches, mbox_source_arg, mbox_subcmds}, - msg::cli::{msg_matches, msg_subcmds}, + msg::cli::{msg_matches, msg_matches_mailto, msg_subcmds}, output::{cli::output_args, model::Output}, }; @@ -24,6 +25,9 @@ error_chain! { MboxCli(himalaya::mbox::cli::Error, himalaya::mbox::cli::ErrorKind); MsgCli(himalaya::msg::cli::Error, himalaya::msg::cli::ErrorKind); } + foreign_links { + Url(url::ParseError); + } } fn parse_args<'a>() -> clap::App<'a, 'a> { @@ -47,6 +51,18 @@ fn run() -> Result<()> { env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "off"), ); + 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)?; + let output = Output::new("plain"); + let mbox = "INBOX"; + let arg_matches = ArgMatches::default(); + let app = Ctx::new(&config, &account, &output, &mbox, &arg_matches); + let url = Url::parse(&raw_args[1])?; + return Ok(msg_matches_mailto(&app, &url)?); + } + let args = parse_args(); let arg_matches = args.get_matches(); diff --git a/src/msg/cli.rs b/src/msg/cli.rs index e66f2c1..3b4ebbf 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -7,6 +7,7 @@ use std::{ io::{self, BufRead}, ops::Deref, }; +use url::Url; use crate::{ ctx::Ctx, @@ -594,3 +595,47 @@ fn msg_matches_save(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { imap_conn.logout(); Ok(true) } + +pub fn msg_matches_mailto(ctx: &Ctx, url: &Url) -> Result<()> { + debug!("mailto command matched"); + + let mut imap_conn = ImapConnector::new(&ctx.account)?; + let tpl = Tpl::mailto(&ctx, &url); + 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::PostEditChoice::Send => { + debug!("sending message…"); + let msg = msg.to_sendable_msg()?; + smtp::send(&ctx.account, &msg)?; + imap_conn.append_msg("Sent", &msg.formatted(), vec![Flag::Seen])?; + input::remove_draft()?; + ctx.output.print("Message successfully sent"); + break; + } + input::PostEditChoice::Edit => { + let content = input::open_editor_with_draft()?; + msg = Msg::from(content); + } + input::PostEditChoice::LocalDraft => break, + input::PostEditChoice::RemoteDraft => { + debug!("saving to draft…"); + imap_conn.append_msg("Drafts", &msg.to_vec()?, vec![Flag::Seen])?; + input::remove_draft()?; + ctx.output.print("Message successfully saved to Drafts"); + break; + } + input::PostEditChoice::Discard => { + input::remove_draft()?; + break; + } + }, + Err(err) => error!("{}", err), + } + } + imap_conn.logout(); + Ok(()) +} diff --git a/src/msg/tpl/model.rs b/src/msg/tpl/model.rs index 29b7cbf..27c2977 100644 --- a/src/msg/tpl/model.rs +++ b/src/msg/tpl/model.rs @@ -1,7 +1,8 @@ use error_chain::error_chain; use mailparse::{self, MailHeaderMap}; use serde::Serialize; -use std::{collections::HashMap, fmt}; +use std::{borrow::Cow, collections::HashMap, fmt}; +use url::Url; use crate::{ctx::Ctx, msg::model::Msg}; @@ -187,6 +188,51 @@ impl Tpl { tpl } + pub fn mailto(ctx: &Ctx, url: &Url) -> Self { + let mut headers = HashMap::new(); + + 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); + } + b"bcc" => { + bcc.push(val); + } + b"subject" => { + subject = val; + } + b"body" => { + body = val; + } + _ => (), + } + } + + headers.insert(String::from("To"), url.path().to_string()); + headers.insert(String::from("Subject"), subject.into()); + if !cc.is_empty() { + headers.insert(String::from("Cc"), cc.join(", ")); + } + if !bcc.is_empty() { + headers.insert(String::from("Bcc"), cc.join(", ")); + } + + let mut tpl = Self { + headers, + body: Some(body.into()), + signature: ctx.config.signature(&ctx.account), + raw: String::new(), + }; + tpl.raw = tpl.to_string(); + tpl + } + pub fn header(&mut self, key: K, val: V) -> &Self { self.headers.insert(key.to_string(), val.to_string()); self