Commit
+136 -8 +/-5 browse
1 | diff --git a/meli/docs/meli.conf.5 b/meli/docs/meli.conf.5 |
2 | index 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 |
43 | index 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 |
98 | index 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 |
156 | index 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 |
169 | index 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; |