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.
manpage-tutorial
Manos Pitsidianakis 2022-09-02 09:50:07 +03:00
parent a484b397c6
commit cbe593cf31
5 changed files with 153 additions and 35 deletions

View File

@ -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.

View File

@ -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<AttachmentBuilder>,
}
@ -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<Self> {
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<bool> {
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<String> {
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<String> {
@ -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(), "-->".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, "<!--\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_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(),
"</mixed>".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</mixed>\nαδφαφσαφασ");
assert!(!default.update(&s).unwrap());
assert_eq!(&original, &default);
default
.set_body(
"hellohello<!--\n<!--\n<--hellohello\nhellohello-->\n-->\n-->hello\n".to_string(),
)
.set_wrap_header_preamble(Some(("<!--".to_string(), "-->".to_string())));
let original = default.clone();
let s = default.to_edit_string();
assert_eq!(s, "<!--\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\n-->\nhellohello<!--\n<!--\n<--hellohello\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

View File

@ -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(

View File

@ -54,6 +54,12 @@ pub struct ComposingSettings {
/// Default: empty
#[serde(default, alias = "default-header-values")]
pub default_header_values: HashMap<String, String>,
/// 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,

View File

@ -299,6 +299,13 @@ pub struct ComposingSettingsOverride {
#[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 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<Option<(String, String)>>,
#[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,