summaryrefslogtreecommitdiffstats
path: root/ui/src/components/mail/view/envelope.rs
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2018-08-18 17:50:31 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-06-10 19:40:30 +0300
commitd146c81d48d8cfbe3a7598c5e438f6625b3c1c22 (patch)
tree83d1fa422cb302f7e10d2786a07b1e61a277f858 /ui/src/components/mail/view/envelope.rs
parent41d87934120a20df4ba67ac1eb3fb0db36e9fb2f (diff)
Add message/rfc822, multipart/digest multipart/mixed views
closes #22
Diffstat (limited to 'ui/src/components/mail/view/envelope.rs')
-rw-r--r--ui/src/components/mail/view/envelope.rs460
1 files changed, 460 insertions, 0 deletions
diff --git a/ui/src/components/mail/view/envelope.rs b/ui/src/components/mail/view/envelope.rs
new file mode 100644
index 00000000..9f3d7c71
--- /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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Pager>,
+ subview: Option<Box<Component>>,
+ 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<Pager>,
+ subview: Option<Box<Component>>,
+ ) -> 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<u8>| {
+ 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::<usize>().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::<usize>().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<Link> = 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;
+ }
+}