diff options
author | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2020-10-05 18:43:08 +0300 |
---|---|---|
committer | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2020-10-05 21:10:00 +0300 |
commit | 23ca41e3e878abb9e9206e984b6c1e1d2b905aaa (patch) | |
tree | 0a3c053b304fa349cfa999d3bdb2b1814ca7632a /src/components/mail/view.rs | |
parent | b9c07bacef256e781a2f6884f9e01f8f211d4741 (diff) |
add libgpgme feature
Diffstat (limited to 'src/components/mail/view.rs')
-rw-r--r-- | src/components/mail/view.rs | 1391 |
1 files changed, 981 insertions, 410 deletions
diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 726cfef2..3329c82f 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -22,6 +22,7 @@ use super::*; use crate::conf::accounts::JobRequest; use crate::jobs::{oneshot, JobId}; +use melib::email::attachment_types::ContentType; use melib::list_management; use melib::parser::BytesExt; use smallvec::SmallVec; @@ -29,6 +30,7 @@ use std::collections::HashSet; use std::io::Write; use std::convert::TryFrom; +use std::os::unix::fs::PermissionsExt; use std::process::{Command, Stdio}; mod html; @@ -39,7 +41,7 @@ pub use self::thread::*; mod envelope; pub use self::envelope::*; -use linkify::{Link, LinkFinder}; +use linkify::LinkFinder; use xdg_utils::query_default_app; #[derive(PartialEq, Copy, Clone, Debug)] @@ -54,7 +56,7 @@ enum ViewMode { Url, Attachment(usize), Source(Source), - Ansi(RawBuffer), + //Ansi(RawBuffer), Subview, ContactSelector(UIDialog<Card>), } @@ -66,12 +68,14 @@ impl Default for ViewMode { } impl ViewMode { + /* fn is_ansi(&self) -> bool { match self { ViewMode::Ansi(_) => true, _ => false, } } + */ fn is_attachment(&self) -> bool { match self { ViewMode::Attachment(_) => true, @@ -86,6 +90,56 @@ impl ViewMode { } } +#[derive(Debug)] +pub enum AttachmentDisplay { + InlineText { + inner: Attachment, + text: String, + }, + InlineOther { + inner: Attachment, + }, + Attachment { + inner: Attachment, + }, + SignedPending { + inner: Attachment, + display: Vec<AttachmentDisplay>, + chan: + std::result::Result<oneshot::Receiver<Result<()>>, oneshot::Receiver<Result<Vec<u8>>>>, + job_id: JobId, + }, + SignedFailed { + inner: Attachment, + display: Vec<AttachmentDisplay>, + error: MeliError, + }, + SignedUnverified { + inner: Attachment, + display: Vec<AttachmentDisplay>, + }, + SignedVerified { + inner: Attachment, + display: Vec<AttachmentDisplay>, + description: String, + }, + EncryptedPending { + inner: Attachment, + chan: oneshot::Receiver<Result<(melib::pgp::DecryptionMetadata, Vec<u8>)>>, + job_id: JobId, + }, + EncryptedFailed { + inner: Attachment, + error: MeliError, + }, + EncryptedSuccess { + inner: Attachment, + plaintext: Attachment, + plaintext_display: Vec<AttachmentDisplay>, + description: String, + }, +} + /// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more /// menus #[derive(Debug, Default)] @@ -98,6 +152,7 @@ pub struct MailView { mode: ViewMode, expand_headers: bool, attachment_tree: String, + attachment_paths: Vec<Vec<usize>>, headers_no: usize, headers_cursor: usize, force_draw_headers: bool, @@ -117,7 +172,7 @@ pub enum PendingReplyAction { } #[derive(Debug)] -pub enum MailViewState { +enum MailViewState { Init { pending_action: Option<PendingReplyAction>, }, @@ -132,10 +187,25 @@ pub enum MailViewState { Loaded { bytes: Vec<u8>, body: Attachment, + display: Vec<AttachmentDisplay>, body_text: String, + links: Vec<Link>, }, } +#[derive(Copy, Clone, Debug)] +enum LinkKind { + Url, + Email, +} + +#[derive(Debug, Copy, Clone)] +struct Link { + start: usize, + end: usize, + kind: LinkKind, +} + impl Default for MailViewState { fn default() -> Self { MailViewState::Init { @@ -152,6 +222,7 @@ impl Clone for MailView { pager: self.pager.clone(), mode: ViewMode::Normal, attachment_tree: self.attachment_tree.clone(), + attachment_paths: self.attachment_paths.clone(), state: MailViewState::default(), active_jobs: self.active_jobs.clone(), ..*self @@ -182,6 +253,7 @@ impl MailView { mode: ViewMode::Normal, expand_headers: false, attachment_tree: String::new(), + attachment_paths: vec![], headers_no: 5, headers_cursor: 0, @@ -235,11 +307,24 @@ impl MailView { .populate_headers(&bytes); } let body = AttachmentBuilder::new(&bytes).build(); - let body_text = self.attachment_to_text(&body, context); + let display = Self::attachment_to( + &body, + context, + self.coordinates, + &mut self.active_jobs, + ); + let (paths, attachment_tree_s) = + self.attachment_displays_to_tree(&display); + self.attachment_tree = attachment_tree_s; + self.attachment_paths = paths; + let body_text = + self.attachment_displays_to_text(&display, context); self.state = MailViewState::Loaded { + display, body, bytes, body_text, + links: vec![], }; } Err(err) => { @@ -331,132 +416,418 @@ impl MailView { .push_back(UIEvent::Action(Tab(New(Some(composer))))); } - /// Returns the string to be displayed in the Viewer - fn attachment_to_text(&mut self, body: &Attachment, context: &mut Context) -> String { - let finder = LinkFinder::new(); - let coordinates = self.coordinates; - let body_text = String::from_utf8_lossy(&decode_rec( - body, - Some(Box::new(move |a: &Attachment, v: &mut Vec<u8>| { - if a.content_type().is_text_html() { - /* FIXME: duplication with view/html.rs */ - if let Some(filter_invocation) = - mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_filter) - .as_ref() - { - let command_obj = Command::new("sh") - .args(&["-c", filter_invocation]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn(); - match command_obj { - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!( - "Failed to start html filter process: {}", - filter_invocation, - )), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - return; - } - Ok(mut html_filter) => { - html_filter - .stdin - .as_mut() - .unwrap() - .write_all(&v) - .expect("Failed to write to stdin"); - *v = format!( + fn attachment_displays_to_text( + &self, + displays: &[AttachmentDisplay], + context: &mut Context, + ) -> String { + let mut acc = String::new(); + for d in displays { + use AttachmentDisplay::*; + match d { + InlineText { inner: _, text } => acc.push_str(&text), + InlineOther { inner } => { + if !acc.ends_with("\n\n") { + acc.push_str("\n\n"); + } + acc.push_str(&inner.to_string()); + if !acc.ends_with("\n\n") { + acc.push_str("\n\n"); + } + } + Attachment { inner: _ } => {} + SignedPending { + inner: _, + display, + chan: _, + job_id: _, + } => { + acc.push_str("Waiting for signature verification.\n\n"); + acc.push_str(&self.attachment_displays_to_text(display, context)); + } + SignedUnverified { inner: _, display } => { + acc.push_str("Unverified signature.\n\n"); + acc.push_str(&self.attachment_displays_to_text(display, context)) + } + SignedFailed { + inner: _, + display, + error, + } => { + acc.push_str(&format!("Failed to verify signature: {}.\n\n", error)); + acc.push_str(&self.attachment_displays_to_text(display, context)); + } + SignedVerified { + inner: _, + display, + description, + } => { + if description.is_empty() { + acc.push_str("Verified signature.\n\n"); + } else { + acc.push_str(&description); + acc.push_str("\n\n"); + } + acc.push_str(&self.attachment_displays_to_text(display, context)); + } + EncryptedPending { .. } => acc.push_str("Waiting for decryption result."), + EncryptedFailed { inner: _, error } => { + acc.push_str(&format!("Decryption failed: {}.", &error)) + } + EncryptedSuccess { + inner: _, + plaintext: _, + plaintext_display, + description, + } => { + if description.is_empty() { + acc.push_str("Succesfully decrypted.\n\n"); + } else { + acc.push_str(&description); + acc.push_str("\n\n"); + } + acc.push_str(&self.attachment_displays_to_text(plaintext_display, context)); + } + } + } + acc + } + + fn attachment_displays_to_tree( + &self, + displays: &[AttachmentDisplay], + ) -> (Vec<Vec<usize>>, String) { + let mut acc = String::new(); + let mut branches = SmallVec::new(); + let mut paths = Vec::with_capacity(displays.len()); + let mut cur_path = vec![]; + let mut idx = 0; + for (i, d) in displays.iter().enumerate() { + use AttachmentDisplay::*; + cur_path.push(i); + match d { + InlineText { inner, text: _ } + | InlineOther { inner } + | Attachment { inner } + | SignedPending { + inner, + display: _, + chan: _, + job_id: _, + } + | SignedUnverified { inner, display: _ } + | SignedFailed { + inner, + display: _, + error: _, + } + | SignedVerified { + inner, + display: _, + description: _, + } + | EncryptedPending { + inner, + chan: _, + job_id: _, + } + | EncryptedFailed { inner, error: _ } + | EncryptedSuccess { + inner: _, + plaintext: inner, + plaintext_display: _, + description: _, + } => { + attachment_tree( + (&mut idx, (0, inner)), + &mut branches, + &mut paths, + &mut cur_path, + i + 1 < displays.len(), + &mut acc, + ); + } + } + cur_path.pop(); + idx += 1; + } + (paths, acc) + } + + fn attachment_to( + body: &Attachment, + context: &mut Context, + coordinates: (AccountHash, MailboxHash, EnvelopeHash), + active_jobs: &mut HashSet<JobId>, + ) -> Vec<AttachmentDisplay> { + let mut ret = vec![]; + fn rec( + a: &Attachment, + context: &mut Context, + coordinates: (AccountHash, MailboxHash, EnvelopeHash), + acc: &mut Vec<AttachmentDisplay>, + active_jobs: &mut HashSet<JobId>, + ) { + if a.content_disposition.kind.is_attachment() { + acc.push(AttachmentDisplay::Attachment { inner: a.clone() }); + } else if a.content_type().is_text_html() { + let bytes = decode(a, None); + let filter_invocation = + mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_filter) + .as_ref() + .map(|s| s.as_str()) + .unwrap_or("w3m -I utf-8 -T text/html"); + let command_obj = Command::new("sh") + .args(&["-c", filter_invocation]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn(); + match command_obj { + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!( + "Failed to start html filter process: {}", + filter_invocation, + )), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + )); + let mut s = format!( + "Failed to start html filter process: `{}`. Press `v` to open in web browser. \n\n", + filter_invocation + ); + s.push_str(&String::from_utf8_lossy(&bytes)); + acc.push(AttachmentDisplay::InlineText { + inner: a.clone(), + text: s, + }); + } + Ok(mut html_filter) => { + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&bytes) + .expect("Failed to write to stdin"); + let mut s = format!( "Text piped through `{}`. Press `v` to open in web browser. \n\n", filter_invocation - ) - .into_bytes(); - v.extend(html_filter.wait_with_output().unwrap().stdout); + ); + s.push_str(&String::from_utf8_lossy( + &html_filter.wait_with_output().unwrap().stdout, + )); + acc.push(AttachmentDisplay::InlineText { + inner: a.clone(), + text: s, + }); + } + } + } else if a.is_text() { + let bytes = decode(a, None); + acc.push(AttachmentDisplay::InlineText { + inner: a.clone(), + text: String::from_utf8_lossy(&bytes).to_string(), + }); + } else if let ContentType::Multipart { + ref kind, + ref parts, + .. + } = a.content_type + { + match kind { + MultipartType::Alternative => { + if let Some(text_attachment_pos) = + parts.iter().position(|a| a.content_type == "text/plain") + { + let bytes = decode(&parts[text_attachment_pos], None); + acc.push(AttachmentDisplay::InlineText { + inner: a.clone(), + text: String::from_utf8_lossy(&bytes).to_string(), + }); + } else { + for a in parts { + rec(a, context, coordinates, acc, active_jobs); } } - } else { - match Command::new("w3m") - .args(&["-I", "utf-8", "-T", "text/html"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - { - Ok(mut html_filter) => { - html_filter - .stdin - .as_mut() - .unwrap() - .write_all(&v) - .expect("Failed to write to html filter stdin"); - *v = String::from( - "Text piped through `w3m`. Press `v` to open in web browser. \n\n", - ) - .into_bytes(); - v.extend(html_filter.wait_with_output().unwrap().stdout); + } + MultipartType::Signed => { + if *mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pgp + .auto_verify_signatures + ) { + if let Some(bin) = mailbox_settings!( + context[coordinates.0][&coordinates.1].pgp.gpg_binary + ) { + let verify_fut = crate::components::mail::pgp::verify( + a.clone(), + Some(bin.to_string()), + ); + let (chan, _handle, job_id) = + context.job_executor.spawn_blocking(verify_fut); + active_jobs.insert(job_id); + acc.push(AttachmentDisplay::SignedPending { + inner: a.clone(), + display: { + let mut v = vec![]; + rec(&parts[0], context, coordinates, &mut v, active_jobs); + v + }, + chan: Err(chan), + job_id, + }); + } else { + #[cfg(not(feature = "gpgme"))] + { + acc.push(AttachmentDisplay::SignedUnverified { + inner: a.clone(), + display: { + let mut v = vec![]; + rec( + &parts[0], + context, + coordinates, + &mut v, + active_jobs, + ); + v + }, + }); + } + #[cfg(feature = "gpgme")] + match melib::gpgme::Context::new().and_then(|mut ctx| { + let sig = ctx.new_data_mem(&parts[1].raw())?; + let mut f = std::fs::File::create("/tmp/sig").unwrap(); + f.write_all(&parts[1].raw())?; + let mut f = std::fs::File::create("/tmp/data").unwrap(); + f.write_all(&parts[0].raw())?; + let data = ctx.new_data_mem(&parts[0].raw())?; + ctx.verify(sig, data) + }) { + Ok(verify_fut) => { + let (chan, _handle, job_id) = + context.job_executor.spawn_specialized(verify_fut); + active_jobs.insert(job_id); + acc.push(AttachmentDisplay::SignedPending { + inner: a.clone(), + display: { + let mut v = vec![]; + rec( + &parts[0], + context, + coordinates, + &mut v, + active_jobs, + ); + v + }, + chan: Ok(chan), + job_id, + }); + } + Err(error) => { + acc.push(AttachmentDisplay::SignedFailed { + inner: a.clone(), + display: { + let mut v = vec![]; + rec( + &parts[0], + context, + coordinates, + &mut v, + active_jobs, + ); + v + }, + error, + }); + } + } } - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some("Failed to launch w3m to use as html filter".to_string()), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); + } else { + acc.push(AttachmentDisplay::SignedUnverified { + inner: a.clone(), + display: { + let mut v = vec![]; + rec(&parts[0], context, coordinates, &mut v, active_jobs); + v + }, + }); + } + } + MultipartType::Encrypted => { + for a in parts { + if a.content_type == "application/octet-stream" { + if *mailbox_settings!( + context[coordinates.0][&coordinates.1].pgp.auto_decrypt + ) { + let _verify = *mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pgp + .auto_verify_signatures + ); + if let Some(bin) = mailbox_settings!( + context[coordinates.0][&coordinates.1].pgp.gpg_binary + ) { + let decrypt_fut = crate::components::mail::pgp::decrypt( + a.raw().to_vec(), + Some(bin.to_string()), + None, + ); + let (chan, _handle, job_id) = + context.job_executor.spawn_blocking(decrypt_fut); + active_jobs.insert(job_id); + acc.push(AttachmentDisplay::EncryptedPending { + inner: a.clone(), + chan, + job_id, + }); + } else { + #[cfg(not(feature = "gpgme"))] + { + acc.push(AttachmentDisplay::EncryptedFailed { + inner: a.clone(), + error: MeliError::new("Cannot decrypt: define `gpg_binary` in configuration."), + }); + } + #[cfg(feature = "gpgme")] + match melib::gpgme::Context::new().and_then(|mut ctx| { + let cipher = ctx.new_data_mem(&a.raw())?; + ctx.decrypt(cipher) + }) { + Ok(decrypt_fut) => { + let (chan, _handle, job_id) = context + .job_executor + .spawn_specialized(decrypt_fut); + active_jobs.insert(job_id); + acc.push(AttachmentDisplay::EncryptedPending { + inner: a.clone(), + chan, + job_id, + }); + } + Err(error) => { + acc.push(AttachmentDisplay::EncryptedFailed { + inner: a.clone(), + error, + }); + } + } + } + } } } } - } else if a.is_signed() { - v.clear(); - if context.settings.pgp.auto_verify_signatures { - v.extend(crate::mail::pgp::verify_signature(a, context).into_iter()); + _ => { + for a in parts { + rec(a, context, coordinates, acc, active_jobs); + } } } - })), - )) - .into_owned(); - if body.count_attachments() > 1 { - self.attachment_tree.clear(); - attachment_tree( - (&mut 0, (0, &body)), - &mut SmallVec::new(), - false, - &mut self.attachment_tree, - ); - } - match self.mode { - ViewMode::Normal - | ViewMode::Subview - | ViewMode::ContactSelector(_) - | ViewMode::Source(Source::Decoded) => { - format!("{}\n\n{}", body_text, self.attachment_tree) - } - ViewMode::Source(Source::Raw) => String::from_utf8_lossy(body.body()).into_owned(), - ViewMode::Url => { - let mut t = body_text; - for (lidx, l) in finder.links(&body.text()).enumerate() { - let offset = if lidx < 10 { - lidx * 3 - } else if lidx < 100 { - 26 + (lidx - 9) * 4 - } else if lidx < 1000 { - 385 + (lidx - 99) * 5 - } else { - panic!("FIXME: Message body with more than 100 urls, fix this"); - }; - t.insert_str(l.start() + offset, &format!("[{}]", lidx)); - } - t.push_str("\n\n"); - t.push_str(&self.attachment_tree); - t } - ViewMode::Attachment(aidx) => { - let attachments = body.attachments(); - let mut ret = "Viewing attachment. Press `r` to return \n".to_string(); - ret.push_str(&attachments[aidx].text()); - ret - } - ViewMode::Ansi(_) => "Viewing attachment. Press `r` to return \n".to_string(), - } + }; + rec(body, context, coordinates, &mut ret, active_jobs); + ret } pub fn update( @@ -471,167 +842,94 @@ impl MailView { self.set_dirty(true); } - fn open_attachment(&mut self, lidx: usize, context: &mut Context, use_mailcap: bool) { - let attachments = if let MailViewState::Loaded { ref body, .. } = self.state { - body.attachments() - } else if let MailViewState::Error { ref err } = self.state { - context.replies.push_back(UIEvent::Notification( - Some("Failed to open e-mail".to_string()), - err.to_string(), - Some(NotificationType::Error(err.kind)), - )); - log( - format!("Failed to open envelope: {}", err.to_string()), - ERROR, - ); - self.init_futures(context); - return; + fn open_attachment( + &'_ self, + lidx: usize, + context: &mut Context, + ) -> Option<&'_ melib::Attachment> { + if lidx == 0 { + return None; + } + let display = if let MailViewState::Loaded { ref display, .. } = self.state { + display } else { - return; + return None; }; - if let Some(u) = attachments.get(lidx) { - if use_mailcap { - if let Ok(()) = crate::mailcap::MailcapEntry::execute(u, context) { - self.set_dirty(true); - } else { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!( - "no mailcap entry found for {}", - u.content_type() - )))); - } - } else { - match u.content_type() { - ContentType::MessageRfc822 => { - match Mail::new(u.body().to_vec(), Some(Flag::SEEN)) { - Ok(wrapper) => { - context.replies.push_back(UIEvent::Action(Tab(New(Some( - Box::new(EnvelopeView::new( - wrapper, - None, - None, - self.coordinates.0, - )), - ))))); - } - Err(e) => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!("{}", e)), - )); - } - } - } - - ContentType::Text { .. } | ContentType::PGPSignature => { - self.mode = ViewMode::Attachment(lidx); - self.initialised = false; - self.dirty = true; - } - ContentType::Multipart { .. } => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage( - "Multipart attachments are not supported yet.".to_string(), - ), - )); + if let Some(path) = + self.attachment_paths.get(lidx).and_then( + |path| { + if path.len() > 0 { + Some(path) + } else { + None } - ContentType::Other { ref name, .. } => { - let attachment_type = u.mime_type(); - let binary = query_default_app(&attachment_type); - let mut name_opt = name.as_ref().and_then(|n| { - melib::email::parser::encodings::phrase(n.as_bytes(), false) - .map(|(_, v)| v) - .ok() - .and_then(|n| String::from_utf8(n).ok()) - }); - if name_opt.is_none() { - name_opt = name.as_ref().map(|n| n.clone()); - } - if let Ok(binary) = binary { - let p = create_temp_file( - &decode(u, None), - name_opt.as_ref().map(String::as_str), - None, - true, - ); - match debug!(context.plugin_manager.activate_hook( - "attachment-view", - p.path().display().to_string().into_bytes() - )) { - Ok(crate::plugins::FilterResult::Ansi(s)) => { - if let Some(buf) = crate::terminal::ansi::ansi_to_cellbuffer(&s) - { - let raw_buf = RawBuffer::new(buf, name_opt); - self.mode = ViewMode::Ansi(raw_buf); - self.initialised = false; - self.dirty = true; - return; - } - } - Ok(crate::plugins::FilterResult::UiMessage(s)) => { - context.replies.push_back(UIEvent::Notification( - None, - s, - Some(NotificationType::Error(melib::ErrorKind::None)), - )); - } - _ => {} - } - match Command::new(&binary) - .arg(p.path()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - { - Ok(child) => { - context.temp_files.push(p); - context.children.push(child); - } - Err(err) => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Failed to start {}: {}", - |