From cbe593cf31308dcf549d7880eea2d82e5024dd73 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Fri, 2 Sep 2022 09:50:07 +0300 Subject: [PATCH] mail/compose: add configurable header preample suffix and prefix for editing This commit adds a new configuration value for the composing section of settings. Quoting the documentation: wrap_header_preamble: Option<(String, String)> optional Wrap header preample when editing a draft in an editor. This allows you to write non-plain text email without the preamble creating syntax errors. They are stripped when you return from the editor. The values should be a two element array of strings, a prefix and suffix. This can be useful when for example you're writing Markdown; you can set the value to [""] which wraps the headers in an HTML comment. --- docs/meli.conf.5 | 14 ++++ melib/src/email/compose.rs | 131 ++++++++++++++++++++++++++++----- src/components/mail/compose.rs | 28 +++---- src/conf/composing.rs | 7 ++ src/conf/overrides.rs | 8 ++ 5 files changed, 153 insertions(+), 35 deletions(-) diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index cf2eca843..d29de883b 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -512,7 +512,21 @@ Add meli User-Agent header in new drafts .\" default value .Pq Em true .It Ic default_header_values Ar hash table String[String] +.Pq Em optional Default header values used when creating a new draft. +.\" default value +.Pq Em [] +.It Ic wrap_header_preamble Ar Option<(String, String)> +.Pq Em optional +Wrap header preample when editing a draft in an editor. +This allows you to write non-plain text email without the preamble creating syntax errors. +They are stripped when you return from the editor. +The values should be a two element array of strings, a prefix and suffix. +This can be useful when for example you're writing Markdown; you can set the value to +.Em [""] +which wraps the headers in an HTML comment. +.\" default value +.Pq Em None .It Ic store_sent_mail Ar boolean .Pq Em optional Store sent mail after successful submission. diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index 82fb431c5..3f91ebb60 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -30,7 +30,7 @@ use data_encoding::BASE64_MIME; use std::ffi::OsStr; use std::io::Read; use std::path::{Path, PathBuf}; -use std::str; +use std::str::FromStr; use xdg_utils::query_mime_info; pub mod mime; @@ -44,6 +44,7 @@ use super::parser; pub struct Draft { pub headers: HeaderMap, pub body: String, + pub wrap_header_preamble: Option<(String, String)>, pub attachments: Vec, } @@ -68,13 +69,14 @@ impl Default for Draft { Draft { headers, body: String::new(), + wrap_header_preamble: None, attachments: Vec::new(), } } } -impl str::FromStr for Draft { +impl FromStr for Draft { type Err = MeliError; fn from_str(s: &str) -> Result { if s.is_empty() { @@ -114,6 +116,37 @@ impl Draft { self } + pub fn set_wrap_header_preamble(&mut self, value: Option<(String, String)>) -> &mut Self { + self.wrap_header_preamble = value; + self + } + + pub fn update(&mut self, value: &str) -> Result { + let mut value: std::borrow::Cow<'_, str> = value.into(); + if let Some((pre, post)) = self.wrap_header_preamble.as_ref() { + let mut s = value.as_ref(); + s = s.strip_prefix(pre).unwrap_or(s); + s = s.strip_prefix('\n').unwrap_or(s); + + if let Some(pos) = s.find(post) { + let mut headers = &s[..pos]; + headers = headers.strip_suffix(post).unwrap_or(headers); + headers = headers.strip_suffix('\n').unwrap_or(headers); + value = format!( + "{headers}{body}", + headers = headers, + body = &s[pos + post.len()..] + ) + .into(); + } + } + let new = Draft::from_str(value.as_ref())?; + let changes: bool = self.headers != new.headers || self.body != new.body; + self.headers = new.headers; + self.body = new.body; + Ok(changes) + } + pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self { let mut ret = Draft::default(); ret.headers_mut().insert( @@ -217,17 +250,35 @@ impl Draft { self } - pub fn to_string(&self) -> Result { + pub fn to_edit_string(&self) -> String { let mut ret = String::new(); + if let Some((pre, _)) = self.wrap_header_preamble.as_ref() { + if !pre.is_empty() { + ret.push_str(&pre); + if !pre.ends_with('\n') { + ret.push('\n'); + } + } + } + for (k, v) in self.headers.deref() { ret.push_str(&format!("{}: {}\n", k, v)); } + if let Some((_, post)) = self.wrap_header_preamble.as_ref() { + if !post.is_empty() { + if !post.starts_with('\n') { + ret.push('\n'); + } + ret.push_str(&post); + } + } + ret.push('\n'); ret.push_str(&self.body); - Ok(ret) + ret } pub fn finalise(mut self) -> Result { @@ -415,27 +466,69 @@ mod tests { use std::str::FromStr; #[test] - fn test_new() { + fn test_new_draft() { let mut default = Draft::default(); - assert_eq!( - Draft::from_str(&default.to_string().unwrap()).unwrap(), - default - ); + assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default); default.set_body("αδφαφσαφασ".to_string()); - assert_eq!( - Draft::from_str(&default.to_string().unwrap()).unwrap(), - default - ); + assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default); default.set_body("ascii only".to_string()); - assert_eq!( - Draft::from_str(&default.to_string().unwrap()).unwrap(), - default - ); + assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default); } + #[test] + fn test_draft_update() { + let mut default = Draft::default(); + default + .set_wrap_header_preamble(Some(("".to_string()))) + .set_body("αδφαφσαφασ".to_string()) + .set_header("Subject", "test_update()".into()) + .set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + + let original = default.clone(); + let s = default.to_edit_string(); + assert_eq!(s, "\nαδφαφσαφασ"); + assert!(!default.update(&s).unwrap()); + assert_eq!(&original, &default); + + default.set_wrap_header_preamble(Some(("".to_string(), "".to_string()))); + let original = default.clone(); + let s = default.to_edit_string(); + assert_eq!(s, "Date: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\nαδφαφσαφασ"); + assert!(!default.update(&s).unwrap()); + assert_eq!(&original, &default); + + default.set_wrap_header_preamble(None); + let original = default.clone(); + let s = default.to_edit_string(); + assert_eq!(s, "Date: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\nαδφαφσαφασ"); + assert!(!default.update(&s).unwrap()); + assert_eq!(&original, &default); + + default.set_wrap_header_preamble(Some(( + "{-\n\n\n===========".to_string(), + "".to_string(), + ))); + let original = default.clone(); + let s = default.to_edit_string(); + assert_eq!(s, "{-\n\n\n===========\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\n\nαδφαφσαφασ"); + assert!(!default.update(&s).unwrap()); + assert_eq!(&original, &default); + + default + .set_body( + "hellohello\n-->\n-->hello\n".to_string(), + ) + .set_wrap_header_preamble(Some(("".to_string()))); + let original = default.clone(); + let s = default.to_edit_string(); + assert_eq!(s, "\nhellohello\n-->\n-->hello\n"); + assert!(!default.update(&s).unwrap()); + assert_eq!(&original, &default); + } + + /* #[test] fn test_attachments() { - /* let mut default = Draft::default(); default.set_body("αδφαφσαφασ".to_string()); @@ -453,8 +546,8 @@ mod tests { .set_content_transfer_encoding(ContentTransferEncoding::Base64); default.attachments_mut().push(attachment); println!("{}", default.finalise().unwrap()); - */ } + */ } /// Reads file from given path, and returns an 'application/octet-stream' AttachmentBuilder object diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 11b7e87de..1a647344c 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -33,7 +33,6 @@ use std::convert::TryInto; use std::future::Future; use std::pin::Pin; use std::process::{Command, Stdio}; -use std::str::FromStr; use std::sync::{Arc, Mutex}; #[cfg(feature = "gpgme")] @@ -725,13 +724,9 @@ To: {} fn update_from_file(&mut self, file: File, context: &mut Context) -> bool { let result = file.read_to_string(); - match Draft::from_str(result.as_str()) { - Ok(mut new_draft) => { - std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut()); - if self.draft != new_draft { - self.has_changes = true; - } - self.draft = new_draft; + match self.draft.update(result.as_str()) { + Ok(has_changes) => { + self.has_changes = has_changes; true } Err(err) => { @@ -1697,8 +1692,13 @@ impl Component for Composer { }; /* update Draft's headers based on form values */ self.update_draft(); + self.draft.set_wrap_header_preamble( + account_settings!(context[self.account_hash].composing.wrap_header_preamble) + .clone(), + ); + let f = create_temp_file( - self.draft.to_string().unwrap().as_str().as_bytes(), + self.draft.to_edit_string().as_str().as_bytes(), None, None, true, @@ -1766,13 +1766,9 @@ impl Component for Composer { } context.replies.push_back(UIEvent::Fork(ForkType::Finished)); let result = f.read_to_string(); - match Draft::from_str(result.as_str()) { - Ok(mut new_draft) => { - std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut()); - if self.draft != new_draft { - self.has_changes = true; - } - self.draft = new_draft; + match self.draft.update(result.as_str()) { + Ok(has_changes) => { + self.has_changes = has_changes; } Err(err) => { context.replies.push_back(UIEvent::Notification( diff --git a/src/conf/composing.rs b/src/conf/composing.rs index edfe46bf3..d67d1cbe8 100644 --- a/src/conf/composing.rs +++ b/src/conf/composing.rs @@ -54,6 +54,12 @@ pub struct ComposingSettings { /// Default: empty #[serde(default, alias = "default-header-values")] pub default_header_values: HashMap, + /// Wrap header preample when editing a draft in an editor. This allows you to write non-plain + /// text email without the preamble creating syntax errors. They are stripped when you return + /// from the editor. The values should be a two element array of strings, a prefix and suffix. + /// Default: None + #[serde(default, alias = "wrap-header-preample")] + pub wrap_header_preamble: Option<(String, String)>, /// Store sent mail after successful submission. This setting is meant to be disabled for /// non-standard behaviour in gmail, which auto-saves sent mail on its own. /// Default: true @@ -96,6 +102,7 @@ impl Default for ComposingSettings { insert_user_agent: true, default_header_values: HashMap::default(), store_sent_mail: true, + wrap_header_preamble: None, attribution_format_string: None, attribution_use_posix_locale: true, forward_as_attachment: ToggleFlag::Ask, diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index 7c312c316..9ded27dc6 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -299,6 +299,13 @@ pub struct ComposingSettingsOverride { #[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"] @@ -342,6 +349,7 @@ impl Default for ComposingSettingsOverride { 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,