Skip to content

Commit 77e7c3d

Browse files
committed
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 <[email protected]>
1 parent b930cb4 commit 77e7c3d

File tree

5 files changed

+136
-8
lines changed

5 files changed

+136
-8
lines changed

meli/docs/meli.conf.5

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,36 @@ or draft body mention attachments but they are missing.
10301030
.Ic empty-draft-warn
10311031
— Warn if draft has no subject and no body.
10321032
.El
1033+
.It Ic signature_file Ar Path
1034+
.Pq Em optional
1035+
Plain text file with signature that will pre-populate an email draft.
1036+
Signatures must be explicitly enabled to be used, otherwise this setting will be ignored.
1037+
.Pq Em None \" default value
1038+
.It Ic use_signature Ar bool
1039+
Pre-populate email drafts with signature, if any.
1040+
.Sy meli
1041+
will lookup the signature value in this order:
1042+
.Bl -enum -compact
1043+
.It
1044+
The
1045+
.Ic signature_file
1046+
setting.
1047+
.It
1048+
.Pa ${XDG_CONFIG_DIR}/meli/<account>/signature
1049+
.It
1050+
.Pa ${XDG_CONFIG_DIR}/meli/signature
1051+
.It
1052+
.Pa ${XDG_CONFIG_DIR}/signature
1053+
.It
1054+
.Pa ${HOME}/.signature
1055+
.It
1056+
No signature otherwise.
1057+
.El
1058+
.Pq Em false \" default value
1059+
.It Ic signature_delimiter Ar String
1060+
.Pq Em optional
1061+
Signature delimiter, that is, text that will be prefixed to your signature to separate it from the email body.
1062+
.Pq Ql \en\en\-\- \en
10331063
.El
10341064
.\"
10351065
.\"

meli/src/accounts.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use std::{
2929
io,
3030
ops::{Index, IndexMut},
3131
os::unix::fs::PermissionsExt,
32+
path::{Path, PathBuf},
3233
pin::Pin,
3334
result,
3435
sync::{Arc, RwLock},
@@ -42,7 +43,7 @@ use melib::{
4243
error::{Error, ErrorKind, NetworkErrorKind, Result},
4344
log,
4445
thread::Threads,
45-
utils::{fnmatch::Fnmatch, futures::sleep, random},
46+
utils::{fnmatch::Fnmatch, futures::sleep, random, shellexpand::ShellExpandTrait},
4647
Contacts, SortField, SortOrder,
4748
};
4849
use smallvec::SmallVec;
@@ -1800,6 +1801,33 @@ impl Account {
18001801
IsAsync::Blocking
18011802
}
18021803
}
1804+
1805+
pub fn signature_file(&self) -> Option<PathBuf> {
1806+
xdg::BaseDirectories::with_profile("meli", &self.name)
1807+
.ok()
1808+
.and_then(|d| {
1809+
d.place_config_file("signature")
1810+
.ok()
1811+
.filter(|p| p.is_file())
1812+
})
1813+
.or_else(|| {
1814+
xdg::BaseDirectories::with_prefix("meli")
1815+
.ok()
1816+
.and_then(|d| {
1817+
d.place_config_file("signature")
1818+
.ok()
1819+
.filter(|p| p.is_file())
1820+
})
1821+
})
1822+
.or_else(|| {
1823+
xdg::BaseDirectories::new().ok().and_then(|d| {
1824+
d.place_config_file("signature")
1825+
.ok()
1826+
.filter(|p| p.is_file())
1827+
})
1828+
})
1829+
.or_else(|| Some(Path::new("~/.signature").expand()).filter(|p| p.is_file()))
1830+
}
18031831
}
18041832

