Commit
Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 77e7c3df608cc1996ef6cf8628d008ce69b39993
Timestamp: Sat, 05 Oct 2024 09:46:21 +0000 (2 months ago)

+136 -8 +/-5 browse
Add support for signatures
Add support for signatures

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/<account>/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 <manos@pitsidianak.is>
1diff --git a/meli/docs/meli.conf.5 b/meli/docs/meli.conf.5
2index da7fbc9..6e5370b 100644
3--- a/meli/docs/meli.conf.5
4+++ b/meli/docs/meli.conf.5
5 @@ -1030,6 +1030,36 @@ or draft body mention attachments but they are missing.
6 .Ic empty-draft-warn
7 — Warn if draft has no subject and no body.
8 .El
9+ .It Ic signature_file Ar Path
10+ .Pq Em optional
11+ Plain text file with signature that will pre-populate an email draft.
12+ Signatures must be explicitly enabled to be used, otherwise this setting will be ignored.
13+ .Pq Em None \" default value
14+ .It Ic use_signature Ar bool
15+ Pre-populate email drafts with signature, if any.
16+ .Sy meli
17+ will lookup the signature value in this order:
18+ .Bl -enum -compact
19+ .It
20+ The
21+ .Ic signature_file
22+ setting.
23+ .It
24+ .Pa ${XDG_CONFIG_DIR}/meli/<account>/signature
25+ .It
26+ .Pa ${XDG_CONFIG_DIR}/meli/signature
27+ .It
28+ .Pa ${XDG_CONFIG_DIR}/signature
29+ .It
30+ .Pa ${HOME}/.signature
31+ .It
32+ No signature otherwise.
33+ .El
34+ .Pq Em false \" default value
35+ .It Ic signature_delimiter Ar String
36+ .Pq Em optional
37+ Signature delimiter, that is, text that will be prefixed to your signature to separate it from the email body.
38+ .Pq Ql \en\en\-\- \en
39 .El
40 .\"
41 .\"
42 diff --git a/meli/src/accounts.rs b/meli/src/accounts.rs
43index 8ca52fe..14cd70a 100644
44--- a/meli/src/accounts.rs
45+++ b/meli/src/accounts.rs
46 @@ -29,6 +29,7 @@ use std::{
47 io,
48 ops::{Index, IndexMut},
49 os::unix::fs::PermissionsExt,
50+ path::{Path, PathBuf},
51 pin::Pin,
52 result,
53 sync::{Arc, RwLock},
54 @@ -42,7 +43,7 @@ use melib::{
55 error::{Error, ErrorKind, NetworkErrorKind, Result},
56 log,
57 thread::Threads,
58- utils::{fnmatch::Fnmatch, futures::sleep, random},
59+ utils::{fnmatch::Fnmatch, futures::sleep, random, shellexpand::ShellExpandTrait},
60 Contacts, SortField, SortOrder,
61 };
62 use smallvec::SmallVec;
63 @@ -1800,6 +1801,33 @@ impl Account {
64 IsAsync::Blocking
65 }
66 }
67+
68+ pub fn signature_file(&self) -> Option<PathBuf> {
69+ xdg::BaseDirectories::with_profile("meli", &self.name)
70+ .ok()
71+ .and_then(|d| {
72+ d.place_config_file("signature")
73+ .ok()
74+ .filter(|p| p.is_file())
75+ })
76+ .or_else(|| {
77+ xdg::BaseDirectories::with_prefix("meli")
78+ .ok()
79+ .and_then(|d| {
80+ d.place_config_file("signature")
81+ .ok()
82+ .filter(|p| p.is_file())
83+ })
84+ })
85+ .or_else(|| {
86+ xdg::BaseDirectories::new().ok().and_then(|d| {
87+ d.place_config_file("signature")
88+ .ok()
89+ .filter(|p| p.is_file())
90+ })
91+ })
92+ .or_else(|| Some(Path::new("~/.signature").expand()).filter(|p| p.is_file()))
93+ }
94 }
95
96 impl Index<&MailboxHash> for Account {
97 diff --git a/meli/src/conf/composing.rs b/meli/src/conf/composing.rs
98index 640ac24..aaf5709 100644
99--- a/meli/src/conf/composing.rs
100+++ b/meli/src/conf/composing.rs
101 @@ -21,6 +21,8 @@
102
103 //! Configuration for composing email.
104
105+ use std::path::PathBuf;
106+
107 use indexmap::IndexMap;
108 use melib::{conf::ActionFlag, email::HeaderName};
109 use serde::{de, Deserialize, Deserializer};
110 @@ -110,6 +112,34 @@ pub struct ComposingSettings {
111 /// Disabled `compose-hooks`.
112 #[serde(default, alias = "disabled-compose-hooks")]
113 pub disabled_compose_hooks: Vec<String>,
114+ /// Plain text file with signature that will pre-populate an email draft.
115+ ///
116+ /// Signatures must be explicitly enabled to be used, otherwise this setting
117+ /// will be ignored.
118+ ///
119+ /// Default: `None`
120+ #[serde(default, alias = "signature-file")]
121+ pub signature_file: Option<PathBuf>,
122+ /// Pre-populate email drafts with signature, if any.
123+ ///
124+ /// `meli` will lookup the signature value in this order:
125+ ///
126+ /// 1. The `signature_file` setting.
127+ /// 2. `${XDG_CONFIG_DIR}/meli/<account>/signature`
128+ /// 3. `${XDG_CONFIG_DIR}/meli/signature`
129+ /// 4. `${XDG_CONFIG_DIR}/signature`
130+ /// 5. `${HOME}/.signature`
131+ /// 6. No signature otherwise.
132+ ///
133+ /// Default: `false`
134+ #[serde(default = "false_val", alias = "use-signature")]
135+ pub use_signature: bool,
136+ /// Signature delimiter, that is, text that will be prefixed to your
137+ /// signature to separate it from the email body.
138+ ///
139+ /// Default: `"\n\n-- \n"`
140+ #[serde(default, alias = "signature-delimiter")]
141+ pub signature_delimiter: Option<String>,
142 }
143
144 impl Default for ComposingSettings {
145 @@ -129,6 +159,9 @@ impl Default for ComposingSettings {
146 reply_prefix: res(),
147 custom_compose_hooks: vec![],
148 disabled_compose_hooks: vec![],
149+ signature_file: None,
150+ use_signature: false,
151+ signature_delimiter: None,
152 }
153 }
154 }
155 diff --git a/meli/src/conf/overrides.rs b/meli/src/conf/overrides.rs
156index 8d20505..3ea3ae7 100644
157--- a/meli/src/conf/overrides.rs
158+++ b/meli/src/conf/overrides.rs
159 @@ -38,7 +38,7 @@ use crate::conf::{*, data_types::*};
160
161 # [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 } } }
162
163- # [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 } } }
164+ # [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/<account>/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 } } }
165
166 # [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 } } }
167
168 diff --git a/meli/src/mail/compose.rs b/meli/src/mail/compose.rs
169index c7b6a7e..7bc1b88 100644
170--- a/meli/src/mail/compose.rs
171+++ b/meli/src/mail/compose.rs
172 @@ -20,7 +20,9 @@
173 */
174
175 use std::{
176+ borrow::Cow,
177 convert::TryInto,
178+ fmt::Write as _,
179 future::Future,
180 io::Write,
181 pin::Pin,
182 @@ -241,7 +243,41 @@ impl Composer {
183 format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
184 );
185 }
186- if *account_settings!(context[account_hash].composing.format_flowed) {
187+ let format_flowed = *account_settings!(context[account_hash].composing.format_flowed);
188+ if *account_settings!(context[account_hash].composing.use_signature) {
189+ let override_value = account_settings!(context[account_hash].composing.signature_file)
190+ .as_deref()
191+ .map(Cow::Borrowed)
192+ .filter(|p| p.as_ref().is_file());
193+ let account_value = || {
194+ context.accounts[&account_hash]
195+ .signature_file()
196+ .map(Cow::Owned)
197+ };
198+ if let Some(path) = override_value.or_else(account_value) {
199+ match std::fs::read_to_string(path.as_ref()).chain_err_related_path(path.as_ref()) {
200+ Ok(sig) => {
201+ let mut delimiter =
202+ account_settings!(context[account_hash].composing.signature_delimiter)
203+ .as_deref()
204+ .map(Cow::Borrowed)
205+ .unwrap_or_else(|| Cow::Borrowed("\n\n-- \n"));
206+ if format_flowed {
207+ delimiter = Cow::Owned(delimiter.replace(" \n", " \n\n"));
208+ }
209+ _ = write!(&mut ret.draft.body, "{}{}", delimiter.as_ref(), sig);
210+ }
211+ Err(err) => {
212+ log::error!(
213+ "Could not open signature file for account `{}`: {}.",
214+ context.accounts[&account_hash].name(),
215+ err
216+ );
217+ }
218+ }
219+ }
220+ }
221+ if format_flowed {
222 ret.pager.set_reflow(melib::text::Reflow::FormatFlowed);
223 }
224 ret
225 @@ -420,7 +456,7 @@ impl Composer {
226 .set_header(HeaderName::TO, envelope.field_from_to_string());
227 }
228 ret.draft.body = {
229- let mut ret = attribution_string(
230+ let mut quoted = attribution_string(
231 account_settings!(
232 context[ret.account_hash]
233 .composing
234 @@ -437,11 +473,12 @@ impl Composer {
235 ),
236 );
237 for l in reply_body.lines() {
238- ret.push('>');
239- ret.push_str(l);
240- ret.push('\n');
241+ quoted.push('>');
242+ quoted.push_str(l);
243+ quoted.push('\n');
244 }
245- ret
246+ _ = write!(&mut quoted, "{}", ret.draft.body);
247+ quoted
248 };
249
250 ret.account_hash = coordinates.0;