From a2f11c341d66715ac5a311b7a68264200e5e2ade Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 8 Oct 2020 16:52:13 +0300 Subject: [PATCH] compose: add async draft filter stack in sending mail Add a stack of "filter" closures that edit a draft before sending it. Add PGP signing filter. An encryption filter will be added in a future commit. --- src/components/mail/compose.rs | 160 ++++++++++++++++++++++----------- src/components/mail/pgp.rs | 68 ++++++++++++++ src/conf/accounts.rs | 63 +++++++++++++ src/state.rs | 2 +- 4 files changed, 241 insertions(+), 52 deletions(-) diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index c877c661..463490be 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -30,6 +30,7 @@ use crate::terminal::embed::EmbedGrid; use indexmap::IndexSet; use nix::sys::wait::WaitStatus; use std::convert::TryInto; +use std::future::Future; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -81,7 +82,6 @@ pub struct Composer { embed_area: Area, embed: Option, sign_mail: ToggleFlag, - encrypt_mail: ToggleFlag, dirty: bool, has_changes: bool, initialized: bool, @@ -104,7 +104,6 @@ impl Default for Composer { mode: ViewMode::Edit, sign_mail: ToggleFlag::Unset, - encrypt_mail: ToggleFlag::Unset, dirty: true, has_changes: false, embed_area: ((0, 0), (0, 0)), @@ -453,33 +452,15 @@ impl Composer { None, ); } - if self.encrypt_mail.is_true() { - 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") - ), - grid, - theme_default.fg, - theme_default.bg, - theme_default.attrs, - (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), - None, - ); - } else { - write_string_to_grid( - "☐ don't encrypt", - grid, - theme_default.fg, - theme_default.bg, - theme_default.attrs, - (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), - None, - ); - } + write_string_to_grid( + "☐ don't encrypt", + grid, + theme_default.fg, + theme_default.bg, + theme_default.attrs, + (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), + None, + ); if attachments_no == 0 { write_string_to_grid( "no attachments", @@ -552,11 +533,6 @@ impl Component for Composer { context[self.account_hash].pgp.auto_sign )); } - if self.encrypt_mail.is_unset() { - self.encrypt_mail = ToggleFlag::InternalVal(*account_settings!( - context[self.account_hash].pgp.auto_encrypt - )); - } if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty() { self.draft.set_header( @@ -752,16 +728,16 @@ impl Component for Composer { { if let Some(true) = result.downcast_ref::() { self.update_draft(); - match send_draft( + match send_draft_async( self.sign_mail, context, self.account_hash, self.draft.clone(), SpecialUsageMailbox::Sent, Flag::SEEN, - false, ) { - Ok(Some((job_id, handle, chan))) => { + Ok(job) => { + let (chan, handle, job_id) = context.job_executor.spawn_blocking(job); self.mode = ViewMode::WaitingForSendResult( UIDialog::new( "Waiting for confirmation.. The tab will close automatically on successful submission.", @@ -773,22 +749,12 @@ impl Component for Composer { Some(Box::new(move |id: ComponentId, results: &[char]| { Some(UIEvent::FinishedUIDialog( id, - Box::new(results.get(0).map(|c| *c).unwrap_or('c')), + Box::new(results.get(0).cloned().unwrap_or('c')), )) })), context, ), handle, job_id, chan); } - Ok(None) => { - context.replies.push_back(UIEvent::Notification( - Some("Sent.".into()), - String::new(), - Some(NotificationType::Info), - )); - context - .replies - .push_back(UIEvent::Action(Tab(Kill(self.id)))); - } Err(err) => { context.replies.push_back(UIEvent::Notification( None, @@ -1358,9 +1324,6 @@ impl Component for Composer { return true; } Action::Compose(ComposeAction::ToggleEncrypt) => { - let is_true = self.encrypt_mail.is_true(); - self.encrypt_mail = ToggleFlag::from(!is_true); - self.dirty = true; return true; } _ => {} @@ -1601,3 +1564,98 @@ pub fn save_draft( } } } + +pub fn send_draft_async( + sign_mail: ToggleFlag, + context: &mut Context, + account_hash: AccountHash, + mut draft: Draft, + mailbox_type: SpecialUsageMailbox, + flags: Flag, +) -> Result> + Send>>> { + let format_flowed = *account_settings!(context[account_hash].composing.format_flowed); + let event_sender = context.sender.clone(); + let mut filters_stack: Vec< + Box< + dyn FnOnce( + AttachmentBuilder, + ) + -> Pin> + Send>> + + Send, + >, + > = vec![]; + if sign_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()), + )?)); + } + let send_mail = account_settings!(context[account_hash].composing.send_mail).clone(); + let send_cb = context.accounts[&account_hash].send_async(send_mail); + let mut content_type = ContentType::default(); + if format_flowed { + if let ContentType::Text { + ref mut parameters, .. + } = content_type + { + parameters.push((b"format".to_vec(), b"flowed".to_vec())); + } + } + let mut body: AttachmentBuilder = Attachment::new( + content_type, + Default::default(), + std::mem::replace(&mut draft.body, String::new()).into_bytes(), + ) + .into(); + if !draft.attachments.is_empty() { + let mut parts = std::mem::replace(&mut draft.attachments, Vec::new()); + parts.insert(0, body); + let boundary = ContentType::make_boundary(&parts); + body = Attachment::new( + ContentType::Multipart { + boundary: boundary.into_bytes(), + kind: MultipartType::Mixed, + parts: parts.into_iter().map(|a| a.into()).collect::>(), + }, + Default::default(), + Vec::new(), + ) + .into(); + } + Ok(Box::pin(async move { + for f in filters_stack { + body = f(body).await?; + } + + draft.attachments.insert(0, body); + let message = Arc::new(draft.finalise()?); + let ret = send_cb(message.clone()).await; + let is_ok = ret.is_ok(); + event_sender + .send(ThreadEvent::UIEvent(UIEvent::Callback(CallbackFn( + Box::new(move |context| { + save_draft( + message.as_bytes(), + context, + if is_ok { + mailbox_type + } else { + SpecialUsageMailbox::Drafts + }, + if is_ok { + flags + } else { + Flag::SEEN | Flag::DRAFT + }, + account_hash, + ); + }), + )))) + .unwrap(); + ret + })) +} diff --git a/src/components/mail/pgp.rs b/src/components/mail/pgp.rs index 837c8d20..e0e40895 100644 --- a/src/components/mail/pgp.rs +++ b/src/components/mail/pgp.rs @@ -21,7 +21,9 @@ use super::*; use melib::email::pgp as melib_pgp; +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> { @@ -177,3 +179,69 @@ pub async fn verify(a: Attachment, gpg_binary: Option) -> Result })?, ) } + +pub fn sign_filter( + gpg_binary: Option, + pgp_key: Option, +) -> 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( + 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()) + }) + }, + ) +} diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs index 593f14b8..dc034673 100644 --- a/src/conf/accounts.rs +++ b/src/conf/accounts.rs @@ -48,6 +48,7 @@ use std::borrow::Cow; use std::collections::VecDeque; use std::convert::TryFrom; use std::fs; +use std::future::Future; use std::io; use std::ops::{Index, IndexMut}; use std::os::unix::fs::PermissionsExt; @@ -1277,6 +1278,68 @@ impl Account { } } + pub fn send_async( + &self, + send_mail: crate::conf::composing::SendMail, + ) -> impl FnOnce(Arc) -> Pin> + Send>> + Send { + |message: Arc| -> Pin> + Send>> { + Box::pin(async move { + use crate::conf::composing::SendMail; + use std::io::Write; + use std::process::{Command, Stdio}; + match send_mail { + SendMail::ShellCommand(ref command) => { + if command.is_empty() { + return Err(MeliError::new( + "send_mail shell command configuration value is empty", + )); + } + let mut msmtp = Command::new("sh") + .args(&["-c", command]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to start mailer command"); + { + let stdin = msmtp.stdin.as_mut().expect("failed to open stdin"); + stdin + .write_all(message.as_bytes()) + .expect("Failed to write to stdin"); + } + let output = msmtp.wait().expect("Failed to wait on mailer"); + if output.success() { + melib::log("Message sent.", melib::LoggingLevel::TRACE); + } else { + let error_message = if let Some(exit_code) = output.code() { + format!( + "Could not send e-mail using `{}`: Process exited with {}", + command, exit_code + ) + } else { + format!( + "Could not send e-mail using `{}`: Process was killed by signal", + command + ) + }; + melib::log(&error_message, melib::LoggingLevel::ERROR); + return Err(MeliError::new(error_message.clone()) + .set_summary("Message not sent.")); + } + Ok(()) + } + #[cfg(feature = "smtp")] + SendMail::Smtp(conf) => { + let mut smtp_connection = + melib::smtp::SmtpConnection::new_connection(conf).await?; + smtp_connection + .mail_transaction(message.as_str(), None) + .await + } + } + }) + } + } + pub fn delete( &mut self, env_hash: EnvelopeHash, diff --git a/src/state.rs b/src/state.rs index 5a1ddbba..0dff0a18 100644 --- a/src/state.rs +++ b/src/state.rs @@ -110,7 +110,7 @@ pub struct Context { /// Events queue that components send back to the state pub replies: VecDeque, - sender: Sender, + pub sender: Sender, receiver: Receiver, input_thread: InputHandler, pub job_executor: Arc,