18051833
impl Index<&MailboxHash> for Account {

meli/src/conf/composing.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
//! Configuration for composing email.
2323
24+
use std::path::PathBuf;
25+
2426
use indexmap::IndexMap;
2527
use melib::{conf::ActionFlag, email::HeaderName};
2628
use serde::{de, Deserialize, Deserializer};
@@ -110,6 +112,34 @@ pub struct ComposingSettings {
110112
/// Disabled `compose-hooks`.
111113
#[serde(default, alias = "disabled-compose-hooks")]
112114
pub disabled_compose_hooks: Vec<String>,
115+
/// Plain text file with signature that will pre-populate an email draft.
116+
///
117+
/// Signatures must be explicitly enabled to be used, otherwise this setting
118+
/// will be ignored.
119+
///
120+
/// Default: `None`
121+
#[serde(default, alias = "signature-file")]
122+
pub signature_file: Option<PathBuf>,
123+
/// Pre-populate email drafts with signature, if any.
124+
///
125+
/// `meli` will lookup the signature value in this order:
126+
///
127+
/// 1. The `signature_file` setting.
128+
/// 2. `${XDG_CONFIG_DIR}/meli/<account>/signature`
129+
/// 3. `${XDG_CONFIG_DIR}/meli/signature`
130+
/// 4. `${XDG_CONFIG_DIR}/signature`
131+
/// 5. `${HOME}/.signature`
132+
/// 6. No signature otherwise.
133+
///
134+
/// Default: `false`
135+
#[serde(default = "false_val", alias = "use-signature")]
136+
pub use_signature: bool,
137+
/// Signature delimiter, that is, text that will be prefixed to your
138+
/// signature to separate it from the email body.
139+
///
140+
/// Default: `"\n\n-- \n"`
141+
#[serde(default, alias = "signature-delimiter")]
142+
pub signature_delimiter: Option<String>,
113143
}
114144

115145
impl Default for ComposingSettings {
@@ -129,6 +159,9 @@ impl Default for ComposingSettings {
129159
reply_prefix: res(),
130160
custom_compose_hooks: vec![],
131161
disabled_compose_hooks: vec![],
162+
signature_file: None,
163+
use_signature: false,
164+
signature_delimiter: None,
132165
}
133166
}
134167
}

meli/src/conf/overrides.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::conf::{*, data_types::*};
3838

3939
# [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 } } }
4040

41-
# [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 } } }
41+
# [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 } } }
4242

4343
# [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 } } }
4444

meli/src/mail/compose.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
*/
2121

2222
use std::{
23+
borrow::Cow,
2324
convert::TryInto,
25+
fmt::Write as _,
2426
future::Future,
2527
io::Write,
2628
pin::Pin,
@@ -241,7 +243,41 @@ impl Composer {
241243
format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
242244
);
243245
}
244-
if *account_settings!(context[account_hash].composing.format_flowed) {
246+
let format_flowed = *account_settings!(context[account_hash].composing.format_flowed);
247+
if *account_settings!(context[account_hash].composing.use_signature) {
248+
let override_value = account_settings!(context[account_hash].composing.signature_file)
249+
.as_deref()
250+
.map(Cow::Borrowed)
251+
.filter(|p| p.as_ref().is_file());
252+
let account_value = || {
253+
context.accounts[&account_hash]
254+
.signature_file()
255+
.map(Cow::Owned)
256+
};
257+
if let Some(path) = override_value.or_else(account_value) {
258+
match std::fs::read_to_string(path.as_ref()).chain_err_related_path(path.as_ref()) {
259+
Ok(sig) => {
260+
let mut delimiter =
261+
account_settings!(context[account_hash].composing.signature_delimiter)
262+
.as_deref()
263+
.map(Cow::Borrowed)
264+
.unwrap_or_else(|| Cow::Borrowed("\n\n-- \n"));
265+
if format_flowed {
266+
delimiter = Cow::Owned(delimiter.replace(" \n", " \n\n"));
267+
}
268+
_ = write!(&mut ret.draft.body, "{}{}", delimiter.as_ref(), sig);
269+
}
270+
Err(err) => {
271+
log::error!(
272+
"Could not open signature file for account `{}`: {}.",
273+
context.accounts[&account_hash].name(),
274+
err
275+
);
276+
}
277+
}
278+
}
279+
}
280+
if format_flowed {
245281
ret.pager.set_reflow(melib::text::Reflow::FormatFlowed);
246282
}
247283
ret
@@ -420,7 +456,7 @@ impl Composer {
420456
.set_header(HeaderName::TO, envelope.field_from_to_string());
421457
}
422458
ret.draft.body = {
423-
let mut ret = attribution_string(
459+
let mut quoted = attribution_string(
424460
account_settings!(
425461
context[ret.account_hash]
426462
.composing
@@ -437,11 +473,12 @@ impl Composer {
437473
),
438474
);
439475
for l in reply_body.lines() {
440-
ret.push('>');
441-
ret.push_str(l);
442-
ret.push('\n');
476+
quoted.push('>');
477+
quoted.push_str(l);
478+
quoted.push('\n');
443479
}
444-
ret
480+
_ = write!(&mut quoted, "{}", ret.draft.body);
481+
quoted
445482
};
446483

447484
ret.account_hash = coordinates.0;

0 commit comments

Comments
 (0)