meli/src/components/mail/view.rs

2613 lines
109 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

/*
* meli
*
* 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 crate::conf::accounts::JobRequest;
use crate::jobs::{JobId, JoinHandle};
use melib::email::attachment_types::ContentType;
use melib::list_management;
use melib::parser::BytesExt;
use smallvec::SmallVec;
use std::collections::HashSet;
use std::io::Write;
use std::convert::TryFrom;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
mod html;
pub use self::html::*;
mod thread;
pub use self::thread::*;
mod envelope;
pub use self::envelope::*;
use linkify::LinkFinder;
use xdg_utils::query_default_app;
#[derive(PartialEq, Copy, Clone, Debug)]
enum Source {
Decoded,
Raw,
}
#[derive(PartialEq, Debug)]
enum ViewMode {
Normal,
Url,
Attachment(usize),
Source(Source),
//Ansi(RawBuffer),
Subview,
ContactSelector(UIDialog<Card>),
}
impl Default for ViewMode {
fn default() -> Self {
ViewMode::Normal
}
}
impl ViewMode {
/*
fn is_ansi(&self) -> bool {
match self {
ViewMode::Ansi(_) => true,
_ => false,
}
}
*/
fn is_attachment(&self) -> bool {
match self {
ViewMode::Attachment(_) => true,
_ => false,
}
}
fn is_contact_selector(&self) -> bool {
match self {
ViewMode::ContactSelector(_) => true,
_ => false,
}
}
}
#[derive(Debug)]
pub enum AttachmentDisplay {
Alternative {
inner: Attachment,
shown_display: usize,
display: Vec<AttachmentDisplay>,
},
InlineText {
inner: Attachment,
comment: Option<String>,
text: String,
},
InlineOther {
inner: Attachment,
},
Attachment {
inner: Attachment,
},
SignedPending {
inner: Attachment,
display: Vec<AttachmentDisplay>,
handle: JoinHandle<Result<()>>,
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>)>>,
},
EncryptedFailed {
inner: Attachment,
error: MeliError,
},
EncryptedSuccess {
inner: Attachment,
plaintext: Attachment,
plaintext_display: Vec<AttachmentDisplay>,
description: String,
},
}
/// 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,
attachment_paths: Vec<Vec<usize>>,
headers_no: usize,
headers_cursor: usize,
force_draw_headers: bool,
theme_default: ThemeAttribute,
active_jobs: HashSet<JobId>,
state: MailViewState,
cmd_buf: String,
id: ComponentId,
}
#[derive(Debug)]
pub enum PendingReplyAction {
Reply,
ReplyToAuthor,
ReplyToAll,
}
#[derive(Debug)]
enum MailViewState {
Init {
pending_action: Option<PendingReplyAction>,
},
LoadingBody {
handle: JoinHandle<Result<Vec<u8>>>,
pending_action: Option<PendingReplyAction>,
},
Error {
err: MeliError,
},
Loaded {
bytes: Vec<u8>,
body: Attachment,
display: Vec<AttachmentDisplay>,
body_text: String,
links: Vec<Link>,
},
}
#[derive(Copy, Clone, Debug)]
enum LinkKind {
Url,
Email,
}
#[derive(Debug, Copy, Clone)]
struct Link {
start: usize,
end: usize,
kind: LinkKind,
}
impl Default for MailViewState {
fn default() -> Self {
MailViewState::Init {
pending_action: None,
}
}
}
impl Clone for MailView {
fn clone(&self) -> Self {
MailView {
subview: None,
cmd_buf: String::with_capacity(4),
pager: self.pager.clone(),
mode: ViewMode::Normal,
attachment_tree: self.attachment_tree.clone(),
attachment_paths: self.attachment_paths.clone(),
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 {
const DESCRIPTION: &'static str = "view mail";
pub fn new(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
pager: Option<Pager>,
subview: Option<Box<dyn Component>>,
context: &mut Context,
) -> Self {
let mut ret = MailView {
coordinates,
pager: pager.unwrap_or_default(),
subview,
dirty: true,
initialised: false,
mode: ViewMode::Normal,
expand_headers: false,
attachment_tree: String::new(),
attachment_paths: vec![],
headers_no: 5,
headers_cursor: 0,
force_draw_headers: false,
theme_default: crate::conf::value(context, "mail.view.body"),
active_jobs: Default::default(),
state: MailViewState::default(),
cmd_buf: String::with_capacity(4),
id: ComponentId::new_v4(),
};
ret.init_futures(context);
ret
}
fn init_futures(&mut self, context: &mut Context) {
debug!("init_futures");
self.theme_default = crate::conf::value(context, "mail.view.body");
let mut pending_action = None;
let account = &mut context.accounts[&self.coordinates.0];
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();
let display = Self::attachment_to(
&body,
context,
self.coordinates,
&mut self.active_jobs,
);
let (paths, attachment_tree_s) =
self.attachment_displays_to_tree(&display);
self.attachment_tree = attachment_tree_s;
self.attachment_paths = paths;
let body_text =
self.attachment_displays_to_text(&display, context, true);
self.state = MailViewState::Loaded {
display,
body,
bytes,
body_text,
links: vec![],
};
}
Err(err) => {
self.state = MailViewState::Error { err };
}
}
} 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)));
}
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("Could not get message: {}", err)),
));
}
}
}
let account = &mut context.accounts[&self.coordinates.0];
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 {
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,
},
);
}
Err(e) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Could not set message as seen: {}",
e
)),
));
}
};
}
}
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)))));
}
fn attachment_displays_to_text(
&self,
displays: &[AttachmentDisplay],
context: &mut Context,
show_comments: bool,
) -> 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),
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: _,
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,
));
}
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))
}
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,
));
}
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");
}
}
acc.push_str(&self.attachment_displays_to_text(
display,
context,
show_comments,
));
}
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");
}
}
acc.push_str(&self.attachment_displays_to_text(
plaintext_display,
context,
show_comments,
));
}
}
}
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,
) {
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: _,
}
| InlineOther { inner }
| Attachment { inner }
| EncryptedPending { inner, handle: _ }
| EncryptedFailed { inner, error: _ } => (inner, &[][..]),
SignedPending {
inner,
display,
handle: _,
job_id: _,
}
| SignedUnverified { inner, display }
| SignedFailed {
inner,
display,
error: _,
}
| SignedVerified {
inner,
display,
description: _,
}
| EncryptedSuccess {
inner: _,
plaintext: inner,
plaintext_display: display,
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();
}
_ => {}
}
}
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,
);
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" {
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!(
"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();
acc.push(AttachmentDisplay::InlineText {
inner: a.clone(),
comment,
text,
});
}
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
));
let text = String::from_utf8_lossy(
&html_filter.wait_with_output().unwrap().stdout,
)
.to_string();
acc.push(AttachmentDisplay::InlineText {
inner: a.clone(),
comment,
text,
});
}
}
} else if a.is_text() {
let bytes = decode(a, None);
acc.push(AttachmentDisplay::InlineText {
inner: a.clone(),
comment: None,
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;
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;
}
}
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,
});
}
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
) {
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),
));
acc.push(AttachmentDisplay::SignedPending {
inner: a.clone(),
job_id: handle.job_id,
display: {
let mut v = vec![];
rec(&parts[0], context, coordinates, &mut v, active_jobs);
v
},
handle,
});
} else {
acc.push(AttachmentDisplay::SignedUnverified {
inner: a.clone(),
display: {
let mut v = vec![];
rec(&parts[0], context, coordinates, &mut v, active_jobs);
v
},
});
}
}
}
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
) {
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),
));
acc.push(AttachmentDisplay::EncryptedPending {
inner: a.clone(),
handle,
});
} else {
acc.push(AttachmentDisplay::EncryptedFailed {
inner: a.clone(),
error: MeliError::new("Undecrypted."),
});
}
}
}
}
}
_ => {
for a in parts {
rec(a, context, coordinates, acc, active_jobs);
}
}
}
}
};
rec(body, context, coordinates, &mut ret, active_jobs);
ret
}
pub fn update(
&mut self,
new_coordinates: (AccountHash, MailboxHash, EnvelopeHash),
context: &mut Context,
) {
if self.coordinates != new_coordinates {
self.coordinates = new_coordinates;
self.mode = ViewMode::Normal;
self.initialised = false;
self.init_futures(context);
self.set_dirty(true);
}
}
fn open_attachment(
&'_ self,
lidx: usize,
context: &mut Context,
) -> Option<&'_ melib::Attachment> {
let display = if let MailViewState::Loaded { ref display, .. } = self.state {
display
} else {
return None;
};
if let Some(path) =
self.attachment_paths.get(lidx).and_then(
|path| {
if path.len() > 0 {
Some(path)
} else {
None
}
},
)
{
let first = path[0];
use AttachmentDisplay::*;
let root_attachment = match &display[first] {
Alternative {
inner,
shown_display: _,
display: _,
}
| InlineText {
inner,
text: _,
comment: _,
}
| InlineOther { inner }
| Attachment { inner }
| SignedPending {
inner,
display: _,
handle: _,
job_id: _,
}
| SignedFailed {
inner,
display: _,
error: _,
}
| SignedVerified {
inner,
display: _,
description: _,
}
| SignedUnverified { inner, display: _ }
| EncryptedPending { inner, handle: _ }
| 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..]);
}
}
_ => {}
}
None
}
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"
{
Some(a)
} else {
None
}
});
} else {
return ret;
}
}
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
"Attachment `{}` not found.",
lidx
))));
None
}
}
impl Component for MailView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() && !self.force_draw_headers {
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];
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);
let headers = crate::conf::value(context, "mail.view.headers");
let headers_names = crate::conf::value(context, "mail.view.headers_names");
let headers_area = crate::conf::value(context, "mail.view.headers_area");
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 {
($(($header:literal, $string:expr)),*$(,)?) => {
$({
if sticky || skip_header_ctr == 0 {
if y <= get_y(bottom_right) {
let (_x, _y) = write_string_to_grid(
$header,
grid,
headers_names.fg,
headers_names.bg,
headers_names.attrs,
(set_y(upper_left, y), bottom_right),
Some(get_x(upper_left)),
);
if let Some(cell) = grid.get_mut(_x,_y) {
cell.set_ch(' ')
.set_fg(headers_area.fg)
.set_bg(headers_area.bg)
.set_attrs(headers_area.attrs);
}
let (_x, _y) = write_string_to_grid(
&$string,
grid,
headers.fg,
headers.bg,
headers.attrs,
((_x +1, _y), bottom_right),
Some(get_x(upper_left)),
);
clear_area(grid, ((std::cmp::min(_x, get_x(bottom_right)), _y), (get_x(bottom_right), _y)), headers_area);
y = _y + 1;
}
} else {
skip_header_ctr -= 1;
}
self.headers_no += 1;
})+
};
}
print_header!(
("Date:", envelope.date_as_str()),
("From:", envelope.field_from_to_string()),
("To:", envelope.field_to_to_string()),
);
if envelope.other_headers().contains_key("Cc")
&& !envelope.other_headers()["Cc"].is_empty()
{
print_header!(("Cc:", envelope.field_cc_to_string()));
}
print_header!(
("Subject:", envelope.subject()),
("Message-ID:", format!("<{}>", envelope.message_id_raw()))
);
if self.expand_headers {
if let Some(val) = envelope.in_reply_to_display() {
print_header!(
("In-Reply-To:", val),
(
"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_area,
);
let (_x, _) = write_string_to_grid(
"List-ID: ",
grid,
headers_names.fg,
headers_names.bg,
headers_names.attrs,
(set_y(upper_left, y), bottom_right),
None,
);
let (_x, _y) = write_string_to_grid(
id,
grid,
headers.fg,
headers.bg,
headers.attrs,
((_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_names.fg,
headers_names.bg,
headers_names.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,
headers.fg,
headers.bg,
headers.attrs,
((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,
headers.fg,
headers.bg,
headers.attrs,
((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,
headers.fg,
headers.bg,
headers.attrs,
((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_ch(']')
.set_fg(headers_names.fg)
.set_bg(headers_names.bg)
.set_attrs(headers_names.attrs);
}
}
for x in x..=get_x(bottom_right) {
grid[(x, y)]
.set_ch(' ')
.set_fg(headers_area.fg)
.set_bg(headers_area.bg);
}
y += 1;
}
}
self.force_draw_headers = false;
clear_area(
grid,
(set_y(upper_left, y), set_y(bottom_right, y)),
headers_area,
);
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;
let height = height!(area).saturating_sub(y).saturating_sub(1);
if self.pager.cursor_pos() >= self.headers_no {
get_y(upper_left)
} 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 {
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(),
Some(NotificationType::Error(err.kind)),
));
log(
format!("Failed to open envelope: {}", err.to_string()),
ERROR,
);
self.init_futures(context);
return;
} 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;
};
self.initialised = true;
//let body = AttachmentBuilder::new(bytes).build();
match self.mode {
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;
}
}
ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(&body, context)));
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;
}
ViewMode::Subview | ViewMode::ContactSelector(_) => {}
ViewMode::Source(source) => {
let text = {
if source == Source::Raw {
String::from_utf8_lossy(bytes).into_owned()
} else {
/* Decode each header value */
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())
.unwrap_or_else(|err: MeliError| err.to_string());
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
}
};
let colors = crate::conf::value(context, "mail.view.body");
self.pager = Pager::from_string(text, Some(context), None, None, colors);
}
/*
ViewMode::Ansi(ref buf) => {
write_string_to_grid(
&format!("Viewing `{}`. Press `r` to return", buf.title()),
grid,
Color::Default,
Color::Default,
Attr::DEFAULT,
(set_y(upper_left, y), bottom_right),
Some(get_x(upper_left)),
);
}
*/
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;
}
_ => {
let mut text = body_text.clone();
if !text.ends_with("\n\n") {
text.push_str("\n\n");
}
text.push_str(&self.attachment_tree);
let cursor_pos = if self.mode.is_attachment() {
0
} else {
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;
}
};
}
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);
}
}
/*
ViewMode::Ansi(ref mut buf) => {
buf.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}*/
_ => {
self.pager
.draw(grid, (set_y(upper_left, y), bottom_right), context);
}
}
if let ViewMode::ContactSelector(ref mut s) = self.mode {
s.draw(grid, area, context);
}
}
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);
match (&mut self.mode, &mut event) {
/*(ViewMode::Ansi(ref mut buf), _) => {
if buf.process_event(event, context) {
return true;
}
}*/
(ViewMode::Subview, _) => {
if let Some(s) = self.subview.as_mut() {
if s.process_event(event, context) {
return true;
}
}
}
(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];
{
for card in results.iter() {
account.address_book.add_card(card.clone());
}
}
}
self.mode = ViewMode::Normal;
self.initialised = false;
self.set_dirty(true);
return true;
}
(ViewMode::ContactSelector(ref mut s), _) => {
if s.process_event(event, context) {
return true;
}
if self.pager.process_event(event, context) {
return true;
}
}
_ => 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;
}
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();
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,
body,
display,
links: vec![],
body_text,
};
}
Err(err) => {
self.state = MailViewState::Error { err };
}
}
}
MailViewState::Init { .. } => {
self.init_futures(context);
}
MailViewState::Loaded {
ref mut display, ..
} => {
let mut caught = false;
for d in display.iter_mut() {
match d {
AttachmentDisplay::SignedPending {
inner,
handle,
display,
job_id: our_job_id,
} if *our_job_id == *job_id => {
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,
};
}
}
}
AttachmentDisplay::EncryptedPending { inner, handle }
if handle.job_id == *job_id =>
{
caught = true;
self.initialised = false;
match handle.chan.try_recv().unwrap().unwrap() {
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);
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;
}
}
}
_ => {}
}
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);
match *event {
UIEvent::ConfigReload { old_settings: _ } => {
self.theme_default = crate::conf::value(context, "theme_default");
self.set_dirty(true);
}
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"]) =>
{
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]
.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,
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,
Some(NotificationType::Error(err.kind)),
));
}
}
}))),
logging_level: melib::LoggingLevel::DEBUG,
},
);
return true;
}
UIEvent::Input(ref key)
if !self.mode.is_contact_selector()
&& shortcut!(
key == shortcuts[MailView::DESCRIPTION]["add_addresses_to_contacts"]
) =>
{
let account = &context.accounts[&self.coordinates.0];
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
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,
Some(Box::new(move |id: ComponentId, results: &[Card]| {
Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
})),
context,
));
self.dirty = true;
self.initialised = false;
return true;
}
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() => {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
}
UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
self.cmd_buf.push(c);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
return true;
}
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"]) =>
{
self.mode = match self.mode {
ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw),
_ => ViewMode::Source(Source::Decoded),
};
self.set_dirty(true);
self.initialised = false;
return true;
}
UIEvent::Input(ref key)
if (self.mode.is_attachment()
/*|| 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"]
) =>
{
self.mode = ViewMode::Normal;
self.set_dirty(true);
self.initialised = false;
return true;
}
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"]) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded { .. } => {
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()
)),
));
}
}
}
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_attachment"])
&& !self.cmd_buf.is_empty()
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded { .. } => {
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 => {
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();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&decode(attachment, None),
filename.as_ref().map(|s| s.as_str()),
None,
true,
);
let (exec_cmd, argument) = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
.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!(
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}
}
} 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 { .. } => {
self.init_futures(context);
}
}
return true;
}
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;
self.set_dirty(true);
return true;
}
UIEvent::Input(ref key)
if !self.cmd_buf.is_empty()
&& self.mode == ViewMode::Url
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["go_to_url"]) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Init { .. } => {
self.init_futures(context);
}
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded {
body: _,
bytes: _,
display: _,
ref body_text,
ref links,
} => {
let (_kind, url) = {
if let Some(l) = links
.get(lidx)
.and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?)))
{
l
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Link `{}` not found.",
lidx
)),
));
return true;
}
};
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(),
Some(NotificationType::Error(melib::ErrorKind::External)),
));
}
}
}
}
return true;
}
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["toggle_url_mode"]) =>
{
match self.mode {
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
_ => {}
}
self.initialised = false;
self.dirty = true;
return true;
}
UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => {
self.coordinates.2 = new_hash;
}
UIEvent::Action(View(ViewAction::ExportMail(ref path))) => {
// Save entire message as eml
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 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) {
if path.is_dir() {
if let Some(filename) = u.filename() {
path.push(filename);
} else {
let u = Uuid::new_v4();
path.push(u.to_hyphenated().to_string());
}
}
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(),
Some(NotificationType::Error(melib::ErrorKind::External)),
));
log(
format!(
"Failed to create file at {}: {}",
path.display(),
err.to_string()
),
ERROR,
);
}
Ok(()) => {
context.replies.push_back(UIEvent::Notification(
None,
format!("Saved at {}", path.display()),
Some(NotificationType::Info),
));
}
}
} else if a_i == 0 {
let account = &context.accounts[&self.coordinates.0];
// Save entire message as eml
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;
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
"Attachment `{}` not found.",
a_i
))));
}
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);
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) =
actions.post.as_ref().unwrap()[0]
{
if let Ok(mailto) = Mailto::try_from(list_post_addr) {
let draft: Draft = mailto.into();
let mut composer =
Composer::with_account(self.coordinates.0, context);
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*/
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) => {
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,
),
);
/* Manually drop stuff because borrowck doesn't do it
* on its own */
drop(detect);
drop(envelope);
if let Err(err) = super::compose::send_draft(
ToggleFlag::False,
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) => {
match Command::new("xdg-open")
.arg(String::from_utf8_lossy(url).into_owned())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
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 */
match Command::new("xdg-open")
.arg(actions.archive.unwrap())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
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;
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
|| self.pager.is_dirty()
|| self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|| if let ViewMode::ContactSelector(ref s) = self.mode {
s.is_dirty()
/*} else if let ViewMode::Ansi(ref r) = self.mode {
r.is_dirty()*/
} else {
false
}
}
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);
}
}
}
}
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)
};
let mut our_map = context.settings.shortcuts.envelope_view.key_values();
if !(self.mode.is_attachment()
/*|| self.mode.is_ansi()*/
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Source(Source::Decoded)
|| self.mode == ViewMode::Source(Source::Raw)
|| self.mode == ViewMode::Url)
{
our_map.remove("return_to_normal_view");
}
if self.mode != ViewMode::Url {
our_map.remove("go_to_url");
}
if !(self.mode == ViewMode::Normal || self.mode == ViewMode::Url) {
our_map.remove("toggle_url_mode");
}
map.insert(MailView::DESCRIPTION, our_map);
map
}
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn kill(&mut self, id: ComponentId, context: &mut Context) {
if self.id == id {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.id))));
}
}
}
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(())
}
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)
);
}
}
*/