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 indexmap::IndexSet;
|
||||||
use nix::sys::wait::WaitStatus;
|
use nix::sys::wait::WaitStatus;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
use std::future::Future;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
@ -81,7 +82,6 @@ pub struct Composer {
|
||||||
embed_area: Area,
|
embed_area: Area,
|
||||||
embed: Option<EmbedStatus>,
|
embed: Option<EmbedStatus>,
|
||||||
sign_mail: ToggleFlag,
|
sign_mail: ToggleFlag,
|
||||||
encrypt_mail: ToggleFlag,
|
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
has_changes: bool,
|
has_changes: bool,
|
||||||
initialized: bool,
|
initialized: bool,
|
||||||
|
@ -104,7 +104,6 @@ impl Default for Composer {
|
||||||
|
|
||||||
mode: ViewMode::Edit,
|
mode: ViewMode::Edit,
|
||||||
sign_mail: ToggleFlag::Unset,
|
sign_mail: ToggleFlag::Unset,
|
||||||
encrypt_mail: ToggleFlag::Unset,
|
|
||||||
dirty: true,
|
dirty: true,
|
||||||
has_changes: false,
|
has_changes: false,
|
||||||
embed_area: ((0, 0), (0, 0)),
|
embed_area: ((0, 0), (0, 0)),
|
||||||
|
@ -453,33 +452,15 @@ impl Composer {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.encrypt_mail.is_true() {
|
write_string_to_grid(
|
||||||
write_string_to_grid(
|
"☐ don't encrypt",
|
||||||
&format!(
|
grid,
|
||||||
"☑ encrypt with {}",
|
theme_default.fg,
|
||||||
account_settings!(context[self.account_hash].pgp.encrypt_key)
|
theme_default.bg,
|
||||||
.as_ref()
|
theme_default.attrs,
|
||||||
.map(|s| s.as_str())
|
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
|
||||||
.unwrap_or("default key")
|
None,
|
||||||
),
|
);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if attachments_no == 0 {
|
if attachments_no == 0 {
|
||||||
write_string_to_grid(
|
write_string_to_grid(
|
||||||
"no attachments",
|
"no attachments",
|
||||||
|
@ -552,11 +533,6 @@ impl Component for Composer {
|
||||||
context[self.account_hash].pgp.auto_sign
|
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()
|
if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
|
||||||
{
|
{
|
||||||
self.draft.set_header(
|
self.draft.set_header(
|
||||||
|
@ -752,16 +728,16 @@ impl Component for Composer {
|
||||||
{
|
{
|
||||||
if let Some(true) = result.downcast_ref::<bool>() {
|
if let Some(true) = result.downcast_ref::<bool>() {
|
||||||
self.update_draft();
|
self.update_draft();
|
||||||
match send_draft(
|
match send_draft_async(
|
||||||
self.sign_mail,
|
self.sign_mail,
|
||||||
context,
|
context,
|
||||||
self.account_hash,
|
self.account_hash,
|
||||||
self.draft.clone(),
|
self.draft.clone(),
|
||||||
SpecialUsageMailbox::Sent,
|
SpecialUsageMailbox::Sent,
|
||||||
Flag::SEEN,
|
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(
|
self.mode = ViewMode::WaitingForSendResult(
|
||||||
UIDialog::new(
|
UIDialog::new(
|
||||||
"Waiting for confirmation.. The tab will close automatically on successful submission.",
|
"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(Box::new(move |id: ComponentId, results: &[char]| {
|
||||||
Some(UIEvent::FinishedUIDialog(
|
Some(UIEvent::FinishedUIDialog(
|
||||||
id,
|
id,
|
||||||
Box::new(results.get(0).map(|c| *c).unwrap_or('c')),
|
Box::new(results.get(0).cloned().unwrap_or('c')),
|
||||||
))
|
))
|
||||||
})),
|
})),
|
||||||
context,
|
context,
|
||||||
), handle, job_id, chan);
|
), 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) => {
|
Err(err) => {
|
||||||
context.replies.push_back(UIEvent::Notification(
|
context.replies.push_back(UIEvent::Notification(
|
||||||
None,
|
None,
|
||||||
|
@ -1358,9 +1324,6 @@ impl Component for Composer {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Action::Compose(ComposeAction::ToggleEncrypt) => {
|
Action::Compose(ComposeAction::ToggleEncrypt) => {
|
||||||
let is_true = self.encrypt_mail.is_true();
|
|
||||||
self.encrypt_mail = ToggleFlag::from(!is_true);
|
|
||||||
self.dirty = true;
|
|
||||||
return 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 super::*;
|
||||||
use melib::email::pgp as melib_pgp;
|
use melib::email::pgp as melib_pgp;
|
||||||
|
use std::future::Future;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
pub fn verify_signature(a: &Attachment, context: &mut Context) -> Result<Vec<u8>> {
|
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::collections::VecDeque;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::future::Future;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
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(
|
pub fn delete(
|
||||||
&mut self,
|
&mut self,
|
||||||
env_hash: EnvelopeHash,
|
env_hash: EnvelopeHash,
|
||||||
|
|
|
@ -110,7 +110,7 @@ pub struct Context {
|
||||||
|
|
||||||
/// Events queue that components send back to the state
|
/// Events queue that components send back to the state
|
||||||
pub replies: VecDeque<UIEvent>,
|
pub replies: VecDeque<UIEvent>,
|
||||||
sender: Sender<ThreadEvent>,
|
pub sender: Sender<ThreadEvent>,
|
||||||
receiver: Receiver<ThreadEvent>,
|
receiver: Receiver<ThreadEvent>,
|
||||||
input_thread: InputHandler,
|
input_thread: InputHandler,
|
||||||
pub job_executor: Arc<JobExecutor>,
|
pub job_executor: Arc<JobExecutor>,
|
||||||
|
|
Loading…
Reference in New Issue