/*
* 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 .
*/
use super::*;
use melib::email::attachment_types::{ContentType, MultipartType};
use melib::list_management;
use melib::Draft;
use crate::conf::accounts::JobRequest;
use crate::jobs::JoinHandle;
use crate::terminal::embed::EmbedTerminal;
use indexmap::IndexSet;
use nix::sys::wait::WaitStatus;
use std::convert::TryInto;
use std::future::Future;
use std::pin::Pin;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
#[cfg(feature = "gpgme")]
mod gpg;
mod edit_attachments;
use edit_attachments::*;
#[derive(Debug, PartialEq, Eq)]
enum Cursor {
Headers,
Body,
Sign,
Encrypt,
Attachments,
}
#[derive(Debug)]
enum EmbedStatus {
Stopped(Arc>, File),
Running(Arc>, File),
}
impl EmbedStatus {
#[inline(always)]
fn is_stopped(&self) -> bool {
matches!(self, Self::Stopped(_, _))
}
}
impl std::ops::Deref for EmbedStatus {
type Target = Arc>;
fn deref(&self) -> &Arc> {
use EmbedStatus::*;
match self {
Stopped(ref e, _) | Running(ref e, _) => e,
}
}
}
impl std::ops::DerefMut for EmbedStatus {
fn deref_mut(&mut self) -> &mut Arc> {
use EmbedStatus::*;
match self {
Stopped(ref mut e, _) | Running(ref mut e, _) => e,
}
}
}
#[derive(Debug)]
pub struct Composer {
reply_context: Option<(MailboxHash, EnvelopeHash)>,
account_hash: AccountHash,
cursor: Cursor,
pager: Pager,
draft: Draft,
form: FormWidget,
mode: ViewMode,
embed_area: Area,
embed: Option,
#[cfg(feature = "gpgme")]
gpg_state: gpg::GpgComposeState,
dirty: bool,
has_changes: bool,
initialized: bool,
id: ComponentId,
}
#[derive(Debug)]
enum ViewMode {
Discard(Uuid, UIDialog),
EditAttachments {
widget: EditAttachments,
},
Edit,
Embed,
SelectRecipients(UIDialog),
#[cfg(feature = "gpgme")]
SelectEncryptKey(bool, gpg::KeySelection),
Send(UIConfirmationDialog),
WaitingForSendResult(UIDialog, JoinHandle>),
}
impl ViewMode {
fn is_edit(&self) -> bool {
matches!(self, ViewMode::Edit)
}
fn is_edit_attachments(&self) -> bool {
matches!(self, ViewMode::EditAttachments { .. })
}
}
impl fmt::Display for Composer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.reply_context.is_some() {
write!(
f,
"reply: {}",
(&self.draft.headers()["Subject"]).trim_at_boundary(8)
)
} else {
write!(f, "composing")
}
}
}
impl Composer {
const DESCRIPTION: &'static str = "composing";
pub fn new(context: &Context) -> Self {
let mut pager = Pager::new(context);
pager.set_show_scrollbar(true);
Composer {
reply_context: None,
account_hash: 0,
cursor: Cursor::Headers,
pager,
draft: Draft::default(),
form: FormWidget::default(),
mode: ViewMode::Edit,
#[cfg(feature = "gpgme")]
gpg_state: gpg::GpgComposeState::default(),
dirty: true,
has_changes: false,
embed_area: ((0, 0), (0, 0)),
embed: None,
initialized: false,
id: ComponentId::new_v4(),
}
}
pub fn with_account(account_hash: AccountHash, context: &Context) -> Self {
let mut ret = Composer {
account_hash,
..Composer::new(context)
};
for (h, v) in
account_settings!(context[account_hash].composing.default_header_values).iter()
{
if v.is_empty() {
continue;
}
ret.draft.set_header(h, v.into());
}
if *account_settings!(context[account_hash].composing.insert_user_agent) {
ret.draft.set_header(
"User-Agent",
format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
);
}
if *account_settings!(context[account_hash].composing.format_flowed) {
ret.pager
.set_reflow(melib::text_processing::Reflow::FormatFlowed);
}
ret
}
pub fn edit(
account_hash: AccountHash,
env_hash: EnvelopeHash,
bytes: &[u8],
context: &Context,
) -> Result {
let mut ret = Composer::with_account(account_hash, context);
let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash);
ret.draft = Draft::edit(&envelope, bytes)?;
ret.account_hash = account_hash;
Ok(ret)
}
pub fn reply_to(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
reply_to_all: bool,
) -> Self {
let mut ret = Composer::with_account(coordinates.0, context);
let account = &context.accounts[&coordinates.0];
let envelope = account.collection.get_env(coordinates.2);
let subject = {
let subject = envelope.subject();
let prefix_list = account_settings!(
context[ret.account_hash]
.composing
.reply_prefix_list_to_strip
)
.as_ref()
.map(|v| v.iter().map(String::as_str).collect::>())
.unwrap_or_default();
let subject_stripped = subject.as_ref().strip_prefixes_from_list(
if prefix_list.is_empty() {
<&str>::USUAL_PREFIXES
} else {
&prefix_list
},
Some(1),
) == &subject.as_ref();
let prefix =
account_settings!(context[ret.account_hash].composing.reply_prefix).as_str();
if subject_stripped {
format!("{prefix} {subject}", prefix = prefix, subject = subject)
} else {
subject.to_string()
}
};
ret.draft.set_header("Subject", subject);
ret.draft.set_header(
"References",
format!(
"{} {}",
envelope
.references()
.iter()
.fold(String::new(), |mut acc, x| {
if !acc.is_empty() {
acc.push(' ');
}
acc.push_str(&x.to_string());
acc
}),
envelope.message_id_display()
),
);
ret.draft
.set_header("In-Reply-To", envelope.message_id_display().into());
if let Some(reply_to) = envelope.other_headers().get("To").map(|v| v.as_str()) {
let to: &str = reply_to;
let extra_identities = &account.settings.account.extra_identities;
if let Some(extra) = extra_identities
.iter()
.find(|extra| to.contains(extra.as_str()))
{
ret.draft.set_header("From", extra.into());
}
}
// "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up,
// Mail-Reply-To/Reply-To/From for reply-to-author."
// source: https://cr.yp.to/proto/replyto.html
if reply_to_all {
let mut to = IndexSet::new();
if let Some(actions) = list_management::ListActions::detect(&envelope) {
if let Some(post) = actions.post {
if let list_management::ListAction::Email(list_post_addr) = post[0] {
if let Ok(list_address) =
melib::email::parser::generic::mailto(list_post_addr)
.map(|(_, m)| m.address)
{
to.insert(list_address);
}
}
}
}
if let Some(reply_to) = envelope
.other_headers()
.get("Mail-Followup-To")
.and_then(|v| v.as_str().try_into().ok())
{
to.insert(reply_to);
} else if let Some(reply_to) = envelope
.other_headers()
.get("Reply-To")
.and_then(|v| v.as_str().try_into().ok())
{
to.insert(reply_to);
} else {
to.extend(envelope.from().iter().cloned());
}
to.extend(envelope.to().iter().cloned());
if let Ok(ours) = TryInto::::try_into(
crate::components::mail::get_display_name(context, coordinates.0).as_str(),
) {
to.remove(&ours);
}
ret.draft.set_header("To", {
let mut ret: String =
to.into_iter()
.fold(String::new(), |mut s: String, n: Address| {
s.push_str(&n.to_string());
s.push_str(", ");
s
});
ret.pop();
ret.pop();
ret
});
ret.draft.set_header("Cc", envelope.field_cc_to_string());
} else if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
ret.draft.set_header("To", reply_to.to_string());
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.draft.set_header("To", reply_to.to_string());
} else {
ret.draft.set_header("To", envelope.field_from_to_string());
}
ret.draft.body = {
let mut ret = attribution_string(
account_settings!(
context[ret.account_hash]
.composing
.attribution_format_string
)
.as_ref()
.map(|s| s.as_str()),
envelope.from().get(0),
envelope.date(),
*account_settings!(
context[ret.account_hash]
.composing
.attribution_use_posix_locale
),
);
for l in reply_body.lines() {
ret.push('>');
ret.push_str(l);
ret.push('\n');
}
ret
};
ret.account_hash = coordinates.0;
ret.reply_context = Some((coordinates.1, coordinates.2));
ret
}
pub fn reply_to_select(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
) -> Self {
let mut ret = Composer::reply_to(coordinates, reply_body, context, false);
let account = &context.accounts[&coordinates.0];
let parent_message = account.collection.get_env(coordinates.2);
/* If message is from a mailing list and we detect a List-Post header, ask user if they
* want to reply to the mailing list or the submitter of the message */
if let Some(actions) = list_management::ListActions::detect(&parent_message) {
if let Some(post) = actions.post {
if let list_management::ListAction::Email(list_post_addr) = post[0] {
if let Ok(list_address) = melib::email::parser::generic::mailto(list_post_addr)
.map(|(_, m)| m.address)
{
let list_address_string = list_address.to_string();
ret.mode = ViewMode::SelectRecipients(UIDialog::new(
"select recipients",
vec![
(
parent_message.from()[0].clone(),
parent_message.field_from_to_string(),
),
(list_address, list_address_string),
],
false,
Some(Box::new(move |id: ComponentId, results: &[Address]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(
results
.iter()
.map(|a| a.to_string())
.collect::>()
.join(", "),
),
))
})),
context,
));
}
}
}
}
ret
}
pub fn reply_to_author(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
) -> Self {
Composer::reply_to(coordinates, reply_body, context, false)
}
pub fn reply_to_all(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
) -> Self {
Composer::reply_to(coordinates, reply_body, context, true)
}
pub fn forward(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
bytes: &[u8],
env: &Envelope,
as_attachment: bool,
context: &mut Context,
) -> Self {
let mut composer = Composer::with_account(coordinates.0, context);
let mut draft: Draft = Draft::default();
draft.set_header("Subject", format!("Fwd: {}", env.subject()));
let preamble = format!(
r#"
---------- Forwarded message ---------
From: {}
Date: {}
Subject: {}
To: {}
"#,
env.field_from_to_string(),
env.date_as_str(),
env.subject(),
env.field_to_to_string()
);
if as_attachment {
let mut attachment = AttachmentBuilder::new(b"");
let mut disposition: ContentDisposition = ContentDispositionKind::Attachment.into();
{
disposition.filename = Some(format!("{}.eml", env.message_id_raw()));
}
attachment
.set_raw(bytes.to_vec())
.set_body_to_raw()
.set_content_type(ContentType::MessageRfc822)
.set_content_transfer_encoding(ContentTransferEncoding::_8Bit)
.set_content_disposition(disposition);
draft.attachments.push(attachment);
draft.body = preamble;
} else {
let content_type = ContentType::default();
let preamble: AttachmentBuilder =
Attachment::new(content_type, Default::default(), preamble.into_bytes()).into();
draft.attachments.push(preamble);
draft.attachments.push(env.body_bytes(bytes).into());
}
composer.set_draft(draft);
composer
}
pub fn set_draft(&mut self, draft: Draft) {
self.draft = draft;
self.update_form();
}
fn update_draft(&mut self) {
let header_values = self.form.values_mut();
let draft_header_map = self.draft.headers_mut();
for (k, v) in draft_header_map.iter_mut() {
if let Some(vn) = header_values.get(k.as_str()) {
*v = vn.as_str().to_string();
}
}
}
fn update_form(&mut self) {
let old_cursor = self.form.cursor();
self.form = FormWidget::new(("Save".into(), true));
self.form.hide_buttons();
self.form.set_cursor(old_cursor);
let headers = self.draft.headers();
let account_hash = self.account_hash;
for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
if k == "To" || k == "Cc" || k == "Bcc" {
self.form.push_cl((
k.into(),
headers[k].to_string(),
Box::new(move |c, term| {
let book: &AddressBook = &c.accounts[&account_hash].address_book;
let results: Vec = book.search(term);
results
.into_iter()
.map(AutoCompleteEntry::from)
.collect::>()
}),
));
} else if k == "From" {
self.form.push_cl((
k.into(),
headers[k].to_string(),
Box::new(move |c, _term| {
c.accounts
.values()
.map(|acc| {
let addr = if let Some(display_name) =
acc.settings.account.display_name()
{
format!(
"{} <{}>",
display_name,
acc.settings.account.identity()
)
} else {
acc.settings.account.identity().to_string()
};
let desc =
match account_settings!(c[acc.hash()].composing.send_mail) {
crate::conf::composing::SendMail::ShellCommand(ref cmd) => {
let mut cmd = cmd.as_str();
cmd.truncate_at_boundary(10);
format!("{} [exec: {}]", acc.name(), cmd)
}
#[cfg(feature = "smtp")]
crate::conf::composing::SendMail::Smtp(ref inner) => {
let mut hostname = inner.hostname.as_str();
hostname.truncate_at_boundary(10);
format!("{} [smtp: {}]", acc.name(), hostname)
}
crate::conf::composing::SendMail::ServerSubmission => {
format!("{} [server submission]", acc.name())
}
};
(addr, desc)
})
.map(AutoCompleteEntry::from)
.collect::>()
}),
));
} else {
self.form.push((k.into(), headers[k].to_string()));
}
}
}
fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, context: &Context) {
let attachments_no = self.draft.attachments().len();
let theme_default = crate::conf::value(context, "theme_default");
clear_area(grid, area, theme_default);
#[cfg(feature = "gpgme")]
if self.gpg_state.sign_mail.is_true() {
let key_list = self
.gpg_state
.sign_keys
.iter()
.map(|k| k.fingerprint())
.collect::>()
.join(", ");
write_string_to_grid(
&format!(
"☑ sign with {}",
if self.gpg_state.sign_keys.is_empty() {
"default key"
} else {
key_list.as_str()
}
),
grid,
theme_default.fg,
if self.cursor == Cursor::Sign {
crate::conf::value(context, "highlight").bg
} else {
theme_default.bg
},
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
None,
);
} else {
write_string_to_grid(
"☐ don't sign",
grid,
theme_default.fg,
if self.cursor == Cursor::Sign {
crate::conf::value(context, "highlight").bg
} else {
theme_default.bg
},
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
None,
);
}
#[cfg(feature = "gpgme")]
if self.gpg_state.encrypt_mail.is_true() {
let key_list = self
.gpg_state
.encrypt_keys
.iter()
.map(|k| k.fingerprint())
.collect::>()
.join(", ");
write_string_to_grid(
&format!(
"{}{}",
if self.gpg_state.encrypt_keys.is_empty() {
"☐ no keys to encrypt with!"
} else {
"☑ encrypt with "
},
if self.gpg_state.encrypt_keys.is_empty() {
""
} else {
key_list.as_str()
}
),
grid,
theme_default.fg,
if self.cursor == Cursor::Encrypt {
crate::conf::value(context, "highlight").bg
} else {
theme_default.bg
},
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
None,
);
} else {
write_string_to_grid(
"☐ don't encrypt",
grid,
theme_default.fg,
if self.cursor == Cursor::Encrypt {
crate::conf::value(context, "highlight").bg
} else {
theme_default.bg
},
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
None,
);
}
if attachments_no == 0 {
write_string_to_grid(
"no attachments",
grid,
theme_default.fg,
if self.cursor == Cursor::Attachments {
crate::conf::value(context, "highlight").bg
} else {
theme_default.bg
},
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 3)), bottom_right!(area)),
None,
);
} else {
write_string_to_grid(
&format!("{} attachments ", attachments_no),
grid,
theme_default.fg,
if self.cursor == Cursor::Attachments {
crate::conf::value(context, "highlight").bg
} else {
theme_default.bg
},
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 3)), bottom_right!(area)),
None,
);
for (i, a) in self.draft.attachments().iter().enumerate() {
if let Some(name) = a.content_type().name() {
write_string_to_grid(
&format!(
"[{}] \"{}\", {} {}",
i,
name,
a.content_type(),
melib::Bytes(a.raw.len())
),
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 4 + i)), bottom_right!(area)),
None,
);
} else {
write_string_to_grid(
&format!("[{}] {} {}", i, a.content_type(), melib::Bytes(a.raw.len())),
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 4 + i)), bottom_right!(area)),
None,
);
}
}
}
}
fn update_from_file(&mut self, file: File, context: &mut Context) -> bool {
let result = file.read_to_string();
match self.draft.update(result.as_str()) {
Ok(has_changes) => {
self.has_changes = has_changes;
true
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some("Could not parse draft headers correctly.".to_string()),
format!(
"{}\nThe invalid text has been set as the body of your draft",
&err
),
Some(NotificationType::Error(melib::error::ErrorKind::None)),
));
self.draft.set_body(result);
self.has_changes = true;
false
}
}
}
}
impl Component for Composer {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let upper_left = set_y(upper_left, get_y(upper_left) + 1);
if height!(area) < 4 {
return;
}
let width = width!(area);
if !self.initialized {
#[cfg(feature = "gpgme")]
if self.gpg_state.sign_mail.is_unset() {
self.gpg_state.sign_mail = ToggleFlag::InternalVal(*account_settings!(
context[self.account_hash].pgp.auto_sign
));
}
if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
{
self.draft.set_header(
"From",
crate::components::mail::get_display_name(context, self.account_hash),
);
}
self.pager.update_from_str(self.draft.body(), Some(77));
self.update_form();
self.initialized = true;
}
let header_height = self.form.len();
let theme_default = crate::conf::value(context, "theme_default");
let mid = if width > 80 {
let width = width - 80;
let mid = width / 2;
if self.dirty {
for i in get_y(upper_left)..=get_y(bottom_right) {
//set_and_join_box(grid, (mid, i), VERT_BOUNDARY);
grid[(mid, i)]
.set_fg(theme_default.fg)
.set_bg(theme_default.bg);
//set_and_join_box(grid, (mid + 80, i), VERT_BOUNDARY);
grid[(mid + 80, i)]
.set_fg(theme_default.fg)
.set_bg(theme_default.bg);
}
}
mid
} else {
0
};
let header_area = (
set_x(upper_left, mid + 1),
(
get_x(bottom_right).saturating_sub(mid),
get_y(upper_left) + header_height,
),
);
let attachments_no = self.draft.attachments().len();
let attachment_area = (
(
mid + 1,
get_y(bottom_right).saturating_sub(4 + attachments_no),
),
pos_dec(bottom_right, (mid, 0)),
);
let body_area = (
(
get_x(upper_left!(header_area)),
get_y(bottom_right!(header_area)) + 1,
),
(
get_x(bottom_right!(header_area)),
get_y(upper_left!(attachment_area)) - 1,
),
);
let (x, y) = write_string_to_grid(
if self.reply_context.is_some() {
"COMPOSING REPLY"
} else {
"COMPOSING MESSAGE"
},
grid,
crate::conf::value(context, "highlight").fg,
crate::conf::value(context, "highlight").bg,
crate::conf::value(context, "highlight").attrs,
(
pos_dec(upper_left!(header_area), (0, 1)),
bottom_right!(header_area),
),
None,
);
clear_area(grid, ((x, y), (set_y(bottom_right, y))), theme_default);
change_colors(
grid,
(
set_x(pos_dec(upper_left!(header_area), (0, 1)), x),
set_y(bottom_right!(header_area), y),
),
crate::conf::value(context, "highlight").fg,
crate::conf::value(context, "highlight").bg,
);
clear_area(
grid,
(
pos_dec(upper_left, (0, 1)),
set_x(bottom_right, get_x(upper_left) + mid),
),
theme_default,
);
clear_area(
grid,
(
(
get_x(bottom_right).saturating_sub(mid),
get_y(upper_left) - 1,
),
bottom_right,
),
theme_default,
);
/* Regardless of view mode, do the following */
self.form.draw(grid, header_area, context);
if let Some(ref mut embed_pty) = self.embed {
let embed_area = (upper_left!(header_area), bottom_right!(body_area));
match embed_pty {
EmbedStatus::Running(_, _) => {
let mut guard = embed_pty.lock().unwrap();
clear_area(grid, embed_area, theme_default);
copy_area(
grid,
guard.grid.buffer(),
embed_area,
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
);
guard.set_terminal_size((width!(embed_area), height!(embed_area)));
context.dirty_areas.push_back(area);
self.dirty = false;
return;
}
EmbedStatus::Stopped(_, _) => {
let guard = embed_pty.lock().unwrap();
copy_area(
grid,
guard.grid.buffer(),
embed_area,
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
);
change_colors(grid, embed_area, Color::Byte(8), theme_default.bg);
let our_map: ShortcutMap =
account_settings!(context[self.account_hash].shortcuts.composing)
.key_values();
let mut shortcuts: ShortcutMaps = Default::default();
shortcuts.insert(Composer::DESCRIPTION, our_map);
let stopped_message: String =
format!("Process with PID {} has stopped.", guard.child_pid);
let stopped_message_2: String = format!(
"-press '{}' (edit_mail shortcut) to re-activate.",
shortcuts[Self::DESCRIPTION]["edit_mail"]
);
const STOPPED_MESSAGE_3: &str =
"-press Ctrl-C to forcefully kill it and return to editor.";
let max_len = std::cmp::max(
stopped_message.len(),
std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()),
);
let inner_area = create_box(
grid,
(
pos_inc(upper_left!(body_area), (1, 0)),
pos_inc(
upper_left!(body_area),
(
std::cmp::min(max_len + 5, width!(body_area)),
std::cmp::min(5, height!(body_area)),
),
),
),
);
clear_area(grid, inner_area, theme_default);
for (i, l) in [
stopped_message.as_str(),
stopped_message_2.as_str(),
STOPPED_MESSAGE_3,
]
.iter()
.enumerate()
{
write_string_to_grid(
l,
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(
pos_inc((0, i), upper_left!(inner_area)),
bottom_right!(inner_area),
),
Some(get_x(upper_left!(inner_area))),
);
}
context.dirty_areas.push_back(area);
self.dirty = false;
return;
}
}
} else {
self.embed_area = (upper_left!(header_area), bottom_right!(body_area));
}
if !self.mode.is_edit_attachments() {
self.pager.set_dirty(true);
if self.pager.size().0 > width!(body_area) {
self.pager.set_initialised(false);
}
self.pager.draw(grid, body_area, context);
}
match self.cursor {
Cursor::Headers => {
change_colors(
grid,
(
pos_dec(upper_left!(body_area), (1, 0)),
pos_dec(
set_y(upper_left!(body_area), get_y(bottom_right!(body_area))),
(1, 0),
),
),
theme_default.fg,
theme_default.bg,
);
}
Cursor::Body => {
change_colors(
grid,
(
pos_dec(upper_left!(body_area), (1, 0)),
pos_dec(
set_y(upper_left!(body_area), get_y(bottom_right!(body_area))),
(1, 0),
),
),
theme_default.fg,
crate::conf::value(context, "highlight").bg,
);
}
Cursor::Sign | Cursor::Encrypt | Cursor::Attachments => {}
}
match self.mode {
ViewMode::Edit | ViewMode::Embed => {}
ViewMode::EditAttachments { ref mut widget } => {
let inner_area = create_box(
grid,
(upper_left!(body_area), bottom_right!(attachment_area)),
);
(EditAttachmentsRefMut {
inner: widget,
draft: &mut self.draft,
})
.draw(
grid,
(
pos_inc(upper_left!(inner_area), (1, 1)),
bottom_right!(inner_area),
),
context,
);
}
ViewMode::Send(ref mut s) => {
s.draw(grid, area, context);
}
#[cfg(feature = "gpgme")]
ViewMode::SelectEncryptKey(
_,
gpg::KeySelection::Loaded {
ref mut widget,
keys: _,
},
) => {
widget.draw(grid, area, context);
}
#[cfg(feature = "gpgme")]
ViewMode::SelectEncryptKey(_, _) => {}
ViewMode::SelectRecipients(ref mut s) => {
s.draw(grid, area, context);
}
ViewMode::Discard(_, ref mut s) => {
/* Let user choose whether to quit with/without saving or cancel */
s.draw(grid, area, context);
}
ViewMode::WaitingForSendResult(ref mut s, _) => {
/* Let user choose whether to wait for success or cancel */
s.draw(grid, area, context);
}
}
if !self.mode.is_edit_attachments() {
self.draw_attachments(grid, attachment_area, context);
}
self.dirty = false;
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
if let UIEvent::VisibilityChange(_) = event {
self.pager.process_event(event, context);
}
let shortcuts = self.get_shortcuts(context);
match (&mut self.mode, &mut event) {
(ViewMode::Edit, _) => {
if self.pager.process_event(event, context) {
return true;
}
}
(ViewMode::EditAttachments { ref mut widget }, _) => {
if (EditAttachmentsRefMut {
inner: widget,
draft: &mut self.draft,
})
.process_event(event, context)
{
if widget.buttons.result() == Some(FormButtonActions::Cancel) {
self.mode = ViewMode::Edit;
self.set_dirty(true);
}
return true;
}
}
(ViewMode::Send(ref selector), UIEvent::FinishedUIDialog(id, result))
if selector.id() == *id =>
{
if let Some(true) = result.downcast_ref::() {
self.update_draft();
match send_draft_async(
#[cfg(feature = "gpgme")]
self.gpg_state.clone(),
context,
self.account_hash,
self.draft.clone(),
SpecialUsageMailbox::Sent,
Flag::SEEN,
) {
Ok(job) => {
let handle = context.job_executor.spawn_blocking(job);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::NewJob(
handle.job_id,
)));
self.mode = ViewMode::WaitingForSendResult(
UIDialog::new(
"Waiting for confirmation.. The tab will close automatically on successful submission.",
vec![
('c', "force close tab".to_string()),
('n', "close this message and return to edit mode".to_string()),
],
true,
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.first().cloned().unwrap_or('c')),
))
})),
context,
), handle);
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
None,
err.to_string(),
Some(NotificationType::Error(err.kind)),
));
save_draft(
self.draft.clone().finalise().unwrap().as_bytes(),
context,
SpecialUsageMailbox::Drafts,
Flag::SEEN | Flag::DRAFT,
self.account_hash,
);
self.mode = ViewMode::Edit;
}
}
}
self.set_dirty(true);
return true;
}
(ViewMode::Send(ref dialog), UIEvent::ComponentKill(ref id)) if *id == dialog.id() => {
self.mode = ViewMode::Edit;
self.set_dirty(true);
}
(ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentKill(ref id))
if *id == dialog.id() =>
{
self.mode = ViewMode::Edit;
self.set_dirty(true);
}
(ViewMode::Discard(_, ref dialog), UIEvent::ComponentKill(ref id))
if *id == dialog.id() =>
{
self.mode = ViewMode::Edit;
self.set_dirty(true);
}
#[cfg(feature = "gpgme")]
(ViewMode::SelectEncryptKey(_, ref mut selector), UIEvent::ComponentKill(ref id))
if *id == selector.id() =>
{
self.mode = ViewMode::Edit;
self.set_dirty(true);
return true;
}
(ViewMode::Send(ref mut selector), _) => {
if selector.process_event(event, context) {
return true;
}
}
(
ViewMode::SelectRecipients(ref selector),
UIEvent::FinishedUIDialog(id, ref mut result),
) if selector.id() == *id => {
if let Some(to_val) = result.downcast_mut::() {
self.draft.set_header("To", std::mem::take(to_val));
self.update_form();
}
self.mode = ViewMode::Edit;
return true;
}
(ViewMode::SelectRecipients(ref mut selector), _) => {
if selector.process_event(event, context) {
return true;
}
}
(ViewMode::Discard(u, ref selector), UIEvent::FinishedUIDialog(id, ref mut result))
if selector.id() == *id =>
{
if let Some(key) = result.downcast_mut::() {
match key {
'x' => {
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
return true;
}
'n' => {}
'y' => {
save_draft(
self.draft.clone().finalise().unwrap().as_bytes(),
context,
SpecialUsageMailbox::Drafts,
Flag::SEEN | Flag::DRAFT,
self.account_hash,
);
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
return true;
}
_ => {}
}
}
self.set_dirty(true);
self.mode = ViewMode::Edit;
return true;
}
(ViewMode::Discard(_, ref mut selector), _) => {
if selector.process_event(event, context) {
return true;
}
}
(
ViewMode::WaitingForSendResult(ref selector, _),
UIEvent::FinishedUIDialog(id, result),
) if selector.id() == *id => {
if let Some(key) = result.downcast_mut::() {
match key {
'c' => {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.id))));
return true;
}
'n' => {
self.set_dirty(true);
if let ViewMode::WaitingForSendResult(_, handle) =
std::mem::replace(&mut self.mode, ViewMode::Edit)
{
context.accounts[&self.account_hash].active_jobs.insert(
handle.job_id,
JobRequest::SendMessageBackground { handle },
);
}
}
_ => {}
}
}
return true;
}
(
ViewMode::WaitingForSendResult(_, ref mut handle),
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)),
) if handle.job_id == *job_id => {
match handle
.chan
.try_recv()
.map_err(|_: futures::channel::oneshot::Canceled| {
MeliError::new("Job was canceled")
}) {
Err(err) | Ok(Some(Err(err))) => {
self.mode = ViewMode::Edit;
context.replies.push_back(UIEvent::Notification(
None,
err.to_string(),
Some(NotificationType::Error(err.kind)),
));
self.set_dirty(true);
}
Ok(None) | Ok(Some(Ok(()))) => {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.id))));
}
}
return false;
}
(ViewMode::WaitingForSendResult(ref mut selector, _), _) => {
if selector.process_event(event, context) {
return true;
}
}
#[cfg(feature = "gpgme")]
(
ViewMode::SelectEncryptKey(is_encrypt, ref mut selector),
UIEvent::FinishedUIDialog(id, result),
) if *id == selector.id() => {
debug!(&result);
if let Some(key) = result.downcast_mut::