From 8c671935f9ad5bd2894c0ecdaec9c2f378e461ca Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 16 May 2023 13:17:13 +0300 Subject: [PATCH] Add compose (pre-submission) hooks for validation/linting compose-hooks run before submitting an e-mail. They perform draft validation and/or transformations. If a hook encounters an error or warning, it will show up as a notification. The currently available hooks are: - past-date-warn Warn if Date header value is far in the past or future. - important-header-warn Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid. - missing-attachment-warn Warn if Subject, draft body mention attachments but they are missing. - empty-draft-warn Warn if draft has no subject and no body. They can be disabled with [composing.disabled_compose_hooks] setting. --- Cargo.lock | 1 + Cargo.toml | 1 + docs/meli.conf.5 | 34 +- melib/src/backends/maildir/backend.rs | 10 +- melib/src/email.rs | 15 +- melib/src/email/compose.rs | 64 ++-- melib/src/email/headers.rs | 8 +- melib/src/email/list_management.rs | 1 - melib/src/error.rs | 4 +- melib/src/logging.rs | 32 +- src/components/mail/compose.rs | 156 +++++---- src/components/mail/compose/hooks.rs | 354 ++++++++++++++++++++ src/components/mail/listing.rs | 5 +- src/conf/composing.rs | 4 + src/conf/overrides.rs | 459 +------------------------- src/conf/themes.rs | 11 +- src/state.rs | 4 +- src/terminal/cells.rs | 4 +- tools/src/embed.rs | 11 +- 19 files changed, 596 insertions(+), 582 deletions(-) create mode 100644 src/components/mail/compose/hooks.rs diff --git a/Cargo.lock b/Cargo.lock index d5e146da..455c554d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,6 +1155,7 @@ dependencies = [ "structopt", "svg", "syn", + "tempfile", "termion", "toml", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 734b7677..48c2df8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ syn = { version = "1.0.92", features = [] } [dev-dependencies] regex = "1" +tempfile = "3.3" [profile.release] lto = "fat" diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index 7a95e9e8..f767db60 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -127,6 +127,7 @@ theme = "light" Available options are listed below. Default values are shown in parentheses. .Sh ACCOUNTS +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic root_mailbox Ar String The backend-specific path of the root_mailbox, usually INBOX. @@ -513,7 +514,8 @@ Useful only for mUTF-7 mailboxes. .Pq Em "utf7", "utf-7", "utf8", "utf-8" .El .Sh COMPOSING -Composing specific options +Composing specific options. +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic send_mail Ar String|SmtpServerConf Command to pipe new mail to (exit code must be 0 for success) or settings for an SMTP server connection. @@ -611,8 +613,30 @@ When used in a reply, the field body MAY start with the string "Re: " (from the Latin "res", in the matter of) followed by the contents of the "Subject:" field body of the original message. .Ed +.It Ic disabled_compose_hooks Ar [String] +.Pq Em optional +Disabled compose-hooks. +compose-hooks run before submitting an e-mail. +They perform draft validation and/or transformations. +If a hook encounters an error or warning, it will show up as a notification. +The currently available hooks are: +.Bl -bullet -compact +.It +.Ic past-date-warn +— Warn if Date header value is far in the past or future. +.It +.Ic important-header-warn +— Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid. +.It +.Ic missing-attachment-warn +— Warn if Subject, draft body mention attachments but they are missing. +.It +.Ic empty-draft-warn +— Warn if draft has no subject and no body. +.El .El .Sh SHORTCUTS +Default values are shown in parentheses. Shortcuts can take the following values: .Bl -bullet -compact .It @@ -1040,6 +1064,7 @@ toggle thread view visibility .El .sp .Sh NOTIFICATIONS +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic enable Ar boolean Enable notifications. @@ -1074,6 +1099,7 @@ Play sound file in notifications if possible. .Pq Em none .El .Sh PAGER +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic headers_sticky Ar boolean .Pq Em optional @@ -1128,6 +1154,7 @@ The URL will be given as the first argument of the command. .Pq Em xdg-open .El .Sh LISTING +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic show_menu_scrollbar Ar boolean .Pq Em optional @@ -1269,6 +1296,7 @@ no_sibling_leaf = " \\_" .Ed .sp .Sh TAGS +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic colours Ar hash table String[Color] .Pq Em optional @@ -1291,6 +1319,7 @@ colors = { signed="#Ff6600", replied="DeepSkyBlue4", draft="#f00", replied="8" } "INBOX" = { tags.ignore_tags=["inbox", ] } .Ed .Sh PGP +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic auto_verify_signatures Ar boolean Auto verify signed e-mail according to RFC3156 @@ -1308,6 +1337,7 @@ Key to be used when signing/encrypting (not functional yet) .Pq Em none .El .Sh TERMINAL +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic theme Ar String .Pq Em optional @@ -1431,6 +1461,7 @@ progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] } .Ed .El .Sh LOG +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic log_file Ar String .Pq Em optional @@ -1467,6 +1498,7 @@ to .Pq Em INFO .El .Sh SMTP Connections +Default values are shown in parentheses. .Bl -tag -width 36n .It Ic hostname Ar String server hostname diff --git a/melib/src/backends/maildir/backend.rs b/melib/src/backends/maildir/backend.rs index 620d3c00..e3b4db6e 100644 --- a/melib/src/backends/maildir/backend.rs +++ b/melib/src/backends/maildir/backend.rs @@ -67,14 +67,15 @@ pub struct MaildirPath { impl Deref for MaildirPath { type Target = PathBuf; - fn deref(&self) -> &PathBuf { + + fn deref(&self) -> &Self::Target { assert!(!(self.removed && self.modified.is_none())); &self.buf } } impl DerefMut for MaildirPath { - fn deref_mut(&mut self) -> &mut PathBuf { + fn deref_mut(&mut self) -> &mut Self::Target { assert!(!(self.removed && self.modified.is_none())); &mut self.buf } @@ -98,13 +99,14 @@ pub struct HashIndex { impl Deref for HashIndex { type Target = HashMap; - fn deref(&self) -> &HashMap { + + fn deref(&self) -> &Self::Target { &self.index } } impl DerefMut for HashIndex { - fn deref_mut(&mut self) -> &mut HashMap { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.index } } diff --git a/melib/src/email.rs b/melib/src/email.rs index 37a65543..294e6a79 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -444,7 +444,6 @@ impl Envelope { if self.bcc.is_empty() { self.other_headers .get("Bcc") - .map(|s| s.as_str()) .unwrap_or_default() .to_string() } else { @@ -460,11 +459,7 @@ impl Envelope { pub fn field_cc_to_string(&self) -> String { if self.cc.is_empty() { - self.other_headers - .get("Cc") - .map(|s| s.as_str()) - .unwrap_or_default() - .to_string() + self.other_headers.get("Cc").unwrap_or_default().to_string() } else { self.cc.iter().fold(String::new(), |mut acc, x| { if !acc.is_empty() { @@ -480,7 +475,6 @@ impl Envelope { if self.from.is_empty() { self.other_headers .get("From") - .map(|s| s.as_str()) .unwrap_or_default() .to_string() } else { @@ -508,11 +502,7 @@ impl Envelope { pub fn field_to_to_string(&self) -> String { if self.to.is_empty() { - self.other_headers - .get("To") - .map(|s| s.as_str()) - .unwrap_or_default() - .to_string() + self.other_headers.get("To").unwrap_or_default().to_string() } else { self.to .iter() @@ -532,7 +522,6 @@ impl Envelope { if refs.is_empty() { self.other_headers .get("References") - .map(|s| s.as_str()) .unwrap_or_default() .to_string() } else { diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index 40cf5152..c89d41ad 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -491,6 +491,38 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) { } } +/// Reads file from given path, and returns an 'application/octet-stream' +/// AttachmentBuilder object +pub fn attachment_from_file(path: &I) -> Result +where + I: AsRef, +{ + let path: PathBuf = Path::new(path).expand(); + if !path.is_file() { + return Err(Error::new(format!("{} is not a file", path.display()))); + } + + let mut file = std::fs::File::open(&path)?; + let mut contents = Vec::new(); + file.read_to_end(&mut contents)?; + let mut attachment = AttachmentBuilder::default(); + + attachment + .set_raw(contents) + .set_body_to_raw() + .set_content_type(ContentType::Other { + name: path.file_name().map(|s| s.to_string_lossy().into()), + tag: if let Ok(mime_type) = query_mime_info(&path) { + mime_type + } else { + b"application/octet-stream".to_vec() + }, + parameters: vec![], + }); + + Ok(attachment) +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -601,35 +633,3 @@ mod tests { } */ } - -/// Reads file from given path, and returns an 'application/octet-stream' -/// AttachmentBuilder object -pub fn attachment_from_file(path: &I) -> Result -where - I: AsRef, -{ - let path: PathBuf = Path::new(path).expand(); - if !path.is_file() { - return Err(Error::new(format!("{} is not a file", path.display()))); - } - - let mut file = std::fs::File::open(&path)?; - let mut contents = Vec::new(); - file.read_to_end(&mut contents)?; - let mut attachment = AttachmentBuilder::default(); - - attachment - .set_raw(contents) - .set_body_to_raw() - .set_content_type(ContentType::Other { - name: path.file_name().map(|s| s.to_string_lossy().into()), - tag: if let Ok(mime_type) = query_mime_info(&path) { - mime_type - } else { - b"application/octet-stream".to_vec() - }, - parameters: vec![], - }); - - Ok(attachment) -} diff --git a/melib/src/email/headers.rs b/melib/src/email/headers.rs index 2be6bd57..bfbd1893 100644 --- a/melib/src/email/headers.rs +++ b/melib/src/email/headers.rs @@ -252,8 +252,10 @@ impl HeaderMap { (self.0).get_mut(HeaderNameType(key).borrow() as &dyn HeaderKey) } - pub fn get(&self, key: &str) -> Option<&String> { - (self.0).get(HeaderNameType(key).borrow() as &dyn HeaderKey) + pub fn get(&self, key: &str) -> Option<&str> { + (self.0) + .get(HeaderNameType(key).borrow() as &dyn HeaderKey) + .map(|x| x.as_str()) } pub fn contains_key(&self, key: &str) -> bool { @@ -274,7 +276,7 @@ impl Deref for HeaderMap { } impl DerefMut for HeaderMap { - fn deref_mut(&mut self) -> &mut IndexMap { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } diff --git a/melib/src/email/list_management.rs b/melib/src/email/list_management.rs index 5129480e..4bea3d7a 100644 --- a/melib/src/email/list_management.rs +++ b/melib/src/email/list_management.rs @@ -87,7 +87,6 @@ pub fn list_id_header(envelope: &'_ Envelope) -> Option<&'_ str> { .other_headers() .get("List-ID") .or_else(|| envelope.other_headers().get("List-Id")) - .map(String::as_str) } pub fn list_id(header: Option<&'_ str>) -> Option<&'_ str> { diff --git a/melib/src/error.rs b/melib/src/error.rs index 4f1515d9..3251be0d 100644 --- a/melib/src/error.rs +++ b/melib/src/error.rs @@ -477,9 +477,9 @@ impl Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "{}", self.summary)?; + write!(f, "{}", self.summary)?; if let Some(details) = self.details.as_ref() { - write!(f, "{}", details)?; + write!(f, "\n{}", details)?; } if let Some(source) = self.source.as_ref() { write!(f, "\nCaused by: {}", source)?; diff --git a/melib/src/logging.rs b/melib/src/logging.rs index 28ab6e26..0e991d71 100644 --- a/melib/src/logging.rs +++ b/melib/src/logging.rs @@ -19,10 +19,10 @@ * along with meli. If not, see . */ +#[cfg(not(test))] +use std::{fs::OpenOptions, path::PathBuf}; use std::{ - fs::OpenOptions, io::{BufWriter, Write}, - path::PathBuf, sync::{ atomic::{AtomicU8, Ordering}, Arc, Mutex, @@ -31,8 +31,6 @@ use std::{ use log::{Level, LevelFilter, Log, Metadata, Record}; -use crate::shellexpand::ShellExpandTrait; - #[derive(Copy, Clone, Default, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)] #[repr(u8)] pub enum LogLevel { @@ -137,6 +135,9 @@ pub enum Destination { #[derive(Clone)] pub struct StderrLogger { + #[cfg(test)] + dest: Arc>>, + #[cfg(not(test))] dest: Arc>>, level: Arc, print_level: bool, @@ -163,6 +164,11 @@ impl Default for StderrLogger { impl StderrLogger { pub fn new(level: LogLevel) -> Self { + use std::sync::Once; + + static INIT_STDERR_LOGGING: Once = Once::new(); + + #[cfg(not(test))] let logger = { let data_dir = xdg::BaseDirectories::with_prefix("meli").unwrap(); let log_file = OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */ @@ -180,6 +186,16 @@ impl StderrLogger { debug_dest: Destination::None, } }; + #[cfg(test)] + let logger = { + StderrLogger { + dest: Arc::new(Mutex::new(BufWriter::new(std::io::stderr()).into())), + level: Arc::new(AtomicU8::new(level as u8)), + print_level: true, + print_module_names: true, + debug_dest: Destination::Stderr, + } + }; #[cfg(feature = "debug-tracing")] log::set_max_level( @@ -191,7 +207,10 @@ impl StderrLogger { ); #[cfg(not(feature = "debug-tracing"))] log::set_max_level(LevelFilter::from(logger.log_level())); - log::set_boxed_logger(Box::new(logger.clone())).unwrap(); + + INIT_STDERR_LOGGING.call_once(|| { + log::set_boxed_logger(Box::new(logger.clone())).unwrap(); + }); logger } @@ -199,7 +218,10 @@ impl StderrLogger { self.level.load(Ordering::SeqCst).into() } + #[cfg(not(test))] pub fn change_log_dest(&mut self, path: PathBuf) { + use crate::shellexpand::ShellExpandTrait; + let path = path.expand(); // expand shell stuff let mut dest = self.dest.lock().unwrap(); *dest = BufWriter::new(OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */ diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 16300ff5..b0ce2b8c 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -22,6 +22,7 @@ use std::{ convert::TryInto, future::Future, + io::Write, pin::Pin, process::{Command, Stdio}, sync::{Arc, Mutex}, @@ -40,9 +41,11 @@ use crate::{conf::accounts::JobRequest, jobs::JoinHandle, terminal::embed::Embed #[cfg(feature = "gpgme")] mod gpg; -mod edit_attachments; +pub mod edit_attachments; use edit_attachments::*; +pub mod hooks; + #[derive(Debug, PartialEq, Eq)] enum Cursor { Headers, @@ -67,19 +70,18 @@ impl EmbedStatus { impl std::ops::Deref for EmbedStatus { type Target = Arc>; - fn deref(&self) -> &Arc> { - use EmbedStatus::*; + + fn deref(&self) -> &Self::Target { match self { - Stopped(ref e, _) | Running(ref e, _) => e, + Self::Stopped(ref e, _) | Self::Running(ref e, _) => e, } } } impl std::ops::DerefMut for EmbedStatus { - fn deref_mut(&mut self) -> &mut Arc> { - use EmbedStatus::*; + fn deref_mut(&mut self) -> &mut Self::Target { match self { - Stopped(ref mut e, _) | Running(ref mut e, _) => e, + Self::Stopped(ref mut e, _) | Self::Running(ref mut e, _) => e, } } } @@ -104,6 +106,7 @@ pub struct Composer { dirty: bool, has_changes: bool, initialized: bool, + hooks: Vec, id: ComponentId, } @@ -156,6 +159,12 @@ impl Composer { cursor: Cursor::Headers, pager, draft: Draft::default(), + hooks: vec![ + hooks::HEADERWARN, + hooks::PASTDATEWARN, + hooks::MISSINGATTACHMENTWARN, + hooks::EMPTYDRAFTWARN, + ], form: FormWidget::default(), mode: ViewMode::Edit, #[cfg(feature = "gpgme")] @@ -174,6 +183,11 @@ impl Composer { account_hash, ..Composer::new(context) }; + ret.hooks.retain(|h| { + !account_settings!(context[account_hash].composing.disabled_compose_hooks) + .iter() + .any(|hn| hn.as_str() == h.name()) + }); for (h, v) in account_settings!(context[account_hash].composing.default_header_values).iter() { @@ -202,6 +216,11 @@ impl Composer { context: &Context, ) -> Result { let mut ret = Composer::with_account(account_hash, context); + ret.hooks.retain(|h| { + !account_settings!(context[account_hash].composing.disabled_compose_hooks) + .iter() + .any(|hn| hn.as_str() == h.name()) + }); let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash); ret.draft = Draft::edit(&envelope, bytes)?; @@ -211,13 +230,18 @@ impl Composer { } pub fn reply_to( - coordinates: (AccountHash, MailboxHash, EnvelopeHash), + coordinates @ (account_hash, _, _): (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 mut ret = Composer::with_account(account_hash, context); + ret.hooks.retain(|h| { + !account_settings!(context[account_hash].composing.disabled_compose_hooks) + .iter() + .any(|hn| hn.as_str() == h.name()) + }); + let account = &context.accounts[&account_hash]; let envelope = account.collection.get_env(coordinates.2); let subject = { let subject = envelope.subject(); @@ -267,7 +291,7 @@ impl Composer { 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()) { + if let Some(reply_to) = envelope.other_headers().get("To") { let to: &str = reply_to; let extra_identities = &account.settings.account.extra_identities; if let Some(extra) = extra_identities @@ -299,13 +323,13 @@ impl Composer { if let Some(reply_to) = envelope .other_headers() .get("Mail-Followup-To") - .and_then(|v| v.as_str().try_into().ok()) + .and_then(|v| v.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()) + .and_then(|v| v.try_into().ok()) { to.insert(reply_to); } else { @@ -368,12 +392,12 @@ impl Composer { } pub fn reply_to_select( - coordinates: (AccountHash, MailboxHash, EnvelopeHash), + coordinates @ (account_hash, _, _): (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 account = &context.accounts[&account_hash]; 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 @@ -496,6 +520,15 @@ To: {} } } + fn run_hooks(&mut self, ctx: &mut Context) -> Result<()> { + for h in self.hooks.iter_mut() { + if let err @ Err(_) = h(ctx, &mut self.draft) { + return err; + } + } + Ok(()) + } + fn update_form(&mut self) { let old_cursor = self.form.cursor(); self.form = FormWidget::new(("Save".into(), true)); @@ -1408,6 +1441,11 @@ impl Component for Composer { && self.mode.is_edit() => { self.update_draft(); + if let Err(err) = self.run_hooks(context) { + context + .replies + .push_back(UIEvent::Notification(None, err.to_string(), None)); + } self.mode = ViewMode::Send(UIConfirmationDialog::new( "send mail?", vec![(true, "yes".to_string()), (false, "no".to_string())], @@ -1434,7 +1472,6 @@ impl Component for Composer { self.set_dirty(true); } UIEvent::EmbedInput((ref k, ref b)) => { - use std::io::Write; if let Some(ref mut embed) = self.embed { let mut embed_guard = embed.lock().unwrap(); if embed_guard.write_all(b).is_err() { @@ -2390,10 +2427,13 @@ fn attribution_string( melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix) } -#[test] -#[ignore] -fn test_compose_reply_subject_prefix() { - let raw_mail = r#"From: "some name" +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compose_reply_subject_prefix() { + let raw_mail = r#"From: "some name" To: "me" Cc: Subject: RE: your e-mail @@ -2403,26 +2443,28 @@ Content-Type: text/plain hello world. "#; - let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail"); - let mut context = Context::new_mock(); - let account_hash = context.accounts[0].hash(); - let mailbox_hash = MailboxHash::default(); - let envelope_hash = envelope.hash(); - context.accounts[0] - .collection - .insert(envelope, mailbox_hash); - let composer = Composer::reply_to( - (account_hash, mailbox_hash, envelope_hash), - String::new(), - &mut context, - false, - ); - assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail"); - assert_eq!( - &composer.draft.headers()["To"], - r#"some name "# - ); - let raw_mail = r#"From: "some name" + let envelope = + Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail"); + let tempdir = tempfile::tempdir().unwrap(); + let mut context = Context::new_mock(&tempdir); + let account_hash = context.accounts[0].hash(); + let mailbox_hash = MailboxHash::default(); + let envelope_hash = envelope.hash(); + context.accounts[0] + .collection + .insert(envelope, mailbox_hash); + let composer = Composer::reply_to( + (account_hash, mailbox_hash, envelope_hash), + String::new(), + &mut context, + false, + ); + assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail"); + assert_eq!( + &composer.draft.headers()["To"], + r#"some name "# + ); + let raw_mail = r#"From: "some name" To: "me" Cc: Subject: your e-mail @@ -2431,20 +2473,22 @@ Content-Type: text/plain hello world. "#; - let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail"); - let envelope_hash = envelope.hash(); - context.accounts[0] - .collection - .insert(envelope, mailbox_hash); - let composer = Composer::reply_to( - (account_hash, mailbox_hash, envelope_hash), - String::new(), - &mut context, - false, - ); - assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail"); - assert_eq!( - &composer.draft.headers()["To"], - r#"some name "# - ); + let envelope = + Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail"); + let envelope_hash = envelope.hash(); + context.accounts[0] + .collection + .insert(envelope, mailbox_hash); + let composer = Composer::reply_to( + (account_hash, mailbox_hash, envelope_hash), + String::new(), + &mut context, + false, + ); + assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail"); + assert_eq!( + &composer.draft.headers()["To"], + r#"some name "# + ); + } } diff --git a/src/components/mail/compose/hooks.rs b/src/components/mail/compose/hooks.rs new file mode 100644 index 00000000..384bfa56 --- /dev/null +++ b/src/components/mail/compose/hooks.rs @@ -0,0 +1,354 @@ +/* + * meli + * + * Copyright 2023 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 . + */ + +//! Pre-submission hooks for draft validation and/or transformations. +pub use std::borrow::Cow; + +use super::*; + +/* +pub enum HookFn { + /// Stateful hook. + Closure(Box Result<()> + Send + Sync>), + + /// Static hook. + Ptr(fn(&mut Context, &mut Draft) -> Result<()>), +} +*/ + +/// Pre-submission hook for draft validation and/or transformations. +pub struct Hook { + /// Hook name for enabling/disabling it from configuration. + /// + /// See [`ComposingSettings::disabled_compose_hooks`]. + name: Cow<'static, str>, + hook_fn: fn(&mut Context, &mut Draft) -> Result<()>, + //hook_fn: HookFn, +} + +impl std::fmt::Debug for Hook { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(self.name.as_ref()).finish() + } +} + +impl Hook { + /// Hook name for enabling/disabling it from configuration. + /// + /// See [`ComposingSettings::disabled_compose_hooks`]. + pub fn name(&self) -> &str { + self.name.as_ref() + } +} + +impl std::ops::Deref for Hook { + type Target = dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync; + + fn deref(&self) -> &Self::Target { + &self.hook_fn + } +} + +impl std::ops::DerefMut for Hook { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.hook_fn + } +} + +fn past_date_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> { + use melib::datetime::*; + if let Some(v) = draft + .headers + .get("Date") + .map(rfc822_to_timestamp) + .and_then(Result::ok) + { + let now: UnixTimestamp = now(); + let diff = now.abs_diff(v); + if diff >= 60 * 60 { + return Err(format!( + "Value of Date header is {} minutes in the {}.", + diff / 60, + if now > v { "past" } else { "future" } + ) + .into()); + } + } + Ok(()) +} + +/// Warn if [`melib::Draft`] Date is far in the past/future. +pub const PASTDATEWARN: Hook = Hook { + name: Cow::Borrowed("past-date-warn"), + hook_fn: past_date_warn, +}; + +fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> { + for hdr in ["From", "To"] { + match draft.headers.get(hdr).map(melib::Address::list_try_from) { + Some(Ok(_)) => {} + Some(Err(err)) => return Err(format!("{hdr} header value is invalid ({err}).").into()), + None => return Err(format!("{hdr} header is missing and should be present.").into()), + } + } + + { + match draft + .headers + .get("Date") + .map(melib::datetime::rfc822_to_timestamp) + { + Some(Err(err)) => return Err(format!("Date header value is invalid ({err}).").into()), + Some(Ok(0)) => return Err("Date header value is invalid.".into()), + _ => {} + } + } + + for hdr in ["Cc", "Bcc"] { + if let Some(Err(err)) = draft + .headers + .get(hdr) + .filter(|v| !v.trim().is_empty()) + .map(melib::Address::list_try_from) + { + return Err(format!("{hdr} header value is invalid ({err}).").into()); + } + } + Ok(()) +} + +/// Warn if important [`melib::Draft`] header is missing or invalid. +pub const HEADERWARN: Hook = Hook { + name: Cow::Borrowed("important-header-warn"), + hook_fn: important_header_warn, +}; + +fn missing_attachment_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> { + if draft + .headers + .get("Subject") + .map(|s| s.to_lowercase().contains("attach")) + .unwrap_or(false) + && draft.attachments.is_empty() + { + return Err("Subject mentions attachments but attachments are empty.".into()); + } + + if draft.body.to_lowercase().contains("attach") && draft.attachments.is_empty() { + return Err("Draft body mentions attachments but attachments are empty.".into()); + } + + Ok(()) +} + +/// Warn if Subject and/or draft body mentions attachments but they are missing. +pub const MISSINGATTACHMENTWARN: Hook = Hook { + name: Cow::Borrowed("missing-attachment-warn"), + hook_fn: missing_attachment_warn, +}; + +fn empty_draft_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> { + if draft + .headers + .get("Subject") + .filter(|v| !v.trim().is_empty()) + .is_none() + && draft.body.trim().is_empty() + { + return Err("Both Subject and body are empty.".into()); + } + + Ok(()) +} + +/// Warn if draft has no subject and no body. +pub const EMPTYDRAFTWARN: Hook = Hook { + name: Cow::Borrowed("empty-draft-warn"), + hook_fn: empty_draft_warn, +}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_draft_hook_datewarn() { + let tempdir = tempfile::tempdir().unwrap(); + let mut ctx = Context::new_mock(&tempdir); + let mut draft = Draft::default(); + draft + .set_body("αδφαφσαφασ".to_string()) + .set_header("Subject", "test_update()".into()) + .set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + println!("Check that past Date header value produces a warning…"); + #[allow(const_item_mutation)] + let err_msg = PASTDATEWARN(&mut ctx, &mut draft).unwrap_err().to_string(); + assert!( + err_msg.starts_with("Value of Date header is "), + "PASTDATEWARN should complain about Date value being in the past: {}", + err_msg + ); + assert!( + err_msg.ends_with(" minutes in the past."), + "PASTDATEWARN should complain about Date value being in the past: {}", + err_msg + ); + } + + #[test] + fn test_draft_hook_headerwarn() { + let tempdir = tempfile::tempdir().unwrap(); + let mut ctx = Context::new_mock(&tempdir); + let mut draft = Draft::default(); + draft + .set_body("αδφαφσαφασ".to_string()) + .set_header("Subject", "test_update()".into()) + .set_header("Date", "Sun sds16 Jun 2013 17:56:45 +0200".into()); + let mut hook = HEADERWARN; + + println!("Check for missing/empty From header value…"); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, + "From header value is invalid (Parsing error. In input: \"...\",\nError: Alternative, \ + Many1, Alternative, atom(): starts with whitespace or empty).", + "HEADERWARN should complain about From value being empty: {}", + err_msg + ); + draft.set_header("From", "user ".into()); + + println!("Check for missing/empty To header value…"); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, + "To header value is invalid (Parsing error. In input: \"...\",\nError: Alternative, \ + Many1, Alternative, atom(): starts with whitespace or empty).", + "HEADERWARN should complain about To value being empty: {}", + err_msg + ); + draft.set_header("To", "other user ".into()); + + println!("Check for invalid Date header value…"); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, "Date header value is invalid.", + "HEADERWARN should complain about Date value being invalid: {}", + err_msg + ); + + println!("Check that valid header values produces no errors…"); + draft = Draft::default(); + draft + .set_body("αδφαφσαφασ".to_string()) + .set_header("From", "user ".into()) + .set_header("To", "other user ".into()) + .set_header("Subject", "test_update()".into()); + hook(&mut ctx, &mut draft).unwrap(); + draft.set_header("From", "user user@example.com>".into()); + + println!("Check for invalid From header value…"); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, + "From header value is invalid (Parsing error. In input: \"user \ + user@example.com>...\",\nError: Alternative, Tag).", + "HEADERWARN should complain about From value being invalid: {}", + err_msg + ); + } + + #[test] + fn test_draft_hook_missingattachmentwarn() { + let tempdir = tempfile::tempdir().unwrap(); + let mut ctx = Context::new_mock(&tempdir); + let mut draft = Draft::default(); + draft + .set_body("αδφαφσαφασ".to_string()) + .set_header("Subject", "Attachments included".into()) + .set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + + let mut hook = MISSINGATTACHMENTWARN; + + println!( + "Check that mentioning attachments in Subject produces a warning if draft has no \ + attachments…" + ); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, "Subject mentions attachments but attachments are empty.", + "MISSINGATTACHMENTWARN should complain about missing attachments: {}", + err_msg + ); + + draft + .set_header("Subject", "Hello.".into()) + .set_body("Attachments included".to_string()); + println!( + "Check that mentioning attachments in body produces a warning if draft has no \ + attachments…" + ); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, "Draft body mentions attachments but attachments are empty.", + "MISSINGATTACHMENTWARN should complain about missing attachments: {}", + err_msg + ); + + println!( + "Check that mentioning attachments produces no warnings if draft has attachments…" + ); + draft.set_header("Subject", "Attachments included".into()); + let mut attachment = AttachmentBuilder::new(b""); + attachment + .set_raw(b"foobar".to_vec()) + .set_content_type(ContentType::Other { + name: Some("info.txt".to_string()), + tag: b"text/plain".to_vec(), + parameters: vec![], + }) + .set_content_transfer_encoding(ContentTransferEncoding::Base64); + draft.attachments_mut().push(attachment); + + hook(&mut ctx, &mut draft).unwrap(); + } + + #[test] + fn test_draft_hook_emptydraftwarn() { + let tempdir = tempfile::tempdir().unwrap(); + let mut ctx = Context::new_mock(&tempdir); + let mut draft = Draft::default(); + draft.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + + let mut hook = EMPTYDRAFTWARN; + + println!("Check that empty draft produces a warning…"); + let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); + assert_eq!( + err_msg, "Both Subject and body are empty.", + "EMPTYDRAFTWARN should complain about empty draft: {}", + err_msg + ); + + println!("Check that non-empty draft produces no warning…"); + draft.set_header("Subject", "Ping".into()); + hook(&mut ctx, &mut draft).unwrap(); + } +} diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index dd2af9c1..785dd5c4 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -374,12 +374,13 @@ macro_rules! column_str { impl Deref for $name { type Target = String; - fn deref(&self) -> &String { + + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for $name { - fn deref_mut(&mut self) -> &mut String { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } diff --git a/src/conf/composing.rs b/src/conf/composing.rs index 5c477d77..34398974 100644 --- a/src/conf/composing.rs +++ b/src/conf/composing.rs @@ -100,6 +100,9 @@ pub struct ComposingSettings { /// The prefix to use in reply subjects. The de facto prefix is "Re:". #[serde(default = "res", alias = "reply-prefix")] pub reply_prefix: String, + /// Disabled `compose-hooks`. + #[serde(default, alias = "disabled-compose-hooks")] + pub disabled_compose_hooks: Vec, } impl Default for ComposingSettings { @@ -118,6 +121,7 @@ impl Default for ComposingSettings { forward_as_attachment: ToggleFlag::Ask, reply_prefix_list_to_strip: None, reply_prefix: res(), + disabled_compose_hooks: vec![], } } } diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index b7f3e0c3..36f87182 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -25,460 +25,19 @@ //! This module is automatically generated by config_macros.rs. use super::*; -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct PagerSettingsOverride { - #[doc = " Number of context lines when going to next page."] - #[doc = " Default: 0"] - #[serde(alias = "pager-context")] - #[serde(default)] - pub pager_context: Option, - #[doc = " Stop at the end instead of displaying next mail."] - #[doc = " Default: false"] - #[serde(alias = "pager-stop")] - #[serde(default)] - pub pager_stop: Option, - #[doc = " Always show headers when scrolling."] - #[doc = " Default: true"] - #[serde(alias = "headers-sticky")] - #[serde(default)] - pub headers_sticky: Option, - #[doc = " The height of the pager in mail view, in percent."] - #[doc = " Default: 80"] - #[serde(alias = "pager-ratio")] - #[serde(default)] - pub pager_ratio: Option, - #[doc = " A command to pipe mail output through for viewing in pager."] - #[doc = " Default: None"] - #[serde(deserialize_with = "non_empty_string")] - #[serde(default)] - pub filter: Option>, - #[doc = " A command to pipe html output before displaying it in a pager"] - #[doc = " Default: None"] - #[serde(deserialize_with = "non_empty_string", alias = "html-filter")] - #[serde(default)] - pub html_filter: Option>, - #[doc = " Respect \"format=flowed\""] - #[doc = " Default: true"] - #[serde(alias = "format-flowed")] - #[serde(default)] - pub format_flowed: Option, - #[doc = " Split long lines that would overflow on the x axis."] - #[doc = " Default: true"] - #[serde(alias = "split-long-lines")] - #[serde(default)] - pub split_long_lines: Option, - #[doc = " Minimum text width in columns."] - #[doc = " Default: 80"] - #[serde(alias = "minimum-width")] - #[serde(default)] - pub minimum_width: Option, - #[doc = " Choose `text/html` alternative if `text/plain` is empty in `multipart/alternative`"] - #[doc = " attachments."] - #[doc = " Default: true"] - #[serde(alias = "auto-choose-multipart-alternative")] - #[serde(default)] - pub auto_choose_multipart_alternative: Option, - #[doc = " Show Date: in my timezone"] - #[doc = " Default: true"] - #[serde(alias = "show-date-in-my-timezone")] - #[serde(default)] - pub show_date_in_my_timezone: Option, - #[doc = " A command to launch URLs with. The URL will be given as the first argument of the \ - command."] - #[doc = " Default: None"] - #[serde(deserialize_with = "non_empty_string")] - #[serde(default)] - pub url_launcher: Option>, - #[doc = " A command to open html files."] - #[doc = " Default: None"] - #[serde(deserialize_with = "non_empty_string", alias = "html-open")] - #[serde(default)] - pub html_open: Option>, -} -impl Default for PagerSettingsOverride { - fn default() -> Self { - PagerSettingsOverride { - pager_context: None, - pager_stop: None, - headers_sticky: None, - pager_ratio: None, - filter: None, - html_filter: None, - format_flowed: None, - split_long_lines: None, - minimum_width: None, - auto_choose_multipart_alternative: None, - show_date_in_my_timezone: None, - url_launcher: None, - html_open: None, - } - } -} +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None } } } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct ListingSettingsOverride { - #[doc = " Number of context lines when going to next page."] - #[doc = " Default: 0"] - #[serde(alias = "context-lines")] - #[serde(default)] - pub context_lines: Option, - #[doc = "Show auto-hiding scrollbar in accounts sidebar menu."] - #[doc = "Default: True"] - #[serde(default)] - pub show_menu_scrollbar: Option, - #[doc = " Datetime formatting passed verbatim to strftime(3)."] - #[doc = " Default: %Y-%m-%d %T"] - #[serde(alias = "datetime-fmt")] - #[serde(default)] - pub datetime_fmt: Option>, - #[doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] - #[doc = " Default: true"] - #[serde(alias = "recent-dates")] - #[serde(default)] - pub recent_dates: Option, - #[doc = " Show only envelopes that match this query"] - #[doc = " Default: None"] - #[serde(default)] - pub filter: Option>, - #[serde(alias = "index-style")] - #[serde(default)] - pub index_style: Option, - #[doc = "Default: \" \""] - #[serde(default)] - pub sidebar_mailbox_tree_has_sibling: Option>, - #[doc = "Default: \" \""] - #[serde(default)] - pub sidebar_mailbox_tree_no_sibling: Option>, - #[doc = "Default: \" \""] - #[serde(default)] - pub sidebar_mailbox_tree_has_sibling_leaf: Option>, - #[doc = "Default: \" \""] - #[serde(default)] - pub sidebar_mailbox_tree_no_sibling_leaf: Option>, - #[doc = "Default: ' '"] - #[serde(default)] - pub sidebar_divider: Option, - #[doc = "Default: 90"] - #[serde(default)] - pub sidebar_ratio: Option, - #[doc = " Flag to show if thread entry contains unseen mail."] - #[doc = " Default: \"●\""] - #[serde(default)] - pub unseen_flag: Option>, - #[doc = " Flag to show if thread has been snoozed."] - #[doc = " Default: \"💤\""] - #[serde(default)] - pub thread_snoozed_flag: Option>, - #[doc = " Flag to show if thread entry has been selected."] - #[doc = " Default: \"☑\u{fe0f}\""] - #[serde(default)] - pub selected_flag: Option>, - #[doc = " Flag to show if thread entry contains attachments."] - #[doc = " Default: \"📎\""] - #[serde(default)] - pub attachment_flag: Option>, - #[doc = " Should threads with differentiating Subjects show a list of those subjects on the \ - entry"] - #[doc = " title?"] - #[doc = " Default: \"true\""] - #[serde(default)] - pub thread_subject_pack: Option, -} -impl Default for ListingSettingsOverride { - fn default() -> Self { - ListingSettingsOverride { - context_lines: None, - show_menu_scrollbar: None, - datetime_fmt: None, - recent_dates: None, - filter: None, - index_style: None, - sidebar_mailbox_tree_has_sibling: None, - sidebar_mailbox_tree_no_sibling: None, - sidebar_mailbox_tree_has_sibling_leaf: None, - sidebar_mailbox_tree_no_sibling_leaf: None, - sidebar_divider: None, - sidebar_ratio: None, - unseen_flag: None, - thread_snoozed_flag: None, - selected_flag: None, - attachment_flag: None, - thread_subject_pack: None, - } - } -} +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = "Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = "Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = "Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = "Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"●\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"☑\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with differentiating Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None } } } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct NotificationsSettingsOverride { - #[doc = " Enable notifications."] - #[doc = " Default: True"] - #[serde(default)] - pub enable: Option, - #[doc = " A command to pipe notifications through."] - #[doc = " Default: None"] - #[serde(default)] - pub script: Option>, - #[doc = " A command to pipe new mail notifications through (preferred over `script`)."] - #[doc = " Default: None"] - #[serde(default)] - pub new_mail_script: Option>, - #[doc = " A file location which has its size changed when new mail arrives (max 128 bytes). \ - Can be"] - #[doc = " used to trigger new mail notifications eg with `xbiff(1)`."] - #[doc = " Default: None"] - #[serde(alias = "xbiff-file-path")] - #[serde(default)] - pub xbiff_file_path: Option>, - #[serde(alias = "play-sound")] - #[serde(default)] - pub play_sound: Option, - #[serde(alias = "sound-file")] - #[serde(default)] - pub sound_file: Option>, -} -impl Default for NotificationsSettingsOverride { - fn default() -> Self { - NotificationsSettingsOverride { - enable: None, - script: None, - new_mail_script: None, - xbiff_file_path: None, - play_sound: None, - sound_file: None, - } - } -} +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { NotificationsSettingsOverride { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct ShortcutsOverride { - #[serde(default)] - pub general: Option, - #[serde(default)] - pub listing: Option, - #[serde(default)] - pub composing: Option, - #[serde(alias = "contact-list")] - #[serde(default)] - pub contact_list: Option, - #[serde(alias = "envelope-view")] - #[serde(default)] - pub envelope_view: Option, - #[serde(alias = "thread-view")] - #[serde(default)] - pub thread_view: Option, - #[serde(default)] - pub pager: Option, -} -impl Default for ShortcutsOverride { - fn default() -> Self { - ShortcutsOverride { - general: None, - listing: None, - composing: None, - contact_list: None, - envelope_view: None, - thread_view: None, - pager: None, - } - } -} +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct ComposingSettingsOverride { - #[doc = " A command to pipe new emails to"] - #[doc = " Required"] - #[serde(default)] - pub send_mail: Option, - #[doc = " Command to launch editor. Can have arguments. Draft filename is given as the last \ - argument. If it's missing, the environment variable $EDITOR is looked up."] - #[serde(alias = "editor-command", alias = "editor-cmd", alias = "editor_cmd")] - #[serde(default)] - pub editor_command: Option>, - #[doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] - #[serde(default)] - pub embed: Option, - #[doc = " Set \"format=flowed\" in plain text attachments."] - #[doc = " Default: true"] - #[serde(alias = "format-flowed")] - #[serde(default)] - pub format_flowed: Option, - #[doc = "Set User-Agent"] - #[doc = "Default: empty"] - #[serde(alias = "insert_user_agent")] - #[serde(default)] - pub insert_user_agent: Option, - #[doc = " Set default header values for new drafts"] - #[doc = " Default: empty"] - #[serde(alias = "default-header-values")] - #[serde(default)] - pub default_header_values: Option>, - #[doc = " Wrap header preample when editing a draft in an editor. This allows you to write \ - non-plain"] - #[doc = " text email without the preamble creating syntax errors. They are stripped when you \ - return"] - #[doc = " from the editor. The values should be a two element array of strings, a prefix and \ - suffix."] - #[doc = " Default: None"] - #[serde(alias = "wrap-header-preample")] - #[serde(default)] - pub wrap_header_preamble: Option>, - #[doc = " Store sent mail after successful submission. This setting is meant to be disabled \ - for"] - #[doc = " non-standard behaviour in gmail, which auto-saves sent mail on its own."] - #[doc = " Default: true"] - #[serde(default)] - pub store_sent_mail: Option, - #[doc = " The attribution line appears above the quoted reply text."] - #[doc = " The format specifiers for the replied address are:"] - #[doc = " - `%+f` — the sender's name and email address."] - #[doc = " - `%+n` — the sender's name (or email address, if no name is included)."] - #[doc = " - `%+a` — the sender's email address."] - #[doc = " The format string is passed to strftime(3) with the replied envelope's date."] - #[doc = " Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] - #[serde(default)] - pub attribution_format_string: Option>, - #[doc = " Whether the strftime call for the attribution string uses the POSIX locale instead \ - of"] - #[doc = " the user's active locale"] - #[doc = " Default: true"] - #[serde(default)] - pub attribution_use_posix_locale: Option, - #[doc = " Forward emails as attachment? (Alternative is inline)"] - #[doc = " Default: ask"] - #[serde(alias = "forward-as-attachment")] - #[serde(default)] - pub forward_as_attachment: Option, - #[doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] - #[doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \ - \"Sv:\", \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \ - \"TR:\", \"AW:\", \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \ - \"ΣΧΕΤ:\", \"Σχετ:\", \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \ - \"Továbbítás:\", \"R:\", \"I:\", \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \ - \"VB:\", \"RV:\", \"RES:\", \"Res\", \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \ - \"İLT:\", \"ATB:\", \"YML:\"]`"] - #[serde(alias = "reply-prefix-list-to-strip")] - #[serde(default)] - pub reply_prefix_list_to_strip: Option>>, - #[doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] - #[serde(alias = "reply-prefix")] - #[serde(default)] - pub reply_prefix: Option, -} -impl Default for ComposingSettingsOverride { - fn default() -> Self { - ComposingSettingsOverride { - send_mail: None, - editor_command: None, - embed: None, - format_flowed: None, - insert_user_agent: None, - default_header_values: None, - wrap_header_preamble: None, - store_sent_mail: None, - attribution_format_string: None, - attribution_use_posix_locale: None, - forward_as_attachment: None, - reply_prefix_list_to_strip: None, - reply_prefix: None, - } - } -} +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = "Set User-Agent"] # [doc = "Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < String , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , disabled_compose_hooks : None } } } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct TagsSettingsOverride { - #[serde(deserialize_with = "tag_color_de")] - #[serde(default)] - pub colors: Option>, - #[serde(deserialize_with = "tag_set_de", alias = "ignore-tags")] - #[serde(default)] - pub ignore_tags: Option>, -} -impl Default for TagsSettingsOverride { - fn default() -> Self { - TagsSettingsOverride { - colors: None, - ignore_tags: None, - } - } -} +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } } -#[cfg(feature = "gpgme")] -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct PGPSettingsOverride { - #[doc = " auto verify signed e-mail according to RFC3156"] - #[doc = " Default: true"] - #[serde(alias = "auto-verify-signatures")] - #[serde(default)] - pub auto_verify_signatures: Option, - #[doc = " auto decrypt encrypted e-mail"] - #[doc = " Default: true"] - #[serde(alias = "auto-decrypt")] - #[serde(default)] - pub auto_decrypt: Option, - #[doc = " always sign sent e-mail"] - #[doc = " Default: false"] - #[serde(alias = "auto-sign")] - #[serde(default)] - pub auto_sign: Option, - #[doc = " Auto encrypt sent e-mail"] - #[doc = " Default: false"] - #[serde(alias = "auto-encrypt")] - #[serde(default)] - pub auto_encrypt: Option, - #[doc = " Default: None"] - #[serde(alias = "sign-key")] - #[serde(default)] - pub sign_key: Option>, - #[doc = " Default: None"] - #[serde(alias = "decrypt-key")] - #[serde(default)] - pub decrypt_key: Option>, - #[doc = " Default: None"] - #[serde(alias = "encrypt-key")] - #[serde(default)] - pub encrypt_key: Option>, - #[doc = " Allow remote lookups"] - #[doc = " Default: None"] - #[serde(alias = "allow-remote-lookups")] - #[serde(default)] - pub allow_remote_lookup: Option, - #[doc = " Remote lookup mechanisms."] - #[doc = " Default: \"local,wkd\""] - #[serde(alias = "remote-lookup-mechanisms")] - #[serde(default)] - pub remote_lookup_mechanisms: Option, -} -#[cfg(feature = "gpgme")] -impl Default for PGPSettingsOverride { - fn default() -> Self { - PGPSettingsOverride { - auto_verify_signatures: None, - auto_decrypt: None, - auto_sign: None, - auto_encrypt: None, - sign_key: None, - decrypt_key: None, - encrypt_key: None, - allow_remote_lookup: None, - remote_lookup_mechanisms: None, - } - } -} +# [cfg (feature = "gpgme")] # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < bool > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < bool > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < bool > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < bool > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: None"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ToggleFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [serde (alias = "remote-lookup-mechanisms")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > } # [cfg (feature = "gpgme")] impl Default for PGPSettingsOverride { fn default () -> Self { PGPSettingsOverride { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } } + +# [cfg (not (feature = "gpgme"))] # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { } # [cfg (not (feature = "gpgme"))] impl Default for PGPSettingsOverride { fn default () -> Self { PGPSettingsOverride { } } } -#[cfg(not(feature = "gpgme"))] -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct PGPSettingsOverride {} -#[cfg(not(feature = "gpgme"))] -impl Default for PGPSettingsOverride { - fn default() -> Self { - PGPSettingsOverride {} - } -} diff --git a/src/conf/themes.rs b/src/conf/themes.rs index 1543be83..b21870b8 100644 --- a/src/conf/themes.rs +++ b/src/conf/themes.rs @@ -30,7 +30,12 @@ //! //! On startup a [DFS](https://en.wikipedia.org/wiki/Depth-first_search) is performed to see if there are any cycles in the link graph. -use std::{borrow::Cow, collections::HashSet, fmt::Write}; +use std::{ + borrow::Cow, + collections::HashSet, + fmt::Write, + ops::{Deref, DerefMut}, +}; use indexmap::IndexMap; use melib::{Error, Result}; @@ -740,16 +745,16 @@ mod regexp { } } -use std::ops::{Deref, DerefMut}; impl Deref for Theme { type Target = IndexMap, ThemeAttributeInner>; + fn deref(&self) -> &Self::Target { &self.keys } } impl DerefMut for Theme { - fn deref_mut(&mut self) -> &mut IndexMap, ThemeAttributeInner> { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.keys } } diff --git a/src/state.rs b/src/state.rs index 89a6e204..bf16c94b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -162,7 +162,7 @@ impl Context { } #[cfg(test)] - pub fn new_mock() -> Self { + pub fn new_mock(dir: &tempfile::TempDir) -> Self { let (sender, receiver) = crossbeam::channel::bounded(32 * ::std::mem::size_of::()); let job_executor = Arc::new(JobExecutor::new(sender.clone())); @@ -177,7 +177,7 @@ impl Context { let mut account_conf = AccountConf::default(); account_conf.conf.format = "maildir".to_string(); account_conf.account.format = "maildir".to_string(); - account_conf.account.root_mailbox = "/tmp/".to_string(); + account_conf.account.root_mailbox = dir.path().display().to_string(); let sender = sender.clone(); let account_hash = AccountHash::from_bytes(name.as_bytes()); Account::new( diff --git a/src/terminal/cells.rs b/src/terminal/cells.rs index 45b04d6f..2e2c93bb 100644 --- a/src/terminal/cells.rs +++ b/src/terminal/cells.rs @@ -438,13 +438,13 @@ impl CellBuffer { impl Deref for CellBuffer { type Target = [Cell]; - fn deref(&self) -> &[Cell] { + fn deref(&self) -> &Self::Target { &self.buf } } impl DerefMut for CellBuffer { - fn deref_mut(&mut self) -> &mut [Cell] { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.buf } } diff --git a/tools/src/embed.rs b/tools/src/embed.rs index c6847a2d..63a7f6a2 100644 --- a/tools/src/embed.rs +++ b/tools/src/embed.rs @@ -67,19 +67,18 @@ impl EmbedStatus { impl std::ops::Deref for EmbedStatus { type Target = Arc>; - fn deref(&self) -> &Arc> { - use EmbedStatus::*; + + fn deref(&self) -> &Self::Target { match self { - Stopped(ref e) | Running(ref e) => e, + Self::Stopped(ref e) | Self::Running(ref e) => e, } } } impl std::ops::DerefMut for EmbedStatus { - fn deref_mut(&mut self) -> &mut Arc> { - use EmbedStatus::*; + fn deref_mut(&mut self) -> &mut Self::Target { match self { - Stopped(ref mut e) | Running(ref mut e) => e, + Self::Stopped(ref mut e) | Self::Running(ref mut e) => e, } } }