From 77e7c3df608cc1996ef6cf8628d008ce69b39993 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 5 Oct 2024 12:46:21 +0300 Subject: [PATCH] Add support for signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add config values to `composing` config section to enable signatures: signature_file Path (optional) Plain text file with signature that will pre-populate an email draft. Signatures must be explicitly enabled to be used, otherwise this setting will be ignored. (None) use_signature bool Pre-populate email drafts with signature, if any. meli will lookup the signature value in this order: 1. The signature_file setting. 2. ${XDG_CONFIG_DIR}/meli//signature 3. ${XDG_CONFIG_DIR}/meli/signature 4. ${XDG_CONFIG_DIR}/signature 5. ${HOME}/.signature 6. No signature otherwise. (false) signature_delimiter String (optional) Signature delimiter, that is, text that will be prefixed to your signature to separate it from the email body. (‘\n\n-- \n’) Closes #498 Resolves: https://git.meli-email.org/meli/meli/issues/498 Signed-off-by: Manos Pitsidianakis --- meli/docs/meli.conf.5 | 30 +++++++++++++++++++++++ meli/src/accounts.rs | 30 ++++++++++++++++++++++- meli/src/conf/composing.rs | 33 +++++++++++++++++++++++++ meli/src/conf/overrides.rs | 2 +- meli/src/mail/compose.rs | 49 +++++++++++++++++++++++++++++++++----- 5 files changed, 136 insertions(+), 8 deletions(-) diff --git a/meli/docs/meli.conf.5 b/meli/docs/meli.conf.5 index da7fbc9a..6e5370bb 100644 --- a/meli/docs/meli.conf.5 +++ b/meli/docs/meli.conf.5 @@ -1030,6 +1030,36 @@ or draft body mention attachments but they are missing. .Ic empty-draft-warn — Warn if draft has no subject and no body. .El +.It Ic signature_file Ar Path +.Pq Em optional +Plain text file with signature that will pre-populate an email draft. +Signatures must be explicitly enabled to be used, otherwise this setting will be ignored. +.Pq Em None \" default value +.It Ic use_signature Ar bool +Pre-populate email drafts with signature, if any. +.Sy meli +will lookup the signature value in this order: +.Bl -enum -compact +.It +The +.Ic signature_file +setting. +.It +.Pa ${XDG_CONFIG_DIR}/meli//signature +.It +.Pa ${XDG_CONFIG_DIR}/meli/signature +.It +.Pa ${XDG_CONFIG_DIR}/signature +.It +.Pa ${HOME}/.signature +.It +No signature otherwise. +.El +.Pq Em false \" default value +.It Ic signature_delimiter Ar String +.Pq Em optional +Signature delimiter, that is, text that will be prefixed to your signature to separate it from the email body. +.Pq Ql \en\en\-\- \en .El .\" .\" diff --git a/meli/src/accounts.rs b/meli/src/accounts.rs index 8ca52fee..14cd70a8 100644 --- a/meli/src/accounts.rs +++ b/meli/src/accounts.rs @@ -29,6 +29,7 @@ use std::{ io, ops::{Index, IndexMut}, os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, pin::Pin, result, sync::{Arc, RwLock}, @@ -42,7 +43,7 @@ use melib::{ error::{Error, ErrorKind, NetworkErrorKind, Result}, log, thread::Threads, - utils::{fnmatch::Fnmatch, futures::sleep, random}, + utils::{fnmatch::Fnmatch, futures::sleep, random, shellexpand::ShellExpandTrait}, Contacts, SortField, SortOrder, }; use smallvec::SmallVec; @@ -1800,6 +1801,33 @@ impl Account { IsAsync::Blocking } } + + pub fn signature_file(&self) -> Option { + xdg::BaseDirectories::with_profile("meli", &self.name) + .ok() + .and_then(|d| { + d.place_config_file("signature") + .ok() + .filter(|p| p.is_file()) + }) + .or_else(|| { + xdg::BaseDirectories::with_prefix("meli") + .ok() + .and_then(|d| { + d.place_config_file("signature") + .ok() + .filter(|p| p.is_file()) + }) + }) + .or_else(|| { + xdg::BaseDirectories::new().ok().and_then(|d| { + d.place_config_file("signature") + .ok() + .filter(|p| p.is_file()) + }) + }) + .or_else(|| Some(Path::new("~/.signature").expand()).filter(|p| p.is_file())) + } } impl Index<&MailboxHash> for Account { diff --git a/meli/src/conf/composing.rs b/meli/src/conf/composing.rs index 640ac24a..aaf57091 100644 --- a/meli/src/conf/composing.rs +++ b/meli/src/conf/composing.rs @@ -21,6 +21,8 @@ //! Configuration for composing email. +use std::path::PathBuf; + use indexmap::IndexMap; use melib::{conf::ActionFlag, email::HeaderName}; use serde::{de, Deserialize, Deserializer}; @@ -110,6 +112,34 @@ pub struct ComposingSettings { /// Disabled `compose-hooks`. #[serde(default, alias = "disabled-compose-hooks")] pub disabled_compose_hooks: Vec, + /// Plain text file with signature that will pre-populate an email draft. + /// + /// Signatures must be explicitly enabled to be used, otherwise this setting + /// will be ignored. + /// + /// Default: `None` + #[serde(default, alias = "signature-file")] + pub signature_file: Option, + /// Pre-populate email drafts with signature, if any. + /// + /// `meli` will lookup the signature value in this order: + /// + /// 1. The `signature_file` setting. + /// 2. `${XDG_CONFIG_DIR}/meli//signature` + /// 3. `${XDG_CONFIG_DIR}/meli/signature` + /// 4. `${XDG_CONFIG_DIR}/signature` + /// 5. `${HOME}/.signature` + /// 6. No signature otherwise. + /// + /// Default: `false` + #[serde(default = "false_val", alias = "use-signature")] + pub use_signature: bool, + /// Signature delimiter, that is, text that will be prefixed to your + /// signature to separate it from the email body. + /// + /// Default: `"\n\n-- \n"` + #[serde(default, alias = "signature-delimiter")] + pub signature_delimiter: Option, } impl Default for ComposingSettings { @@ -129,6 +159,9 @@ impl Default for ComposingSettings { reply_prefix: res(), custom_compose_hooks: vec![], disabled_compose_hooks: vec![], + signature_file: None, + use_signature: false, + signature_delimiter: None, } } } diff --git a/meli/src/conf/overrides.rs b/meli/src/conf/overrides.rs index 8d205059..3ea3ae75 100644 --- a/meli/src/conf/overrides.rs +++ b/meli/src/conf/overrides.rs @@ -38,7 +38,7 @@ use crate::conf::{*, data_types::*}; # [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 { Self { 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 = " 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 = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : 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 < IndexMap < HeaderName , String > > , # [doc = " Wrap header preamble 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-preamble")] # [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 that appears above the quoted reply text."] # [doc = ""] # [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 = ""] # [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 < ActionFlag > , # [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 = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [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 { Self { editor_command : None , embedded_pty : 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 , custom_compose_hooks : None , disabled_compose_hooks : None } } } +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [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 = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : 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 < IndexMap < HeaderName , String > > , # [doc = " Wrap header preamble 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-preamble")] # [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 that appears above the quoted reply text."] # [doc = ""] # [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 = ""] # [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 < ActionFlag > , # [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 = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > , # [doc = " Plain text file with signature that will pre-populate an email draft."] # [doc = ""] # [doc = " Signatures must be explicitly enabled to be used, otherwise this setting"] # [doc = " will be ignored."] # [doc = ""] # [doc = " Default: `None`"] # [serde (alias = "signature-file")] # [serde (default)] pub signature_file : Option < Option < PathBuf > > , # [doc = " Pre-populate email drafts with signature, if any."] # [doc = ""] # [doc = " `meli` will lookup the signature value in this order:"] # [doc = ""] # [doc = " 1. The `signature_file` setting."] # [doc = " 2. `${XDG_CONFIG_DIR}/meli//signature`"] # [doc = " 3. `${XDG_CONFIG_DIR}/meli/signature`"] # [doc = " 4. `${XDG_CONFIG_DIR}/signature`"] # [doc = " 5. `${HOME}/.signature`"] # [doc = " 6. No signature otherwise."] # [doc = ""] # [doc = " Default: `false`"] # [serde (alias = "use-signature")] # [serde (default)] pub use_signature : Option < bool > , # [doc = " Signature delimiter, that is, text that will be prefixed to your"] # [doc = " signature to separate it from the email body."] # [doc = ""] # [doc = " Default: `\"\\n\\n-- \\n\"`"] # [serde (alias = "signature-delimiter")] # [serde (default)] pub signature_delimiter : Option < Option < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { Self { editor_command : None , embedded_pty : 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 , custom_compose_hooks : None , disabled_compose_hooks : None , signature_file : None , use_signature : None , signature_delimiter : None } } } # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < IndexMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < IndexSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { Self { colors : None , ignore_tags : None } } } diff --git a/meli/src/mail/compose.rs b/meli/src/mail/compose.rs index c7b6a7e8..7bc1b883 100644 --- a/meli/src/mail/compose.rs +++ b/meli/src/mail/compose.rs @@ -20,7 +20,9 @@ */ use std::{ + borrow::Cow, convert::TryInto, + fmt::Write as _, future::Future, io::Write, pin::Pin, @@ -241,7 +243,41 @@ impl Composer { format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")), ); } - if *account_settings!(context[account_hash].composing.format_flowed) { + let format_flowed = *account_settings!(context[account_hash].composing.format_flowed); + if *account_settings!(context[account_hash].composing.use_signature) { + let override_value = account_settings!(context[account_hash].composing.signature_file) + .as_deref() + .map(Cow::Borrowed) + .filter(|p| p.as_ref().is_file()); + let account_value = || { + context.accounts[&account_hash] + .signature_file() + .map(Cow::Owned) + }; + if let Some(path) = override_value.or_else(account_value) { + match std::fs::read_to_string(path.as_ref()).chain_err_related_path(path.as_ref()) { + Ok(sig) => { + let mut delimiter = + account_settings!(context[account_hash].composing.signature_delimiter) + .as_deref() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Borrowed("\n\n-- \n")); + if format_flowed { + delimiter = Cow::Owned(delimiter.replace(" \n", " \n\n")); + } + _ = write!(&mut ret.draft.body, "{}{}", delimiter.as_ref(), sig); + } + Err(err) => { + log::error!( + "Could not open signature file for account `{}`: {}.", + context.accounts[&account_hash].name(), + err + ); + } + } + } + } + if format_flowed { ret.pager.set_reflow(melib::text::Reflow::FormatFlowed); } ret @@ -420,7 +456,7 @@ impl Composer { .set_header(HeaderName::TO, envelope.field_from_to_string()); } ret.draft.body = { - let mut ret = attribution_string( + let mut quoted = attribution_string( account_settings!( context[ret.account_hash] .composing @@ -437,11 +473,12 @@ impl Composer { ), ); for l in reply_body.lines() { - ret.push('>'); - ret.push_str(l); - ret.push('\n'); + quoted.push('>'); + quoted.push_str(l); + quoted.push('\n'); } - ret + _ = write!(&mut quoted, "{}", ret.draft.body); + quoted }; ret.account_hash = coordinates.0;