diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index fa511369..7e1f2fb8 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -997,6 +997,10 @@ When active, it prepends an index next to each url that you can select by typing View raw envelope source in a pager. .\" default value .Pq Em M-r +.It Ic change_charset +Force attachment charset for decoding. +.\" default value +.Pq Em d .El .sp .Em thread-view diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index c02d1a60..0be39635 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -32,6 +32,23 @@ use smallvec::SmallVec; use crate::email::attachment_types::*; +pub type Filter<'a> = Box) + 'a>; + +#[derive(Default)] +pub struct DecodeOptions<'att> { + pub filter: Option>, + pub force_charset: Option, +} + +impl<'att> From> for DecodeOptions<'att> { + fn from(force_charset: Option) -> DecodeOptions<'att> { + Self { + filter: None, + force_charset, + } + } +} + #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AttachmentBuilder { pub content_type: ContentType, @@ -983,11 +1000,3 @@ impl Attachment { pub fn interpret_format_flowed(_t: &str) -> String { unimplemented!() } - -pub type Filter<'a> = Box) + 'a>; - -#[derive(Default)] -pub struct DecodeOptions<'att> { - pub filter: Option>, - pub force_charset: Option, -} diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 9efb2e4c..88b6fb56 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -45,6 +45,23 @@ pub use self::envelope::*; use linkify::LinkFinder; use xdg_utils::query_default_app; +#[derive(Debug, Default)] +enum ForceCharset { + #[default] + None, + Dialog(Box>>), + Forced(Charset), +} + +impl Into> for &ForceCharset { + fn into(self) -> Option { + match self { + ForceCharset::Forced(val) => Some(*val), + ForceCharset::None | ForceCharset::Dialog(_) => None, + } + } +} + #[derive(PartialEq, Eq, Copy, Clone, Debug)] enum Source { Decoded, @@ -160,6 +177,7 @@ pub struct MailView { theme_default: ThemeAttribute, active_jobs: HashSet, state: MailViewState, + force_charset: ForceCharset, cmd_buf: String, id: ComponentId, @@ -196,6 +214,74 @@ enum MailViewState { }, } +impl MailViewState { + fn load_bytes(self_: &mut MailView, bytes: Vec, context: &mut Context) { + let account = &mut context.accounts[&self_.coordinates.0]; + if account + .collection + .get_env(self_.coordinates.2) + .other_headers() + .is_empty() + { + let _ = account + .collection + .get_env_mut(self_.coordinates.2) + .populate_headers(&bytes); + } + let env = Box::new(account.collection.get_env(self_.coordinates.2).clone()); + let body = Box::new(AttachmentBuilder::new(&bytes).build()); + let display = MailView::attachment_to( + &body, + context, + self_.coordinates, + &mut self_.active_jobs, + (&self_.force_charset).into(), + ); + 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, true); + self_.state = MailViewState::Loaded { + display, + env, + body, + bytes, + body_text, + links: vec![], + }; + } + + fn redecode(self_: &mut MailView, context: &mut Context) { + let (new_display, new_body_text) = + if let MailViewState::Loaded { ref body, .. } = self_.state { + let new_display = MailView::attachment_to( + body, + context, + self_.coordinates, + &mut self_.active_jobs, + (&self_.force_charset).into(), + ); + let (paths, attachment_tree_s) = self_.attachment_displays_to_tree(&new_display); + self_.attachment_tree = attachment_tree_s; + self_.attachment_paths = paths; + let body_text = self_.attachment_displays_to_text(&new_display, context, true); + (new_display, body_text) + } else { + return; + }; + + if let MailViewState::Loaded { + ref mut display, + ref mut body_text, + .. + } = self_.state + { + *display = new_display; + *body_text = new_body_text; + } + } +} + #[derive(Copy, Clone, Debug)] enum LinkKind { Url, @@ -228,6 +314,7 @@ impl Clone for MailView { attachment_paths: self.attachment_paths.clone(), state: MailViewState::default(), active_jobs: self.active_jobs.clone(), + force_charset: ForceCharset::None, ..*self } } @@ -264,7 +351,7 @@ impl MailView { theme_default: crate::conf::value(context, "mail.view.body"), active_jobs: Default::default(), state: MailViewState::default(), - + force_charset: ForceCharset::None, cmd_buf: String::with_capacity(4), id: ComponentId::new_v4(), }; @@ -298,41 +385,7 @@ impl MailView { if let Ok(Some(bytes_result)) = try_recv_timeout!(&mut handle.chan) { match bytes_result { Ok(bytes) => { - if account - .collection - .get_env(self.coordinates.2) - .other_headers() - .is_empty() - { - let _ = account - .collection - .get_env_mut(self.coordinates.2) - .populate_headers(&bytes); - } - let env = Box::new( - account.collection.get_env(self.coordinates.2).clone(), - ); - let body = Box::new(AttachmentBuilder::new(&bytes).build()); - 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, true); - self.state = MailViewState::Loaded { - display, - env, - body, - bytes, - body_text, - links: vec![], - }; + MailViewState::load_bytes(self, bytes, context); } Err(err) => { self.state = MailViewState::Error { err }; @@ -719,6 +772,7 @@ impl MailView { context: &mut Context, coordinates: (AccountHash, MailboxHash, EnvelopeHash), active_jobs: &mut HashSet, + force_charset: Option, ) -> Vec { let mut ret = vec![]; fn rec( @@ -727,13 +781,14 @@ impl MailView { coordinates: (AccountHash, MailboxHash, EnvelopeHash), acc: &mut Vec, active_jobs: &mut HashSet, + force_charset: Option, ) { if a.content_disposition.kind.is_attachment() || a.content_type == "message/rfc822" { acc.push(AttachmentDisplay::Attachment { inner: Box::new(a.clone()), }); } else if a.content_type().is_text_html() { - let bytes = a.decode(Default::default()); + let bytes = a.decode(force_charset.into()); let filter_invocation = mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_filter) .as_ref() @@ -788,7 +843,7 @@ impl MailView { } } } else if a.is_text() { - let bytes = a.decode(Default::default()); + let bytes = a.decode(force_charset.into()); acc.push(AttachmentDisplay::InlineText { inner: Box::new(a.clone()), comment: None, @@ -810,7 +865,7 @@ impl MailView { if let Some(text_attachment_pos) = parts.iter().position(|a| a.content_type == "text/plain") { - let bytes = &parts[text_attachment_pos].decode(Default::default()); + let bytes = &parts[text_attachment_pos].decode(force_charset.into()); if bytes.trim().is_empty() && mailbox_settings!( context[coordinates.0][&coordinates.1] @@ -831,7 +886,14 @@ impl MailView { } } for a in parts { - rec(a, context, coordinates, &mut display, active_jobs); + rec( + a, + context, + coordinates, + &mut display, + active_jobs, + force_charset, + ); } acc.push(AttachmentDisplay::Alternative { inner: Box::new(a.clone()), @@ -846,7 +908,14 @@ impl MailView { inner: Box::new(a.clone()), display: { let mut v = vec![]; - rec(&parts[0], context, coordinates, &mut v, active_jobs); + rec( + &parts[0], + context, + coordinates, + &mut v, + active_jobs, + force_charset, + ); v }, }); @@ -869,7 +938,14 @@ impl MailView { job_id: handle.job_id, display: { let mut v = vec![]; - rec(&parts[0], context, coordinates, &mut v, active_jobs); + rec( + &parts[0], + context, + coordinates, + &mut v, + active_jobs, + force_charset, + ); v }, handle, @@ -879,7 +955,14 @@ impl MailView { inner: Box::new(a.clone()), display: { let mut v = vec![]; - rec(&parts[0], context, coordinates, &mut v, active_jobs); + rec( + &parts[0], + context, + coordinates, + &mut v, + active_jobs, + force_charset, + ); v }, }); @@ -925,13 +1008,20 @@ impl MailView { } _ => { for a in parts { - rec(a, context, coordinates, acc, active_jobs); + rec(a, context, coordinates, acc, active_jobs, force_charset); } } } } } - rec(body, context, coordinates, &mut ret, active_jobs); + rec( + body, + context, + coordinates, + &mut ret, + active_jobs, + force_charset, + ); ret } @@ -1655,12 +1745,46 @@ impl Component for MailView { if let ViewMode::ContactSelector(ref mut s) = self.mode { s.draw(grid, area, context); } + + if let ForceCharset::Dialog(ref mut s) = self.force_charset { + s.draw(grid, area, context); + } } fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool { if self.coordinates.0.is_null() || self.coordinates.1.is_null() { return false; } + + match (&mut self.force_charset, &event) { + (ForceCharset::Dialog(selector), UIEvent::FinishedUIDialog(id, results)) + if *id == selector.id() => + { + self.force_charset = + if let Some(results) = results.downcast_ref::>>() { + if results.len() != 1 { + ForceCharset::None + } else if let Some(charset) = results[0] { + ForceCharset::Forced(charset) + } else { + ForceCharset::None + } + } else { + ForceCharset::None + }; + MailViewState::redecode(self, context); + self.initialised = false; + self.set_dirty(true); + return true; + } + (ForceCharset::Dialog(selector), _) => { + if selector.process_event(event, context) { + return true; + } + } + _ => {} + } + let shortcuts = self.get_shortcuts(context); match (&mut self.mode, &mut event) { /*(ViewMode::Ansi(ref mut buf), _) => { @@ -1745,44 +1869,7 @@ impl Component for MailView { Ok(None) => { /* something happened, perhaps a worker thread panicked */ } Ok(Some(Ok(bytes))) => { - if context.accounts[&self.coordinates.0] - .collection - .get_env(self.coordinates.2) - .other_headers() - .is_empty() - { - let _ = context.accounts[&self.coordinates.0] - .collection - .get_env_mut(self.coordinates.2) - .populate_headers(&bytes); - } - let env = Box::new( - context.accounts[&self.coordinates.0] - .collection - .get_env(self.coordinates.2) - .clone(), - ); - let body = Box::new(AttachmentBuilder::new(&bytes).build()); - 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, true); - self.state = MailViewState::Loaded { - bytes, - env, - body, - display, - links: vec![], - body_text, - }; + MailViewState::load_bytes(self, bytes, context); } Ok(Some(Err(err))) => { self.state = MailViewState::Error { err }; @@ -1855,6 +1942,7 @@ impl Component for MailView { context, self.coordinates, &mut self.active_jobs, + (&self.force_charset).into(), ); *d = AttachmentDisplay::EncryptedSuccess { inner: std::mem::replace( @@ -2700,6 +2788,55 @@ impl Component for MailView { .push_back(UIEvent::Action(Tab(New(Some(Box::new(self.clone())))))); return true; } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["change_charset"]) => + { + let entries = vec![ + (None, "default".to_string()), + (Some(Charset::Ascii), Charset::Ascii.to_string()), + (Some(Charset::UTF8), Charset::UTF8.to_string()), + (Some(Charset::UTF16), Charset::UTF16.to_string()), + (Some(Charset::ISO8859_1), Charset::ISO8859_1.to_string()), + (Some(Charset::ISO8859_2), Charset::ISO8859_2.to_string()), + (Some(Charset::ISO8859_3), Charset::ISO8859_3.to_string()), + (Some(Charset::ISO8859_4), Charset::ISO8859_4.to_string()), + (Some(Charset::ISO8859_5), Charset::ISO8859_5.to_string()), + (Some(Charset::ISO8859_6), Charset::ISO8859_6.to_string()), + (Some(Charset::ISO8859_7), Charset::ISO8859_7.to_string()), + (Some(Charset::ISO8859_8), Charset::ISO8859_8.to_string()), + (Some(Charset::ISO8859_10), Charset::ISO8859_10.to_string()), + (Some(Charset::ISO8859_13), Charset::ISO8859_13.to_string()), + (Some(Charset::ISO8859_14), Charset::ISO8859_14.to_string()), + (Some(Charset::ISO8859_15), Charset::ISO8859_15.to_string()), + (Some(Charset::ISO8859_16), Charset::ISO8859_16.to_string()), + (Some(Charset::Windows1250), Charset::Windows1250.to_string()), + (Some(Charset::Windows1251), Charset::Windows1251.to_string()), + (Some(Charset::Windows1252), Charset::Windows1252.to_string()), + (Some(Charset::Windows1253), Charset::Windows1253.to_string()), + (Some(Charset::GBK), Charset::GBK.to_string()), + (Some(Charset::GB2312), Charset::GB2312.to_string()), + (Some(Charset::GB18030), Charset::GB18030.to_string()), + (Some(Charset::BIG5), Charset::BIG5.to_string()), + (Some(Charset::ISO2022JP), Charset::ISO2022JP.to_string()), + (Some(Charset::EUCJP), Charset::EUCJP.to_string()), + (Some(Charset::KOI8R), Charset::KOI8R.to_string()), + (Some(Charset::KOI8U), Charset::KOI8U.to_string()), + ]; + self.force_charset = ForceCharset::Dialog(Box::new(Selector::new( + "select charset to force", + entries, + true, + Some(Box::new( + move |id: ComponentId, results: &[Option]| { + Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec()))) + }, + )), + context, + ))); + self.initialised = false; + self.dirty = true; + return true; + } _ => {} } false @@ -2709,13 +2846,8 @@ impl Component for MailView { self.dirty || self.pager.is_dirty() || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false) - || if let ViewMode::ContactSelector(ref s) = self.mode { - s.is_dirty() - /*} else if let ViewMode::Ansi(ref r) = self.mode { - r.is_dirty()*/ - } else { - false - } + || matches!(self.force_charset, ForceCharset::Dialog(ref s) if s.is_dirty()) + || matches!(self.mode, ViewMode::ContactSelector(ref s) if s.is_dirty()) } fn set_dirty(&mut self, value: bool) { diff --git a/src/components/mail/view/envelope.rs b/src/components/mail/view/envelope.rs index bb968459..616d835a 100644 --- a/src/components/mail/view/envelope.rs +++ b/src/components/mail/view/envelope.rs @@ -51,6 +51,7 @@ pub struct EnvelopeView { mail: Mail, _account_hash: AccountHash, + force_charset: ForceCharset, cmd_buf: String, id: ComponentId, } @@ -73,6 +74,7 @@ impl EnvelopeView { subview, dirty: true, mode: ViewMode::Normal, + force_charset: ForceCharset::None, mail, _account_hash, cmd_buf: String::with_capacity(4), @@ -122,7 +124,11 @@ impl EnvelopeView { } } })), - ..Default::default() + force_charset: if let ForceCharset::Forced(val) = self.force_charset { + Some(val) + } else { + None + }, })) .into_owned(); match self.mode { @@ -312,9 +318,42 @@ impl Component for EnvelopeView { } else if let Some(p) = self.pager.as_mut() { p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); } + + if let ForceCharset::Dialog(ref mut s) = self.force_charset { + s.draw(grid, area, context); + } } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { + match (&mut self.force_charset, &event) { + (ForceCharset::Dialog(selector), UIEvent::FinishedUIDialog(id, results)) + if *id == selector.id() => + { + if let Some(results) = results.downcast_ref::>>() { + if results.len() != 1 { + self.force_charset = ForceCharset::None; + self.set_dirty(true); + return true; + } + if let Some(charset) = results[0] { + self.force_charset = ForceCharset::Forced(charset); + } else { + self.force_charset = ForceCharset::None; + } + } else { + self.force_charset = ForceCharset::None; + } + self.set_dirty(true); + return true; + } + (ForceCharset::Dialog(selector), _) => { + if selector.process_event(event, context) { + return true; + } + } + _ => {} + } + if let Some(ref mut sub) = self.subview { if sub.process_event(event, context) { return true; @@ -324,6 +363,7 @@ impl Component for EnvelopeView { return true; } } + match *event { UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => { self.cmd_buf.clear(); @@ -521,15 +561,64 @@ impl Component for EnvelopeView { self.dirty = true; return true; } + UIEvent::Input(Key::Char('d')) => { + let entries = vec![ + (None, "default".to_string()), + (Some(Charset::Ascii), Charset::Ascii.to_string()), + (Some(Charset::UTF8), Charset::UTF8.to_string()), + (Some(Charset::UTF16), Charset::UTF16.to_string()), + (Some(Charset::ISO8859_1), Charset::ISO8859_1.to_string()), + (Some(Charset::ISO8859_2), Charset::ISO8859_2.to_string()), + (Some(Charset::ISO8859_3), Charset::ISO8859_3.to_string()), + (Some(Charset::ISO8859_4), Charset::ISO8859_4.to_string()), + (Some(Charset::ISO8859_5), Charset::ISO8859_5.to_string()), + (Some(Charset::ISO8859_6), Charset::ISO8859_6.to_string()), + (Some(Charset::ISO8859_7), Charset::ISO8859_7.to_string()), + (Some(Charset::ISO8859_8), Charset::ISO8859_8.to_string()), + (Some(Charset::ISO8859_10), Charset::ISO8859_10.to_string()), + (Some(Charset::ISO8859_13), Charset::ISO8859_13.to_string()), + (Some(Charset::ISO8859_14), Charset::ISO8859_14.to_string()), + (Some(Charset::ISO8859_15), Charset::ISO8859_15.to_string()), + (Some(Charset::ISO8859_16), Charset::ISO8859_16.to_string()), + (Some(Charset::Windows1250), Charset::Windows1250.to_string()), + (Some(Charset::Windows1251), Charset::Windows1251.to_string()), + (Some(Charset::Windows1252), Charset::Windows1252.to_string()), + (Some(Charset::Windows1253), Charset::Windows1253.to_string()), + (Some(Charset::GBK), Charset::GBK.to_string()), + (Some(Charset::GB2312), Charset::GB2312.to_string()), + (Some(Charset::GB18030), Charset::GB18030.to_string()), + (Some(Charset::BIG5), Charset::BIG5.to_string()), + (Some(Charset::ISO2022JP), Charset::ISO2022JP.to_string()), + (Some(Charset::EUCJP), Charset::EUCJP.to_string()), + (Some(Charset::KOI8R), Charset::KOI8R.to_string()), + (Some(Charset::KOI8U), Charset::KOI8U.to_string()), + ]; + self.force_charset = ForceCharset::Dialog(Box::new(Selector::new( + "select charset to force", + entries, + true, + Some(Box::new( + move |id: ComponentId, results: &[Option]| { + Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec()))) + }, + )), + context, + ))); + self.dirty = true; + return true; + } _ => {} } false } + fn is_dirty(&self) -> bool { self.dirty || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false) || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false) + || matches!(self.force_charset, ForceCharset::Dialog(ref s) if s.is_dirty()) } + fn set_dirty(&mut self, value: bool) { self.dirty = value; } diff --git a/src/components/utilities/dialogs.rs b/src/components/utilities/dialogs.rs index 2d2e1a69..1ace1dec 100644 --- a/src/components/utilities/dialogs.rs +++ b/src/components/utilities/dialogs.rs @@ -891,8 +891,8 @@ impl Selec self.vertical_alignment, self.horizontal_alignment, ); - clear_area(grid, dialog_area, self.theme_default); let inner_area = create_box(grid, dialog_area); + clear_area(grid, inner_area, self.theme_default); write_string_to_grid( &self.title, grid, diff --git a/src/conf/shortcuts.rs b/src/conf/shortcuts.rs index 4a3e8fdb..28d99e85 100644 --- a/src/conf/shortcuts.rs +++ b/src/conf/shortcuts.rs @@ -246,7 +246,8 @@ shortcut_key_values! { "envelope-view", return_to_normal_view |> "Return to envelope if viewing raw source or attachment." |> Key::Char('r'), toggle_expand_headers |> "Expand extra headers (References and others)." |> Key::Char('h'), toggle_url_mode |> "Toggles url open mode." |> Key::Char('u'), - view_raw_source |> "View envelope source in a pager. (toggles between raw and decoded source)" |> Key::Alt('r') + view_raw_source |> "View envelope source in a pager. (toggles between raw and decoded source)" |> Key::Alt('r'), + change_charset |> "Force attachment charset for decoding." |> Key::Char('d') } }