Browse Source

compose: add key selection state for gpg operations

Closes #81
master
Manos Pitsidianakis 2 weeks ago
parent
commit
9ce62c735a
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS. GPG Key ID: 73627C2F690DF710
13 changed files with 1058 additions and 599 deletions
  1. +10
    -0
      config_macros.rs
  2. +0
    -5
      docs/meli.conf.5
  3. +18
    -3
      melib/src/conf.rs
  4. +21
    -33
      melib/src/gpgme/bindings.rs
  5. +305
    -26
      melib/src/gpgme/mod.rs
  6. +0
    -1
      samples/sample-config.toml
  7. +1
    -0
      src/components/mail.rs
  8. +514
    -95
      src/components/mail/compose.rs
  9. +37
    -233
      src/components/mail/pgp.rs
  10. +69
    -193
      src/components/mail/view.rs
  11. +4
    -0
      src/components/utilities/widgets.rs
  12. +37
    -5
      src/conf/overrides.rs
  13. +42
    -5
      src/conf/pgp.rs

+ 10
- 0
config_macros.rs View File

@ -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 {

+ 0
- 5
docs/meli.conf.5 View File

@ -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

+ 18
- 3
melib/src/conf.rs View File

@ -128,6 +128,7 @@ pub enum ToggleFlag {
InternalVal(bool),
False,
True,
Ask,
}
impl From<bool> 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 = <bool>::deserialize(deserializer);
let s = <String>::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
)))
}
})
}
}

+ 21
- 33
melib/src/gpgme/bindings.rs View File

@ -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,

+ 305
- 26
melib/src/gpgme/mod.rs View File

@ -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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if let Ok(s) = <String>::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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl LocateKey {
pub fn from_string_de<'de, D, T: AsRef<str>>(s: T) -> std::result::Result<Self, D::Error>
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<usize, GpgmeFd>,
@ -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<impl Future<Output = Result<()>> + 'd> {
let sig = std::ptr::null_mut();
sign_keys: Vec<Key>,
mut text: Data,
) -> Result<impl Future<Output = Result<Vec<u8>>>> {
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<Vec<Key>>,
encrypt_keys: Vec<Key>,
mut plain: Data,
) -> Result<impl Future<Output = Result<Vec<u8>>> + 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<gpgme_key_t> = 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::<Vec<Async<GpgmeFd>>>();
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<()> {

+ 0
- 1
samples/sample-config.toml View File

@ -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"

+ 1
- 0
src/components/mail.rs View File

@ -33,6 +33,7 @@ pub use crate::view::*;
mod compose;
pub use self::compose::*;
#[cfg(feature = "gpgme")]
pub mod pgp;
mod status;

+ 514
- 95
src/components/mail/compose.rs View File

@ -84,8 +84,8 @@ pub struct Composer {
embed_area: Area,
embed: Option<EmbedStatus>,
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<Address>),
#[cfg(feature = "gpgme")]
SelectEncryptKey(bool, gpg::KeySelection),
Send(UIConfirmationDialog),
WaitingForSendResult(UIDialog<char>, JoinHandle<Result<()>>),
}
@ -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::<Vec<_>>()
.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::<Vec<_>>()
.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::<bool>() {
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::<Option<melib::gpgme::Key>>() {
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);
}
_ => {}
};
@ -1195,6 +1271,86 @@ impl Component for Composer {
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<Option<JoinHandle<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<Pin<Box<dyn Future<Output = 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<Result<Vec<melib::gpgme::Key>>>,
progress_spinner: ProgressSpinner,
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
},
Error {
id: ComponentId,
err: MeliError,
},
Loaded {
widget: UIDialog<melib::gpgme::Key>,
keys: Vec<melib::gpgme::Key>,
},
}
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<Self> {
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<melib::gpgme::Key> = 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::<Vec<(melib::gpgme::Key, String)>>(),
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