diff --git a/Cargo.lock b/Cargo.lock index 8401d4940..0e85415a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -947,6 +947,7 @@ dependencies = [ "encoding", "flate2", "futures", + "indexmap", "isahc", "libc", "libloading", diff --git a/melib/Cargo.toml b/melib/Cargo.toml index ce9ee4942..dbaf3c333 100644 --- a/melib/Cargo.toml +++ b/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 } diff --git a/melib/src/email/address.rs b/melib/src/email/address.rs index 53c47754a..d43e92e97 100644 --- a/melib/src/email/address.rs +++ b/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> { + 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(&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
{ + 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 { diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index edfe53943..9984499ef 100644 --- a/melib/src/email/compose.rs +++ b/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, - pub header_order: Vec, + pub headers: IndexMap, pub body: String, pub attachments: Vec, @@ -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 { + pub fn headers_mut(&mut self) -> &mut IndexMap { &mut self.headers } - pub fn headers(&self) -> &HashMap { + pub fn headers(&self) -> &IndexMap { &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 { 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) { let boundary = ContentType::make_boundary(&parts); ret.extend( diff --git a/melib/src/lib.rs b/melib/src/lib.rs index 510dbd362..cf45e9c58 100644 --- a/melib/src/lib.rs +++ b/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; diff --git a/src/command/actions.rs b/src/command/actions.rs index 7e9dde583..3a6d6a90f 100644 --- a/src/command/actions.rs +++ b/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>), NewDraft(AccountHash, Option), - Reply((AccountHash, MailboxHash), EnvelopeHash), // thread coordinates (account, mailbox) and envelope Close, Edit(AccountHash, EnvelopeHash), // account_position, envelope hash Kill(Uuid), diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index a341887cd..78f3dfa13 100644 --- a/src/components/mail/compose.rs +++ b/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>)>, 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 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::
::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 mut ret = Composer::default(); - ret.pager - .set_colors(crate::conf::value(context, "theme_default")); - let parent_message = account.collection.get_env(msg); + 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; diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 398e58bde..ca61fa6d9 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -109,12 +109,22 @@ pub struct MailView { id: ComponentId, } +#[derive(Debug)] +pub enum PendingReplyAction { + Reply, + ReplyToAuthor, + ReplyToAll, +} + #[derive(Debug)] pub enum MailViewState { - Init, + Init { + pending_action: Option, + }, LoadingBody { job_id: JobId, chan: oneshot::Receiver>>, + pending_action: Option, }, Loaded { body: Result>, @@ -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 { .. } => {} diff --git a/src/components/utilities.rs b/src/components/utilities.rs index eab4aef90..f7f03e99f 100644 --- a/src/components/utilities.rs +++ b/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, diff --git a/src/conf/shortcuts.rs b/src/conf/shortcuts.rs index 5478e5635..22553e3a1 100644 --- a/src/conf/shortcuts.rs +++ b/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'),