diff --git a/meli/src/mail/view/envelope.rs b/meli/src/mail/view/envelope.rs index 482acbb9..bcfb1473 100644 --- a/meli/src/mail/view/envelope.rs +++ b/meli/src/mail/view/envelope.rs @@ -1061,6 +1061,8 @@ impl Component for EnvelopeView { context, ) { self.filters.push(filter); + } else if let Ok(filter) = ViewFilter::new_attachment(&body, context) { + self.filters.push(filter); } } else if let Ok(filter) = ViewFilter::new_attachment(&body, context) { self.filters.push(filter); @@ -1070,35 +1072,46 @@ impl Component for EnvelopeView { ) .to_string(); } - if !self.initialised { + if dbg!(!self.initialised) { self.initialised = true; - let mut text = if let Some(ViewFilter { - filter_invocation, - body_text, - notice, - .. - }) = self.filters.last() - { - let mut text = if self.filters.len() == 1 { - if filter_invocation.is_empty() { - String::new() - } else { - format!("Text piped through `{filter_invocation}`\n\n") - } - } else { - notice - .as_ref() - .map(|s| s.to_string()) - .or_else(|| { - if filter_invocation.is_empty() { - None - } else { - Some(format!("Text piped through `{filter_invocation}`\n\n")) + let mut text = if !self.filters.is_empty() { + let mut text = String::new(); + if let Some(last) = self.filters.last() { + let mut stack = vec![last]; + while let Some(ViewFilter { + filter_invocation, + body_text, + notice, + .. + }) = stack.pop() + { + text.push_str( + ¬ice + .as_ref() + .map(|s| s.to_string()) + .or_else(|| { + if filter_invocation.is_empty() { + None + } else { + Some(format!("Text filtered by `{filter_invocation}`\n\n")) + } + }) + .unwrap_or_default(), + ); + match body_text { + ViewFilterContent::Filtered { inner } => text.push_str( + &self.options.convert(&mut self.links, &self.body, inner), + ), + ViewFilterContent::Error { inner } => text.push_str(&inner.to_string()), + ViewFilterContent::Running { .. } => { + text.push_str("Filter job running in background.") } - }) - .unwrap_or_default() - }; - text.push_str(&self.options.convert(&mut self.links, &self.body, body_text)); + ViewFilterContent::InlineAttachments { parts } => { + stack.extend(parts.iter()); + } + } + } + } text } else { self.options @@ -1156,6 +1169,134 @@ impl Component for EnvelopeView { } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { + if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(_))) { + match *event { + UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) + if self.active_jobs.contains(job_id) => + { + let mut caught = false; + for d in self.display.iter_mut() { + match d { + AttachmentDisplay::SignedPending { + ref mut inner, + handle, + display, + job_id: our_job_id, + } if *our_job_id == *job_id => { + caught = true; + match handle.chan.try_recv() { + Err(_) => { /* Job was canceled */ } + Ok(None) => { /* something happened, + * perhaps a worker thread + * panicked */ + } + Ok(Some(Ok(()))) => { + *d = AttachmentDisplay::SignedVerified { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + display: std::mem::take(display), + description: String::new(), + }; + } + Ok(Some(Err(error))) => { + *d = AttachmentDisplay::SignedFailed { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + display: std::mem::take(display), + error, + }; + } + } + } + AttachmentDisplay::EncryptedPending { + ref mut inner, + handle, + } if handle.job_id == *job_id => { + caught = true; + match handle.chan.try_recv() { + Err(_) => { /* Job was canceled */ } + Ok(None) => { /* something happened, + * perhaps a worker thread + * panicked */ + } + Ok(Some(Ok((metadata, decrypted_bytes)))) => { + let plaintext = Box::new( + AttachmentBuilder::new(&decrypted_bytes).build(), + ); + let mut plaintext_display = vec![]; + Self::attachment_to_display_helper( + &plaintext, + &self.main_loop_handler, + &mut self.active_jobs, + &mut plaintext_display, + &self.view_settings, + (&self.force_charset).into(), + ); + *d = AttachmentDisplay::EncryptedSuccess { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + plaintext, + plaintext_display, + description: format!("{:?}", metadata), + }; + } + Ok(Some(Err(error))) => { + *d = AttachmentDisplay::EncryptedFailed { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + error, + }; + } + } + } + _ => {} + } + } + if caught { + self.links.clear(); + self.regenerate_body_text(); + self.initialised = false; + self.set_dirty(true); + } + + self.active_jobs.remove(job_id); + } + UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) + if dbg!(self.filters.iter_mut().any(|filter| { + dbg!(&filter); + dbg!(if let Some(cb) = filter.event_handler { + if cb( + filter, + &mut UIEvent::StatusEvent(StatusEvent::JobFinished(*job_id)), + context, + ) { + return true; + } + }); + false + })) => + { + log::trace!( + "after calling job event handles, filters are: {:?}", + &self.filters + ); + self.links.clear(); + self.regenerate_body_text(); + self.initialised = false; + self.set_dirty(true); + } + _ => {} + } + log::trace!("envelope.rs got job finished: {:?}", event); + } match (&mut self.force_charset, &event) { (ForceCharset::Dialog(selector), UIEvent::FinishedUIDialog(id, results)) if *id == selector.id() => @@ -1669,103 +1810,6 @@ impl Component for EnvelopeView { self.dirty = true; return true; } - UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) - if self.active_jobs.contains(job_id) => - { - let mut caught = false; - for d in self.display.iter_mut() { - match d { - AttachmentDisplay::SignedPending { - ref mut inner, - handle, - display, - job_id: our_job_id, - } if *our_job_id == *job_id => { - caught = true; - match handle.chan.try_recv() { - Err(_) => { /* Job was canceled */ } - Ok(None) => { /* something happened, - * perhaps a worker thread - * panicked */ - } - Ok(Some(Ok(()))) => { - *d = AttachmentDisplay::SignedVerified { - inner: std::mem::replace( - inner, - Box::new(AttachmentBuilder::new(&[]).build()), - ), - display: std::mem::take(display), - description: String::new(), - }; - } - Ok(Some(Err(error))) => { - *d = AttachmentDisplay::SignedFailed { - inner: std::mem::replace( - inner, - Box::new(AttachmentBuilder::new(&[]).build()), - ), - display: std::mem::take(display), - error, - }; - } - } - } - AttachmentDisplay::EncryptedPending { - ref mut inner, - handle, - } if handle.job_id == *job_id => { - caught = true; - match handle.chan.try_recv() { - Err(_) => { /* Job was canceled */ } - Ok(None) => { /* something happened, - * perhaps a worker thread - * panicked */ - } - Ok(Some(Ok((metadata, decrypted_bytes)))) => { - let plaintext = - Box::new(AttachmentBuilder::new(&decrypted_bytes).build()); - let mut plaintext_display = vec![]; - Self::attachment_to_display_helper( - &plaintext, - &self.main_loop_handler, - &mut self.active_jobs, - &mut plaintext_display, - &self.view_settings, - (&self.force_charset).into(), - ); - *d = AttachmentDisplay::EncryptedSuccess { - inner: std::mem::replace( - inner, - Box::new(AttachmentBuilder::new(&[]).build()), - ), - plaintext, - plaintext_display, - description: format!("{:?}", metadata), - }; - } - Ok(Some(Err(error))) => { - *d = AttachmentDisplay::EncryptedFailed { - inner: std::mem::replace( - inner, - Box::new(AttachmentBuilder::new(&[]).build()), - ), - error, - }; - } - } - } - _ => {} - } - } - if caught { - self.links.clear(); - self.regenerate_body_text(); - self.initialised = false; - self.set_dirty(true); - } - - self.active_jobs.remove(job_id); - } _ => {} } false diff --git a/meli/src/mail/view/filters.rs b/meli/src/mail/view/filters.rs index d598a57f..cac642c7 100644 --- a/meli/src/mail/view/filters.rs +++ b/meli/src/mail/view/filters.rs @@ -31,25 +31,77 @@ type ProcessEventFn = fn(&mut ViewFilter, &mut UIEvent, &mut Context) -> bool; use melib::{ attachment_types::{ContentType, MultipartType, Text}, error::*, + log, parser::BytesExt, text::Truncate, utils::xdg::query_default_app, - Attachment, Result, + Attachment, AttachmentBuilder, Result, }; use crate::{ components::*, desktop_exec_to_command, + jobs::{JobId, JoinHandle}, terminal::{Area, CellBuffer}, - Context, ErrorKind, File, StatusEvent, UIEvent, + try_recv_timeout, Context, ErrorKind, File, StatusEvent, UIEvent, }; -#[derive(Clone)] +use smallvec::SmallVec; + +type FilterResult = std::result::Result<(Attachment, Vec), (Error, Vec)>; +type OnSuccessNoticeCb = Arc Cow<'static, str>) + Send + Sync>; + +pub enum ViewFilterContent { + Running { + job_id: JobId, + on_success_notice_cb: OnSuccessNoticeCb, + job_handle: JoinHandle, + }, + Error { + inner: Error, + }, + Filtered { + inner: String, + }, + InlineAttachments { + parts: Vec, + }, +} + +impl std::fmt::Debug for ViewFilterContent { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + use ViewFilterContent::*; + match self { + Running { + ref job_id, + on_success_notice_cb: _, + job_handle: _, + } => fmt + .debug_struct(stringify!(ViewFilterContent::Running)) + .field("job_id", &job_id) + .finish(), + Error { ref inner } => fmt + .debug_struct(stringify!(ViewFilterContent::Error)) + .field("error", inner) + .finish(), + Filtered { ref inner } => fmt + .debug_struct(stringify!(ViewFilterContent::Filtered)) + .field("body_text", &inner.trim_at_boundary(18)) + .field("body_text_len", &inner.len()) + .finish(), + InlineAttachments { ref parts } => fmt + .debug_struct(stringify!(ViewFilterContent::InlineAttachments)) + .field("parts", &parts.len()) + .finish(), + } + } +} + pub struct ViewFilter { pub filter_invocation: String, pub content_type: ContentType, pub notice: Option>, - pub body_text: String, + pub body_text: ViewFilterContent, pub unfiltered: Vec, pub event_handler: Option, pub id: ComponentId, @@ -61,8 +113,7 @@ impl std::fmt::Debug for ViewFilter { .field("filter_invocation", &self.filter_invocation) .field("content_type", &self.content_type) .field("notice", &self.notice) - .field("body_text", &self.body_text.trim_at_boundary(18)) - .field("body_text_len", &self.body_text.len()) + .field("body_text", &self.body_text) .field("event_handler", &self.event_handler.is_some()) .field("id", &self.id) .finish() @@ -171,53 +222,99 @@ impl ViewFilter { _ => {} } } + let settings = &context.settings; + let (filter_invocation, cmd, args): ( + Cow<'static, str>, + &'static str, + SmallVec<[Cow<'static, str>; 8]>, + ) = if let Some(filter_invocation) = settings.pager.html_filter.as_ref() { + ( + filter_invocation.to_string().into(), + "sh", + smallvec::smallvec!["-c".into(), filter_invocation.to_string().into()], + ) + } else { + ( + "w3m -I utf-8 -T text/html".into(), + "w3m", + smallvec::smallvec!["-I".into(), "utf-8".into(), "-T".into(), "text/html".into()], + ) + }; let bytes: Vec = att.decode(Default::default()); - let settings = &context.settings; - if let Some(filter_invocation) = settings.pager.html_filter.as_ref() { - match run("sh", &["-c", filter_invocation], &bytes) { + let filter_invocation2 = filter_invocation.to_string(); + let bytes2 = bytes.clone(); + let job = async move { + let filter_invocation = filter_invocation2; + let bytes = bytes2; + let borrowed_args = args + .iter() + .map(|a| a.as_ref()) + .collect::>(); + match run(cmd, &borrowed_args, &bytes) { Err(err) => { - return Err(Error::new(format!( - "Failed to start html filter process `{}`", - filter_invocation, - )) - .set_source(Some(Arc::new(err))) - .set_kind(ErrorKind::External)); + return Err(( + Error::new(format!( + "Failed to start html filter process `{}`", + filter_invocation, + )) + .set_source(Some(Arc::new(err))) + .set_kind(ErrorKind::External), + bytes, + )); } Ok(body_text) => { - let notice = - Some(format!("Text piped through `{}`.\n\n", filter_invocation).into()); - return Ok(Self { - filter_invocation: filter_invocation.clone(), - content_type: att.content_type.clone(), - notice, - body_text, - unfiltered: bytes, - event_handler: Some(Self::html_process_event), - id: ComponentId::default(), - }); + let mut att = AttachmentBuilder::default(); + att.set_raw(body_text.into_bytes()).set_body_to_raw(); + return Ok((att.build(), bytes)); } } + }; + let filter_invocation2 = filter_invocation.to_string(); + let open_html_shortcut = settings.shortcuts.envelope_view.open_html.clone(); + let on_success_notice_cb = move || { + format!( + "Text piped through `{}` Press `{}` to open in web browser.\n\n", + filter_invocation2, open_html_shortcut + ) + .into() + }; + let mut job_handle = context + .main_loop_handler + .job_executor + .spawn_blocking(filter_invocation.to_string().into(), job); + let mut retval = Self { + filter_invocation: filter_invocation.to_string(), + content_type: att.content_type.clone(), + notice: None, + unfiltered: bytes, + body_text: ViewFilterContent::Filtered { + inner: String::new(), + }, + event_handler: Some(Self::job_process_event), + id: ComponentId::default(), + }; + if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) { + retval.event_handler = Some(Self::html_process_event); + Self::process_job_result( + &mut retval, + Ok(Some(job_result)), + Arc::new(on_success_notice_cb), + context, + ); + return Ok(retval); } - if let Ok(body_text) = run("w3m", &["-I", "utf-8", "-T", "text/html"], &bytes) { - return Ok(Self { - filter_invocation: "w3m -I utf-8 -T text/html".into(), - content_type: att.content_type.clone(), - notice: Some("Text piped through `w3m -I utf-8 -T text/html`.\n\n".into()), - body_text, - unfiltered: bytes, - event_handler: Some(Self::html_process_event), - id: ComponentId::default(), - }); - } - - Err( - Error::new("Failed to find any application to use as html filter") - .set_kind(ErrorKind::Configuration), - ) + return Ok(Self { + body_text: ViewFilterContent::Running { + job_id: job_handle.job_id, + on_success_notice_cb: Arc::new(on_success_notice_cb), + job_handle, + }, + ..retval + }); } - pub fn new_attachment(att: &Attachment, context: &mut Context) -> Result { + pub fn new_attachment(att: &Attachment, context: &Context) -> Result { if matches!( att.content_type, ContentType::Other { .. } | ContentType::OctetStream { .. } @@ -247,15 +344,21 @@ impl ViewFilter { .. } = att.content_type { - if let Some(v @ Ok(_)) = parts.iter().find_map(|p| { - if let v @ Ok(_) = Self::new_attachment(p, context) { - Some(v) - } else { - None - } - }) { - return v; - } + let notice = Some(format!("multipart/related with {} parts.\n\n", parts.len()).into()); + return Ok(Self { + filter_invocation: String::new(), + content_type: att.content_type.clone(), + notice, + body_text: ViewFilterContent::InlineAttachments { + parts: parts + .into_iter() + .filter_map(|p| ViewFilter::new_attachment(p, context).ok()) + .collect::>(), + }, + unfiltered: att.decode(Default::default()), + event_handler: None, + id: ComponentId::default(), + }); } if att.is_html() { return Self::new_html(att, context); @@ -271,13 +374,87 @@ impl ViewFilter { filter_invocation: String::new(), content_type: att.content_type.clone(), notice: None, - body_text: String::new(), + body_text: ViewFilterContent::Filtered { + inner: String::new(), + }, unfiltered: vec![], event_handler: None, id: ComponentId::default(), }); - } - if let ContentType::Multipart { + } else if let ContentType::Multipart { + kind: MultipartType::Encrypted, + ref parts, + .. + } = att.content_type + { + #[cfg(not(feature = "gpgme"))] + { + return Ok(Self { + filter_invocation: String::new(), + content_type: att.content_type.clone(), + notice: None, + body_text: ViewFilterContent::Error { + inner: Error::new( + "Cannot decrypt: meli must be compiled with libgpgme support.", + ), + }, + unfiltered: vec![], + event_handler: None, + id: ComponentId::default(), + }); + } + #[cfg(feature = "gpgme")] + { + for a in parts { + if a.content_type == "application/octet-stream" { + let content = a.raw(); + let bytes = content.trim().to_vec(); + let decrypt_fut = async { + let (_metadata, bytes) = crate::mail::pgp::decrypt( + melib::email::pgp::convert_attachment_to_rfc_spec(&bytes), + ) + .await + .map_err(|err| (err, bytes))?; + Ok((AttachmentBuilder::new(&bytes).build(), bytes)) + }; + let mut job_handle = context + .main_loop_handler + .job_executor + .spawn_specialized("gpg::decrypt".into(), decrypt_fut); + let on_success_notice_cb = || "Decrypted content.\n\n".into(); + let mut retval = Self { + filter_invocation: "gpg::decrypt".into(), + content_type: att.content_type.clone(), + notice: None, + body_text: ViewFilterContent::Filtered { + inner: String::new(), + }, + unfiltered: a.raw().to_vec(), + event_handler: Some(Self::job_process_event), + id: ComponentId::default(), + }; + if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) { + retval.event_handler = None; + Self::process_job_result( + &mut retval, + Ok(Some(job_result)), + Arc::new(on_success_notice_cb), + context, + ); + return Ok(retval); + } + return Ok(Self { + body_text: ViewFilterContent::Running { + job_id: job_handle.job_id, + on_success_notice_cb: Arc::new(on_success_notice_cb), + job_handle, + }, + ..retval + }); + } + } + } + } else if let ContentType::Multipart { kind: MultipartType::Mixed, ref parts, .. @@ -294,12 +471,72 @@ impl ViewFilter { return Ok(res); } } - let notice = Some("Viewing attachment.\n\n".into()); + #[cfg(feature = "gpgme")] + if let ContentType::Text { + kind: Text::Plain, .. + } = att.content_type + { + let content = att.text(); + if content + .trim_start() + .starts_with("-----BEGIN PGP MESSAGE-----") + && content.trim_end().ends_with("-----END PGP MESSAGE-----") + { + let bytes = content.trim().to_string().into_bytes(); + let decrypt_fut = async { + let (_metadata, bytes) = crate::mail::pgp::decrypt( + melib::email::pgp::convert_attachment_to_rfc_spec(&bytes), + ) + .await + .map_err(|err| (err, bytes))?; + Ok((AttachmentBuilder::new(&bytes).build(), bytes)) + }; + let mut job_handle = context + .main_loop_handler + .job_executor + .spawn_specialized("gpg::decrypt".into(), decrypt_fut); + let on_success_notice_cb = || "Decrypted content.\n\n".into(); + let mut retval = Self { + filter_invocation: "gpg::decrypt".into(), + content_type: att.content_type.clone(), + notice: None, + body_text: ViewFilterContent::Filtered { + inner: String::new(), + }, + unfiltered: content.into_bytes(), + event_handler: Some(Self::job_process_event), + id: ComponentId::default(), + }; + if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) { + retval.event_handler = None; + Self::process_job_result( + &mut retval, + Ok(Some(job_result)), + Arc::new(on_success_notice_cb), + context, + ); + return Ok(retval); + } + return Ok(Self { + body_text: ViewFilterContent::Running { + job_id: job_handle.job_id, + on_success_notice_cb: Arc::new(on_success_notice_cb), + job_handle, + }, + ..retval + }); + } + } + let notice = if att.content_type.is_text_plain() { + None + } else { + Some("Viewing attachment.\n\n".into()) + }; Ok(Self { filter_invocation: String::new(), content_type: att.content_type.clone(), notice, - body_text: att.text(), + body_text: ViewFilterContent::Filtered { inner: att.text() }, unfiltered: att.decode(Default::default()), event_handler: None, id: ComponentId::default(), @@ -372,6 +609,97 @@ impl ViewFilter { } false } + + pub fn contains_job_id(&self, match_job_id: JobId) -> bool { + if let ViewFilterContent::Running { ref job_id, .. } = self.body_text { + return *job_id == match_job_id; + } + if let ViewFilterContent::InlineAttachments { ref parts, .. } = self.body_text { + return parts.iter().any(|p| p.contains_job_id(match_job_id)); + } + false + } + + fn job_process_event(_self: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool { + log::trace!( + "job_process_event: _self = {:?}, event = {:?}", + _self, + event + ); + if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) if _self.contains_job_id(*job_id)) + { + if let ViewFilterContent::Running { + job_id: _, + mut job_handle, + on_success_notice_cb, + } = std::mem::replace( + &mut _self.body_text, + ViewFilterContent::Filtered { + inner: String::new(), + }, + ) { + log::trace!("job_process_event: inside if let "); + let job_result = job_handle.chan.try_recv(); + Self::process_job_result(_self, job_result, on_success_notice_cb, context); + } + return true; + } + false + } + + fn process_job_result( + _self: &mut Self, + result: std::result::Result, ::futures::channel::oneshot::Canceled>, + on_success_notice_cb: OnSuccessNoticeCb, + context: &Context, + ) { + match result { + Err(err) => { + _self.event_handler = None; + /* Job was cancelled */ + _self.body_text = ViewFilterContent::Error { + inner: Error::new("Job was cancelled.").set_source(Some(Arc::new(err))), + }; + _self.notice = Some(format!("{} cancelled", _self.filter_invocation).into()); + } + Ok(None) => { + _self.event_handler = None; + // something happened, perhaps a worker thread panicked + _self.body_text = ViewFilterContent::Error { + inner: Error::new( + "Unknown error. Maybe some process panicked in the background?", + ), + }; + _self.notice = Some(format!("{} failed", _self.filter_invocation).into()); + } + Ok(Some(Ok((att, bytes)))) => { + _self.event_handler = None; + log::trace!("job_process_event: OK "); + match ViewFilter::new_attachment(&att, context) { + Ok(mut new_self) => { + if _self.content_type.is_text_html() { + new_self.event_handler = Some(Self::html_process_event); + } + new_self.unfiltered = bytes; + new_self.notice = Some(on_success_notice_cb()); + *_self = new_self; + } + Err(err) => { + _self.body_text = ViewFilterContent::Error { inner: err }; + _self.notice = Some( + format!("decoding result of {} failed", _self.filter_invocation).into(), + ); + } + } + } + Ok(Some(Err((error, bytes)))) => { + _self.event_handler = None; + _self.body_text = ViewFilterContent::Error { inner: error }; + _self.unfiltered = bytes; + _self.notice = Some(format!("{} failed", _self.filter_invocation).into()); + } + } + } } impl Component for ViewFilter {