Browse Source

Add Reply{ToAuthor,ToAll} actions

- previous Reply action now lets you select recipients by default
- ReplyToAuthor selects the Envelope author as recipient
- ReplyToAll selects all addresses
tags/alpha-0.6.2
Manos Pitsidianakis 1 year ago
parent
commit
9928ee78e7
Signed by untrusted user: epilys GPG Key ID: 73627C2F690DF710
  1. 1
      Cargo.lock
  2. 1
      melib/Cargo.toml
  3. 31
      melib/src/email/address.rs
  4. 147
      melib/src/email/compose.rs
  5. 1
      melib/src/lib.rs
  6. 3
      src/command/actions.rs
  7. 269
      src/components/mail/compose.rs
  8. 97
      src/components/mail/view.rs
  9. 13
      src/components/utilities.rs
  10. 2
      src/conf/shortcuts.rs

1
Cargo.lock

@ -947,6 +947,7 @@ dependencies = [
"encoding",
"flate2",
"futures",
"indexmap",
"isahc",
"libc",
"libloading",

1
melib/Cargo.toml

@ -25,6 +25,7 @@ encoding = "0.2.33"
memmap = { version = "0.5.2", optional = true }
nom = { version = "5.1.1" }
indexmap = { version = "^1.5", features = ["serde-1", ] }
notify = { version = "4.0.1", optional = true }
xdg = "2.1.0"
native-tls = { version ="0.2.3", optional=true }

31
melib/src/email/address.rs

@ -20,6 +20,8 @@
*/
use super::*;
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GroupAddress {
@ -116,6 +118,12 @@ impl Address {
.map(str::to_string)
.collect::<_>()
}
pub fn list_try_from(val: &str) -> Result<Vec<Address>> {
Ok(parser::address::rfc2822address_list(val.as_bytes())?
.1
.to_vec())
}
}
impl Eq for Address {}
@ -139,6 +147,22 @@ impl PartialEq for Address {
}
}
impl Hash for Address {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Address::Mailbox(s) => {
s.address_spec.display_bytes(&s.raw).hash(state);
}
Address::Group(s) => {
s.display_name.display_bytes(&s.raw).hash(state);
for sub in &s.mailbox_list {
sub.hash(state);
}
}
}
}
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
@ -169,6 +193,13 @@ impl fmt::Debug for Address {
}
}
impl TryFrom<&str> for Address {
type Error = MeliError;
fn try_from(val: &str) -> Result<Address> {
Ok(parser::address::address(val.as_bytes())?.1)
}
}
/// Helper struct to return slices from a struct field on demand.
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, Copy)]
pub struct StrBuilder {

147
melib/src/email/compose.rs

@ -24,7 +24,7 @@ use crate::backends::BackendOp;
use crate::email::attachments::AttachmentBuilder;
use crate::shellexpand::ShellExpandTrait;
use data_encoding::BASE64_MIME;
use std::collections::HashMap;
use indexmap::IndexMap;
use std::ffi::OsStr;
use std::io::Read;
use std::path::{Path, PathBuf};
@ -39,8 +39,7 @@ use super::parser;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Draft {
pub headers: HashMap<String, String>,
pub header_order: Vec<String>,
pub headers: IndexMap<String, String>,
pub body: String,
pub attachments: Vec<AttachmentBuilder>,
@ -48,28 +47,19 @@ pub struct Draft {
impl Default for Draft {
fn default() -> Self {
let mut headers = HashMap::with_capacity_and_hasher(8, Default::default());
let mut header_order = Vec::with_capacity(8);
let mut headers = IndexMap::with_capacity_and_hasher(8, Default::default());
headers.insert(
"Date".into(),
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
);
headers.insert("From".into(), "".into());
headers.insert("To".into(), "".into());
headers.insert("Cc".into(), "".into());
headers.insert("Bcc".into(), "".into());
headers.insert(
"Date".into(),
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
);
headers.insert("Subject".into(), "".into());
header_order.push("Date".into());
header_order.push("From".into());
header_order.push("To".into());
header_order.push("Cc".into());
header_order.push("Bcc".into());
header_order.push("Subject".into());
header_order.push("User-Agent".into());
Draft {
headers,
header_order,
body: String::new(),
attachments: Vec::new(),
@ -88,35 +78,16 @@ impl str::FromStr for Draft {
let mut ret = Draft::default();
for (k, v) in headers {
if ignore_header(k) {
continue;
}
if ret
.headers
.insert(
String::from_utf8(k.to_vec())?,
String::from_utf8(v.to_vec())?,
)
.is_none()
{
ret.header_order.push(String::from_utf8(k.to_vec())?);
}
ret.headers.insert(
String::from_utf8(k.to_vec())?,
String::from_utf8(v.to_vec())?,
);
}
if ret.headers.contains_key("From") && !ret.headers.contains_key("Message-ID") {
if let Ok((_, addr)) = super::parser::address::mailbox(ret.headers["From"].as_bytes()) {
if let Some(fqdn) = addr.get_fqdn() {
if ret
.headers
.insert("Message-ID".into(), random::gen_message_id(&fqdn))
.is_none()
{
let pos = ret
.header_order
.iter()
.position(|h| h == "Subject")
.unwrap();
ret.header_order.insert(pos, "Message-ID".into());
}
ret.headers
.insert("Message-ID".into(), random::gen_message_id(&fqdn));
}
}
}
@ -136,12 +107,7 @@ impl Draft {
{
let bytes = futures::executor::block_on(op.as_bytes()?)?;
for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) {
if ignore_header(k.as_bytes()) {
continue;
}
if ret.headers.insert(k.into(), v.into()).is_none() {
ret.header_order.push(k.into());
}
ret.headers.insert(k.into(), v.into());
}
}
@ -150,12 +116,12 @@ impl Draft {
Ok(ret)
}
pub fn set_header(&mut self, header: &str, value: String) {
if self.headers.insert(header.to_string(), value).is_none() {
self.header_order.push(header.to_string());
}
pub fn set_header(&mut self, header: &str, value: String) -> &mut Self {
self.headers.insert(header.to_string(), value);
self
}
pub fn new_reply(envelope: &Envelope, bytes: &[u8]) -> Self {
pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self {
let mut ret = Draft::default();
ret.headers_mut().insert(
"References".into(),
@ -174,15 +140,32 @@ impl Draft {
envelope.message_id_display()
),
);
ret.header_order.push("References".into());
ret.headers_mut()
.insert("In-Reply-To".into(), envelope.message_id_display().into());
ret.header_order.push("In-Reply-To".into());
if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.headers_mut().insert("To".into(), reply_to.to_string());
// "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 {
if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") {
ret.headers_mut().insert("To".into(), reply_to.to_string());
} else {
if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.headers_mut().insert("To".into(), reply_to.to_string());
} else {
ret.headers_mut()
.insert("To".into(), envelope.field_from_to_string());
}
// FIXME: add To/Cc
}
} else {
ret.headers_mut()
.insert("To".into(), envelope.field_from_to_string());
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
ret.headers_mut().insert("To".into(), reply_to.to_string());
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.headers_mut().insert("To".into(), reply_to.to_string());
} else {
ret.headers_mut()
.insert("To".into(), envelope.field_from_to_string());
}
}
ret.headers_mut()
.insert("Cc".into(), envelope.field_cc_to_string());
@ -208,11 +191,11 @@ impl Draft {
ret
}
pub fn headers_mut(&mut self) -> &mut HashMap<String, String> {
pub fn headers_mut(&mut self) -> &mut IndexMap<String, String> {
&mut self.headers
}
pub fn headers(&self) -> &HashMap<String, String> {
pub fn headers(&self) -> &IndexMap<String, String> {
&self.headers
}
@ -228,15 +211,15 @@ impl Draft {
&self.body
}
pub fn set_body(&mut self, s: String) {
pub fn set_body(&mut self, s: String) -> &mut Self {
self.body = s;
self
}
pub fn to_string(&self) -> Result<String> {
let mut ret = String::new();
for k in &self.header_order {
let v = &self.headers[k];
for (k, v) in &self.headers {
ret.extend(format!("{}: {}\n", k, v).chars());
}
@ -253,23 +236,12 @@ impl Draft {
if let Ok((_, addr)) = super::parser::address::mailbox(self.headers["From"].as_bytes())
{
if let Some(fqdn) = addr.get_fqdn() {
if self
.headers
.insert("Message-ID".into(), random::gen_message_id(&fqdn))
.is_none()
{
let pos = self
.header_order
.iter()
.position(|h| h == "Subject")
.unwrap();
self.header_order.insert(pos, "Message-ID".into());
}
self.headers
.insert("Message-ID".into(), random::gen_message_id(&fqdn));
}
}
}
for k in &self.header_order {
let v = &self.headers[k];
for (k, v) in &self.headers {
if v.is_ascii() {
ret.extend(format!("{}: {}\n", k, v).chars());
} else {
@ -306,25 +278,6 @@ impl Draft {
}
}
fn ignore_header(header: &[u8]) -> bool {
match header {
b"From" => false,
b"To" => false,
b"Date" => false,
b"Message-ID" => false,
b"User-Agent" => false,
b"Subject" => false,
b"Reply-to" => false,
b"Cc" => false,
b"Bcc" => false,
b"In-Reply-To" => false,
b"References" => false,
b"MIME-Version" => true,
h if h.starts_with(b"X-") => false,
_ => true,
}
}
fn build_multipart(ret: &mut String, kind: MultipartType, parts: Vec<AttachmentBuilder>) {
let boundary = ContentType::make_boundary(&parts);
ret.extend(

1
melib/src/lib.rs

@ -132,6 +132,7 @@ pub use nom;
#[macro_use]
extern crate bitflags;
pub extern crate indexmap;
extern crate uuid;
pub use smallvec;

3
src/command/actions.rs

@ -24,7 +24,7 @@
*/
use crate::components::Component;
use melib::backends::{AccountHash, MailboxHash};
use melib::backends::AccountHash;
pub use melib::thread::{SortField, SortOrder};
use melib::{Draft, EnvelopeHash};
@ -60,7 +60,6 @@ pub enum ListingAction {
pub enum TabAction {
New(Option<Box<dyn Component>>),
NewDraft(AccountHash, Option<Draft>),
Reply((AccountHash, MailboxHash), EnvelopeHash), // thread coordinates (account, mailbox) and envelope
Close,
Edit(AccountHash, EnvelopeHash), // account_position, envelope hash
Kill(Uuid),

269
src/components/mail/compose.rs

@ -26,7 +26,9 @@ use melib::Draft;
use crate::conf::accounts::JobRequest;
use crate::jobs::{JobChannel, JobId, JoinHandle};
use crate::terminal::embed::EmbedGrid;
use indexmap::IndexSet;
use nix::sys::wait::WaitStatus;
use std::convert::TryInto;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use xdg_utils::query_mime_info;
@ -66,7 +68,6 @@ impl std::ops::DerefMut for EmbedStatus {
#[derive(Debug)]
pub struct Composer {
reply_context: Option<(MailboxHash, EnvelopeHash)>,
reply_bytes_request: Option<(JobId, JobChannel<Vec<u8>>)>,
account_hash: AccountHash,
cursor: Cursor,
@ -92,7 +93,6 @@ impl Default for Composer {
pager.set_reflow(text_processing::Reflow::FormatFlowed);
Composer {
reply_context: None,
reply_bytes_request: None,
account_hash: 0,
cursor: Cursor::Headers,
@ -167,7 +167,6 @@ impl Composer {
let _k = k.clone();
ret.draft.headers_mut().insert(_k, v.into());
} else {
/* set_header() also updates draft's header_order field */
ret.draft.set_header(h, v.into());
}
}
@ -193,16 +192,148 @@ impl Composer {
Ok(ret)
}
pub fn with_context(
coordinates: (AccountHash, MailboxHash),
msg: EnvelopeHash,
pub fn reply_to(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
bytes: &[u8],
context: &mut Context,
reply_to_all: bool,
) -> Self {
let mut ret = Composer::new(coordinates.0, context);
let account = &context.accounts[&coordinates.0];
let mut ret = Composer::default();
ret.pager
.set_colors(crate::conf::value(context, "theme_default"));
let parent_message = account.collection.get_env(msg);
let envelope = account.collection.get_env(coordinates.2);
let subject = envelope.subject();
ret.draft.headers_mut().insert(
"Subject".into(),
if !subject.starts_with("Re: ") {
format!("Re: {}", subject)
} else {
subject.into()
},
);
ret.draft.headers_mut().insert(
"References".into(),
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
.headers_mut()
.insert("In-Reply-To".into(), envelope.message_id_display().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 Some(ours) = TryInto::<Address>::try_into(
crate::components::mail::get_display_name(context, coordinates.0).as_str(),
)
.ok()
{
to.remove(&ours);
}
ret.draft.headers_mut().insert("To".into(), {
let mut ret: String =
to.into_iter()
.fold(String::new(), |mut s: String, n: Address| {
s.extend(n.to_string().chars());
s.push_str(", ");
s
});
ret.pop();
ret.pop();
ret
});
ret.draft
.headers_mut()
.insert("Cc".into(), envelope.field_cc_to_string());
} else {
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
ret.draft
.headers_mut()
.insert("To".into(), reply_to.to_string());
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.draft
.headers_mut()
.insert("To".into(), reply_to.to_string());
} else {
ret.draft
.headers_mut()
.insert("To".into(), envelope.field_from_to_string());
}
}
let body = envelope.body_bytes(bytes);
ret.draft.body = {
let reply_body_bytes = decode_rec(&body, None);
let reply_body = String::from_utf8_lossy(&reply_body_bytes);
let mut ret = format!(
"On {} {} wrote:\n",
envelope.date_as_str(),
envelope.from()[0],
);
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),
bytes: &[u8],
context: &mut Context,
) -> Self {
let mut ret = Composer::reply_to(coordinates, bytes, 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) {
@ -240,69 +371,25 @@ impl Composer {
}
}
}
let subject = parent_message.subject();
ret.draft.headers_mut().insert(
"Subject".into(),
if !subject.starts_with("Re: ") {
format!("Re: {}", subject)
} else {
subject.into()
},
);
drop(parent_message);
match context.accounts[&coordinates.0]
.operation(msg)
.and_then(|mut op| op.as_bytes())
{
Err(err) => {
context.replies.push_back(UIEvent::Notification(
None,
err.to_string(),
Some(NotificationType::ERROR),
));
}
Ok(fut) => {
let (mut rcvr, handle, job_id) = context.accounts[&coordinates.0]
.job_executor
.spawn_specialized(fut);
context.accounts[&coordinates.0]
.active_jobs
.insert(job_id, JobRequest::AsBytes(handle));
if let Ok(Some(parent_bytes)) = try_recv_timeout!(&mut rcvr) {
match parent_bytes {
Err(err) => {
context.replies.push_back(UIEvent::Notification(
None,
err.to_string(),
Some(NotificationType::ERROR),
));
}
Ok(parent_bytes) => {
let env_hash = msg;
let parent_message = context.accounts[&coordinates.0]
.collection
.get_env(env_hash);
let mut new_draft = Draft::new_reply(&parent_message, &parent_bytes);
new_draft
.headers_mut()
.extend(ret.draft.headers_mut().drain());
new_draft
.attachments_mut()
.extend(ret.draft.attachments_mut().drain(..));
ret.set_draft(new_draft);
}
}
} else {
ret.reply_bytes_request = Some((job_id, rcvr));
}
}
}
ret.account_hash = coordinates.0;
ret.reply_context = Some((coordinates.1, msg));
ret
}
pub fn reply_to_author(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
bytes: &[u8],
context: &mut Context,
) -> Self {
Composer::reply_to(coordinates, bytes, context, false)
}
pub fn reply_to_all(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
bytes: &[u8],
context: &mut Context,
) -> Self {
Composer::reply_to(coordinates, bytes, context, true)
}
pub fn set_draft(&mut self, draft: Draft) {
self.draft = draft;
self.update_form();
@ -633,48 +720,6 @@ impl Component for Composer {
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match (&mut self.mode, &mut event) {
(_, UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)))
if self
.reply_bytes_request
.as_ref()
.map(|(j, _)| j == job_id)
.unwrap_or(false) =>
{
let bytes = self
.reply_bytes_request
.take()
.unwrap()
.1
.try_recv()
.unwrap()
.unwrap();
match bytes {
Ok(parent_bytes) => {
let env_hash = self.reply_context.unwrap().1;
let parent_message = context.accounts[&self.account_hash]
.collection
.get_env(env_hash);
let mut new_draft = Draft::new_reply(&parent_message, &parent_bytes);
new_draft
.headers_mut()
.extend(self.draft.headers_mut().drain());
new_draft
.attachments_mut()
.extend(self.draft.attachments_mut().drain(..));
self.set_draft(new_draft);
self.set_dirty(true);
self.initialized = false;
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to load parent envelope")),
err.to_string(),
Some(NotificationType::ERROR),
));
}
}
return true;
}
(ViewMode::Edit, _) => {
if self.pager.process_event(event, context) {
return true;

97
src/components/mail/view.rs

@ -110,11 +110,21 @@ pub struct MailView {
}
#[derive(Debug)]
pub enum PendingReplyAction {
Reply,
ReplyToAuthor,
ReplyToAll,
}
#[derive(Debug)]
pub enum MailViewState {
Init,
Init {
pending_action: Option<PendingReplyAction>,
},
LoadingBody {
job_id: JobId,
chan: oneshot::Receiver<Result<Vec<u8>>>,
pending_action: Option<PendingReplyAction>,
},
Loaded {
body: Result<Vec<u8>>,
@ -123,7 +133,9 @@ pub enum MailViewState {
impl Default for MailViewState {
fn default() -> Self {
MailViewState::Init
MailViewState::Init {
pending_action: None,
}
}
}
@ -185,6 +197,7 @@ impl MailView {
fn init_futures(&mut self, context: &mut Context) {
debug!("init_futures");
let mut pending_action = None;
let account = &mut context.accounts[&self.coordinates.0];
if debug!(account.contains_key(self.coordinates.2)) {
{
@ -195,10 +208,22 @@ impl MailView {
Ok(fut) => {
let (mut chan, handle, job_id) =
account.job_executor.spawn_specialized(fut);
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 chan) {
self.state = MailViewState::Loaded { body: bytes_result };
} else {
self.state = MailViewState::LoadingBody { job_id, chan };
self.state = MailViewState::LoadingBody {
job_id,
chan,
pending_action: pending_action.take(),
};
self.active_jobs.insert(job_id);
account.insert_job(job_id, JobRequest::AsBytes(handle));
context
@ -238,6 +263,46 @@ impl MailView {
};
}
}
if let Some(p) = pending_action {
self.perform_action(p, context);
}
}
fn perform_action(&mut self, action: PendingReplyAction, context: &mut Context) {
let bytes = 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 { body: Ok(ref b) } => b,
MailViewState::Loaded { body: Err(_) } => {
return;
}
};
let composer = match action {
PendingReplyAction::Reply => {
Box::new(Composer::reply_to_select(self.coordinates, bytes, context))
}
PendingReplyAction::ReplyToAuthor => {
Box::new(Composer::reply_to_author(self.coordinates, bytes, context))
}
PendingReplyAction::ReplyToAll => {
Box::new(Composer::reply_to_all(self.coordinates, bytes, context))
}
};
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(composer)))));
}
/// Returns the string to be displayed in the Viewer
@ -1027,12 +1092,13 @@ impl Component for MailView {
MailViewState::LoadingBody {
job_id: ref id,
ref mut chan,
pending_action: _,
} if job_id == id => {
let bytes_result = chan.try_recv().unwrap().unwrap();
debug!("bytes_result");
self.state = MailViewState::Loaded { body: bytes_result };
}
MailViewState::Init => {
MailViewState::Init { .. } => {
self.init_futures(context);
}
_ => {}
@ -1053,10 +1119,19 @@ impl Component for MailView {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply"]) =>
{
context.replies.push_back(UIEvent::Action(Tab(Reply(
(self.coordinates.0, self.coordinates.1),
self.coordinates.2,
))));
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)
@ -1168,7 +1243,7 @@ impl Component for MailView {
MailViewState::Loaded { .. } => {
self.open_attachment(lidx, context, true);
}
MailViewState::Init => {
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
@ -1189,7 +1264,7 @@ impl Component for MailView {
MailViewState::Loaded { .. } => {
self.open_attachment(lidx, context, false);
}
MailViewState::Init => {
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
@ -1213,7 +1288,7 @@ impl Component for MailView {
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Init => {
MailViewState::Init { .. } => {
self.init_futures(context);
}
MailViewState::LoadingBody { .. } => {}

13
src/components/utilities.rs

@ -1675,19 +1675,6 @@ impl Component for Tabbed {
self.help_curr_views = children_maps;
return true;
}
UIEvent::Action(Tab(Reply(coordinates, msg))) => {
self.add_component(Box::new(Composer::with_context(
*coordinates,
*msg,
context,
)));
self.cursor_pos = self.children.len() - 1;
self.children[self.cursor_pos].set_dirty(true);
let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context);
children_maps.extend(self.get_shortcuts(context));
self.help_curr_views = children_maps;
return true;
}
UIEvent::Action(Tab(Edit(account_hash, msg))) => {
let composer = match Composer::edit(*account_hash, *msg, context) {
Ok(c) => c,

2
src/conf/shortcuts.rs

@ -238,6 +238,8 @@ shortcut_key_values! { "envelope-view",
open_attachment |> "Opens selected attachment with xdg-open." |> Key::Char('a'),
open_mailcap |> "Opens selected attachment according to its mailcap entry." |> Key::Char('m'),
reply |> "Reply to envelope." |> Key::Char('R'),
reply_to_author |> "Reply to author." |> Key::Ctrl('r'),
reply_to_all |> "Reply to all/Reply to list/Follow up." |> Key::Ctrl('g'),
return_to_normal_view |> "Return to envelope if viewing raw source or attachment." |> Key::Char('r'),
toggle_expand_headers |> "Expand extra headers (References and others)." |> Key::Char('h'),
toggle_url_mode |> "Toggles url open mode." |> Key::Char('u'),

Loading…
Cancel
Save