meli/src/components/mail/view.rs

2584 lines
108 KiB
Rust
Raw Normal View History

2018-08-07 15:01:15 +03:00
/*
* meli
2018-08-07 15:01:15 +03:00
*
* 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::*;
2020-07-04 17:38:57 +03:00
use crate::conf::accounts::JobRequest;
use crate::jobs::{JobId, JoinHandle};
2020-10-05 18:43:08 +03:00
use melib::email::attachment_types::ContentType;
use melib::list_management;
use melib::parser::BytesExt;
use smallvec::SmallVec;
2020-07-04 17:38:57 +03:00
use std::collections::HashSet;
use std::io::Write;
2019-06-18 21:13:58 +03:00
use std::convert::TryFrom;
2020-10-05 18:43:08 +03:00
use std::os::unix::fs::PermissionsExt;
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::*;
2020-10-05 18:43:08 +03:00
use linkify::LinkFinder;
use xdg_utils::query_default_app;
#[derive(PartialEq, Copy, Clone, Debug)]
enum Source {
Decoded,
Raw,
}
2020-02-22 11:47:13 +02:00
#[derive(PartialEq, Debug)]
enum ViewMode {
Normal,
Url,
Attachment(usize),
Source(Source),
2020-10-05 18:43:08 +03:00
//Ansi(RawBuffer),
2018-08-09 16:50:33 +03:00
Subview,
ContactSelector(UIDialog<Card>),
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 {
2020-10-05 18:43:08 +03:00
/*
2019-12-27 15:20:02 +02:00
fn is_ansi(&self) -> bool {
match self {
ViewMode::Ansi(_) => true,
_ => false,
}
}
2020-10-05 18:43:08 +03:00
*/
2018-07-25 22:37:28 +03:00
fn is_attachment(&self) -> bool {
match self {
ViewMode::Attachment(_) => true,
_ => false,
}
}
fn is_contact_selector(&self) -> bool {
match self {
ViewMode::ContactSelector(_) => true,
_ => false,
}
}
}
2020-10-05 18:43:08 +03:00
#[derive(Debug)]
pub enum AttachmentDisplay {
Alternative {
inner: Attachment,
shown_display: usize,
display: Vec<AttachmentDisplay>,
},
2020-10-05 18:43:08 +03:00
InlineText {
inner: Attachment,
comment: Option<String>,
2020-10-05 18:43:08 +03:00
text: String,
},
InlineOther {
inner: Attachment,
},
Attachment {
inner: Attachment,
},
SignedPending {
inner: Attachment,
display: Vec<AttachmentDisplay>,
handle: JoinHandle<Result<()>>,
2020-10-05 18:43:08 +03:00
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,
handle: JoinHandle<Result<(melib::pgp::DecryptionMetadata, Vec<u8>)>>,
2020-10-05 18:43:08 +03:00
},
EncryptedFailed {
inner: Attachment,
error: MeliError,
},
EncryptedSuccess {
inner: Attachment,
plaintext: Attachment,
plaintext_display: Vec<AttachmentDisplay>,
description: String,
},
}
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 {
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
pager: Pager,
subview: Option<Box<dyn Component>>,
dirty: bool,
initialised: bool,
mode: ViewMode,
expand_headers: bool,
attachment_tree: String,
2020-10-05 18:43:08 +03:00
attachment_paths: Vec<Vec<usize>>,
headers_no: usize,
headers_cursor: usize,
force_draw_headers: bool,
theme_default: ThemeAttribute,
2020-07-04 17:38:57 +03:00
active_jobs: HashSet<JobId>,
state: MailViewState,
cmd_buf: String,
2019-04-10 22:01:02 +03:00
id: ComponentId,
}
#[derive(Debug)]
pub enum PendingReplyAction {
Reply,
ReplyToAuthor,
ReplyToAll,
}
2020-07-04 17:38:57 +03:00
#[derive(Debug)]
2020-10-05 18:43:08 +03:00
enum MailViewState {
Init {
pending_action: Option<PendingReplyAction>,
},
2020-07-04 17:38:57 +03:00
LoadingBody {
handle: JoinHandle<Result<Vec<u8>>>,
pending_action: Option<PendingReplyAction>,
2020-07-04 17:38:57 +03:00
},
Error {
err: MeliError,
},
2020-07-04 17:38:57 +03:00
Loaded {
bytes: Vec<u8>,
body: Attachment,
2020-10-05 18:43:08 +03:00
display: Vec<AttachmentDisplay>,
body_text: String,
2020-10-05 18:43:08 +03:00
links: Vec<Link>,
2020-07-04 17:38:57 +03:00
},
}
2020-10-05 18:43:08 +03:00
#[derive(Copy, Clone, Debug)]
enum LinkKind {
Url,
Email,
}
#[derive(Debug, Copy, Clone)]
struct Link {
start: usize,
end: usize,
kind: LinkKind,
}
2020-07-04 17:38:57 +03:00
impl Default for MailViewState {
fn default() -> Self {
MailViewState::Init {
pending_action: None,
}
2020-07-04 17:38:57 +03:00
}
}
impl Clone for MailView {
fn clone(&self) -> Self {
MailView {
subview: None,
cmd_buf: String::with_capacity(4),
pager: self.pager.clone(),
2020-02-22 11:47:13 +02:00
mode: ViewMode::Normal,
attachment_tree: self.attachment_tree.clone(),
2020-10-05 18:43:08 +03:00
attachment_paths: self.attachment_paths.clone(),
2020-07-04 17:38:57 +03:00
state: MailViewState::default(),
active_jobs: self.active_jobs.clone(),
..*self
}
}
}
impl fmt::Display for MailView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", MailView::DESCRIPTION)
}
}
impl MailView {
2019-11-27 01:43:03 +02:00
const DESCRIPTION: &'static str = "view mail";
2018-07-27 18:01:52 +03:00
pub fn new(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
2018-07-27 18:01:52 +03:00
pager: Option<Pager>,
subview: Option<Box<dyn Component>>,
2020-07-04 17:38:57 +03:00
context: &mut Context,
2018-08-23 15:36:52 +03:00
) -> Self {
2020-07-04 17:38:57 +03:00
let mut ret = MailView {
2018-08-07 15:01:15 +03:00
coordinates,
pager: pager.unwrap_or_default(),
2018-08-07 15:01:15 +03:00
subview,
dirty: true,
initialised: false,
mode: ViewMode::Normal,
expand_headers: false,
attachment_tree: String::new(),
2020-10-05 18:43:08 +03:00
attachment_paths: vec![],
headers_no: 5,
headers_cursor: 0,
force_draw_headers: false,
theme_default: crate::conf::value(context, "mail.view.body"),
2020-07-04 17:38:57 +03:00
active_jobs: Default::default(),
state: MailViewState::default(),
cmd_buf: String::with_capacity(4),
2019-04-11 00:04:17 +03:00
id: ComponentId::new_v4(),
2020-07-04 17:38:57 +03:00
};
ret.init_futures(context);
ret
}
fn init_futures(&mut self, context: &mut Context) {
debug!("init_futures");
let mut pending_action = None;
let account = &mut context.accounts[&self.coordinates.0];
2020-07-04 17:38:57 +03:00
if debug!(account.contains_key(self.coordinates.2)) {
{
match account
.operation(self.coordinates.2)
.and_then(|mut op| op.as_bytes())
{
Ok(fut) => {
let mut handle = account.job_executor.spawn_specialized(fut);
let job_id = handle.job_id;
pending_action = if let MailViewState::Init {
ref mut pending_action,
} = self.state
{
pending_action.take()
} else {
None
};
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 body = AttachmentBuilder::new(&bytes).build();
2020-10-05 18:43:08 +03:00
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 {
2020-10-05 18:43:08 +03:00
display,
body,
bytes,
body_text,
2020-10-05 18:43:08 +03:00
links: vec![],
};
}
Err(err) => {
self.state = MailViewState::Error { err };
}
}
2020-07-05 13:22:48 +03:00
} else {
self.state = MailViewState::LoadingBody {
handle,
pending_action: pending_action.take(),
};
self.active_jobs.insert(job_id);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::NewJob(job_id)));
2020-07-05 13:22:48 +03:00
}
2020-07-04 17:38:57 +03:00
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("Could not get message: {}", err)),
));
}
}
}
let account = &mut context.accounts[&self.coordinates.0];
2020-07-04 17:38:57 +03:00
if !account.collection.get_env(self.coordinates.2).is_seen() {
let job = account.backend.write().unwrap().set_flags(
self.coordinates.2.into(),
self.coordinates.1,
smallvec::smallvec![(Ok(Flag::SEEN), true)],
);
match job {
2020-07-04 17:38:57 +03:00
Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut);
account.insert_job(
handle.job_id,
JobRequest::SetFlags {
env_hashes: self.coordinates.2.into(),
handle,
},
);
2020-07-04 17:38:57 +03:00
}
Err(e) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Could not set message as seen: {}",
e
)),
));
}
};
2020-07-04 17:38:57 +03:00
}
}
if let Some(p) = pending_action {
self.perform_action(p, context);
}
}
fn perform_action(&mut self, action: PendingReplyAction, context: &mut Context) {
let reply_body = match self.state {
MailViewState::Init {
ref mut pending_action,
..
}
| MailViewState::LoadingBody {
ref mut pending_action,
..
} => {
if pending_action.is_none() {
*pending_action = Some(action);
}
return;
}
MailViewState::Loaded { ref display, .. } => {
self.attachment_displays_to_text(&display, context, false)
}
MailViewState::Error { .. } => {
return;
}
};
let composer = match action {
PendingReplyAction::Reply => Box::new(Composer::reply_to_select(
self.coordinates,
reply_body,
context,
)),
PendingReplyAction::ReplyToAuthor => Box::new(Composer::reply_to_author(
self.coordinates,
reply_body,
context,
)),
PendingReplyAction::ReplyToAll => Box::new(Composer::reply_to_all(
self.coordinates,
reply_body,
context,
)),
};
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(composer)))));
}
2020-10-05 18:43:08 +03:00
fn attachment_displays_to_text(
&self,
displays: &[AttachmentDisplay],
context: &mut Context,
show_comments: bool,
2020-10-05 18:43:08 +03:00
) -> String {
let mut acc = String::new();
for d in displays {
use AttachmentDisplay::*;
match d {
Alternative {
inner: _,
shown_display,
display,
} => {
acc.push_str(&self.attachment_displays_to_text(
&display[*shown_display..(*shown_display + 1)],
context,
show_comments,
));
}
InlineText {
inner: _,
text,
comment: Some(comment),
} if show_comments => {
acc.push_str(comment);
if !acc.ends_with("\n\n") {
acc.push_str("\n\n");
}
acc.push_str(&text);
}
InlineText {
inner: _,
text,
comment: _,
} => acc.push_str(&text),
2020-10-05 18:43:08 +03:00
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,
handle: _,
2020-10-05 18:43:08 +03:00
job_id: _,
} => {
if show_comments {
acc.push_str("Waiting for signature verification.\n\n");
}
acc.push_str(&self.attachment_displays_to_text(
display,
context,
show_comments,
));
2020-10-05 18:43:08 +03:00
}
SignedUnverified { inner: _, display } => {
if show_comments {
acc.push_str("Unverified signature.\n\n");
}
acc.push_str(&self.attachment_displays_to_text(display, context, show_comments))
2020-10-05 18:43:08 +03:00
}
SignedFailed {
inner: _,
display,
error,
} => {
if show_comments {
acc.push_str(&format!("Failed to verify signature: {}.\n\n", error));
}
acc.push_str(&self.attachment_displays_to_text(
display,
context,
show_comments,
));
2020-10-05 18:43:08 +03:00
}
SignedVerified {
inner: _,
display,
description,
} => {
if show_comments {
if description.is_empty() {
acc.push_str("Verified signature.\n\n");
} else {
acc.push_str(&description);
acc.push_str("\n\n");
}
2020-10-05 18:43:08 +03:00
}
acc.push_str(&self.attachment_displays_to_text(
display,
context,
show_comments,
));
2020-10-05 18:43:08 +03:00
}
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 show_comments {
if description.is_empty() {
acc.push_str("Succesfully decrypted.\n\n");
} else {
acc.push_str(&description);
acc.push_str("\n\n");
}
2020-10-05 18:43:08 +03:00
}
acc.push_str(&self.attachment_displays_to_text(
plaintext_display,
context,
show_comments,
));
2020-10-05 18:43:08 +03:00
}
}
}
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;
fn append_entry(
(idx, (depth, att_display)): (&mut usize, (usize, &AttachmentDisplay)),
branches: &mut SmallVec<[bool; 8]>,
paths: &mut Vec<Vec<usize>>,
cur_path: &mut Vec<usize>,
has_sibling: bool,
s: &mut String,
) {
2020-10-05 18:43:08 +03:00
use AttachmentDisplay::*;
let mut default_alternative: Option<usize> = None;
let (att, sub_att_display_vec) = match att_display {
Alternative {
inner,
shown_display,
display,
} => {
default_alternative = Some(*shown_display);
(inner, display.as_slice())
}
InlineText {
inner,
text: _,
comment: _,
}
2020-10-05 18:43:08 +03:00
| InlineOther { inner }
| Attachment { inner }
| EncryptedPending { inner, handle: _ }
| EncryptedFailed { inner, error: _ } => (inner, &[][..]),
SignedPending {
2020-10-05 18:43:08 +03:00
inner,
display,
handle: _,
2020-10-05 18:43:08 +03:00
job_id: _,
}
| SignedUnverified { inner, display }
2020-10-05 18:43:08 +03:00
| SignedFailed {
inner,
display,
2020-10-05 18:43:08 +03:00
error: _,
}
| SignedVerified {
inner,
display,
2020-10-05 18:43:08 +03:00
description: _,
}
| EncryptedSuccess {
inner: _,
plaintext: inner,
plaintext_display: display,
2020-10-05 18:43:08 +03:00
description: _,
} => (inner, display.as_slice()),
};
s.extend(format!("\n[{}]", idx).chars());
for &b in branches.iter() {
if b {
s.push('|');
} else {
s.push(' ');
}
s.push(' ');
}
if depth > 0 {
if has_sibling {
s.push('|');
} else {
s.push(' ');
}
s.push_str("\\_ ");
} else {
s.push(' ');
s.push(' ');
}
s.extend(att.to_string().chars());
paths.push(cur_path.clone());
match att.content_type {
ContentType::Multipart { .. } => {
let mut iter = (0..sub_att_display_vec.len()).peekable();
if has_sibling {
branches.push(true);
} else {
branches.push(false);
}
while let Some(i) = iter.next() {
*idx += 1;
cur_path.push(i);
append_entry(
(idx, (depth + 1, &sub_att_display_vec[i])),
branches,
paths,
cur_path,
iter.peek() != None,
s,
);
if Some(i) == default_alternative {
s.push_str(" (displayed by default)");
}
cur_path.pop();
}
branches.pop();
2020-10-05 18:43:08 +03:00
}
_ => {}
2020-10-05 18:43:08 +03:00
}
}
for (i, d) in displays.iter().enumerate() {
cur_path.push(i);
append_entry(
(&mut idx, (0, d)),
&mut branches,
&mut paths,
&mut cur_path,
i + 1 < displays.len(),
&mut acc,
);
2020-10-05 18:43:08 +03:00
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() || a.content_type == "message/rfc822" {
2020-10-05 18:43:08 +03:00
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 comment = Some(format!(
2020-10-05 18:43:08 +03:00
"Failed to start html filter process: `{}`. Press `v` to open in web browser. \n\n",
filter_invocation
));
let text = String::from_utf8_lossy(&bytes).to_string();
2020-10-05 18:43:08 +03:00
acc.push(AttachmentDisplay::InlineText {
inner: a.clone(),
comment,
text,
2020-10-05 18:43:08 +03:00
});
}
Ok(mut html_filter) => {
html_filter
.stdin
.as_mut()
.unwrap()
.write_all(&bytes)
.expect("Failed to write to stdin");
let comment = Some(format!(
"Text piped through `{}`. Press `v` to open in web browser. \n\n",
filter_invocation
2020-10-05 18:43:08 +03:00
));
let text = String::from_utf8_lossy(
&html_filter.wait_with_output().unwrap().stdout,
)
.to_string();
2020-10-05 18:43:08 +03:00
acc.push(AttachmentDisplay::InlineText {
inner: a.clone(),
comment,
text,
2020-10-05 18:43:08 +03:00
});
}
}
} else if a.is_text() {
let bytes = decode(a, None);
acc.push(AttachmentDisplay::InlineText {
inner: a.clone(),
comment: None,
2020-10-05 18:43:08 +03:00
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 parts.is_empty() {
return;
}
let mut display = vec![];
let mut chosen_attachment_idx = 0;
2020-10-05 18:43:08 +03:00
if let Some(text_attachment_pos) =
parts.iter().position(|a| a.content_type == "text/plain")
{
let bytes = decode(&parts[text_attachment_pos], None);
if bytes.trim().is_empty()
&& mailbox_settings!(
context[coordinates.0][&coordinates.1]
.pager
.auto_choose_multipart_alternative
)
.is_true()
{
if let Some(text_attachment_pos) =
parts.iter().position(|a| a.content_type == "text/html")
{
/* Select html alternative since text/plain is empty */
chosen_attachment_idx = text_attachment_pos;
}
} else {
/* Select text/plain alternative */
chosen_attachment_idx = text_attachment_pos;
2020-09-13 15:23:14 +03:00
}
}
for a in parts {
rec(a, context, coordinates, &mut display, active_jobs);
}
acc.push(AttachmentDisplay::Alternative {
inner: a.clone(),
shown_display: chosen_attachment_idx,
display,
});
2020-10-05 18:43:08 +03:00
}
MultipartType::Signed => {
#[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")]
{
if *mailbox_settings!(
context[coordinates.0][&coordinates.1]
.pgp
.auto_verify_signatures
2020-10-05 18:43:08 +03:00
) {
let verify_fut = crate::components::mail::pgp::verify(a.clone());
let handle = context.job_executor.spawn_specialized(verify_fut);
active_jobs.insert(handle.job_id);
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::NewJob(handle.job_id),
));
2020-10-05 18:43:08 +03:00
acc.push(AttachmentDisplay::SignedPending {
inner: a.clone(),
job_id: handle.job_id,
2020-10-05 18:43:08 +03:00
display: {
let mut v = vec![];
rec(&parts[0], context, coordinates, &mut v, active_jobs);
v
},
handle,
2020-10-05 18:43:08 +03:00
});
} else {
acc.push(AttachmentDisplay::SignedUnverified {
inner: a.clone(),
display: {
let mut v = vec![];
rec(&parts[0], context, coordinates, &mut v, active_jobs);
v
},
});
2020-09-13 15:23:14 +03:00
}
2020-10-05 18:43:08 +03:00
}
}
MultipartType::Encrypted => {
for a in parts {
if a.content_type == "application/octet-stream" {
#[cfg(not(feature = "gpgme"))]
{
acc.push(AttachmentDisplay::EncryptedFailed {
inner: a.clone(),
error: MeliError::new("Cannot decrypt: meli must be compiled with libgpgme support."),
});
}
#[cfg(feature = "gpgme")]
{
if *mailbox_settings!(
context[coordinates.0][&coordinates.1].pgp.auto_decrypt
2020-10-05 18:43:08 +03:00
) {
let decrypt_fut =
crate::components::mail::pgp::decrypt(a.raw().to_vec());
let handle =
context.job_executor.spawn_specialized(decrypt_fut);
active_jobs.insert(handle.job_id);
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::NewJob(handle.job_id),
));
2020-10-05 18:43:08 +03:00
acc.push(AttachmentDisplay::EncryptedPending {
inner: a.clone(),
handle,
2020-10-05 18:43:08 +03:00
});
} else {
acc.push(AttachmentDisplay::EncryptedFailed {
inner: a.clone(),
error: MeliError::new("Undecrypted."),
});
2020-10-05 18:43:08 +03:00
}
}
2020-09-13 15:23:14 +03:00
}
}
}
2020-10-05 18:43:08 +03:00
_ => {
for a in parts {
rec(a, context, coordinates, acc, active_jobs);
}
}
2018-08-23 15:36:52 +03:00
}
}
2020-10-05 18:43:08 +03:00
};
rec(body, context, coordinates, &mut ret, active_jobs);
ret
}
2020-07-04 17:38:57 +03:00
pub fn update(
&mut self,
new_coordinates: (AccountHash, MailboxHash, EnvelopeHash),
2020-07-04 17:38:57 +03:00
context: &mut Context,
) {
self.coordinates = new_coordinates;
self.mode = ViewMode::Normal;
self.initialised = false;
2020-07-04 17:38:57 +03:00
self.init_futures(context);
self.set_dirty(true);
}
2020-07-04 17:38:57 +03:00
2020-10-05 18:43:08 +03:00
fn open_attachment(
&'_ self,
lidx: usize,
context: &mut Context,
) -> Option<&'_ melib::Attachment> {
let display = if let MailViewState::Loaded { ref display, .. } = self.state {
display
2020-07-04 17:38:57 +03:00
} else {
2020-10-05 18:43:08 +03:00
return None;
2020-07-04 17:38:57 +03:00
};
2020-10-05 18:43:08 +03:00
if let Some(path) =
self.attachment_paths.get(lidx).and_then(
|path| {
if path.len() > 0 {
Some(path)
} else {
None
2020-07-04 17:38:57 +03:00
}
2020-10-05 18:43:08 +03:00
},
)
{
let first = path[0];
use AttachmentDisplay::*;
let root_attachment = match &display[first] {
Alternative {
inner,
shown_display: _,
display: _,
}
| InlineText {
inner,
text: _,
comment: _,
}
2020-10-05 18:43:08 +03:00
| InlineOther { inner }
| Attachment { inner }
| SignedPending {
inner,
display: _,
handle: _,
2020-10-05 18:43:08 +03:00
job_id: _,
}
| SignedFailed {
inner,
display: _,
error: _,
}
| SignedVerified {
inner,
display: _,
description: _,
}
| SignedUnverified { inner, display: _ }
| EncryptedPending { inner, handle: _ }
2020-10-05 18:43:08 +03:00
| EncryptedFailed { inner, error: _ }
| EncryptedSuccess {
inner: _,
plaintext: inner,
plaintext_display: _,
description: _,
} => inner,
};
fn find_attachment<'a>(
a: &'a melib::Attachment,
path: &[usize],
) -> Option<&'a melib::Attachment> {
if path.is_empty() {
return Some(a);
}
match a.content_type {
ContentType::Multipart { ref parts, .. } => {
let first = path[0];
if first < parts.len() {
return find_attachment(&parts[first], &path[1..]);
2020-07-04 17:38:57 +03:00
}
}
2020-10-05 18:43:08 +03:00
_ => {}
2020-07-04 17:38:57 +03:00
}
2020-10-05 18:43:08 +03:00
None
2020-07-04 17:38:57 +03:00
}
2020-10-05 18:43:08 +03:00
2020-10-14 20:13:15 +03:00
let ret = find_attachment(root_attachment, &path[1..]);
if lidx == 0 {
return ret.and_then(|a| {
if a.content_disposition.kind.is_attachment()
|| a.content_type == "message/rfc822"
{
2020-10-14 20:13:15 +03:00
Some(a)
} else {
None
}
});
} else {
return ret;
}
2020-07-04 17:38:57 +03:00
}
2020-10-05 18:43:08 +03:00
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
"Attachment `{}` not found.",
lidx
))));
None
2020-07-04 17:38:57 +03:00
}
}
impl Component for MailView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() && !self.force_draw_headers {
2019-05-01 13:55:12 +03:00
return;
}
self.dirty = false;
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let y: usize = {
let account = &context.accounts[&self.coordinates.0];
2019-06-18 21:13:58 +03:00
if !account.contains_key(self.coordinates.2) {
/* The envelope has been renamed or removed, so wait for the appropriate event to
* arrive */
return;
}
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
2019-05-01 13:55:12 +03:00
let headers = crate::conf::value(context, "mail.view.headers");
2019-10-06 10:58:47 +03:00
if let ViewMode::Source(_) = self.mode {
clear_area(grid, area, self.theme_default);
context.dirty_areas.push_back(area);
get_y(upper_left)
} else {
let height_p = self.pager.size().1;
let height = height!(area) - self.headers_no - 1;
self.headers_no = 0;
let mut skip_header_ctr = self.headers_cursor;
let sticky = *mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.headers_sticky
) || height_p < height;
let (_, mut y) = upper_left;
macro_rules! print_header {
($($string:expr)+) => {
$({
if sticky || skip_header_ctr == 0 {
let (_x, _y) = write_string_to_grid(
&$string,
grid,
headers.fg,
headers.bg,
headers.attrs,
(set_y(upper_left, y), bottom_right),
Some(get_x(upper_left)),
);
clear_area(grid, ((_x, _y), (get_x(bottom_right), _y)), headers);
y = _y + 1;
} else {
skip_header_ctr -= 1;
}
self.headers_no += 1;
})+
};
}
print_header!(
format!("Date: {}", envelope.date_as_str())
format!("From: {}", envelope.field_from_to_string())
format!("To: {}", envelope.field_to_to_string())
);
if envelope.other_headers().contains_key("Cc")
&& !envelope.other_headers()["Cc"].is_empty()
{
print_header!(format!("Cc: {}", envelope.field_cc_to_string()));
}
print_header!(
format!("Subject: {}", envelope.subject())
format!("Message-ID: <{}>", envelope.message_id_raw())
);
2020-01-15 12:38:18 +02:00
if self.expand_headers {
if let Some(val) = envelope.in_reply_to_display() {
print_header!(
format!("In-Reply-To: {}", val)
format!(
"References: {}",
envelope
.references()
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
.join(", ")
)
);
}
}
if let Some(list_management::ListActions {
ref id,
ref archive,
ref post,
ref unsubscribe,
}) = list_management::ListActions::detect(&envelope)
{
let mut x = get_x(upper_left);
if let Some(id) = id {
if sticky || skip_header_ctr == 0 {
clear_area(
grid,
(set_y(upper_left, y), set_y(bottom_right, y)),
headers,
);
let (_x, _) = write_string_to_grid(
"List-ID: ",
grid,
headers.fg,
headers.bg,
headers.attrs,
(set_y(upper_left, y), bottom_right),
None,
);
let (_x, _y) = write_string_to_grid(
id,
grid,
Color::Default,
Color::Default,
Attr::DEFAULT,
((_x, y), bottom_right),
None,
);
x = _x;
if _y != y {
x = get_x(upper_left);
}
y = _y;
}
self.headers_no += 1;
}
if sticky || skip_header_ctr == 0 {
if archive.is_some() || post.is_some() || unsubscribe.is_some() {
let (_x, _y) = write_string_to_grid(
" Available actions: [ ",
grid,
headers.fg,
headers.bg,
headers.attrs,
((x, y), bottom_right),
Some(get_x(upper_left)),
);
x = _x;
y = _y;
}
if archive.is_some() {
let (_x, _y) = write_string_to_grid(
"list-archive, ",
grid,
Color::Default,
Color::Default,
Attr::DEFAULT,
((x, y), bottom_right),
Some(get_x(upper_left)),
);
x = _x;
y = _y;
}
if post.is_some() {
let (_x, _y) = write_string_to_grid(
"list-post, ",
grid,
Color::Default,
Color::Default,
Attr::DEFAULT,
((x, y), bottom_right),
Some(get_x(upper_left)),
);
x = _x;
y = _y;
}
if unsubscribe.is_some() {
let (_x, _y) = write_string_to_grid(
"list-unsubscribe, ",
grid,
Color::Default,
Color::Default,
Attr::DEFAULT,
((x, y), bottom_right),
Some(get_x(upper_left)),
);
x = _x;
y = _y;
}
if archive.is_some() || post.is_some() || unsubscribe.is_some() {
if x >= 2 {
grid[(x - 2, y)].set_ch(' ');
}
if x > 0 {
grid[(x - 1, y)].set_fg(headers.fg);
grid[(x - 1, y)].set_bg(headers.bg);
grid[(x - 1, y)].set_attrs(headers.attrs);
grid[(x - 1, y)].set_ch(']');
}
}
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);
}
y += 1;
}
}
self.force_draw_headers = false;
clear_area(
grid,
(set_y(upper_left, y), set_y(bottom_right, y)),
headers,
);
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 3)));
if !*mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.headers_sticky
) {
let height_p = self.pager.size().1;
2020-02-08 13:45:55 +02:00
let height = height!(area).saturating_sub(y).saturating_sub(1);
if self.pager.cursor_pos() >= self.headers_no {
get_y(upper_left)
2020-07-05 15:28:55 +03:00
} else if (height_p > height && self.headers_cursor < self.headers_no + 1)
|| self.headers_cursor == 0
|| height_p < height
{
y + 1
} else {
get_y(upper_left)
}
} else {
y + 1
}
}
};
if !self.initialised {
2020-10-05 18:43:08 +03:00
let (body, body_text, bytes, links) = if let MailViewState::Loaded {
ref body,
ref body_text,
ref bytes,
ref mut links,
..
} = self.state
{
(body, body_text, bytes, links)
} else if let MailViewState::Error { ref err } = self.state {
clear_area(
grid,
(set_y(upper_left, y), bottom_right),
self.theme_default,
);
context
.dirty_areas
.push_back((set_y(upper_left, y), bottom_right));
context.replies.push_back(UIEvent::Notification(
Some("Failed to open e-mail".to_string()),
err.to_string(),
2020-09-13 15:23:14 +03:00
Some(NotificationType::Error(err.kind)),
));
log(
format!("Failed to open envelope: {}", err.to_string()),
ERROR,
);
self.init_futures(context);
return;
2020-07-04 17:38:57 +03:00
} else {
clear_area(
grid,
(set_y(upper_left, y), bottom_right),
self.theme_default,
);
context
.dirty_areas
.push_back((set_y(upper_left, y), bottom_right));
return;
};
2020-07-04 17:38:57 +03:00
self.initialised = true;
2020-10-05 18:43:08 +03:00
//let body = AttachmentBuilder::new(bytes).build();
2018-08-09 16:50:33 +03:00
match self.mode {
2020-10-05 18:43:08 +03:00
ViewMode::Attachment(aidx) => {
let mut text = "Viewing attachment. Press `r` to return \n".to_string();
if let Some(attachment) = self.open_attachment(aidx, context) {
if attachment.is_html() {
self.subview = Some(Box::new(HtmlView::new(&attachment, context)));
self.mode = ViewMode::Subview;
} else {
text.push_str(&attachment.text());
let colors = crate::conf::value(context, "mail.view.body");
self.pager =
Pager::from_string(text, Some(context), Some(0), None, colors);
self.subview = None;
}
} else {
text.push_str("Internal error. MailView::open_attachment failed.");
let colors = crate::conf::value(context, "mail.view.body");
self.pager = Pager::from_string(text, Some(context), Some(0), None, colors);
self.subview = None;
}
}
2018-08-09 16:50:33 +03:00
ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(&body, context)));
2018-08-09 16:50:33 +03:00
self.mode = ViewMode::Subview;
}
ViewMode::Normal
if mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.auto_choose_multipart_alternative
)
.is_true()
&& match body.content_type {
ContentType::Multipart {
kind: MultipartType::Alternative,
ref parts,
..
} => parts.iter().all(|p| {
p.is_html() || (p.is_text() && p.body().trim().is_empty())
}),
_ => false,
} =>
{
self.subview = Some(Box::new(HtmlView::new(
&body
.content_type
.parts()
.unwrap()
.into_iter()
.find(|a| a.is_html())
.unwrap_or(&body),
context,
)));
self.mode = ViewMode::Subview;
self.initialised = false;
}
2019-02-26 10:55:22 +02:00
ViewMode::Subview | ViewMode::ContactSelector(_) => {}
ViewMode::Source(source) => {
2019-05-07 21:53:36 +03:00
let text = {
if source == Source::Raw {
2020-07-04 17:38:57 +03:00
String::from_utf8_lossy(bytes).into_owned()
} else {
/* Decode each header value */
2020-07-04 17:38:57 +03:00
let mut ret = melib::email::parser::headers::headers(bytes)
.map(|(_, v)| v)
.map_err(|err| err.into())
.and_then(|headers| {
Ok(headers
.into_iter()
.map(|(h, v)| {
melib::email::parser::encodings::phrase(v, true)
.map(|(_, v)| {
let mut h = h.to_vec();
h.push(b':');
h.push(b' ');
h.extend(v.into_iter());
h
})
.map_err(|err| err.into())
})
.collect::<Result<Vec<Vec<u8>>>>()?
.join(&b"\n"[..]))
})
.map(|v| String::from_utf8_lossy(&v).into_owned())
2020-07-04 17:38:57 +03:00
.unwrap_or_else(|err: MeliError| err.to_string());
2020-10-05 18:43:08 +03:00
if !ret.ends_with("\n\n") {
ret.push_str("\n\n");
}
ret.extend(body_text.chars());
if !ret.ends_with("\n\n") {
ret.push_str("\n\n");
}
ret.push_str(&self.attachment_tree);
ret
}
2019-05-07 21:53:36 +03:00
};
let colors = crate::conf::value(context, "mail.view.body");
self.pager = Pager::from_string(text, Some(context), None, None, colors);
2019-05-07 21:53:36 +03:00
}
2020-10-05 18:43:08 +03:00
/*
2019-12-27 15:20:02 +02:00
ViewMode::Ansi(ref buf) => {
write_string_to_grid(
&format!("Viewing `{}`. Press `r` to return", buf.title()),
grid,
Color::Default,
Color::Default,
Attr::DEFAULT,
2019-12-27 15:20:02 +02:00
(set_y(upper_left, y), bottom_right),
Some(get_x(upper_left)),
);
}
2020-10-05 18:43:08 +03:00
*/
ViewMode::Url => {
let mut text = body_text.clone();
if links.is_empty() {
let finder = LinkFinder::new();
*links = finder
.links(&text)
.filter_map(|l| {
if *l.kind() == linkify::LinkKind::Url {
Some(Link {
start: l.start(),
end: l.end(),
kind: LinkKind::Url,
})
} else if *l.kind() == linkify::LinkKind::Email {
Some(Link {
start: l.start(),
end: l.end(),
kind: LinkKind::Email,
})
} else {
None
}
})
.collect::<Vec<Link>>();
}
for (lidx, l) in links.iter().enumerate().rev() {
text.insert_str(l.start, &format!("[{}]", lidx));
}
if !text.ends_with("\n\n") {
text.push_str("\n\n");
}
text.push_str(&self.attachment_tree);
let cursor_pos = self.pager.cursor_pos();
let colors = crate::conf::value(context, "mail.view.body");
self.pager =
Pager::from_string(text, Some(context), Some(cursor_pos), None, colors);
self.subview = None;
}
2018-08-09 16:50:33 +03:00
_ => {
2020-10-05 18:43:08 +03:00
let mut text = body_text.clone();
if !text.ends_with("\n\n") {
text.push_str("\n\n");
}
text.push_str(&self.attachment_tree);
2018-08-09 16:50:33 +03:00
let cursor_pos = if self.mode.is_attachment() {
0
2018-08-09 16:50:33 +03:00
} else {
self.pager.cursor_pos()
2018-08-09 16:50:33 +03:00
};
let colors = crate::conf::value(context, "mail.view.body");
self.pager =
Pager::from_string(text, Some(context), Some(cursor_pos), None, colors);
self.subview = None;
}
};
}
match self.mode {
ViewMode::Subview if self.subview.is_some() => {
if let Some(s) = self.subview.as_mut() {
s.draw(grid, (set_y(upper_left, y), bottom_right), context);
}
}
2020-10-05 18:43:08 +03:00
/*
2019-12-27 15:20:02 +02:00
ViewMode::Ansi(ref mut buf) => {
buf.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
2020-10-05 18:43:08 +03:00
}*/
_ => {
self.pager
.draw(grid, (set_y(upper_left, y), bottom_right), context);
}
2018-08-07 15:01:15 +03:00
}
if let ViewMode::ContactSelector(ref mut s) = self.mode {
s.draw(grid, area, context);
}
}
2020-02-22 11:47:13 +02:00
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
if self.coordinates.0 == 0 || self.coordinates.1 == 0 {
return false;
}
let shortcuts = self.get_shortcuts(context);
2020-02-22 11:47:13 +02:00
match (&mut self.mode, &mut event) {
2020-10-05 18:43:08 +03:00
/*(ViewMode::Ansi(ref mut buf), _) => {
2019-12-27 15:20:02 +02:00
if buf.process_event(event, context) {
return true;
}
2020-10-05 18:43:08 +03:00
}*/
2020-02-22 11:47:13 +02:00
(ViewMode::Subview, _) => {
if let Some(s) = self.subview.as_mut() {
if s.process_event(event, context) {
return true;
}
}
2019-03-14 12:19:25 +02:00
}
2020-02-22 11:47:13 +02:00
(ViewMode::ContactSelector(ref s), UIEvent::FinishedUIDialog(id, results))
if *id == s.id() =>
{
if let Some(results) = results.downcast_ref::<Vec<Card>>() {
let account = &mut context.accounts[&self.coordinates.0];
2020-02-22 11:47:13 +02:00
{
for card in results.iter() {
account.address_book.add_card(card.clone());
}
}
2020-02-22 11:47:13 +02:00
}
self.mode = ViewMode::Normal;
self.initialised = false;
self.set_dirty(true);
2020-02-22 11:47:13 +02:00
return true;
}
(ViewMode::ContactSelector(ref mut s), _) => {
if s.process_event(event, context) {
2019-02-15 09:06:42 +02:00
return true;
}
if self.pager.process_event(event, context) {
return true;
2019-10-03 19:11:02 +03:00
}
2019-03-14 12:19:25 +02:00
}
_ => match event {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Pager::DESCRIPTION]["scroll_up"])
&& !*mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.headers_sticky
)
&& self.headers_cursor <= self.headers_no =>
{
self.force_draw_headers = true;
if self.pager.cursor_pos() == 0 {
self.headers_cursor = self.headers_cursor.saturating_sub(1);
} else {
if self.pager.process_event(event, context) {
return true;
}
}
self.pager.set_dirty(true);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Pager::DESCRIPTION]["scroll_down"])
&& !*mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.headers_sticky
)
&& self.headers_cursor < self.headers_no =>
{
self.force_draw_headers = true;
self.headers_cursor += 1;
self.pager.set_dirty(true);
return true;
}
2020-07-04 17:38:57 +03:00
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self.active_jobs.contains(job_id) =>
{
match self.state {
MailViewState::LoadingBody {
ref mut handle,
pending_action: _,
} if handle.job_id == *job_id => {
let bytes_result = handle.chan.try_recv().unwrap().unwrap();
match bytes_result {
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 body = AttachmentBuilder::new(&bytes).build();
2020-10-05 18:43:08 +03:00
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,
2020-10-05 18:43:08 +03:00
body,
display,
links: vec![],
body_text,
};
}
Err(err) => {
self.state = MailViewState::Error { err };
}
}
2020-07-04 17:38:57 +03:00
}
MailViewState::Init { .. } => {
2020-07-04 17:38:57 +03:00
self.init_futures(context);
}
2020-10-05 18:43:08 +03:00
MailViewState::Loaded {
ref mut display, ..
} => {
let mut caught = false;
for d in display.iter_mut() {
match d {
AttachmentDisplay::SignedPending {
inner,
handle,
2020-10-05 18:43:08 +03:00
display,
job_id: our_job_id,
} if *our_job_id == *job_id => {
2020-10-05 18:43:08 +03:00
caught = true;
self.initialised = false;
match handle.chan.try_recv().unwrap().unwrap() {
Ok(()) => {
*d = AttachmentDisplay::SignedVerified {
inner: std::mem::replace(
inner,
AttachmentBuilder::new(&[]).build(),
),
display: std::mem::replace(display, vec![]),
description: String::new(),
};
}
Err(error) => {
*d = AttachmentDisplay::SignedFailed {
inner: std::mem::replace(
inner,
AttachmentBuilder::new(&[]).build(),
),
display: std::mem::replace(display, vec![]),
error,
};
}
2020-10-05 18:43:08 +03:00
}
}
AttachmentDisplay::EncryptedPending { inner, handle }
if handle.job_id == *job_id =>
{
2020-10-05 18:43:08 +03:00
caught = true;
self.initialised = false;
match handle.chan.try_recv().unwrap().unwrap() {
2020-10-05 18:43:08 +03:00
Ok((metadata, decrypted_bytes)) => {
let plaintext =
AttachmentBuilder::new(&decrypted_bytes)
.build();
let plaintext_display = Self::attachment_to(
&plaintext,
context,
self.coordinates,
&mut self.active_jobs,
);
*d = AttachmentDisplay::EncryptedSuccess {
inner: std::mem::replace(
inner,
AttachmentBuilder::new(&[]).build(),
),
plaintext,
plaintext_display,
description: format!("{:?}", metadata),
};
}
Err(error) => {
*d = AttachmentDisplay::EncryptedFailed {
inner: std::mem::replace(
inner,
AttachmentBuilder::new(&[]).build(),
),
error,
};
}
}
}
_ => {}
}
}
if caught {
let mut new_body_text = String::new();
if let MailViewState::Loaded { ref display, .. } = self.state {
new_body_text =
self.attachment_displays_to_text(&display, context, true);
2020-10-05 18:43:08 +03:00
let (paths, attachment_tree_s) =
self.attachment_displays_to_tree(&display);
self.attachment_tree = attachment_tree_s;
self.attachment_paths = paths;
}
if let MailViewState::Loaded {
ref mut body_text,
ref mut links,
..
} = self.state
{
links.clear();
*body_text = new_body_text;
}
}
}
2020-07-04 17:38:57 +03:00
_ => {}
}
self.active_jobs.remove(job_id);
self.set_dirty(true);
}
_ => {
if self.pager.process_event(event, context) {
return true;
}
}
},
}
let shortcuts = &self.get_shortcuts(context);
2019-04-10 23:37:20 +03:00
match *event {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply"]) =>
{
self.perform_action(PendingReplyAction::Reply, context);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply_to_all"]) =>
{
self.perform_action(PendingReplyAction::ReplyToAll, context);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply_to_author"]) =>
{
self.perform_action(PendingReplyAction::ReplyToAuthor, context);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["edit"]) =>
{
2020-08-25 15:39:43 +03:00
let account_hash = self.coordinates.0;
let env_hash = self.coordinates.2;
let (sender, mut receiver) = crate::jobs::oneshot::channel();
let operation = context.accounts[&account_hash].operation(env_hash);
let bytes_job = async move {
let _ = sender.send(operation?.as_bytes()?.await);
Ok(())
};
let handle = if context.accounts[&account_hash]
2020-08-25 15:39:43 +03:00
.backend_capabilities
.is_async
{
context.accounts[&account_hash]
.job_executor
.spawn_specialized(bytes_job)
} else {
context.accounts[&account_hash]
.job_executor
.spawn_blocking(bytes_job)
};
context.accounts[&account_hash].insert_job(
handle.job_id,
2020-08-25 15:39:43 +03:00
crate::conf::accounts::JobRequest::Generic {
name: "fetch envelope".into(),
handle,
on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| {
let result = receiver.try_recv().unwrap().unwrap();
match result.and_then(|bytes| {
Composer::edit(account_hash, env_hash, &bytes, context)
}) {
Ok(composer) => {
context.replies.push_back(UIEvent::Action(Tab(New(Some(
Box::new(composer),
)))));
}
Err(err) => {
let err_string = format!(
"Failed to open envelope {}: {}",
context.accounts[&account_hash]
.collection
.envelopes
.read()
.unwrap()
.get(&env_hash)
.map(|env| env.message_id_display())
.unwrap_or_else(|| "Not found".into()),
err.to_string()
);
log(&err_string, ERROR);
context.replies.push_back(UIEvent::Notification(
Some("Failed to open e-mail".to_string()),
err_string,
2020-09-13 15:23:14 +03:00
Some(NotificationType::Error(err.kind)),
2020-08-25 15:39:43 +03:00
));
}
}
}))),
logging_level: melib::LoggingLevel::DEBUG,
2020-08-25 15:39:43 +03:00
},
);
return true;
}
2019-11-11 22:20:16 +02:00
UIEvent::Input(ref key)
if !self.mode.is_contact_selector()
&& shortcut!(
key == shortcuts[MailView::DESCRIPTION]["add_addresses_to_contacts"]
) =>
2019-11-11 22:20:16 +02:00
{
let account = &context.accounts[&self.coordinates.0];
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
2019-02-15 09:06:42 +02:00
let mut entries = Vec::new();
for addr in envelope.from().iter().chain(envelope.to().iter()) {
let mut new_card: Card = Card::new();
new_card.set_email(addr.get_email());
if let Some(display_name) = addr.get_display_name() {
new_card.set_name(display_name);
}
entries.push((new_card, format!("{}", addr)));
}
drop(envelope);
self.mode = ViewMode::ContactSelector(Selector::new(
"select contacts to add",
entries,
false,
2020-02-22 11:47:13 +02:00
Some(Box::new(move |id: ComponentId, results: &[Card]| {
Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
})),
context,
));
2019-02-26 10:55:22 +02:00
self.dirty = true;
self.initialised = false;
2019-07-04 15:31:12 +03:00
return true;
2019-03-14 12:19:25 +02:00
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if self.mode.is_contact_selector() =>
{
self.mode = ViewMode::Normal;
self.set_dirty(true);
self.initialised = false;
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => {
2018-07-25 22:37:28 +03:00
self.cmd_buf.clear();
2019-04-10 23:37:20 +03:00
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
2019-07-04 15:31:12 +03:00
return true;
2018-07-27 18:01:52 +03:00
}
2019-04-10 23:37:20 +03:00
UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
self.cmd_buf.push(c);
2019-04-10 23:37:20 +03:00
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
2019-07-04 15:31:12 +03:00
return true;
2018-07-27 18:01:52 +03:00
}
2019-11-11 22:20:16 +02:00
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Source(Source::Decoded)
|| self.mode == ViewMode::Source(Source::Raw))
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["view_raw_source"]) =>
2018-08-07 15:01:15 +03:00
{
self.mode = match self.mode {
ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw),
_ => ViewMode::Source(Source::Decoded),
};
self.set_dirty(true);
self.initialised = false;
2019-07-04 15:31:12 +03:00
return true;
2018-08-05 12:44:31 +03:00
}
2019-11-11 22:20:16 +02:00
UIEvent::Input(ref key)
if (self.mode.is_attachment()
2020-10-05 18:43:08 +03:00
/*|| self.mode.is_ansi()*/
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Url
|| self.mode == ViewMode::Source(Source::Decoded)
|| self.mode == ViewMode::Source(Source::Raw))
&& shortcut!(
key == shortcuts[MailView::DESCRIPTION]["return_to_normal_view"]
) =>
2018-08-23 15:36:52 +03:00
{
2018-07-25 22:37:28 +03:00
self.mode = ViewMode::Normal;
self.set_dirty(true);
self.initialised = false;
2019-07-04 15:31:12 +03:00
return true;
2018-07-27 18:01:52 +03:00
}
2019-11-11 22:20:16 +02:00
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
&& !self.cmd_buf.is_empty()
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_mailcap"]) =>
2019-11-11 22:20:16 +02:00
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
2020-07-04 17:38:57 +03:00
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
2020-07-04 17:38:57 +03:00
MailViewState::Loaded { .. } => {
2020-10-05 18:43:08 +03:00
if let Some(attachment) = self.open_attachment(lidx, context) {
if let Ok(()) =
crate::mailcap::MailcapEntry::execute(attachment, context)
{
self.set_dirty(true);
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"no mailcap entry found for {}",
attachment.content_type()
)),
));
}
}
2020-07-04 17:38:57 +03:00
}
MailViewState::Init { .. } => {
2020-07-04 17:38:57 +03:00
self.init_futures(context);
2019-11-11 22:20:16 +02:00
}
}
2020-07-04 17:38:57 +03:00
return true;
2019-11-11 22:20:16 +02:00
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_attachment"])
2019-11-11 22:20:16 +02:00
&& !self.cmd_buf.is_empty()
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
2018-07-27 18:01:52 +03:00
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
2019-04-10 23:37:20 +03:00
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
2020-07-04 17:38:57 +03:00
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
2020-07-04 17:38:57 +03:00
MailViewState::Loaded { .. } => {
2020-10-05 18:43:08 +03:00
if let Some(attachment) = self.open_attachment(lidx, context) {
match attachment.content_type() {
ContentType::MessageRfc822 => {
match Mail::new(attachment.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
| ContentType::CMSSignature => {
2020-10-05 18:43:08 +03:00
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(),
),
));
}
ContentType::Other { .. } => {
let attachment_type = attachment.mime_type();
let filename = attachment.filename();
2020-11-28 14:44:50 +02:00
if let Ok(command) = query_default_app(&attachment_type) {
2020-10-05 18:43:08 +03:00
let p = create_temp_file(
&decode(attachment, None),
filename.as_ref().map(|s| s.as_str()),
None,
true,
);
2020-11-28 14:44:50 +02:00
let (exec_cmd, argument) = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
2020-10-05 18:43:08 +03:00
.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!(
2020-11-28 14:44:50 +02:00
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
2020-10-05 18:43:08 +03:00
)),
));
}
}
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(if let Some(filename) = filename.as_ref() {
format!(
"Couldn't find a default application for file {} (type {})",
filename,
attachment_type
)
} else {
format!(
"Couldn't find a default application for type {}",
attachment_type
)
}),
));
}
}
ContentType::OctetStream { ref name } => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to open {}. application/octet-stream isn't supported yet",
name.as_ref().map(|n| n.as_str()).unwrap_or("file")
)),
));
}
}
}
}
MailViewState::Init { .. } => {
2020-07-04 17:38:57 +03:00
self.init_futures(context);
}
}
return true;
2018-07-27 18:01:52 +03:00
}
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
&& shortcut!(
key == shortcuts[MailView::DESCRIPTION]["toggle_expand_headers"]
) =>
{
self.expand_headers = !self.expand_headers;
2020-03-28 11:46:10 +02:00
self.set_dirty(true);
return true;
}
2019-11-11 22:20:16 +02:00
UIEvent::Input(ref key)
if !self.cmd_buf.is_empty()
&& self.mode == ViewMode::Url
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["go_to_url"]) =>
2018-07-27 18:01:52 +03:00
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
2019-04-10 23:37:20 +03:00
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
2020-07-04 17:38:57 +03:00
match self.state {
MailViewState::Init { .. } => {
2020-07-04 17:38:57 +03:00
self.init_futures(context);
}
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded {
body: _,
bytes: _,
2020-10-05 18:43:08 +03:00
display: _,
ref body_text,
2020-10-05 18:43:08 +03:00
ref links,
} => {
2020-10-05 18:43:08 +03:00
let (_kind, url) = {
if let Some(l) = links
.get(lidx)
.and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?)))
{
l
2020-07-04 17:38:57 +03:00
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Link `{}` not found.",
lidx
)),
));
return true;
}
};
2020-07-04 17:38:57 +03:00
match Command::new("xdg-open")
.arg(url)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.children.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some("Failed to launch xdg-open".to_string()),
err.to_string(),
2020-09-13 15:23:14 +03:00
Some(NotificationType::Error(melib::ErrorKind::External)),
2020-07-04 17:38:57 +03:00
));
}
}
2020-03-01 17:56:58 +02:00
}
2019-11-11 22:20:16 +02:00
}
2019-07-04 15:31:12 +03:00
return true;
2018-07-27 18:01:52 +03:00
}
2019-11-11 22:20:16 +02:00
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["toggle_url_mode"]) =>
2019-11-11 22:20:16 +02:00
{
match self.mode {
2018-07-27 18:01:52 +03:00
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
_ => {}
}
self.initialised = false;
self.dirty = true;
2019-07-04 15:31:12 +03:00
return true;
2018-07-27 18:01:52 +03:00
}
2019-06-18 21:13:58 +03:00
UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => {
self.coordinates.2 = new_hash;
}
2020-10-14 20:13:15 +03:00
UIEvent::Action(View(ViewAction::ExportMail(ref path))) => {
// Save entire message as eml
let account = &context.accounts[&self.coordinates.0];
2020-07-04 17:38:57 +03:00
if !account.contains_key(self.coordinates.2) {
/* The envelope has been renamed or removed, so wait for the appropriate event to
* arrive */
return true;
}
let bytes = if let MailViewState::Loaded { ref bytes, .. } = self.state {
bytes
} 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(),
2020-09-13 15:23:14 +03:00
Some(NotificationType::Error(err.kind)),
));
log(
format!("Failed to open envelope: {}", err.to_string()),
ERROR,
);
self.init_futures(context);
return true;
2020-07-04 17:38:57 +03:00
} else {
return true;
};
2020-10-05 18:43:08 +03:00
let mut path = std::path::Path::new(path).to_path_buf();
2020-10-14 20:13:15 +03:00
if path.is_dir() {
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
path.push(format!("{}.eml", envelope.message_id_raw()));
}
match save_attachment(&path, bytes) {
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to create file at {}", path.display())),
err.to_string(),
Some(NotificationType::Error(melib::ErrorKind::External)),
));
log(
format!(
"Failed to create file at {}: {}",
path.display(),
err.to_string()
),
ERROR,
);
return true;
}
Ok(()) => {
context.replies.push_back(UIEvent::Notification(
None,
format!("Saved at {}", &path.display()),
Some(NotificationType::Info),
));
}
}
return true;
}
UIEvent::Action(View(ViewAction::SaveAttachment(a_i, ref path))) => {
{
let account = &context.accounts[&self.coordinates.0];
if !account.contains_key(self.coordinates.2) {
/* The envelope has been renamed or removed, so wait for the appropriate event to
* arrive */
return true;
}
}
let bytes = if let MailViewState::Loaded { ref bytes, .. } = self.state {
bytes
} 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 true;
} else {
return true;
};
let mut path = std::path::Path::new(path).to_path_buf();
if let Some(u) = self.open_attachment(a_i, context) {
2020-07-04 17:38:57 +03:00
if path.is_dir() {
2020-10-14 20:13:15 +03:00
if let Some(filename) = u.filename() {
path.push(filename);
} else {
let u = Uuid::new_v4();
path.push(u.to_hyphenated().to_string());
}
2020-07-04 17:38:57 +03:00
}
2020-10-14 20:13:15 +03:00
match save_attachment(&path, &decode(u, None)) {
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to create file at {}", path.display())),
err.to_string(),
2020-09-13 15:23:14 +03:00
Some(NotificationType::Error(melib::ErrorKind::External)),
));
log(
format!(
"Failed to create file at {}: {}",
path.display(),
err.to_string()
),
ERROR,
);
}
2020-10-05 18:43:08 +03:00
Ok(()) => {
context.replies.push_back(UIEvent::Notification(
None,
2020-10-14 20:13:15 +03:00
format!("Saved at {}", path.display()),
2020-10-05 18:43:08 +03:00
Some(NotificationType::Info),
));
}
}
2020-10-14 20:13:15 +03:00
} else if a_i == 0 {
let account = &context.accounts[&self.coordinates.0];
// Save entire message as eml
2020-10-05 18:43:08 +03:00
if path.is_dir() {
2020-10-14 20:13:15 +03:00
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
path.push(format!("{}.eml", envelope.message_id_raw()));
2020-10-05 18:43:08 +03:00
}
2020-10-14 20:13:15 +03:00
match save_attachment(&path, bytes) {
2020-10-05 18:43:08 +03:00
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to create file at {}", path.display())),
err.to_string(),
Some(NotificationType::Error(melib::ErrorKind::External)),
));
2020-10-05 18:43:08 +03:00
log(
format!(
"Failed to create file at {}: {}",
path.display(),
err.to_string()
),
ERROR,
);
2020-10-14 20:13:15 +03:00
return true;
}
2020-10-05 18:43:08 +03:00
Ok(()) => {
context.replies.push_back(UIEvent::Notification(
None,
2020-10-14 20:13:15 +03:00
format!("Saved at {}", &path.display()),
2020-10-05 18:43:08 +03:00
Some(NotificationType::Info),
));
}
}
2020-10-14 20:13:15 +03:00
return true;
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
"Attachment `{}` not found.",
a_i
))));
}
2020-10-05 18:43:08 +03:00
return true;
}
UIEvent::Action(MailingListAction(ref e)) => {
let account = &context.accounts[&self.coordinates.0];
if !account.contains_key(self.coordinates.2) {
/* The envelope has been renamed or removed, so wait for the appropriate event to
* arrive */
return true;
}
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
2020-07-04 17:38:57 +03:00
let detect = list_management::ListActions::detect(&envelope);
if let Some(ref actions) = detect {
match e {
MailingListAction::ListPost if actions.post.is_some() => {
/* open composer */
let mut failure = true;
if let list_management::ListAction::Email(list_post_addr) =
2020-07-04 17:38:57 +03:00
actions.post.as_ref().unwrap()[0]
{
if let Ok(mailto) = Mailto::try_from(list_post_addr) {
let draft: Draft = mailto.into();
2020-10-16 12:35:51 +03:00
let mut composer =
Composer::with_account(self.coordinates.0, context);
2020-08-25 15:39:43 +03:00
composer.set_draft(draft);
context.replies.push_back(UIEvent::Action(Tab(New(Some(
Box::new(composer),
)))));
failure = false;
}
}
if failure {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(String::from(
"Couldn't parse List-Post header value",
)),
));
}
return true;
}
MailingListAction::ListUnsubscribe if actions.unsubscribe.is_some() => {
/* autosend or open unsubscribe option*/
2020-07-04 17:38:57 +03:00
let unsubscribe = actions.unsubscribe.as_ref().unwrap();
for option in unsubscribe.iter() {
/* TODO: Ask for confirmation before proceding with an action */
match option {
list_management::ListAction::Email(email) => {
2020-07-04 17:38:57 +03:00
if let Ok(mailto) = Mailto::try_from(*email) {
let mut draft: Draft = mailto.into();
draft.set_header(
"From",
crate::components::mail::get_display_name(
context,
self.coordinates.0,
),
);
2020-07-04 17:38:57 +03:00
/* Manually drop stuff because borrowck doesn't do it
* on its own */
drop(detect);
drop(envelope);
if let Err(err) = super::compose::send_draft(
2019-09-28 10:46:49 +03:00
ToggleFlag::False,
2020-07-04 17:38:57 +03:00
context,
self.coordinates.0,
draft,
SpecialUsageMailbox::Sent,
Flag::SEEN,
true,
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Couldn't send unsubscribe e-mail: {}",
err
)),
));
}
return true;
}
}
list_management::ListAction::Url(url) => {
2020-03-01 17:56:58 +02:00
match Command::new("xdg-open")
.arg(String::from_utf8_lossy(url).into_owned())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
2020-03-01 17:56:58 +02:00
Ok(child) => {
context.children.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Couldn't launch xdg-open: {}",
err
)),
));
}
}
return true;
}
list_management::ListAction::No => {}
}
}
}
MailingListAction::ListArchive if actions.archive.is_some() => {
/* open archive url with xdg-open */
2020-03-01 17:56:58 +02:00
match Command::new("xdg-open")
.arg(actions.archive.unwrap())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
2020-03-01 17:56:58 +02:00
Ok(child) => context.children.push(child),
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Couldn't launch xdg-open: {}",
err
)),
));
}
}
return true;
}
_ => { /* error print message to user */ }
}
};
}
UIEvent::Action(Listing(OpenInNewTab)) => {
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(self.clone()))))));
return true;
}
2019-07-04 15:31:12 +03:00
_ => {}
}
2019-07-04 15:31:12 +03:00
false
}
2020-07-04 17:38:57 +03:00
fn is_dirty(&self) -> bool {
2019-03-14 12:19:25 +02:00
self.dirty
|| self.pager.is_dirty()
2018-07-27 18:01:52 +03:00
|| 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()
2020-10-05 18:43:08 +03:00
/*} else if let ViewMode::Ansi(ref r) = self.mode {
r.is_dirty()*/
2019-02-26 10:55:22 +02:00
} else {
false
}
}
2020-07-04 17:38:57 +03:00
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
match self.mode {
ViewMode::Normal | ViewMode::Url | ViewMode::Source(_) | ViewMode::Attachment(_) => {
self.pager.set_dirty(value);
}
ViewMode::ContactSelector(ref mut s) => {
self.pager.set_dirty(value);
s.set_dirty(value);
}
ViewMode::Subview => {
if let Some(s) = self.subview.as_mut() {
s.set_dirty(value);
}
}
}
}
2020-07-04 17:38:57 +03:00
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if let Some(ref sbv) = self.subview {
sbv.get_shortcuts(context)
} else {
self.pager.get_shortcuts(context)
};
2019-11-27 01:43:03 +02:00
let mut our_map = context.settings.shortcuts.envelope_view.key_values();
if !(self.mode.is_attachment()
2020-10-05 18:43:08 +03:00
/*|| self.mode.is_ansi()*/
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Source(Source::Decoded)
|| self.mode == ViewMode::Source(Source::Raw)
2019-11-27 01:43:03 +02:00
|| self.mode == ViewMode::Url)
{
2019-11-27 01:43:03 +02:00
our_map.remove("return_to_normal_view");
}
2019-11-27 01:43:03 +02:00
if self.mode != ViewMode::Url {
our_map.remove("go_to_url");
}
2019-11-27 01:43:03 +02:00
if !(self.mode == ViewMode::Normal || self.mode == ViewMode::Url) {
our_map.remove("toggle_url_mode");
}
2019-11-18 22:20:18 +02:00
map.insert(MailView::DESCRIPTION, our_map);
map
}
2019-04-10 22:01:02 +03:00
fn id(&self) -> ComponentId {
self.id
}
2019-04-10 22:01:02 +03:00
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn kill(&mut self, id: ComponentId, context: &mut Context) {
2020-10-05 18:43:08 +03:00
if self.id == id {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.id))));
}
}
}
2020-10-05 18:43:08 +03:00
fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
let mut f = std::fs::File::create(path)?;
let mut permissions = f.metadata()?.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
f.set_permissions(permissions)?;
f.write_all(bytes)?;
f.flush()?;
Ok(())
}
2020-11-28 14:44:50 +02:00
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> (String, String) {
/* Purge unused field codes */
let command = command
.replace("%i", "")
.replace("%c", "")
.replace("%k", "");
if let Some(pos) = command.find("%f").or_else(|| command.find("%F")) {
(command[0..pos].trim().to_string(), path)
} else if let Some(pos) = command.find("%u").or_else(|| command.find("%U")) {
if is_url {
(command[0..pos].trim().to_string(), path)
} else {
(
command[0..pos].trim().to_string(),
format!("file://{}", path),
)
}
} else {
(command, path)
}
}
/*
#[test]
fn test_desktop_exec() {
for cmd in [
"ristretto %F",
"/usr/lib/firefox-esr/firefox-esr %u",
"/usr/bin/vlc --started-from-file %U",
"zathura %U",
]
.iter()
{
println!(
"cmd = {} output = {:?}, is_url = false",
cmd,
desktop_exec_to_command(cmd, "/tmp/file".to_string(), false)
);
println!(
"cmd = {} output = {:?}, is_url = true",
cmd,
desktop_exec_to_command(cmd, "www.example.com".to_string(), true)
);
}
}
*/