Browse Source

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.
jmap-eventsource
Manos Pitsidianakis 1 year ago
parent
commit
a2f11c341d
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 160
      src/components/mail/compose.rs
  2. 68
      src/components/mail/pgp.rs
  3. 63
      src/conf/accounts.rs
  4. 2
      src/state.rs

160
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<EmbedStatus>,
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::<bool>() {
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<Pin<Box<dyn Future<Output = 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<Box<dyn Future<Output = Result<AttachmentBuilder>> + 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::<Vec<_>>(),
},
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
}))
}

68
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<Vec<u8>> {
@ -177,3 +179,69 @@ pub async fn verify(a: Attachment, gpg_binary: Option<String>) -> Result<Vec<u8>
})?,
)
}
pub fn sign_filter(
gpg_binary: Option<String>,
pgp_key: Option<String>,
) -> Result<
impl FnOnce(AttachmentBuilder) -> Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + 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<Box<dyn Future<Output = Result<AttachmentBuilder>>+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::<Vec<_>>(),
},
Default::default(),
Vec::new(),
)
.into())
})
},
)
}

63
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<String>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send {
|message: Arc<String>| -> Pin<Box<dyn Future<Output = Result<()>> + 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,

2
src/state.rs

@ -110,7 +110,7 @@ pub struct Context {
/// Events queue that components send back to the state
pub replies: VecDeque<UIEvent>,
sender: Sender<ThreadEvent>,
pub sender: Sender<ThreadEvent>,
receiver: Receiver<ThreadEvent>,
input_thread: InputHandler,
pub job_executor: Arc<JobExecutor>,

Loading…
Cancel
Save