meli/ui/src/components/mail/view.rs

632 lines
25 KiB
Rust
Raw Normal View History

2018-08-07 15:01:15 +03:00
/*
* 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::*;
2018-07-27 18:01:52 +03:00
use linkify::{Link, LinkFinder};
use std::process::{Command, Stdio};
2018-08-09 16:50:33 +03:00
mod html;
pub use self::html::*;
mod thread;
pub use self::thread::*;
2018-08-09 16:50:33 +03:00
mod envelope;
pub use self::envelope::*;
use mime_apps::query_default_app;
#[derive(PartialEq, Debug)]
enum ViewMode {
Normal,
Url,
Attachment(usize),
2018-08-05 12:44:31 +03:00
Raw,
2018-08-09 16:50:33 +03:00
Subview,
2019-02-15 09:06:42 +02:00
ContactSelector(Selector),
2018-07-25 22:37:28 +03:00
}
impl Default for ViewMode {
fn default() -> Self {
ViewMode::Normal
}
}
2018-07-25 22:37:28 +03:00
impl ViewMode {
fn is_attachment(&self) -> bool {
match self {
ViewMode::Attachment(_) => true,
_ => false,
}
}
}
2018-07-22 23:11:07 +03:00
/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
/// menus
#[derive(Debug, Default)]
pub struct MailView {
2018-09-06 13:05:35 +03:00
coordinates: (usize, usize, EnvelopeHash),
pager: Option<Pager>,
2018-08-09 16:50:33 +03:00
subview: Option<Box<Component>>,
dirty: bool,
mode: ViewMode,
cmd_buf: String,
}
impl fmt::Display for MailView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "view mail")
}
}
impl MailView {
2018-07-27 18:01:52 +03:00
pub fn new(
2018-09-06 13:05:35 +03:00
coordinates: (usize, usize, EnvelopeHash),
2018-07-27 18:01:52 +03:00
pager: Option<Pager>,
2018-08-09 16:50:33 +03:00
subview: Option<Box<Component>>,
2018-08-23 15:36:52 +03:00
) -> Self {
MailView {
2018-08-07 15:01:15 +03:00
coordinates,
pager,
subview,
dirty: true,
mode: ViewMode::Normal,
cmd_buf: String::with_capacity(4),
}
}
/// Returns the string to be displayed in the Viewer
2018-08-23 15:36:52 +03:00
fn attachment_to_text(&self, body: &Attachment) -> String {
let finder = LinkFinder::new();
let body_text = String::from_utf8_lossy(&decode_rec(
2018-08-23 15:36:52 +03:00
&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};
2018-08-23 15:36:52 +03:00
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");
2018-08-23 15:36:52 +03:00
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 {
2018-08-09 16:50:33 +03:00
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
}
2018-08-09 16:50:33 +03:00
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 {
2018-08-09 16:50:33 +03:00
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
2019-02-15 09:06:42 +02:00
},
ViewMode::ContactSelector(_) => { unimplemented!()},
}
}
2018-08-23 15:36:52 +03:00
pub fn plain_text_to_buf(s: &str, highlight_urls: bool) -> CellBuffer {
2018-08-09 16:50:33 +03:00
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 MailView {
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 = {
2018-08-08 20:58:15 +03:00
let accounts = &mut context.accounts;
let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1]
2018-07-27 18:01:52 +03:00
.as_ref()
.unwrap();
2018-09-06 13:05:35 +03:00
let envelope: &Envelope = &mailbox.collection[&self.coordinates.2];
2018-08-05 12:44:31 +03:00
if self.mode == ViewMode::Raw {
clear_area(grid, area);
2018-08-07 15:01:15 +03:00
context.dirty_areas.push_back(area);
get_y(upper_left) - 1
2018-08-05 12:44:31 +03:00
} else {
let (x, y) = write_string_to_grid(
&format!("Date: {}", envelope.date_as_str()),
grid,
Color::Byte(33),
Color::Default,
area,
true,
2018-08-07 15:01:15 +03:00
);
2018-08-05 12:44:31 +03:00
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(
2018-08-23 15:36:52 +03:00
&format!("From: {}", envelope.field_from_to_string()),
2018-08-05 12:44:31 +03:00
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
2018-08-07 15:01:15 +03:00
);
2018-08-05 12:44:31 +03:00
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(
2018-08-23 15:36:52 +03:00
&format!("To: {}", envelope.field_to_to_string()),
2018-08-05 12:44:31 +03:00
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
2018-08-07 15:01:15 +03:00
);
2018-08-05 12:44:31 +03:00
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,
2018-08-07 15:01:15 +03:00
);
2018-08-05 12:44:31 +03:00
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()),
2018-08-05 12:44:31 +03:00
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
2018-08-07 15:01:15 +03:00
);
2018-08-05 12:44:31 +03:00
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 {
2018-08-09 16:50:33 +03:00
let mailbox_idx = self.coordinates; // coordinates are mailbox idxs
let mailbox = &context.accounts[mailbox_idx.0][mailbox_idx.1]
2018-08-09 16:50:33 +03:00
.as_ref()
.unwrap();
2018-09-06 13:05:35 +03:00
let envelope: &Envelope = &mailbox.collection[&mailbox_idx.2];
2018-08-23 15:36:52 +03:00
let op = context.accounts[mailbox_idx.0]
.backend
.operation(envelope.hash(), mailbox.folder.hash());
let body = envelope.body(op);
2018-08-09 16:50:33 +03:00
match self.mode {
ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
self.pager = None;
self.subview = Some(Box::new(HtmlView::new(decode(
&body.attachments()[aidx],
None,
))));
self.mode = ViewMode::Subview;
}
2018-08-09 16:50:33 +03:00
ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(decode(&body, None))));
self.pager = None;
2018-08-09 16:50:33 +03:00
self.mode = ViewMode::Subview;
}
2019-02-26 10:55:22 +02:00
ViewMode::Subview | ViewMode::ContactSelector(_) => {}
2018-08-09 16:50:33 +03:00
_ => {
let buf = {
2018-08-23 15:36:52 +03:00
let text = self.attachment_to_text(&body);
2018-08-09 16:50:33 +03:00
// URL indexes must be colored (ugh..)
MailView::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())
};
2018-08-21 19:51:48 +03:00
self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos));
self.subview = None;
}
};
self.dirty = false;
}
2019-02-15 09:06:42 +02:00
match self.mode {
ViewMode::Subview => {
if let Some(s) = self.subview.as_mut() {
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
2019-02-15 09:06:42 +02:00
},
ViewMode::ContactSelector(ref mut s) => {
2019-02-26 10:55:22 +02:00
clear_area(grid, (set_y(upper_left, y + 1), bottom_right));
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
_ => {
if let Some(p) = self.pager.as_mut() {
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
}
2018-08-07 15:01:15 +03:00
}
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool {
match self.mode {
ViewMode::Subview => {
if let Some(s) = self.subview.as_mut() {
if s.process_event(event, context) {
return true;
}
}
2019-02-15 09:06:42 +02:00
},
ViewMode::ContactSelector(ref mut s) => {
if s.process_event(event, context) {
return true;
}
},
_ => {
if let Some(p) = self.pager.as_mut() {
if p.process_event(event, context) {
return true;
}
}
}
}
match event.event_type {
2019-02-15 09:06:42 +02:00
UIEventType::Input(Key::Char('c')) => {
/*
let mut new_card: Card = Card::new();
new_card.set_email(&envelope.from()[0].get_email());
new_card.set_firstname(&envelope.from()[0].get_display_name());
eprintln!("{:?}", new_card);
*/
if let ViewMode::ContactSelector(_) = self.mode {
if let ViewMode::ContactSelector(s) = std::mem::replace(&mut self.mode, ViewMode::Normal) {
for c in s.collect() {
let mut new_card: Card = Card::new();
let email = String::from_utf8(c).unwrap();
new_card.set_email(&email);
new_card.set_firstname("");
context.accounts[self.coordinates.0].address_book.add_card(new_card);
2019-02-15 09:06:42 +02:00
}
//eprintln!("{:?}", s.collect());
}
return true;
2019-02-15 09:06:42 +02:00
}
let accounts = &context.accounts;
let mailbox = &accounts[self.coordinates.0][self.coordinates.1]
.as_ref()
.unwrap();
let envelope: &Envelope = &mailbox.collection[&self.coordinates.2];
let mut entries = Vec::new();
entries.push((envelope.from()[0].get_email().into_bytes(), format!("{}", envelope.from()[0])));
entries.push((String::from("foo@bar.de").into_bytes(), String::from("Johann de Vir <foo@bar.de>")));
self.mode = ViewMode::ContactSelector(Selector::new(entries, true));
2019-02-26 10:55:22 +02:00
self.dirty = true;
2019-02-15 09:06:42 +02:00
//context.accounts.context(self.coordinates.0).address_book.add_card(new_card);
},
2018-08-26 19:29:12 +03:00
UIEventType::Input(Key::Esc) | UIEventType::Input(Key::Alt('')) => {
2018-07-25 22:37:28 +03:00
self.cmd_buf.clear();
2018-08-26 19:29:12 +03:00
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusEvent(StatusEvent::BufClear),
});
2018-07-27 18:01:52 +03:00
}
UIEventType::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
self.cmd_buf.push(c);
2018-08-26 19:29:12 +03:00
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusEvent(StatusEvent::BufSet(self.cmd_buf.clone())),
});
2018-07-27 18:01:52 +03:00
}
2018-08-07 15:01:15 +03:00
UIEventType::Input(Key::Char('r'))
if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw =>
{
2018-08-05 12:44:31 +03:00
self.mode = if self.mode == ViewMode::Raw {
ViewMode::Normal
} else {
ViewMode::Raw
};
self.dirty = true;
}
2018-08-23 15:36:52 +03:00
UIEventType::Input(Key::Char('r'))
if self.mode.is_attachment() || self.mode == ViewMode::Subview =>
{
2018-07-25 22:37:28 +03:00
self.mode = ViewMode::Normal;
self.subview.take();
2018-07-25 22:37:28 +03:00
self.dirty = true;
2018-07-27 18:01:52 +03:00
}
UIEventType::Input(Key::Char('a'))
2018-08-07 15:01:15 +03:00
if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal =>
2018-07-27 18:01:52 +03:00
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
2018-08-26 19:29:12 +03:00
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusEvent(StatusEvent::BufClear),
});
2018-07-24 20:20:32 +03:00
{
let accounts = &context.accounts;
let mailbox = &accounts[self.coordinates.0][self.coordinates.1]
2018-07-27 18:01:52 +03:00
.as_ref()
.unwrap();
2018-09-06 13:05:35 +03:00
let envelope: &Envelope = &mailbox.collection[&self.coordinates.2];
2018-08-23 15:36:52 +03:00
let op = context.accounts[self.coordinates.0]
.backend
.operation(envelope.hash(), mailbox.folder.hash());
if let Some(u) = envelope.body(op).attachments().get(lidx) {
2018-08-17 13:21:24 +03:00
match u.content_type() {
ContentType::MessageRfc822 => {
self.mode = ViewMode::Subview;
2018-08-18 23:19:39 +03:00
match EnvelopeWrapper::new(u.bytes().to_vec()) {
Ok(wrapper) => {
2018-08-23 15:36:52 +03:00
self.subview =
Some(Box::new(EnvelopeView::new(wrapper, None, None)));
}
2018-08-18 23:19:39 +03:00
Err(e) => {
context.replies.push_back(UIEvent {
id: 0,
2018-08-26 19:29:12 +03:00
event_type: UIEventType::StatusEvent(
StatusEvent::DisplayMessage(format!("{}", e)),
),
2018-08-18 23:19:39 +03:00
});
}
}
return true;
2018-08-23 15:36:52 +03:00
}
ContentType::Text { .. } => {
2018-07-25 22:37:28 +03:00
self.mode = ViewMode::Attachment(lidx);
self.dirty = true;
2018-07-27 18:01:52 +03:00
}
2018-07-25 22:37:28 +03:00
ContentType::Multipart { .. } => {
2018-07-27 18:01:52 +03:00
context.replies.push_back(UIEvent {
id: 0,
2018-08-26 19:29:12 +03:00
event_type: UIEventType::StatusEvent(
StatusEvent::DisplayMessage(
"Multipart attachments are not supported yet."
.to_string(),
),
2018-08-07 15:01:15 +03:00
),
2018-07-27 18:01:52 +03:00
});
return true;
2018-07-27 18:01:52 +03:00
}
2018-07-25 22:37:28 +03:00
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()
2018-08-07 15:01:15 +03:00
.unwrap_or_else(|_| {
panic!("Failed to start {}", binary.display())
});
2018-08-08 20:58:15 +03:00
context.temp_files.push(p);
} else {
2018-07-27 18:01:52 +03:00
context.replies.push_back(UIEvent {
id: 0,
2018-08-26 19:29:12 +03:00
event_type: UIEventType::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Couldn't find a default application for type {}",
attachment_type
)),
),
2018-07-27 18:01:52 +03:00
});
return true;
}
2018-07-27 18:01:52 +03:00
}
2018-07-25 22:37:28 +03:00
}
} else {
2018-07-27 18:01:52 +03:00
context.replies.push_back(UIEvent {
id: 0,
2018-08-26 19:29:12 +03:00
event_type: UIEventType::StatusEvent(StatusEvent::DisplayMessage(
format!("Attachment `{}` not found.", lidx),
2018-07-27 18:01:52 +03:00
)),
});
return true;
}
};
2018-07-27 18:01:52 +03:00
}
UIEventType::Input(Key::Char('g'))
2018-08-07 15:01:15 +03:00
if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
2018-07-27 18:01:52 +03:00
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
2018-08-26 19:29:12 +03:00
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusEvent(StatusEvent::BufClear),
});
let url = {
let accounts = &context.accounts;
let mailbox = &accounts[self.coordinates.0][self.coordinates.1]
2018-07-27 18:01:52 +03:00
.as_ref()
.unwrap();
2018-09-06 13:05:35 +03:00
let envelope: &Envelope = &mailbox.collection[&self.coordinates.2];
let finder = LinkFinder::new();
2018-08-23 15:36:52 +03:00
let op = context.accounts[self.coordinates.0]
.backend
.operation(envelope.hash(), mailbox.folder.hash());
let mut t = envelope.body(op).text().to_string();
let links: Vec<Link> = finder.links(&t).collect();
if let Some(u) = links.get(lidx) {
u.as_str().to_string()
} else {
2018-07-27 18:01:52 +03:00
context.replies.push_back(UIEvent {
id: 0,
2018-08-26 19:29:12 +03:00
event_type: UIEventType::StatusEvent(StatusEvent::DisplayMessage(
format!("Link `{}` not found.", lidx),
2018-07-27 18:01:52 +03:00
)),
});
return true;
}
};
2018-07-24 20:20:32 +03:00
Command::new("xdg-open")
.arg(url)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start xdg_open");
2018-07-27 18:01:52 +03:00
}
UIEventType::Input(Key::Char('u')) => {
match self.mode {
2018-07-27 18:01:52 +03:00
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
_ => {}
}
self.dirty = true;
2018-07-27 18:01:52 +03:00
}
2018-08-23 15:36:52 +03:00
_ => {
return false;
}
}
true
}
fn is_dirty(&self) -> bool {
self.dirty
2018-07-27 18:01:52 +03:00
|| self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|| self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
2019-02-26 10:55:22 +02:00
|| if let ViewMode::ContactSelector(ref s) = self.mode {
s.is_dirty()
} else {
false
}
}
fn set_dirty(&mut self) {
self.dirty = true;
match self.mode {
ViewMode::Normal => {
if let Some(p) = self.pager.as_mut() {
p.set_dirty();
}
}
ViewMode::Subview => {
if let Some(s) = self.subview.as_mut() {
s.set_dirty();
}
}
_ => {}
}
}
}