From d146c81d48d8cfbe3a7598c5e438f6625b3c1c22 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 18 Aug 2018 17:50:31 +0300 Subject: [PATCH] Add message/rfc822, multipart/digest multipart/mixed views closes #22 --- melib/src/mailbox/email/attachment_types.rs | 2 + melib/src/mailbox/email/attachments.rs | 68 ++- melib/src/mailbox/email/mod.rs | 239 ++++++---- ui/src/components/mail/view/envelope.rs | 460 ++++++++++++++++++++ ui/src/components/mail/view/mod.rs | 41 +- ui/src/types/cells.rs | 2 +- 6 files changed, 681 insertions(+), 131 deletions(-) create mode 100644 ui/src/components/mail/view/envelope.rs diff --git a/melib/src/mailbox/email/attachment_types.rs b/melib/src/mailbox/email/attachment_types.rs index 9da1e2c11..d4c3c7703 100644 --- a/melib/src/mailbox/email/attachment_types.rs +++ b/melib/src/mailbox/email/attachment_types.rs @@ -104,6 +104,7 @@ impl Display for MultipartType { pub enum ContentType { Text { kind: Text, charset: Charset }, Multipart { boundary: SliceBuild, kind: MultipartType, subattachments: Vec}, + MessageRfc822, Unsupported { tag: Vec }, } @@ -122,6 +123,7 @@ impl Display for ContentType { ContentType::Text { kind: t, .. } => t.fmt(f), ContentType::Multipart { kind: k, .. } => k.fmt(f), ContentType::Unsupported { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)), + ContentType::MessageRfc822 => write!(f, "message/rfc822"), } } } diff --git a/melib/src/mailbox/email/attachments.rs b/melib/src/mailbox/email/attachments.rs index 2cfda03eb..ce8d86778 100644 --- a/melib/src/mailbox/email/attachments.rs +++ b/melib/src/mailbox/email/attachments.rs @@ -19,6 +19,7 @@ * along with meli. If not, see . */ use data_encoding::BASE64_MIME; +use mailbox::email::EnvelopeWrapper; use mailbox::email::parser; use mailbox::email::parser::BytesExt; use std::fmt; @@ -128,9 +129,15 @@ impl AttachmentBuilder { _ => {}, } } + } else if ct.eq_ignore_ascii_case(b"message") && cst.eq_ignore_ascii_case(b"rfc822") { + self.content_type = ContentType::MessageRfc822; } else { + let mut tag: Vec = Vec::with_capacity(ct.len() + cst.len() + 1); + tag.extend(ct); + tag.push(b'/'); + tag.extend(cst); self.content_type = ContentType::Unsupported { - tag: ct.into(), + tag }; }, Err(v) => { @@ -211,7 +218,7 @@ impl AttachmentBuilder { let offset = body.as_ptr() as usize - a.as_ptr() as usize; SliceBuild::new(offset, body.len()) }; - builder.raw = body_slice.get(a).into(); + builder.raw = body_slice.get(a).ltrim().into(); for (name, value) in headers { if name.eq_ignore_ascii_case(b"content-type") { builder.content_type(value); @@ -239,6 +246,11 @@ impl AttachmentBuilder { impl fmt::Display for Attachment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.content_type { + ContentType::MessageRfc822 => { + let wrapper = EnvelopeWrapper::new(self.bytes().to_vec()); + write!(f, "message/rfc822: {} - {} - {}", wrapper.date(), wrapper.from_to_string(), wrapper.subject()) + } + ContentType::Unsupported { .. } => { write!(f, "Data attachment of type {}", self.mime_type()) } @@ -315,7 +327,6 @@ impl Attachment { let mut ret = Vec::new(); fn count_recursive(att: &Attachment, ret: &mut Vec) { match att.content_type { - ContentType::Unsupported { .. } | ContentType::Text { .. } => ret.push(att.clone()), ContentType::Multipart { subattachments: ref sub_att_vec, .. @@ -326,6 +337,7 @@ impl Attachment { count_recursive(a, ret); } } + _ => ret.push(att.clone()), } } @@ -354,13 +366,31 @@ pub fn interpret_format_flowed(_t: &str) -> String { unimplemented!() } -fn decode_rec_helper(a: &Attachment, filter: &Option Vec>>) -> Vec { - if let Some(filter) = filter { - return filter(a); - } - match a.content_type { +fn decode_rfc822(_raw: &[u8]) -> Attachment { + let builder = AttachmentBuilder::new(b""); + return builder.build(); + + /* + eprintln!("raw is\n{:?}", str::from_utf8(raw).unwrap()); + let e = match Envelope::from_bytes(raw) { + Some(e) => e, + None => { + eprintln!("error in parsing mail"); + let error_msg = b"Mail cannot be shown because of errors."; + let mut builder = AttachmentBuilder::new(error_msg); + return builder.build(); + } + }; + e.body(None) + */ + +} + +fn decode_rec_helper(a: &Attachment, filter: &Option) -> ()>>) -> Vec { + let mut ret = match a.content_type { ContentType::Unsupported { .. } => Vec::new(), ContentType::Text { .. } => decode_helper(a, filter), + ContentType::MessageRfc822 => decode_rec(&decode_rfc822(&a.raw), None), ContentType::Multipart { kind: ref multipart_type, subattachments: ref sub_att_vec, @@ -380,16 +410,16 @@ fn decode_rec_helper(a: &Attachment, filter: &Option Vec< } vec }, + }; + if let Some(filter) = filter { + filter(a, &mut ret); } + ret } -pub fn decode_rec(a: &Attachment, filter: Option Vec>>) -> Vec { +pub fn decode_rec(a: &Attachment, filter: Option) -> ()>>) -> Vec { decode_rec_helper(a, &filter) } -fn decode_helper(a: &Attachment, filter: &Option Vec>>) -> Vec { - if let Some(filter) = filter { - return filter(a); - } - +fn decode_helper(a: &Attachment, filter: &Option) -> ()>>) -> Vec { let charset = match a.content_type { ContentType::Text { charset: c, .. } => c, _ => Default::default(), @@ -408,7 +438,7 @@ fn decode_helper(a: &Attachment, filter: &Option Vec> | ContentTransferEncoding::Other { .. } => a.bytes().to_vec(), }; - if a.content_type.is_text() { + let mut ret = if a.content_type.is_text() { if let Ok(v) = parser::decode_charset(&bytes, charset) { v.into_bytes() } else { @@ -416,8 +446,14 @@ fn decode_helper(a: &Attachment, filter: &Option Vec> } } else { bytes.to_vec() + }; + if let Some(filter) = filter { + filter(a, &mut ret); } + + ret + } -pub fn decode(a: &Attachment, filter: Option Vec>>) -> Vec { +pub fn decode(a: &Attachment, filter: Option) -> ()>>) -> Vec { decode_helper(a, &filter) } diff --git a/melib/src/mailbox/email/mod.rs b/melib/src/mailbox/email/mod.rs index bbe18721e..79f9f5d5a 100644 --- a/melib/src/mailbox/email/mod.rs +++ b/melib/src/mailbox/email/mod.rs @@ -207,29 +207,39 @@ bitflags! { } #[derive(Debug, Clone, Default)] -pub struct EnvelopeBuilder { - from: Option>, - to: Vec
, - body: Option, - in_reply_to: Option, - flags: Flag, +pub struct EnvelopeWrapper { + envelope: Envelope, + buffer: Vec, } -impl EnvelopeBuilder { - pub fn new() -> Self { - Default::default() + +use std::ops::Deref; + +impl Deref for EnvelopeWrapper { + type Target = Envelope; + + fn deref(&self) -> &Envelope { + &self.envelope + } +} + +impl EnvelopeWrapper { + pub fn new(buffer: Vec) -> Self { + EnvelopeWrapper { + envelope: Envelope::from_bytes(&buffer).unwrap(), + buffer, + } } - pub fn build(self) -> Envelope { - unimplemented!(); + pub fn update(&mut self, new_buffer: Vec) { + *self = EnvelopeWrapper::new(new_buffer); + } - /* - * 1. Check for date. Default is now - * 2. - Envelope { - - - */ + pub fn envelope(&self) -> &Envelope { + &self.envelope + } + pub fn buffer(&self) -> &[u8] { + &self.buffer } } @@ -240,7 +250,7 @@ impl EnvelopeBuilder { /// Access to the underlying email object in the account's backend (for example the file or the /// entry in an IMAP server) is given through `operation_token`. For more information see /// `BackendOp`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Envelope { date: String, from: Vec
, @@ -279,101 +289,119 @@ impl Envelope { flags: Flag::default(), } } - pub fn from_token(operation: Box, hash: u64) -> Option { + pub fn from_bytes(bytes: &[u8]) -> Option { + let mut h = DefaultHasher::new(); + h.write(bytes); + let mut e = Envelope::new(h.finish()); + let res = e.populate_headers(bytes).ok(); + if res.is_some() { + return Some(e); + } + None + } + pub fn from_token(mut operation: Box, hash: u64) -> Option { let mut e = Envelope::new(hash); e.flags = operation.fetch_flags(); - let res = e.populate_headers(operation).ok(); - if res.is_some() { - Some(e) - } else { - None + if let Ok(bytes) = operation.as_bytes() { + let res = e.populate_headers(bytes).ok(); + if res.is_some() { + return Some(e); + } } + None } pub fn hash(&self) -> u64 { self.hash } - pub fn populate_headers(&mut self, mut operation: Box) -> Result<()> { - { - let headers = match parser::headers(operation.fetch_headers()?).to_full_result() { - Ok(v) => v, - Err(e) => { - eprintln!("error in parsing mail\n"); - return Err(MeliError::from(e)); - } - }; + pub fn populate_headers(&mut self, bytes: &[u8]) -> Result<()> { - let mut in_reply_to = None; - let mut datetime = None; + let (headers, _) = match parser::mail(bytes).to_full_result() { + Ok(v) => v, + Err(e) => { + eprintln!("error in parsing mail\n{:?}\n", e); + let error_msg = String::from("Mail cannot be shown because of errors."); + return Err(MeliError::new(error_msg)); + } + }; + let mut in_reply_to = None; + let mut datetime = None; - for (name, value) in headers { - if value.len() == 1 && value.is_empty() { - continue; - } - if name.eq_ignore_ascii_case(b"to") { - let parse_result = parser::rfc2822address_list(value); - let value = if parse_result.is_done() { - parse_result.to_full_result().unwrap() - } else { - Vec::new() - }; - self.set_to(value); - } else if name.eq_ignore_ascii_case(b"from") { - let parse_result = parser::rfc2822address_list(value); - let value = if parse_result.is_done() { - parse_result.to_full_result().unwrap() - } else { - Vec::new() - }; - self.set_from(value); - } else if name.eq_ignore_ascii_case(b"subject") { - let parse_result = parser::phrase(value.trim()); - let value = if parse_result.is_done() { - parse_result.to_full_result().unwrap() - } else { - "".into() - }; - self.set_subject(value); - } else if name.eq_ignore_ascii_case(b"message-id") { - self.set_message_id(value); - } else if name.eq_ignore_ascii_case(b"references") { - { - let parse_result = parser::references(value); - if parse_result.is_done() { - for v in parse_result.to_full_result().unwrap() { - self.push_references(v); - } + for (name, value) in headers { + if value.len() == 1 && value.is_empty() { + continue; + } + if name.eq_ignore_ascii_case(b"to") { + let parse_result = parser::rfc2822address_list(value); + let value = if parse_result.is_done() { + parse_result.to_full_result().unwrap() + } else { + Vec::new() + }; + self.set_to(value); + } else if name.eq_ignore_ascii_case(b"from") { + let parse_result = parser::rfc2822address_list(value); + let value = if parse_result.is_done() { + parse_result.to_full_result().unwrap() + } else { + Vec::new() + }; + self.set_from(value); + } else if name.eq_ignore_ascii_case(b"subject") { + let parse_result = parser::phrase(value.trim()); + let value = if parse_result.is_done() { + parse_result.to_full_result().unwrap() + } else { + "".into() + }; + self.set_subject(value); + } else if name.eq_ignore_ascii_case(b"message-id") { + self.set_message_id(value); + } else if name.eq_ignore_ascii_case(b"references") { + { + let parse_result = parser::references(value); + if parse_result.is_done() { + for v in parse_result.to_full_result().unwrap() { + self.push_references(v); } } - self.set_references(value); - } else if name.eq_ignore_ascii_case(b"in-reply-to") { - self.set_in_reply_to(value); - in_reply_to = Some(value); - } else if name.eq_ignore_ascii_case(b"date") { - self.set_date(value); - datetime = Some(value); } + self.set_references(value); + } else if name.eq_ignore_ascii_case(b"in-reply-to") { + self.set_in_reply_to(value); + in_reply_to = Some(value); + } else if name.eq_ignore_ascii_case(b"date") { + self.set_date(value); + datetime = Some(value); } - /* - * https://tools.ietf.org/html/rfc5322#section-3.6.4 - * - * if self.message_id.is_none() ... - */ - if let Some(ref mut x) = in_reply_to { - self.push_references(x); + } + /* + * https://tools.ietf.org/html/rfc5322#section-3.6.4 + * + * if self.message_id.is_none() ... + */ + if let Some(ref mut x) = in_reply_to { + self.push_references(x); + } + if let Some(ref mut d) = datetime { + if let Some(d) = parser::date(d) { + self.set_datetime(d); } - if let Some(ref mut d) = datetime { - if let Some(d) = parser::date(d) { - self.set_datetime(d); - } - } - } + } if self.message_id.is_none() { let mut h = DefaultHasher::new(); - h.write(&self.bytes(operation)); + h.write(bytes); self.set_message_id(format!("<{:x}>", h.finish()).as_bytes()); } Ok(()) } + + + pub fn populate_headers_from_token(&mut self, mut operation: Box) -> Result<()> { + { + let headers = operation.fetch_headers()?; + return self.populate_headers(headers); + } + } pub fn date(&self) -> u64 { self.timestamp } @@ -410,6 +438,29 @@ impl Envelope { .map(|v| v.into()) .unwrap_or_else(|_| Vec::new()) } + pub fn body_bytes(&self, bytes: &[u8]) -> Attachment { + let (headers, body) = match parser::mail(bytes).to_full_result() { + Ok(v) => v, + Err(_) => { + eprintln!("error in parsing mail\n"); + let error_msg = b"Mail cannot be shown because of errors."; + let mut builder = AttachmentBuilder::new(error_msg); + return builder.build(); + } + }; + let mut builder = AttachmentBuilder::new(body); + for (name, value) in headers { + if value.len() == 1 && value.is_empty() { + continue; + } + if name.eq_ignore_ascii_case(b"content-transfer-encoding") { + builder.content_transfer_encoding(value); + } else if name.eq_ignore_ascii_case(b"content-type") { + builder.content_type(value); + } + } + builder.build() + } pub fn body(&self, mut operation: Box) -> Attachment { let file = operation.as_bytes(); let (headers, body) = match parser::mail(file.unwrap()).to_full_result() { diff --git a/ui/src/components/mail/view/envelope.rs b/ui/src/components/mail/view/envelope.rs new file mode 100644 index 000000000..9f3d7c71e --- /dev/null +++ b/ui/src/components/mail/view/envelope.rs @@ -0,0 +1,460 @@ +/* + * meli - ui crate. + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; +use linkify::{Link, LinkFinder}; +use std::process::{Command, Stdio}; + +use mime_apps::query_default_app; + +#[derive(PartialEq, Debug)] +enum ViewMode { + Normal, + Url, + Attachment(usize), + Raw, + Subview, +} + +impl ViewMode { + fn is_attachment(&self) -> bool { + match self { + ViewMode::Attachment(_) => true, + _ => false, + } + } +} + +/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more +/// menus +pub struct EnvelopeView { + pager: Option, + subview: Option>, + dirty: bool, + mode: ViewMode, + wrapper: EnvelopeWrapper, + + cmd_buf: String, +} + +impl fmt::Display for EnvelopeView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "view mail") + } +} + +impl EnvelopeView { + pub fn new( + wrapper: EnvelopeWrapper, + pager: Option, + subview: Option>, + ) -> Self { + EnvelopeView { + pager, + subview, + dirty: true, + mode: ViewMode::Normal, + wrapper, + + cmd_buf: String::with_capacity(4), + } + } + + /// Returns the string to be displayed in the Viewer + fn attachment_to_text(&self, body: Attachment) -> String { + let finder = LinkFinder::new(); + let body_text = String::from_utf8_lossy(&decode_rec( + &body, + Some(Box::new(|a: &Attachment, v: &mut Vec| { + if a.content_type().is_text_html() { + use std::io::Write; + use std::process::{Command, Stdio}; + + let mut html_filter = Command::new("w3m") + .args(&["-I", "utf-8", "-T", "text/html"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to start html filter process"); + + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&v) + .expect("Failed to write to w3m stdin"); + *v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n".to_vec(); + v.extend(html_filter.wait_with_output().unwrap().stdout); + } + })), + )).into_owned(); + match self.mode { + ViewMode::Normal | ViewMode::Subview => { + let mut t = body_text.to_string(); + if body.count_attachments() > 1 { + t = body + .attachments() + .iter() + .enumerate() + .fold(t, |mut s, (idx, a)| { + s.push_str(&format!("[{}] {}\n\n", idx, a)); + s + }); + } + t + } + ViewMode::Raw => String::from_utf8_lossy(body.bytes()).into_owned(), + ViewMode::Url => { + let mut t = body_text.to_string(); + 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!("BUG: Message body with more than 100 urls, fix this"); + }; + t.insert_str(l.start() + offset, &format!("[{}]", lidx)); + } + if body.count_attachments() > 1 { + t = body + .attachments() + .iter() + .enumerate() + .fold(t, |mut s, (idx, a)| { + s.push_str(&format!("[{}] {}\n\n", idx, a)); + s + }); + } + 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 + } + } + } + pub fn plain_text_to_buf(s: &String, highlight_urls: bool) -> CellBuffer { + let mut buf = CellBuffer::from(s); + + if highlight_urls { + let lines: Vec<&str> = s.split('\n').map(|l| l.trim_right()).collect(); + let mut shift = 0; + let mut lidx_total = 0; + let finder = LinkFinder::new(); + for r in &lines { + for l in finder.links(&r) { + let offset = if lidx_total < 10 { + 3 + } else if lidx_total < 100 { + 4 + } else if lidx_total < 1000 { + 5 + } else { + panic!("BUG: Message body with more than 100 urls"); + }; + for i in 1..=offset { + buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226)); + //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226)); + //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226)); + } + lidx_total += 1; + } + // Each Cell represents one char so next line will be: + shift += r.chars().count() + 1; + } + } + buf + } +} + +impl Component for EnvelopeView { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + + let y :usize = { + let envelope: &Envelope = &self.wrapper; + + if self.mode == ViewMode::Raw { + clear_area(grid, area); + context.dirty_areas.push_back(area); + get_y(upper_left) - 1 + } else { + let (x, y) = write_string_to_grid( + &format!("Date: {}", envelope.date_as_str()), + grid, + Color::Byte(33), + Color::Default, + area, + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("From: {}", envelope.from_to_string()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("To: {}", envelope.to_to_string()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("Subject: {}", envelope.subject()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("Message-ID: <{}>", envelope.message_id_raw()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2))); + context + .dirty_areas + .push_back((upper_left, set_y(bottom_right, y + 1))); + y + 1 + } + }; + + if self.dirty { + let body = self.wrapper.body_bytes(self.wrapper.buffer()); + match self.mode { + ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { + self.subview = Some(Box::new(HtmlView::new(decode( + &body.attachments()[aidx], + None, + )))); + } + ViewMode::Normal if body.is_html() => { + self.subview = Some(Box::new(HtmlView::new(decode(&body, None)))); + self.mode = ViewMode::Subview; + } + _ => { + let buf = { + let text = self.attachment_to_text(body); + // URL indexes must be colored (ugh..) + EnvelopeView::plain_text_to_buf(&text, self.mode == ViewMode::Url) + }; + let cursor_pos = if self.mode.is_attachment() { + Some(0) + } else { + self.pager.as_mut().map(|p| p.cursor_pos()) + }; + self.pager = Some(Pager::from_buf(&buf, cursor_pos)); + } + }; + self.dirty = false; + } + if let Some(s) = self.subview.as_mut() { + s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + } else if let Some(p) = self.pager.as_mut() { + p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + } + } + + fn process_event(&mut self, event: &UIEvent, context: &mut Context) { + match event.event_type { + UIEventType::Input(Key::Esc) => { + self.cmd_buf.clear(); + } + UIEventType::Input(Key::Char(c)) if c >= '0' && c <= '9' => { + self.cmd_buf.push(c); + } + UIEventType::Input(Key::Char('r')) + if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw => + { + self.mode = if self.mode == ViewMode::Raw { + ViewMode::Normal + } else { + ViewMode::Raw + }; + self.dirty = true; + } + UIEventType::Input(Key::Char('r')) if self.mode.is_attachment() || self.mode == ViewMode::Subview => { + self.mode = ViewMode::Normal; + self.subview.take(); + self.dirty = true; + } + UIEventType::Input(Key::Char('a')) + if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal => + { + let lidx = self.cmd_buf.parse::().unwrap(); + self.cmd_buf.clear(); + + { + let envelope: &Envelope = self.wrapper.envelope(); + if let Some(u) = envelope.body_bytes(self.wrapper.buffer()).attachments().get(lidx) { + match u.content_type() { + ContentType::MessageRfc822 => { + self.mode = ViewMode::Subview; + self.subview = Some(Box::new(Pager::from_str(&String::from_utf8_lossy(&decode_rec(u, None)).to_string(), None))); + }, + + ContentType::Text { .. } => { + self.mode = ViewMode::Attachment(lidx); + self.dirty = true; + } + ContentType::Multipart { .. } => { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::StatusNotification( + "Multipart attachments are not supported yet.".to_string(), + ), + }); + return; + } + ContentType::Unsupported { .. } => { + let attachment_type = u.mime_type(); + let binary = query_default_app(&attachment_type); + if let Ok(binary) = binary { + let mut p = create_temp_file(&decode(u, None), None); + Command::new(&binary) + .arg(p.path()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap_or_else(|_| { + panic!("Failed to start {}", binary.display()) + }); + context.temp_files.push(p); + } else { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::StatusNotification(format!( + "Couldn't find a default application for type {}", + attachment_type + )), + }); + return; + } + } + } + } else { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::StatusNotification(format!( + "Attachment `{}` not found.", + lidx + )), + }); + return; + } + }; + } + UIEventType::Input(Key::Char('g')) + if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url => + { + let lidx = self.cmd_buf.parse::().unwrap(); + self.cmd_buf.clear(); + let url = { + let envelope: &Envelope = self.wrapper.envelope(); + let finder = LinkFinder::new(); + let mut t = envelope.body_bytes(self.wrapper.buffer()).text().to_string(); + let links: Vec = finder.links(&t).collect(); + if let Some(u) = links.get(lidx) { + u.as_str().to_string() + } else { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::StatusNotification(format!( + "Link `{}` not found.", + lidx + )), + }); + return; + } + }; + + Command::new("xdg-open") + .arg(url) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to start xdg_open"); + } + UIEventType::Input(Key::Char('u')) => { + match self.mode { + ViewMode::Normal => self.mode = ViewMode::Url, + ViewMode::Url => self.mode = ViewMode::Normal, + _ => {} + } + self.dirty = true; + } + _ => {} + } + if let Some(ref mut sub) = self.subview { + sub.process_event(event, context); + } else if let Some(ref mut p) = self.pager { + p.process_event(event, context); + } + } + 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) + } + fn set_dirty(&mut self) { + self.dirty = true; + } +} diff --git a/ui/src/components/mail/view/mod.rs b/ui/src/components/mail/view/mod.rs index b9979d563..cd6a93ce5 100644 --- a/ui/src/components/mail/view/mod.rs +++ b/ui/src/components/mail/view/mod.rs @@ -28,6 +28,9 @@ pub use self::html::*; mod thread; pub use self::thread::*; +mod envelope; +pub use self::envelope::*; + use mime_apps::query_default_app; #[derive(PartialEq, Debug)] @@ -74,7 +77,7 @@ impl MailView { local_collection: Vec, pager: Option, subview: Option>, - ) -> Self { + ) -> Self { MailView { coordinates, local_collection, @@ -90,17 +93,13 @@ impl MailView { /// Returns the string to be displayed in the Viewer fn attachment_to_text(&self, body: Attachment) -> String { let finder = LinkFinder::new(); - let body_text = if body.content_type().is_text_html() { - let mut s = - String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n"); - s.extend( - String::from_utf8_lossy(&decode( - &body, - Some(Box::new(|a: &Attachment| { + let body_text = String::from_utf8_lossy(&decode_rec( + &body, + Some(Box::new(|a: &Attachment, v: &mut Vec| { + if a.content_type().is_text_html() { use std::io::Write; use std::process::{Command, Stdio}; - let raw = decode(a, None); let mut html_filter = Command::new("w3m") .args(&["-I", "utf-8", "-T", "text/html"]) .stdin(Stdio::piped()) @@ -112,17 +111,13 @@ impl MailView { .stdin .as_mut() .unwrap() - .write_all(&raw) + .write_all(&v) .expect("Failed to write to w3m stdin"); - html_filter.wait_with_output().unwrap().stdout - })), - )).into_owned() - .chars(), - ); - s - } else { - String::from_utf8_lossy(&decode_rec(&body, None)).into() - }; + *v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n".to_vec(); + v.extend(html_filter.wait_with_output().unwrap().stdout); + } + })), + )).into_owned(); match self.mode { ViewMode::Normal | ViewMode::Subview => { let mut t = body_text.to_string(); @@ -364,7 +359,7 @@ impl Component for MailView { }; self.dirty = true; } - UIEventType::Input(Key::Char('r')) if self.mode.is_attachment() => { + UIEventType::Input(Key::Char('r')) if self.mode.is_attachment() || self.mode == ViewMode::Subview => { self.mode = ViewMode::Normal; self.subview.take(); self.dirty = true; @@ -391,6 +386,12 @@ impl Component for MailView { let op = context.accounts[self.coordinates.0].backend.operation(envelope.hash()); if let Some(u) = envelope.body(op).attachments().get(lidx) { match u.content_type() { + ContentType::MessageRfc822 => { + self.mode = ViewMode::Subview; + let wrapper = EnvelopeWrapper::new(u.bytes().to_vec()); + self.subview = Some(Box::new(EnvelopeView::new(wrapper, None, None))); + }, + ContentType::Text { .. } => { self.mode = ViewMode::Attachment(lidx); self.dirty = true; diff --git a/ui/src/types/cells.rs b/ui/src/types/cells.rs index 86d6fd6f4..551e8b2b7 100644 --- a/ui/src/types/cells.rs +++ b/ui/src/types/cells.rs @@ -188,7 +188,7 @@ impl Default for CellBuffer { impl<'a> From<&'a String> for CellBuffer { fn from(s: &'a String) -> Self { let lines: Vec<&str> = s.lines().map(|l| l.trim_right()).collect(); - let len = s.len(); + let len = s.len() + lines.len(); let mut buf = CellBuffer::new(len, 1, Cell::default()); let mut x = 0; for l in lines.iter() {