From 9ce62c735a80a5168c9d5982fe8ae9ac0e250fe0 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 11 Oct 2020 00:32:55 +0300 Subject: [PATCH] compose: add key selection state for gpg operations Closes #81 --- config_macros.rs | 10 + docs/meli.conf.5 | 5 - melib/src/conf.rs | 21 +- melib/src/gpgme/bindings.rs | 54 +-- melib/src/gpgme/mod.rs | 331 +++++++++++++-- samples/sample-config.toml | 1 - src/components/mail.rs | 1 + src/components/mail/compose.rs | 609 +++++++++++++++++++++++----- src/components/mail/pgp.rs | 270 ++---------- src/components/mail/view.rs | 270 ++++-------- src/components/utilities/widgets.rs | 4 + src/conf/overrides.rs | 42 +- src/conf/pgp.rs | 47 ++- 13 files changed, 1062 insertions(+), 603 deletions(-) diff --git a/config_macros.rs b/config_macros.rs index a70fd1e34..e4c3ca5c1 100644 --- a/config_macros.rs +++ b/config_macros.rs @@ -87,6 +87,14 @@ use super::*; } let override_ident: syn::Ident = format_ident!("{}Override", s.ident); let mut field_tokentrees = vec![]; + let mut attrs_tokens = vec![]; + for attr in &s.attrs { + if let Ok(syn::Meta::List(ml)) = attr.parse_meta() { + if ml.path.get_ident().is_some() && ml.path.get_ident().unwrap() == "cfg" { + attrs_tokens.push(attr); + } + } + } let mut field_idents = vec![]; for f in &s.fields { let ident = &f.ident; @@ -146,6 +154,7 @@ use super::*; //let fields = &s.fields; let literal_struct = quote! { + #(#attrs_tokens)* #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct #override_ident { @@ -153,6 +162,7 @@ use super::*; } + #(#attrs_tokens)* impl Default for #override_ident { fn default() -> Self { #override_ident { diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index 174bec26e..f42477ff5 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -892,11 +892,6 @@ Always sign sent messages Key to be used when signing/encrypting (not functional yet) .\" default value .Pq Em none -.It Ic gpg_binary Ar String -.Pq Em optional -The gpg binary name or file location to use -.\" default value -.Pq Em "gpg2" .El .Sh TERMINAL .Bl -tag -width 36n diff --git a/melib/src/conf.rs b/melib/src/conf.rs index a78cbc14d..0395dd4d5 100644 --- a/melib/src/conf.rs +++ b/melib/src/conf.rs @@ -128,6 +128,7 @@ pub enum ToggleFlag { InternalVal(bool), False, True, + Ask, } impl From for ToggleFlag { @@ -157,9 +158,15 @@ impl ToggleFlag { false } } + + pub fn is_ask(&self) -> bool { + *self == ToggleFlag::Ask + } + pub fn is_false(&self) -> bool { ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self } + pub fn is_true(&self) -> bool { ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self } @@ -174,6 +181,7 @@ impl Serialize for ToggleFlag { ToggleFlag::Unset | ToggleFlag::InternalVal(_) => serializer.serialize_none(), ToggleFlag::False => serializer.serialize_bool(false), ToggleFlag::True => serializer.serialize_bool(true), + ToggleFlag::Ask => serializer.serialize_str("ask"), } } } @@ -183,10 +191,17 @@ impl<'de> Deserialize<'de> for ToggleFlag { where D: Deserializer<'de>, { - let s = ::deserialize(deserializer); + let s = ::deserialize(deserializer); Ok(match s? { - true => ToggleFlag::True, - false => ToggleFlag::False, + s if s.eq_ignore_ascii_case("true") => ToggleFlag::True, + s if s.eq_ignore_ascii_case("false") => ToggleFlag::False, + s if s.eq_ignore_ascii_case("ask") => ToggleFlag::Ask, + s => { + return Err(serde::de::Error::custom(format!( + r#"expected one of "true", "false", "ask", found `{}`"#, + s + ))) + } }) } } diff --git a/melib/src/gpgme/bindings.rs b/melib/src/gpgme/bindings.rs index 390e3bc75..49273015a 100644 --- a/melib/src/gpgme/bindings.rs +++ b/melib/src/gpgme/bindings.rs @@ -7220,12 +7220,8 @@ extern "C" { extern "C" { pub fn gpgme_get_protocol_name(proto: gpgme_protocol_t) -> *const ::std::os::raw::c_char; } -extern "C" { - pub fn gpgme_set_armor(ctx: gpgme_ctx_t, yes: ::std::os::raw::c_int); -} -extern "C" { - pub fn gpgme_get_armor(ctx: gpgme_ctx_t) -> ::std::os::raw::c_int; -} +pub type gpgme_set_armor = unsafe extern "C" fn(ctx: gpgme_ctx_t, yes: ::std::os::raw::c_int); +pub type gpgme_get_armor = unsafe extern "C" fn(ctx: gpgme_ctx_t) -> ::std::os::raw::c_int; extern "C" { pub fn gpgme_set_textmode(ctx: gpgme_ctx_t, yes: ::std::os::raw::c_int); } @@ -7309,12 +7305,10 @@ extern "C" { home_dir: *const ::std::os::raw::c_char, ) -> gpgme_error_t; } -extern "C" { - pub fn gpgme_signers_clear(ctx: gpgme_ctx_t); -} -extern "C" { - pub fn gpgme_signers_add(ctx: gpgme_ctx_t, key: gpgme_key_t) -> gpgme_error_t; -} +pub type gpgme_signers_clear = unsafe extern "C" fn(ctx: gpgme_ctx_t); + +pub type gpgme_signers_add = + unsafe extern "C" fn(ctx: gpgme_ctx_t, key: gpgme_key_t) -> gpgme_error_t; extern "C" { pub fn gpgme_signers_count(ctx: gpgme_ctx_t) -> ::std::os::raw::c_uint; } @@ -7763,9 +7757,7 @@ fn bindgen_test_layout__gpgme_op_encrypt_result() { ); } pub type gpgme_encrypt_result_t = *mut _gpgme_op_encrypt_result; -extern "C" { - pub fn gpgme_op_encrypt_result(ctx: gpgme_ctx_t) -> gpgme_encrypt_result_t; -} +pub type gpgme_op_encrypt_result = unsafe extern "C" fn(ctx: gpgme_ctx_t) -> gpgme_encrypt_result_t; pub const gpgme_encrypt_flags_t_GPGME_ENCRYPT_ALWAYS_TRUST: gpgme_encrypt_flags_t = 1; pub const gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_ENCRYPT_TO: gpgme_encrypt_flags_t = 2; pub const gpgme_encrypt_flags_t_GPGME_ENCRYPT_PREPARE: gpgme_encrypt_flags_t = 4; @@ -7776,15 +7768,13 @@ pub const gpgme_encrypt_flags_t_GPGME_ENCRYPT_THROW_KEYIDS: gpgme_encrypt_flags_ pub const gpgme_encrypt_flags_t_GPGME_ENCRYPT_WRAP: gpgme_encrypt_flags_t = 128; pub const gpgme_encrypt_flags_t_GPGME_ENCRYPT_WANT_ADDRESS: gpgme_encrypt_flags_t = 256; pub type gpgme_encrypt_flags_t = u32; -extern "C" { - pub fn gpgme_op_encrypt_start( - ctx: gpgme_ctx_t, - recp: *mut gpgme_key_t, - flags: gpgme_encrypt_flags_t, - plain: gpgme_data_t, - cipher: gpgme_data_t, - ) -> gpgme_error_t; -} +pub type gpgme_op_encrypt_start = unsafe extern "C" fn( + ctx: gpgme_ctx_t, + recp: *mut gpgme_key_t, + flags: gpgme_encrypt_flags_t, + plain: gpgme_data_t, + cipher: gpgme_data_t, +) -> gpgme_error_t; extern "C" { pub fn gpgme_op_encrypt( ctx: gpgme_ctx_t, @@ -7814,15 +7804,13 @@ extern "C" { cipher: gpgme_data_t, ) -> gpgme_error_t; } -extern "C" { - pub fn gpgme_op_encrypt_sign_start( - ctx: gpgme_ctx_t, - recp: *mut gpgme_key_t, - flags: gpgme_encrypt_flags_t, - plain: gpgme_data_t, - cipher: gpgme_data_t, - ) -> gpgme_error_t; -} +pub type gpgme_op_encrypt_sign_start = unsafe extern "C" fn( + ctx: gpgme_ctx_t, + recp: *mut gpgme_key_t, + flags: gpgme_encrypt_flags_t, + plain: gpgme_data_t, + cipher: gpgme_data_t, +) -> gpgme_error_t; extern "C" { pub fn gpgme_op_encrypt_sign( ctx: gpgme_ctx_t, diff --git a/melib/src/gpgme/mod.rs b/melib/src/gpgme/mod.rs index e7d334d6f..b06746196 100644 --- a/melib/src/gpgme/mod.rs +++ b/melib/src/gpgme/mod.rs @@ -25,11 +25,16 @@ use crate::email::{ }; use crate::error::{ErrorKind, IntoMeliError, MeliError, Result, ResultIntoMeliError}; use futures::FutureExt; +use serde::{ + de::{self, Deserialize}, + Deserializer, Serialize, Serializer, +}; use smol::Async; use std::borrow::Cow; use std::collections::HashMap; use std::ffi::{CStr, CString, OsStr}; use std::future::Future; +use std::io::Seek; use std::os::unix::ffi::OsStrExt; use std::os::unix::io::{AsRawFd, RawFd}; use std::path::Path; @@ -66,6 +71,7 @@ pub enum GpgmeFlag { ///"auto-key-retrieve" AutoKeyRetrieve, OfflineMode, + AsciiArmor, } bitflags! { @@ -91,6 +97,85 @@ bitflags! { } } +impl<'de> Deserialize<'de> for LocateKey { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + if let Ok(s) = ::deserialize(deserializer) { + LocateKey::from_string_de::<'de, D, String>(s) + } else { + Err(de::Error::custom("LocateKey value must be a string.")) + } + } +} + +impl Serialize for LocateKey { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl LocateKey { + pub fn from_string_de<'de, D, T: AsRef>(s: T) -> std::result::Result + where + D: Deserializer<'de>, + { + Ok(match s.as_ref().trim() { + s if s.eq_ignore_ascii_case("cert") => LocateKey::CERT, + s if s.eq_ignore_ascii_case("pka") => LocateKey::PKA, + s if s.eq_ignore_ascii_case("dane") => LocateKey::DANE, + s if s.eq_ignore_ascii_case("wkd") => LocateKey::WKD, + s if s.eq_ignore_ascii_case("ldap") => LocateKey::LDAP, + s if s.eq_ignore_ascii_case("keyserver") => LocateKey::KEYSERVER, + s if s.eq_ignore_ascii_case("keyserver-url") => LocateKey::KEYSERVER_URL, + s if s.eq_ignore_ascii_case("local") => LocateKey::LOCAL, + combination if combination.contains(",") => { + let mut ret = LocateKey::NODEFAULT; + for c in combination.trim().split(",") { + ret |= Self::from_string_de::<'de, D, &str>(c.trim())?; + } + ret + } + _ => { + return Err(de::Error::custom( + r#"Takes valid auto-key-locate GPG values: "cert", "pka", "dane", "wkd", "ldap", "keyserver", "keyserver-URL", "local", "nodefault""#, + )) + } + }) + } +} + +impl std::fmt::Display for LocateKey { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + if *self == LocateKey::NODEFAULT { + write!(fmt, "clear,nodefault") + } else { + let mut accum = String::new(); + macro_rules! is_set { + ($flag:expr, $string:literal) => {{ + if self.intersects($flag) { + accum.push_str($string); + accum.push_str(","); + } + }}; + } + is_set!(LocateKey::CERT, "cert"); + is_set!(LocateKey::PKA, "pka"); + is_set!(LocateKey::WKD, "wkd"); + is_set!(LocateKey::LDAP, "ldap"); + is_set!(LocateKey::KEYSERVER, "keyserver"); + is_set!(LocateKey::KEYSERVER_URL, "keyserver-url"); + is_set!(LocateKey::LOCAL, "local"); + accum.pop(); + write!(fmt, "{}", accum) + } + } +} + struct IoState { max_idx: usize, ops: HashMap, @@ -187,6 +272,7 @@ impl Context { }; ret.set_flag(GpgmeFlag::AutoKeyRetrieve, false)?; ret.set_flag(GpgmeFlag::OfflineMode, true)?; + ret.set_flag(GpgmeFlag::AsciiArmor, true)?; ret.set_auto_key_locate(LocateKey::LOCAL)?; Ok(ret) } @@ -221,12 +307,21 @@ impl Context { }; return Ok(()); } + GpgmeFlag::AsciiArmor => { + unsafe { + call!(&self.inner.lib, gpgme_set_armor)( + self.inner.inner.as_ptr(), + if value { 1 } else { 0 }, + ); + }; + return Ok(()); + } }; const VALUE_ON: &[u8; 2] = b"1\0"; const VALUE_OFF: &[u8; 2] = b"0\0"; let raw_flag = match flag { GpgmeFlag::AutoKeyRetrieve => c_string_literal!("auto-key-retrieve"), - GpgmeFlag::OfflineMode => unreachable!(), + GpgmeFlag::AsciiArmor | GpgmeFlag::OfflineMode => unreachable!(), }; self.set_flag_inner( raw_flag, @@ -249,6 +344,11 @@ impl Context { call!(&self.inner.lib, gpgme_get_offline)(self.inner.inner.as_ptr()) > 0 }); } + GpgmeFlag::AsciiArmor => { + return Ok(unsafe { + call!(&self.inner.lib, gpgme_get_armor)(self.inner.inner.as_ptr()) > 0 + }); + } }; let val = self.get_flag_inner(raw_flag); Ok(!val.is_null()) @@ -259,23 +359,7 @@ impl Context { if val == LocateKey::NODEFAULT { self.set_flag_inner(auto_key_locate, c_string_literal!("clear,nodefault")) } else { - let mut accum = String::new(); - macro_rules! is_set { - ($flag:expr, $string:literal) => {{ - if val.intersects($flag) { - accum.push_str($string); - accum.push_str(","); - } - }}; - } - is_set!(LocateKey::CERT, "cert"); - is_set!(LocateKey::PKA, "pka"); - is_set!(LocateKey::WKD, "wkd"); - is_set!(LocateKey::LDAP, "ldap"); - is_set!(LocateKey::KEYSERVER, "keyserver"); - is_set!(LocateKey::KEYSERVER_URL, "keyserver-url"); - is_set!(LocateKey::LOCAL, "local"); - accum.pop(); + let mut accum = val.to_string(); accum.push('\0'); self.set_flag_inner( auto_key_locate, @@ -592,11 +676,34 @@ impl Context { }) } - pub fn sign<'d>( + pub fn sign( &mut self, - text: &'d mut Data, - ) -> Result> + 'd> { - let sig = std::ptr::null_mut(); + sign_keys: Vec, + mut text: Data, + ) -> Result>>> { + if sign_keys.is_empty() { + return Err( + MeliError::new("gpgme: Call to sign() with zero keys.").set_kind(ErrorKind::Bug) + ); + } + let mut sig: gpgme_data_t = std::ptr::null_mut(); + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_data_new)(&mut sig), + )?; + call!(&self.inner.lib, gpgme_signers_clear)(self.inner.inner.as_ptr()); + for k in sign_keys { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_signers_add)( + self.inner.inner.as_ptr(), + k.inner.inner.as_ptr(), + ), + )?; + } + } + unsafe { gpgme_error_try( &self.inner.lib, @@ -608,6 +715,13 @@ impl Context { ), )?; } + let mut sig = Data { + lib: self.inner.lib.clone(), + kind: DataKind::Memory, + inner: core::ptr::NonNull::new(sig).ok_or_else(|| { + MeliError::new("internal libgpgme error").set_kind(ErrorKind::Bug) + })?, + }; let io_state = self.io_state.clone(); let io_state_lck = self.io_state.lock().unwrap(); @@ -672,13 +786,17 @@ impl Context { }; let _ = rcv.recv().await; let io_state_lck = io_state.lock().unwrap(); - let ret = io_state_lck + io_state_lck .done .lock() .unwrap() .take() - .unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error"))); - ret + .unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")))?; + sig.seek(std::io::SeekFrom::Start(0)) + .chain_err_summary(|| { + "libgpgme error: could not perform seek on signature data object" + })?; + Ok(sig.into_bytes()?) }) } @@ -830,7 +948,6 @@ impl Context { recipient_iter = (*recipient_iter).next; } } - use std::io::Seek; /* Rewind cursor */ plain .seek(std::io::SeekFrom::Start(0)) @@ -846,6 +963,168 @@ impl Context { )) }) } + + pub fn encrypt( + &mut self, + sign_keys: Option>, + encrypt_keys: Vec, + mut plain: Data, + ) -> Result>> + Send> { + if encrypt_keys.is_empty() { + return Err( + MeliError::new("gpgme: Call to encrypt() with zero keys.").set_kind(ErrorKind::Bug) + ); + } + unsafe { + call!(&self.inner.lib, gpgme_signers_clear)(self.inner.inner.as_ptr()); + } + + let also_sign: bool = if let Some(keys) = sign_keys { + if keys.is_empty() { + false + } else { + for k in keys { + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_signers_add)( + self.inner.inner.as_ptr(), + k.inner.inner.as_ptr(), + ), + )?; + } + } + true + } + } else { + false + }; + let mut cipher: gpgme_data_t = std::ptr::null_mut(); + let mut raw_keys: Vec = Vec::with_capacity(encrypt_keys.len() + 1); + raw_keys.extend(encrypt_keys.iter().map(|k| k.inner.inner.as_ptr())); + raw_keys.push(std::ptr::null_mut()); + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_data_new)(&mut cipher), + )?; + gpgme_error_try( + &self.inner.lib, + if also_sign { + call!(&self.inner.lib, gpgme_op_encrypt_sign_start)( + self.inner.inner.as_ptr(), + raw_keys.as_mut_ptr(), + gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_ENCRYPT_TO + | gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_COMPRESS, + plain.inner.as_mut(), + cipher, + ) + } else { + call!(&self.inner.lib, gpgme_op_encrypt_start)( + self.inner.inner.as_ptr(), + raw_keys.as_mut_ptr(), + gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_ENCRYPT_TO + | gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_COMPRESS, + plain.inner.as_mut(), + cipher, + ) + }, + )?; + } + let mut cipher = Data { + lib: self.inner.lib.clone(), + kind: DataKind::Memory, + inner: core::ptr::NonNull::new(cipher).ok_or_else(|| { + MeliError::new("internal libgpgme error").set_kind(ErrorKind::Bug) + })?, + }; + + let ctx = self.inner.clone(); + let io_state = self.io_state.clone(); + let io_state_lck = self.io_state.lock().unwrap(); + let done = io_state_lck.done.clone(); + let fut = io_state_lck + .ops + .values() + .map(|a| Async::new(a.clone()).unwrap()) + .collect::>>(); + drop(io_state_lck); + Ok(async move { + futures::future::join_all(fut.iter().map(|fut| { + let done = done.clone(); + if fut.get_ref().write { + futures::future::select( + fut.get_ref().receiver.recv().boxed(), + fut.write_with(move |_f| { + if done.lock().unwrap().is_some() { + return Ok(()); + } + unsafe { + (fut.get_ref().fnc.unwrap())( + fut.get_ref().fnc_data, + fut.get_ref().fd, + ) + }; + if done.lock().unwrap().is_none() { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + Ok(()) + }) + .boxed(), + ) + .boxed() + } else { + futures::future::select( + fut.get_ref().receiver.recv().boxed(), + fut.read_with(move |_f| { + if done.lock().unwrap().is_some() { + return Ok(()); + } + unsafe { + (fut.get_ref().fnc.unwrap())( + fut.get_ref().fnc_data, + fut.get_ref().fd, + ) + }; + if done.lock().unwrap().is_none() { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + Ok(()) + }) + .boxed(), + ) + .boxed() + } + })) + .await; + let rcv = { + let io_state_lck = io_state.lock().unwrap(); + io_state_lck.receiver.clone() + }; + let _ = rcv.recv().await; + let io_state_lck = io_state.lock().unwrap(); + io_state_lck + .done + .lock() + .unwrap() + .take() + .unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")))?; + + let encrypt_result = + unsafe { call!(&ctx.lib, gpgme_op_encrypt_result)(ctx.inner.as_ptr()) }; + if encrypt_result.is_null() { + return Err(MeliError::new( + "Unspecified libgpgme error: gpgme_op_encrypt_result returned NULL.", + ) + .set_err_kind(ErrorKind::External)); + } + /* Rewind cursor */ + cipher + .seek(std::io::SeekFrom::Start(0)) + .chain_err_summary(|| "libgpgme error: could not perform seek on plain text")?; + Ok(cipher.into_bytes()?) + }) + } } fn gpgme_error_try(lib: &libloading::Library, error_code: GpgmeError) -> Result<()> { diff --git a/samples/sample-config.toml b/samples/sample-config.toml index fad4bd155..367310a79 100644 --- a/samples/sample-config.toml +++ b/samples/sample-config.toml @@ -112,7 +112,6 @@ #[pgp] #auto_sign = false # always sign sent messages #auto_verify_signatures = true # always verify signatures when reading signed e-mails -#gpg_binary = "/usr/bin/gpg2" #optional # #[terminal] #theme = "dark" # or "light" diff --git a/src/components/mail.rs b/src/components/mail.rs index 0aa194896..cb9be3d2b 100644 --- a/src/components/mail.rs +++ b/src/components/mail.rs @@ -33,6 +33,7 @@ pub use crate::view::*; mod compose; pub use self::compose::*; +#[cfg(feature = "gpgme")] pub mod pgp; mod status; diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index c5f3e53f7..31cbf5b58 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -84,8 +84,8 @@ pub struct Composer { embed_area: Area, embed: Option, - sign_mail: ToggleFlag, - encrypt_mail: ToggleFlag, + #[cfg(feature = "gpgme")] + gpg_state: gpg::GpgComposeState, dirty: bool, has_changes: bool, initialized: bool, @@ -107,8 +107,8 @@ impl Default for Composer { form: FormWidget::default(), mode: ViewMode::Edit, - sign_mail: ToggleFlag::Unset, - encrypt_mail: ToggleFlag::Unset, + #[cfg(feature = "gpgme")] + gpg_state: gpg::GpgComposeState::new(), dirty: true, has_changes: false, embed_area: ((0, 0), (0, 0)), @@ -125,6 +125,8 @@ enum ViewMode { Edit, Embed, SelectRecipients(UIDialog
), + #[cfg(feature = "gpgme")] + SelectEncryptKey(bool, gpg::KeySelection), Send(UIConfirmationDialog), WaitingForSendResult(UIDialog, JoinHandle>), } @@ -430,14 +432,23 @@ impl Composer { let attachments_no = self.draft.attachments().len(); let theme_default = crate::conf::value(context, "theme_default"); clear_area(grid, area, theme_default); - if self.sign_mail.is_true() { + #[cfg(feature = "gpgme")] + if self.gpg_state.sign_mail.is_true() { + let key_list = self + .gpg_state + .sign_keys + .iter() + .map(|k| k.fingerprint()) + .collect::>() + .join(", "); write_string_to_grid( &format!( "☑ sign with {}", - account_settings!(context[self.account_hash].pgp.sign_key) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or("default key") + if self.gpg_state.sign_keys.is_empty() { + "default key" + } else { + key_list.as_str() + } ), grid, theme_default.fg, @@ -465,14 +476,29 @@ impl Composer { None, ); } - if self.encrypt_mail.is_true() { + #[cfg(feature = "gpgme")] + if self.gpg_state.encrypt_mail.is_true() { + let key_list = self + .gpg_state + .encrypt_keys + .iter() + .map(|k| k.fingerprint()) + .collect::>() + .join(", "); + write_string_to_grid( &format!( - "☑ encrypt with {}", - account_settings!(context[self.account_hash].pgp.encrypt_key) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or("default key") + "{}{}", + if self.gpg_state.encrypt_keys.is_empty() { + "☐ no keys to encrypt with!" + } else { + "☑ encrypt with " + }, + if self.gpg_state.encrypt_keys.is_empty() { + "" + } else { + key_list.as_str() + } ), grid, theme_default.fg, @@ -575,8 +601,9 @@ impl Component for Composer { let width = width!(area); if !self.initialized { - if self.sign_mail.is_unset() { - self.sign_mail = ToggleFlag::InternalVal(*account_settings!( + #[cfg(feature = "gpgme")] + if self.gpg_state.sign_mail.is_unset() { + self.gpg_state.sign_mail = ToggleFlag::InternalVal(*account_settings!( context[self.account_hash].pgp.auto_sign )); } @@ -752,6 +779,18 @@ impl Component for Composer { ViewMode::Send(ref mut s) => { s.draw(grid, center_area(area, s.content.size()), context); } + #[cfg(feature = "gpgme")] + ViewMode::SelectEncryptKey( + _, + gpg::KeySelection::Loaded { + ref mut widget, + keys: _, + }, + ) => { + widget.draw(grid, center_area(area, widget.content.size()), context); + } + #[cfg(feature = "gpgme")] + ViewMode::SelectEncryptKey(_, _) => {} ViewMode::SelectRecipients(ref mut s) => { s.draw(grid, center_area(area, s.content.size()), context); } @@ -783,8 +822,8 @@ impl Component for Composer { if let Some(true) = result.downcast_ref::() { self.update_draft(); match send_draft_async( - self.sign_mail, - self.encrypt_mail, + #[cfg(feature = "gpgme")] + self.gpg_state.clone(), context, self.account_hash, self.draft.clone(), @@ -846,6 +885,14 @@ impl Component for Composer { self.mode = ViewMode::Edit; self.set_dirty(true); } + #[cfg(feature = "gpgme")] + (ViewMode::SelectEncryptKey(_, ref mut selector), UIEvent::ComponentKill(ref id)) + if *id == selector.id() => + { + self.mode = ViewMode::Edit; + self.set_dirty(true); + return true; + } (ViewMode::Send(ref mut selector), _) => { if selector.process_event(event, context) { return true; @@ -954,6 +1001,34 @@ impl Component for Composer { return true; } } + #[cfg(feature = "gpgme")] + ( + ViewMode::SelectEncryptKey(is_encrypt, ref mut selector), + UIEvent::FinishedUIDialog(id, result), + ) if *id == selector.id() => { + debug!(&result); + if let Some(key) = result.downcast_mut::>() { + debug!("got key {:?}", key); + if let Some(key) = key { + if *is_encrypt { + self.gpg_state.encrypt_keys.clear(); + self.gpg_state.encrypt_keys.push(key.clone()); + } else { + self.gpg_state.sign_keys.clear(); + self.gpg_state.sign_keys.push(key.clone()); + } + } + } + self.mode = ViewMode::Edit; + self.set_dirty(true); + return true; + } + #[cfg(feature = "gpgme")] + (ViewMode::SelectEncryptKey(_, ref mut selector), _) => { + if selector.process_event(event, context) { + return true; + } + } _ => {} } if self.cursor == Cursor::Headers @@ -1025,14 +1100,15 @@ impl Component for Composer { if self.mode.is_edit() && (self.cursor == Cursor::Sign || self.cursor == Cursor::Encrypt) => { + #[cfg(feature = "gpgme")] match self.cursor { Cursor::Sign => { - let is_true = self.sign_mail.is_true(); - self.sign_mail = ToggleFlag::from(!is_true); + let is_true = self.gpg_state.sign_mail.is_true(); + self.gpg_state.sign_mail = ToggleFlag::from(!is_true); } Cursor::Encrypt => { - let is_true = self.encrypt_mail.is_true(); - self.encrypt_mail = ToggleFlag::from(!is_true); + let is_true = self.gpg_state.encrypt_mail.is_true(); + self.gpg_state.encrypt_mail = ToggleFlag::from(!is_true); } _ => {} }; @@ -1194,6 +1270,86 @@ impl Component for Composer { self.set_dirty(true); return true; } + UIEvent::Input(ref key) + if self.mode.is_edit() + && self.cursor == Cursor::Sign + && shortcut!(key == shortcuts[Self::DESCRIPTION]["edit_mail"]) => + { + #[cfg(feature = "gpgme")] + match melib::email::parser::address::rfc2822address_list( + self.form.values()["From"].as_str().as_bytes(), + ) + .map_err(|_err| -> MeliError { "No valid sender address in `From:`".into() }) + .and_then(|(_, list)| { + list.get(0) + .cloned() + .ok_or_else(|| "No valid sender address in `From:`".into()) + }) + .and_then(|addr| { + gpg::KeySelection::new( + false, + account_settings!(context[self.account_hash].pgp.allow_remote_lookup) + .is_true(), + addr.get_email(), + *account_settings!(context[self.account_hash].pgp.allow_remote_lookup), + context, + ) + }) { + Ok(widget) => { + self.gpg_state.sign_mail = ToggleFlag::from(true); + self.mode = ViewMode::SelectEncryptKey(false, widget); + } + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some("Could not list keys.".to_string()), + format!("libgpgme error: {}", &err), + Some(NotificationType::Error(melib::error::ErrorKind::External)), + )); + } + } + self.set_dirty(true); + return true; + } + UIEvent::Input(ref key) + if self.mode.is_edit() + && self.cursor == Cursor::Encrypt + && shortcut!(key == shortcuts[Self::DESCRIPTION]["edit_mail"]) => + { + #[cfg(feature = "gpgme")] + match melib::email::parser::address::rfc2822address_list( + self.form.values()["To"].as_str().as_bytes(), + ) + .map_err(|_err| -> MeliError { "No valid recipient addresses in `To:`".into() }) + .and_then(|(_, list)| { + list.get(0) + .cloned() + .ok_or_else(|| "No valid recipient addresses in `To:`".into()) + }) + .and_then(|addr| { + gpg::KeySelection::new( + false, + account_settings!(context[self.account_hash].pgp.allow_remote_lookup) + .is_true(), + addr.get_email(), + *account_settings!(context[self.account_hash].pgp.allow_remote_lookup), + context, + ) + }) { + Ok(widget) => { + self.gpg_state.encrypt_mail = ToggleFlag::from(true); + self.mode = ViewMode::SelectEncryptKey(true, widget); + } + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some("Could not list keys.".to_string()), + format!("libgpgme error: {}", &err), + Some(NotificationType::Error(melib::error::ErrorKind::External)), + )); + } + } + self.set_dirty(true); + return true; + } UIEvent::Input(ref key) if self.embed.is_some() && shortcut!(key == shortcuts[Self::DESCRIPTION]["edit_mail"]) => @@ -1498,15 +1654,17 @@ impl Component for Composer { ); return true; } + #[cfg(feature = "gpgme")] Action::Compose(ComposeAction::ToggleSign) => { - let is_true = self.sign_mail.is_true(); - self.sign_mail = ToggleFlag::from(!is_true); + let is_true = self.gpg_state.sign_mail.is_true(); + self.gpg_state.sign_mail = ToggleFlag::from(!is_true); self.dirty = true; return true; } + #[cfg(feature = "gpgme")] Action::Compose(ComposeAction::ToggleEncrypt) => { - let is_true = self.encrypt_mail.is_true(); - self.encrypt_mail = ToggleFlag::from(!is_true); + let is_true = self.gpg_state.encrypt_mail.is_true(); + self.gpg_state.encrypt_mail = ToggleFlag::from(!is_true); self.dirty = true; return true; } @@ -1527,6 +1685,10 @@ impl Component for Composer { ViewMode::SelectRecipients(ref widget) => { widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty() } + #[cfg(feature = "gpgme")] + ViewMode::SelectEncryptKey(_, ref widget) => { + widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty() + } ViewMode::Send(ref widget) => { widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty() } @@ -1626,7 +1788,7 @@ impl Component for Composer { } pub fn send_draft( - sign_mail: ToggleFlag, + _sign_mail: ToggleFlag, context: &mut Context, account_hash: AccountHash, mut draft: Draft, @@ -1635,7 +1797,7 @@ pub fn send_draft( complete_in_background: bool, ) -> Result>>> { let format_flowed = *account_settings!(context[account_hash].composing.format_flowed); - if sign_mail.is_true() { + /* if sign_mail.is_true() { let mut content_type = ContentType::default(); if format_flowed { if let ContentType::Text { @@ -1667,41 +1829,44 @@ pub fn send_draft( ) .into(); } - let output = crate::components::mail::pgp::sign( - body.into(), - account_settings!(context[account_hash].pgp.gpg_binary) - .as_ref() - .map(|s| s.as_str()), - account_settings!(context[account_hash].pgp.sign_key) - .as_ref() - .map(|s| s.as_str()), - ); - match output { - Err(err) => { - debug!("{:?} could not sign draft msg", err); - log( - format!( - "Could not sign draft in account `{}`: {}.", - context.accounts[&account_hash].name(), - err.to_string() - ), - ERROR, - ); - context.replies.push_back(UIEvent::Notification( - Some(format!( - "Could not sign draft in account `{}`.", - context.accounts[&account_hash].name() - )), - err.to_string(), - Some(NotificationType::Error(err.kind)), - )); - return Err(err); - } - Ok(output) => { - draft.attachments.push(output); - } + let output = todo!(); + crate::components::mail::pgp::sign( + body.into(), + account_settings!(context[account_hash].pgp.gpg_binary) + .as_ref() + .map(|s| s.as_str()), + account_settings!(context[account_hash].pgp.sign_key) + .as_ref() + .map(|s| s.as_str()), + ); + match output { + Err(err) => { + debug!("{:?} could not sign draft msg", err); + log( + format!( + "Could not sign draft in account `{}`: {}.", + context.accounts[&account_hash].name(), + err.to_string() + ), + ERROR, + ); + context.replies.push_back(UIEvent::Notification( + Some(format!( + "Could not sign draft in account `{}`.", + context.accounts[&account_hash].name() + )), + err.to_string(), + Some(NotificationType::Error(err.kind)), + )); + return Err(err); } + Ok(output) => { + draft.attachments.push(output); + } + } } else { + */ + { let mut content_type = ContentType::default(); if format_flowed { if let ContentType::Text { @@ -1762,8 +1927,7 @@ pub fn save_draft( } pub fn send_draft_async( - sign_mail: ToggleFlag, - encrypt_mail: ToggleFlag, + #[cfg(feature = "gpgme")] gpg_state: gpg::GpgComposeState, context: &mut Context, account_hash: AccountHash, mut draft: Draft, @@ -1772,6 +1936,7 @@ pub fn send_draft_async( ) -> Result> + Send>>> { let format_flowed = *account_settings!(context[account_hash].composing.format_flowed); let event_sender = context.sender.clone(); + #[cfg(feature = "gpgme")] let mut filters_stack: Vec< Box< dyn FnOnce( @@ -1781,40 +1946,19 @@ pub fn send_draft_async( + Send, >, > = vec![]; - if sign_mail.is_true() { + #[cfg(feature = "gpgme")] + if gpg_state.sign_mail.is_true() && !gpg_state.encrypt_mail.is_true() { filters_stack.push(Box::new(crate::components::mail::pgp::sign_filter( - account_settings!(context[account_hash].pgp.gpg_binary) - .as_ref() - .map(|s| s.to_string()), - account_settings!(context[account_hash].pgp.sign_key) - .as_ref() - .map(|s| s.to_string()), + gpg_state.sign_keys.clone(), )?)); - } - if encrypt_mail.is_true() { - let mut recipients = vec![]; - if let Ok((_, v)) = - melib::email::parser::address::rfc2822address_list(draft.headers()["To"].as_bytes()) - { - for addr in v { - recipients.push(addr.get_email()); - } - } - if let Ok((_, v)) = - melib::email::parser::address::rfc2822address_list(draft.headers()["Cc"].as_bytes()) - { - for addr in v { - recipients.push(addr.get_email()); - } - } + } else if gpg_state.encrypt_mail.is_true() { filters_stack.push(Box::new(crate::components::mail::pgp::encrypt_filter( - account_settings!(context[account_hash].pgp.gpg_binary) - .as_ref() - .map(|s| s.to_string()), - account_settings!(context[account_hash].pgp.encrypt_key) - .as_ref() - .map(|s| s.to_string()), - recipients, + if gpg_state.sign_mail.is_true() { + Some(gpg_state.sign_keys.clone()) + } else { + None + }, + gpg_state.encrypt_keys.clone(), )?)); } let send_mail = account_settings!(context[account_hash].composing.send_mail).clone(); @@ -1850,6 +1994,7 @@ pub fn send_draft_async( .into(); } Ok(Box::pin(async move { + #[cfg(feature = "gpgme")] for f in filters_stack { body = f(body).await?; } @@ -1882,3 +2027,277 @@ pub fn send_draft_async( ret })) } + +#[cfg(feature = "gpgme")] +mod gpg { + use super::*; + + #[derive(Debug)] + pub enum KeySelection { + LoadingKeys { + handle: JoinHandle>>, + progress_spinner: ProgressSpinner, + secret: bool, + local: bool, + pattern: String, + allow_remote_lookup: ToggleFlag, + }, + Error { + id: ComponentId, + err: MeliError, + }, + Loaded { + widget: UIDialog, + keys: Vec, + }, + } + + impl std::fmt::Display for KeySelection { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "select pgp keys") + } + } + + impl KeySelection { + pub fn new( + secret: bool, + local: bool, + pattern: String, + allow_remote_lookup: ToggleFlag, + context: &mut Context, + ) -> Result { + use melib::gpgme::*; + debug!("KeySelection::new"); + debug!(&secret); + debug!(&local); + debug!(&pattern); + debug!(&allow_remote_lookup); + let mut ctx = Context::new()?; + if local { + ctx.set_auto_key_locate(LocateKey::LOCAL)?; + } else { + ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?; + } + let job = ctx.keylist(secret, Some(pattern.clone()))?; + let handle = context.job_executor.spawn_specialized(job); + let mut progress_spinner = ProgressSpinner::new(8); + progress_spinner.start(); + Ok(KeySelection::LoadingKeys { + handle, + secret, + local, + pattern, + allow_remote_lookup, + progress_spinner, + }) + } + } + + impl Component for KeySelection { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + match self { + KeySelection::LoadingKeys { + ref mut progress_spinner, + .. + } => progress_spinner.draw(grid, center_area(area, (2, 2)), context), + KeySelection::Error { ref err, .. } => { + let theme_default = crate::conf::value(context, "theme_default"); + write_string_to_grid( + &err.to_string(), + grid, + theme_default.fg, + theme_default.bg, + theme_default.attrs, + center_area(area, (15, 2)), + Some(0), + ); + } + KeySelection::Loaded { ref mut widget, .. } => { + widget.draw(grid, center_area(area, widget.content.size()), context) + } + } + } + + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { + debug!(&self); + debug!(&event); + match self { + KeySelection::LoadingKeys { + ref mut progress_spinner, + ref mut handle, + secret, + local, + ref mut pattern, + allow_remote_lookup, + .. + } => match event { + UIEvent::StatusEvent(StatusEvent::JobFinished(ref id)) + if *id == handle.job_id => + { + match handle.chan.try_recv().unwrap().unwrap() { + Ok(keys) => { + if keys.is_empty() { + let id = progress_spinner.id(); + if allow_remote_lookup.is_true() { + match Self::new( + *secret, + *local, + std::mem::replace(pattern, String::new()), + *allow_remote_lookup, + context, + ) { + Ok(w) => { + *self = w; + } + Err(err) => *self = KeySelection::Error { err, id }, + } + } else if !*local && allow_remote_lookup.is_ask() { + *self = KeySelection::Error { + err: MeliError::new(format!( + "No keys found for {}, perform remote lookup?", + pattern + )), + id, + } + } else { + *self = KeySelection::Error { + err: MeliError::new(format!( + "No keys found for {}.", + pattern + )), + id, + } + } + if let KeySelection::Error { ref err, .. } = self { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(err.to_string()), + )); + let res: Option = None; + context.replies.push_back(UIEvent::FinishedUIDialog( + id, + Box::new(res), + )); + } + return true; + } + let mut widget = UIDialog::new( + "select key", + keys.iter() + .map(|k| { + ( + k.clone(), + if let Some(primary_uid) = k.primary_uid() { + format!("{} {}", k.fingerprint(), primary_uid) + } else { + k.fingerprint().to_string() + }, + ) + }) + .collect::>(), + true, + Some(Box::new( + move |id: ComponentId, results: &[melib::gpgme::Key]| { + Some(UIEvent::FinishedUIDialog( + id, + Box::new(results.get(0).map(|k| k.clone())), + )) + }, + )), + context, + ); + widget.set_dirty(true); + *self = KeySelection::Loaded { widget, keys }; + } + Err(err) => { + *self = KeySelection::Error { + err, + id: ComponentId::new_v4(), + }; + } + } + true + } + _ => progress_spinner.process_event(event, context), + }, + KeySelection::Error { .. } => false, + KeySelection::Loaded { ref mut widget, .. } => widget.process_event(event, context), + } + } + + fn is_dirty(&self) -> bool { + match self { + KeySelection::LoadingKeys { + ref progress_spinner, + .. + } => progress_spinner.is_dirty(), + KeySelection::Error { .. } => true, + KeySelection::Loaded { ref widget, .. } => widget.is_dirty(), + } + } + + fn set_dirty(&mut self, value: bool) { + match self { + KeySelection::LoadingKeys { + ref mut progress_spinner, + .. + } => progress_spinner.set_dirty(value), + KeySelection::Error { .. } => {} + KeySelection::Loaded { ref mut widget, .. } => widget.set_dirty(value), + } + } + + fn kill(&mut self, _uuid: Uuid, _context: &mut Context) {} + + fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { + match self { + KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => { + ShortcutMaps::default() + } + KeySelection::Loaded { ref widget, .. } => widget.get_shortcuts(context), + } + } + + fn id(&self) -> ComponentId { + match self { + KeySelection::LoadingKeys { + ref progress_spinner, + .. + } => progress_spinner.id(), + KeySelection::Error { ref id, .. } => *id, + KeySelection::Loaded { ref widget, .. } => widget.id(), + } + } + + fn set_id(&mut self, new_id: ComponentId) { + match self { + KeySelection::LoadingKeys { + ref mut progress_spinner, + .. + } => progress_spinner.set_id(new_id), + KeySelection::Error { ref mut id, .. } => *id = new_id, + KeySelection::Loaded { ref mut widget, .. } => widget.set_id(new_id), + } + } + } + + #[derive(Debug, Clone)] + pub struct GpgComposeState { + pub sign_mail: ToggleFlag, + pub encrypt_mail: ToggleFlag, + pub encrypt_keys: Vec, + pub encrypt_for_self: bool, + pub sign_keys: Vec, + } + + impl GpgComposeState { + pub fn new() -> Self { + GpgComposeState { + sign_mail: ToggleFlag::Unset, + encrypt_mail: ToggleFlag::Unset, + encrypt_keys: vec![], + encrypt_for_self: true, + sign_keys: vec![], + } + } + } +} diff --git a/src/components/mail/pgp.rs b/src/components/mail/pgp.rs index 64c13c16b..c378ca46d 100644 --- a/src/components/mail/pgp.rs +++ b/src/components/mail/pgp.rs @@ -19,215 +19,49 @@ * along with meli. If not, see . */ -use super::*; -use melib::email::pgp as melib_pgp; +use melib::email::{ + attachment_types::{ContentDisposition, ContentType, MultipartType}, + pgp as melib_pgp, Attachment, AttachmentBuilder, +}; +use melib::error::*; +use melib::gpgme::*; use std::future::Future; -use std::io::Write; use std::pin::Pin; -use std::process::{Command, Stdio}; -pub fn verify_signature(a: &Attachment, context: &mut Context) -> Result> { - let (bytes, sig) = - melib_pgp::verify_signature(a).chain_err_summary(|| "Could not verify signature.")?; - let bytes_file = create_temp_file(&bytes, None, None, true); - let signature_file = create_temp_file(sig, None, None, true); - let binary = context - .settings - .pgp - .gpg_binary - .as_ref() - .map(String::as_str) - .unwrap_or("gpg2"); - Ok(Command::new(binary) - .args(&[ - "--output", - "-", - "--verify", - signature_file.path.to_str().unwrap(), - bytes_file.path.to_str().unwrap(), - ]) - .stdin(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .and_then(|gpg| gpg.wait_with_output()) - .map(|gpg| gpg.stderr) - .chain_err_summary(|| { - format!( - "Failed to launch {} to verify PGP signature", - context - .settings - .pgp - .gpg_binary - .as_ref() - .map(String::as_str) - .unwrap_or("gpg2"), - ) - })?) +pub async fn decrypt(raw: Vec) -> Result<(melib_pgp::DecryptionMetadata, Vec)> { + let mut ctx = Context::new()?; + let cipher = ctx.new_data_mem(&raw)?; + ctx.decrypt(cipher)?.await } -/// Returns multipart/signed -pub fn sign( - a: AttachmentBuilder, - gpg_binary: Option<&str>, - pgp_key: Option<&str>, -) -> Result { - let binary = gpg_binary.unwrap_or("gpg2"); - let mut command = Command::new(binary); - command.args(&[ - "--digest-algo", - "sha512", - "--output", - "-", - "--detach-sig", - "--armor", - ]); - if let Some(key) = pgp_key { - command.args(&["--local-user", key]); - } - let a: Attachment = a.into(); - - let sig_attachment = command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .and_then(|mut gpg| { - gpg.stdin - .as_mut() - .expect("Could not get gpg stdin") - .write_all(&melib_pgp::convert_attachment_to_rfc_spec( - a.into_raw().as_bytes(), - ))?; - let gpg = gpg.wait_with_output()?; - Ok(Attachment::new( - ContentType::PGPSignature, - Default::default(), - gpg.stdout, - )) - }) - .chain_err_summary(|| format!("Failed to launch {} to verify PGP signature", binary))?; - - let a: AttachmentBuilder = a.into(); - let parts = vec![a, sig_attachment.into()]; - let boundary = ContentType::make_boundary(&parts); - Ok(Attachment::new( - ContentType::Multipart { - boundary: boundary.into_bytes(), - kind: MultipartType::Signed, - parts: parts.into_iter().map(|a| a.into()).collect::>(), - }, - Default::default(), - Vec::new(), - ) - .into()) -} - -pub async fn decrypt( - raw: Vec, - gpg_binary: Option, - decrypt_key: Option, -) -> Result<(melib_pgp::DecryptionMetadata, Vec)> { - let bin = gpg_binary.as_ref().map(|s| s.as_str()).unwrap_or("gpg2"); - let mut command = Command::new(bin); - command.args(&["--digest-algo", "sha512", "--output", "-"]); - if let Some(ref key) = decrypt_key { - command.args(&["--local-user", key]); - } - - let stdout = command - .args(&["--decrypt"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .and_then(|mut gpg| { - gpg.stdin - .as_mut() - .expect("Could not get gpg stdin") - .write_all(&raw)?; - let gpg = gpg.wait_with_output()?; - Ok(gpg.stdout) - }) - .chain_err_summary(|| format!("Failed to launch {} to verify PGP signature", bin))?; - Ok((melib_pgp::DecryptionMetadata::default(), stdout)) -} - -pub async fn verify(a: Attachment, gpg_binary: Option) -> Result> { - let (bytes, sig) = +pub async fn verify(a: Attachment) -> Result<()> { + let (data, sig) = melib_pgp::verify_signature(&a).chain_err_summary(|| "Could not verify signature.")?; - let bytes_file = create_temp_file(&bytes, None, None, true); - let signature_file = create_temp_file(sig, None, None, true); - Ok( - Command::new(gpg_binary.as_ref().map(String::as_str).unwrap_or("gpg2")) - .args(&[ - "--output", - "-", - "--verify", - signature_file.path.to_str().unwrap(), - bytes_file.path.to_str().unwrap(), - ]) - .stdin(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .and_then(|gpg| gpg.wait_with_output()) - .map(|gpg| gpg.stderr) - .chain_err_summary(|| { - format!( - "Failed to launch {} to verify PGP signature", - gpg_binary.as_ref().map(String::as_str).unwrap_or("gpg2"), - ) - })?, - ) + let mut ctx = Context::new()?; + let sig = ctx.new_data_mem(&sig)?; + let data = ctx.new_data_mem(&data)?; + ctx.verify(sig, data)?.await } pub fn sign_filter( - gpg_binary: Option, - pgp_key: Option, + sign_keys: Vec, ) -> Result< impl FnOnce(AttachmentBuilder) -> Pin> + Send>> + Send, > { - let binary = gpg_binary.unwrap_or("gpg2".to_string()); - let mut command = Command::new(&binary); - command.args(&[ - "--digest-algo", - "sha512", - "--output", - "-", - "--detach-sig", - "--armor", - ]); - if let Some(key) = pgp_key.as_ref() { - command.args(&["--local-user", key]); - } Ok( move |a: AttachmentBuilder| -> Pin>+Send>> { Box::pin(async move { let a: Attachment = a.into(); - - let sig_attachment = command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .and_then(|mut gpg| { - gpg.stdin - .as_mut() - .expect("Could not get gpg stdin") - .write_all(&melib_pgp::convert_attachment_to_rfc_spec( + let mut ctx = Context::new()?; + let data = ctx.new_data_mem(&melib_pgp::convert_attachment_to_rfc_spec( a.into_raw().as_bytes(), - ))?; - let gpg = gpg.wait_with_output()?; - Ok(Attachment::new( - ContentType::PGPSignature, - Default::default(), - gpg.stdout, - )) - }) - .chain_err_summary(|| { - format!("Failed to launch {} to verify PGP signature", binary) - })?; - + ))?; + let sig_attachment = Attachment::new( + ContentType::PGPSignature, + Default::default(), + ctx.sign(sign_keys, data)?.await?, + ); let a: AttachmentBuilder = a.into(); let parts = vec![a, sig_attachment.into()]; let boundary = ContentType::make_boundary(&parts); @@ -247,61 +81,31 @@ pub fn sign_filter( } pub fn encrypt_filter( - gpg_binary: Option, - my_public_key: Option, - recipients: Vec, + sign_keys: Option>, + encrypt_keys: Vec, ) -> Result< impl FnOnce(AttachmentBuilder) -> Pin> + Send>> + Send, > { - let binary = gpg_binary.unwrap_or("gpg2".to_string()); - let mut command = Command::new(&binary); - command.args(&[ - "--batch", - "--no-tty", - "--encrypt", - "--armor", - "--output", - "-", - ]); - if let Some(key) = my_public_key.as_ref() { - command.args(&["--recipient", key]); - } else { - command.arg("--default-recipient-self"); - } - for r in &recipients { - command.args(&["--recipient", r.as_str()]); - } Ok( move |a: AttachmentBuilder| -> Pin>+Send>> { Box::pin(async move { let a: Attachment = a.into(); + debug!("main attachment is {:?}", &a); + let mut ctx = Context::new()?; + let data = ctx.new_data_mem( + a.into_raw().as_bytes() + )?; - let sig_attachment = command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .and_then(|mut gpg| { - gpg.stdin - .as_mut() - .expect("Could not get gpg stdin") - .write_all(&melib_pgp::convert_attachment_to_rfc_spec( - a.into_raw().as_bytes(), - ))?; - let gpg = gpg.wait_with_output()?; - let mut a = Attachment::new( + let sig_attachment = { + let mut a = Attachment::new( ContentType::OctetStream { name: None }, Default::default(), - gpg.stdout, + ctx.encrypt(sign_keys, encrypt_keys, data)?.await?, ); a.content_disposition = ContentDisposition::from(r#"attachment; filename="msg.asc""#.as_bytes()); - Ok(a) - }) - .chain_err_summary(|| { - format!("Failed to launch {} to verify PGP signature", binary) - })?; - + a + }; let mut a: AttachmentBuilder = AttachmentBuilder::new("Version: 1".as_bytes()); a.set_content_type_from_bytes("application/pgp-encrypted".as_bytes()); a.set_content_disposition(ContentDisposition::from("attachment".as_bytes())); diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 62a03302b..3757a02cf 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -105,7 +105,7 @@ pub enum AttachmentDisplay { SignedPending { inner: Attachment, display: Vec, - handle: std::result::Result>, JoinHandle>>>, + handle: JoinHandle>, job_id: JobId, }, SignedFailed { @@ -645,99 +645,8 @@ impl MailView { } } MultipartType::Signed => { - if *mailbox_settings!( - context[coordinates.0][&coordinates.1] - .pgp - .auto_verify_signatures - ) { - if let Some(bin) = mailbox_settings!( - context[coordinates.0][&coordinates.1].pgp.gpg_binary - ) { - let verify_fut = crate::components::mail::pgp::verify( - a.clone(), - Some(bin.to_string()), - ); - let handle = context.job_executor.spawn_blocking(verify_fut); - active_jobs.insert(handle.job_id); - acc.push(AttachmentDisplay::SignedPending { - inner: a.clone(), - display: { - let mut v = vec![]; - rec(&parts[0], context, coordinates, &mut v, active_jobs); - v - }, - job_id: handle.job_id, - handle: Err(handle), - }); - } else { - #[cfg(not(feature = "gpgme"))] - { - acc.push(AttachmentDisplay::SignedUnverified { - inner: a.clone(), - display: { - let mut v = vec![]; - rec( - &parts[0], - context, - coordinates, - &mut v, - active_jobs, - ); - v - }, - }); - } - #[cfg(feature = "gpgme")] - match melib::gpgme::Context::new().and_then(|mut ctx| { - let sig = ctx.new_data_mem(&parts[1].raw())?; - let mut f = std::fs::File::create("/tmp/sig").unwrap(); - f.write_all(&parts[1].raw())?; - let mut f = std::fs::File::create("/tmp/data").unwrap(); - f.write_all(&parts[0].raw())?; - let data = ctx.new_data_mem(&parts[0].raw())?; - ctx.verify(sig, data) - }) { - Ok(verify_fut) => { - let handle = - context.job_executor.spawn_specialized(verify_fut); - active_jobs.insert(handle.job_id); - acc.push(AttachmentDisplay::SignedPending { - inner: a.clone(), - job_id: handle.job_id, - display: { - let mut v = vec![]; - rec( - &parts[0], - context, - coordinates, - &mut v, - active_jobs, - ); - v - }, - handle: Ok(handle), - }); - } - Err(error) => { - acc.push(AttachmentDisplay::SignedFailed { - inner: a.clone(), - display: { - let mut v = vec![]; - rec( - &parts[0], - context, - coordinates, - &mut v, - active_jobs, - ); - v - }, - error, - }); - } - } - } - } else { + #[cfg(not(feature = "gpgme"))] + { acc.push(AttachmentDisplay::SignedUnverified { inner: a.clone(), display: { @@ -747,63 +656,67 @@ impl MailView { }, }); } + #[cfg(feature = "gpgme")] + { + if *mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pgp + .auto_verify_signatures + ) { + let verify_fut = crate::components::mail::pgp::verify(a.clone()); + let handle = context.job_executor.spawn_specialized(verify_fut); + active_jobs.insert(handle.job_id); + acc.push(AttachmentDisplay::SignedPending { + inner: a.clone(), + job_id: handle.job_id, + display: { + let mut v = vec![]; + rec(&parts[0], context, coordinates, &mut v, active_jobs); + v + }, + handle, + }); + } else { + acc.push(AttachmentDisplay::SignedUnverified { + inner: a.clone(), + display: { + let mut v = vec![]; + rec(&parts[0], context, coordinates, &mut v, active_jobs); + v + }, + }); + } + } } MultipartType::Encrypted => { for a in parts { if a.content_type == "application/octet-stream" { - if *mailbox_settings!( - context[coordinates.0][&coordinates.1].pgp.auto_decrypt - ) { - let _verify = *mailbox_settings!( - context[coordinates.0][&coordinates.1] - .pgp - .auto_verify_signatures - ); - if let Some(bin) = mailbox_settings!( - context[coordinates.0][&coordinates.1].pgp.gpg_binary + #[cfg(not(feature = "gpgme"))] + { + acc.push(AttachmentDisplay::EncryptedFailed { + inner: a.clone(), + error: MeliError::new("Cannot decrypt: meli must be compiled with libgpgme support."), + }); + } + #[cfg(feature = "gpgme")] + { + if *mailbox_settings!( + context[coordinates.0][&coordinates.1].pgp.auto_decrypt ) { - let decrypt_fut = crate::components::mail::pgp::decrypt( - a.raw().to_vec(), - Some(bin.to_string()), - None, - ); + let decrypt_fut = + crate::components::mail::pgp::decrypt(a.raw().to_vec()); let handle = - context.job_executor.spawn_blocking(decrypt_fut); + context.job_executor.spawn_specialized(decrypt_fut); active_jobs.insert(handle.job_id); acc.push(AttachmentDisplay::EncryptedPending { inner: a.clone(), handle, }); } else { - #[cfg(not(feature = "gpgme"))] - { - acc.push(AttachmentDisplay::EncryptedFailed { - inner: a.clone(), - error: MeliError::new("Cannot decrypt: define `gpg_binary` in configuration."), - }); - } - #[cfg(feature = "gpgme")] - match melib::gpgme::Context::new().and_then(|mut ctx| { - let cipher = ctx.new_data_mem(&a.raw())?; - ctx.decrypt(cipher) - }) { - Ok(decrypt_fut) => { - let handle = context - .job_executor - .spawn_specialized(decrypt_fut); - active_jobs.insert(handle.job_id); - acc.push(AttachmentDisplay::EncryptedPending { - inner: a.clone(), - handle, - }); - } - Err(error) => { - acc.push(AttachmentDisplay::EncryptedFailed { - inner: a.clone(), - error, - }); - } - } + acc.push(AttachmentDisplay::EncryptedFailed { + inner: a.clone(), + error: MeliError::new("Undecrypted."), + }); } } } @@ -1529,64 +1442,27 @@ impl Component for MailView { } if *our_job_id == *job_id => { caught = true; self.initialised = false; - match handle.as_mut() { - Ok(handle) => match handle - .chan - .try_recv() - .unwrap() - .unwrap() - { - Ok(()) => { - *d = AttachmentDisplay::SignedVerified { - inner: std::mem::replace( - inner, - AttachmentBuilder::new(&[]).build(), - ), - display: std::mem::replace(display, vec![]), - description: String::new(), - }; - } - Err(error) => { - *d = AttachmentDisplay::SignedFailed { - inner: std::mem::replace( - inner, - AttachmentBuilder::new(&[]).build(), - ), - display: std::mem::replace(display, vec![]), - error, - }; - } - }, - Err(handle) => match handle - .chan - .try_recv() - .unwrap() - .unwrap() - { - Ok(verify_bytes) => { - *d = AttachmentDisplay::SignedVerified { - inner: std::mem::replace( - inner, - AttachmentBuilder::new(&[]).build(), - ), - display: std::mem::replace(display, vec![]), - description: String::from_utf8_lossy( - &verify_bytes, - ) - .to_string(), - }; - } - Err(error) => { - *d = AttachmentDisplay::SignedFailed { - inner: std::mem::replace( - inner, - AttachmentBuilder::new(&[]).build(), - ), - display: std::mem::replace(display, vec![]), - error, - }; - } - }, + match handle.chan.try_recv().unwrap().unwrap() { + Ok(()) => { + *d = AttachmentDisplay::SignedVerified { + inner: std::mem::replace( + inner, + AttachmentBuilder::new(&[]).build(), + ), + display: std::mem::replace(display, vec![]), + description: String::new(), + }; + } + Err(error) => { + *d = AttachmentDisplay::SignedFailed { + inner: std::mem::replace( + inner, + AttachmentBuilder::new(&[]).build(), + ), + display: std::mem::replace(display, vec![]), + error, + }; + } } } AttachmentDisplay::EncryptedPending { inner, handle } diff --git a/src/components/utilities/widgets.rs b/src/components/utilities/widgets.rs index 045e6d806..363bf37b3 100644 --- a/src/components/utilities/widgets.rs +++ b/src/components/utilities/widgets.rs @@ -375,6 +375,10 @@ impl FormWidget { self.fields.insert(value.0, value.1); } + pub fn values(&self) -> &HashMap { + &self.fields + } + pub fn values_mut(&mut self) -> &mut HashMap { &mut self.fields } diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index de24f536b..577e084e0 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -285,45 +285,77 @@ impl Default for TagsSettingsOverride { } } +#[cfg(feature = "gpgme")] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct PGPSettingsOverride { #[doc = " auto verify signed e-mail according to RFC3156"] + #[doc = " Default: true"] #[serde(alias = "auto-verify-signatures")] #[serde(default)] pub auto_verify_signatures: Option, #[doc = " auto decrypt encrypted e-mail"] + #[doc = " Default: true"] #[serde(alias = "auto-decrypt")] #[serde(default)] pub auto_decrypt: Option, - #[doc = " always sign sent messages"] + #[doc = " always sign sent e-mail"] + #[doc = " Default: false"] #[serde(alias = "auto-sign")] #[serde(default)] pub auto_sign: Option, + #[doc = " Auto encrypt sent e-mail"] + #[doc = " Default: false"] + #[serde(alias = "auto-encrypt")] + #[serde(default)] + pub auto_encrypt: Option, + #[doc = " Default: None"] #[serde(alias = "sign-key")] #[serde(default)] pub sign_key: Option>, + #[doc = " Default: None"] #[serde(alias = "decrypt-key")] #[serde(default)] pub decrypt_key: Option>, + #[doc = " Default: None"] #[serde(alias = "encrypt-key")] #[serde(default)] pub encrypt_key: Option>, - #[doc = " gpg binary name or file location to use"] - #[serde(alias = "gpg-binary")] + #[doc = " Allow remote lookups"] + #[doc = " Default: None"] + #[serde(alias = "allow-remote-lookups")] #[serde(default)] - pub gpg_binary: Option>, + pub allow_remote_lookup: Option, + #[doc = " Remote lookup mechanisms."] + #[doc = " Default: \"local,wkd\""] + #[serde(alias = "remote-lookup-mechanisms")] + #[serde(default)] + pub remote_lookup_mechanisms: Option, } +#[cfg(feature = "gpgme")] impl Default for PGPSettingsOverride { fn default() -> Self { PGPSettingsOverride { auto_verify_signatures: None, auto_decrypt: None, auto_sign: None, + auto_encrypt: None, sign_key: None, decrypt_key: None, encrypt_key: None, - gpg_binary: None, + allow_remote_lookup: None, + remote_lookup_mechanisms: None, } } } + +#[cfg(not(feature = "gpgme"))] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct PGPSettingsOverride {} +#[cfg(not(feature = "gpgme"))] +impl Default for PGPSettingsOverride { + fn default() -> Self { + PGPSettingsOverride {} + } +} diff --git a/src/conf/pgp.rs b/src/conf/pgp.rs index bdb8584c6..7d9ce7e25 100644 --- a/src/conf/pgp.rs +++ b/src/conf/pgp.rs @@ -19,49 +19,86 @@ * along with meli. If not, see . */ +#[cfg(feature = "gpgme")] use super::default_vals::*; +#[cfg(feature = "gpgme")] +use melib::conf::ToggleFlag; +#[cfg(feature = "gpgme")] /// Settings for digital signing and encryption #[derive(Debug, Deserialize, Clone, Serialize)] #[serde(deny_unknown_fields)] pub struct PGPSettings { /// auto verify signed e-mail according to RFC3156 + /// Default: true #[serde(default = "true_val", alias = "auto-verify-signatures")] pub auto_verify_signatures: bool, /// auto decrypt encrypted e-mail + /// Default: true #[serde(default = "true_val", alias = "auto-decrypt")] pub auto_decrypt: bool, - /// always sign sent messages + /// always sign sent e-mail + /// Default: false #[serde(default = "false_val", alias = "auto-sign")] pub auto_sign: bool, + /// Auto encrypt sent e-mail + /// Default: false + #[serde(default = "false_val", alias = "auto-encrypt")] + pub auto_encrypt: bool, + // https://tools.ietf.org/html/rfc4880#section-12.2 + /// Default: None #[serde(default = "none", alias = "sign-key")] pub sign_key: Option, + /// Default: None #[serde(default = "none", alias = "decrypt-key")] pub decrypt_key: Option, + /// Default: None #[serde(default = "none", alias = "encrypt-key")] pub encrypt_key: Option, - /// gpg binary name or file location to use - #[serde(default, alias = "gpg-binary")] - pub gpg_binary: Option, + /// Allow remote lookups + /// Default: None + #[serde(default = "internal_value_false", alias = "allow-remote-lookups")] + pub allow_remote_lookup: ToggleFlag, + + /// Remote lookup mechanisms. + /// Default: "local,wkd" + #[serde( + default = "default_lookup_mechanism", + alias = "remote-lookup-mechanisms" + )] + pub remote_lookup_mechanisms: melib::gpgme::LocateKey, } +#[cfg(feature = "gpgme")] +fn default_lookup_mechanism() -> melib::gpgme::LocateKey { + melib::gpgme::LocateKey::LOCAL | melib::gpgme::LocateKey::WKD +} + +#[cfg(feature = "gpgme")] impl Default for PGPSettings { fn default() -> Self { PGPSettings { auto_verify_signatures: true, auto_decrypt: true, auto_sign: false, + auto_encrypt: false, sign_key: None, decrypt_key: None, encrypt_key: None, - gpg_binary: None, + allow_remote_lookup: internal_value_false::(), + remote_lookup_mechanisms: default_lookup_mechanism(), } } } + +#[cfg(not(feature = "gpgme"))] +#[derive(Debug, Default, Deserialize, Clone, Serialize)] +#[serde(deny_unknown_fields)] +pub struct PGPSettings;