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
parent
afee1e2be5
commit
a2f11c341d
|
@ -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
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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…
Reference in New Issue