+103 -152 +/-9 browse
1 | diff --git a/CHANGELOG.md b/CHANGELOG.md |
2 | index a567112..1d5e413 100644 |
3 | --- a/CHANGELOG.md |
4 | +++ b/CHANGELOG.md |
5 | @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 |
6 | |
7 | ## [Unreleased] |
8 | |
9 | + *WARNING*: This release contains a breaking change in the configuration file: a |
10 | + global composing option is not required anymore. Now, composing options are per |
11 | + account. |
12 | + |
13 | <!-- ### Added --> |
14 | |
15 | <!-- ### Bug Fixes --> |
16 | diff --git a/meli/docs/external-tools.md b/meli/docs/external-tools.md |
17 | index 00919c9..fb91e78 100644 |
18 | --- a/meli/docs/external-tools.md |
19 | +++ b/meli/docs/external-tools.md |
20 | @@ -2,7 +2,7 @@ |
21 | |
22 | ## Sending mail with a command line tool |
23 | |
24 | - `composing.send_mail` can use either settings for an SMTP server or a shell |
25 | + `send_mail` can use either settings for an SMTP server or a shell |
26 | command to which it pipes new mail to. |
27 | |
28 | ### `msmtp` and `send_mail` |
29 | @@ -12,7 +12,6 @@ with many SMTP servers. It supports queuing and other small useful features. |
30 | See [the documentation](https://marlam.de/msmtp/msmtp.html). |
31 | |
32 | ```toml |
33 | - [composing] |
34 | send_mail = 'msmtp --logfile=/home/user/.mail/msmtp.log --read-recipients |
35 | --read-envelope-from' |
36 | ``` |
37 | diff --git a/meli/docs/samples/sample-config.toml b/meli/docs/samples/sample-config.toml |
38 | index a8b4141..9567959 100644 |
39 | --- a/meli/docs/samples/sample-config.toml |
40 | +++ b/meli/docs/samples/sample-config.toml |
41 | @@ -11,6 +11,8 @@ |
42 | #[accounts.account-name] |
43 | #root_mailbox = "/path/to/root/mailbox" |
44 | #format = "Maildir" |
45 | + #send_mail = 'msmtp --read-recipients --read-envelope-from' |
46 | + ##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } } |
47 | #listing.index_style = "Conversations" # or [plain, threaded, compact] |
48 | #identity="email@example.com" |
49 | #display_name = "Name" |
50 | @@ -26,6 +28,7 @@ |
51 | #[accounts.mbox] |
52 | #root_mailbox = "/var/mail/username" |
53 | #format = "mbox" |
54 | + #send_mail = 'false' |
55 | #listing.index_style = "Compact" |
56 | #identity="username@hostname.local" |
57 | # |
58 | @@ -33,6 +36,7 @@ |
59 | #[accounts."imap"] |
60 | #root_mailbox = "INBOX" |
61 | #format = "imap" |
62 | + #send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } } |
63 | #server_hostname="mail.example.com" |
64 | #server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m" |
65 | #server_username="username@example.com" |
66 | @@ -51,6 +55,7 @@ |
67 | ##[accounts.notmuch] |
68 | ##root_mailbox = "/path/to/folder" # where .notmuch/ directory is located |
69 | ##format = "notmuch" |
70 | + ##send_mail = 'msmtp --read-recipients --read-envelope-from' |
71 | ##listing.index_style = "conversations" |
72 | ##identity="username@example.com" |
73 | ##display_name = "Name Name" |
74 | @@ -64,6 +69,7 @@ |
75 | #[accounts."gmail"] |
76 | #root_mailbox = '[Gmail]' |
77 | #format = "imap" |
78 | + #send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } } |
79 | #server_hostname='imap.gmail.com' |
80 | #server_password="password" |
81 | #server_username="username@gmail.com" |
82 | @@ -73,20 +79,19 @@ |
83 | #display_name = "Name Name" |
84 | ### match every mailbox: |
85 | #subscribed_mailboxes = ["*" ] |
86 | - #composing.send_mail = 'msmtp --read-recipients --read-envelope-from' |
87 | ### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort: |
88 | #composing.store_sent_mail = false |
89 | # |
90 | ##[accounts."jmap account"] |
91 | ##root_mailbox = "INBOX" |
92 | ##format = "jmap" |
93 | + ##send_mail = 'server_submission' |
94 | ##server_url="http://localhost:8080" |
95 | ##server_username="user@hostname.local" |
96 | ##server_password="changeme" |
97 | ##listing.index_style = "Conversations" |
98 | ##identity = "user@hostname.local" |
99 | ##subscribed_mailboxes = ["*", ] |
100 | - ##composing.send_mail = 'server_submission' |
101 | # |
102 | #[pager] |
103 | #filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email" |
104 | @@ -128,12 +133,8 @@ |
105 | #page_down = "PageDown" |
106 | # |
107 | #[composing] |
108 | - ##required for sending e-mail |
109 | - #send_mail = 'msmtp --read-recipients --read-envelope-from' |
110 | - ##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } } |
111 | #editor_command = 'vim +/^$' # optional, by default $EDITOR is used. |
112 | # |
113 | - # |
114 | #[pgp] |
115 | #auto_sign = false # always sign sent messages |
116 | #auto_verify_signatures = true # always verify signatures when reading signed e-mails |
117 | diff --git a/meli/src/conf.rs b/meli/src/conf.rs |
118 | index 065a74e..afc1eba 100644 |
119 | --- a/meli/src/conf.rs |
120 | +++ b/meli/src/conf.rs |
121 | @@ -38,10 +38,7 @@ use melib::{ |
122 | ShellExpandTrait, SortField, SortOrder, StderrLogger, |
123 | }; |
124 | |
125 | - use crate::{ |
126 | - conf::deserializers::non_empty_opt_string, |
127 | - terminal::{Ask, Color}, |
128 | - }; |
129 | + use crate::{conf::deserializers::non_empty_opt_string, terminal::Color}; |
130 | |
131 | #[rustfmt::skip] |
132 | mod overrides; |
133 | @@ -67,7 +64,7 @@ use std::{ |
134 | |
135 | use indexmap::IndexMap; |
136 | use melib::{ |
137 | - conf::{AccountSettings, ActionFlag, MailboxConf, ToggleFlag}, |
138 | + conf::{ActionFlag, MailboxConf, ToggleFlag}, |
139 | error::*, |
140 | }; |
141 | use pager::PagerSettings; |
142 | @@ -98,6 +95,9 @@ macro_rules! account_settings { |
143 | .as_ref() |
144 | .unwrap_or(&$context.settings.$setting.$field) |
145 | }}; |
146 | + ($context:ident[$account_hash:expr].$field:ident) => {{ |
147 | + &$context.accounts[&$account_hash].settings.$field |
148 | + }}; |
149 | } |
150 | #[macro_export] |
151 | macro_rules! mailbox_settings { |
152 | @@ -121,6 +121,7 @@ macro_rules! mailbox_settings { |
153 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] |
154 | #[serde(deny_unknown_fields)] |
155 | pub struct MailUIConf { |
156 | + pub send_mail: Option<SendMail>, |
157 | #[serde(default)] |
158 | pub pager: PagerSettingsOverride, |
159 | #[serde(default)] |
160 | @@ -173,12 +174,12 @@ pub struct FileAccount { |
161 | #[serde(default = "none", skip_serializing_if = "Option::is_none")] |
162 | pub default_mailbox: Option<String>, |
163 | pub format: String, |
164 | + pub send_mail: SendMail, |
165 | pub identity: String, |
166 | #[serde(default)] |
167 | pub extra_identities: Vec<String>, |
168 | #[serde(default = "none", skip_serializing_if = "Option::is_none")] |
169 | pub display_name: Option<String>, |
170 | - |
171 | #[serde(default = "false_val")] |
172 | pub read_only: bool, |
173 | #[serde(default)] |
174 | @@ -227,6 +228,7 @@ pub struct FileSettings { |
175 | pub notifications: NotificationsSettings, |
176 | #[serde(default)] |
177 | pub shortcuts: Shortcuts, |
178 | + #[serde(default)] |
179 | pub composing: ComposingSettings, |
180 | #[serde(default)] |
181 | pub tags: TagsSettings, |
182 | @@ -240,7 +242,10 @@ pub struct FileSettings { |
183 | |
184 | #[derive(Clone, Debug, Default, Serialize)] |
185 | pub struct AccountConf { |
186 | - pub account: AccountSettings, |
187 | + pub account: melib::AccountSettings, |
188 | + /// How to send e-mail for this account. |
189 | + /// Required |
190 | + pub send_mail: SendMail, |
191 | pub default_mailbox: Option<MailboxHash>, |
192 | pub sent_mailbox: Option<MailboxHash>, |
193 | pub conf: FileAccount, |
194 | @@ -249,10 +254,10 @@ pub struct AccountConf { |
195 | } |
196 | |
197 | impl AccountConf { |
198 | - pub fn account(&self) -> &AccountSettings { |
199 | + pub fn account(&self) -> &melib::AccountSettings { |
200 | &self.account |
201 | } |
202 | - pub fn account_mut(&mut self) -> &mut AccountSettings { |
203 | + pub fn account_mut(&mut self) -> &mut melib::AccountSettings { |
204 | &mut self.account |
205 | } |
206 | pub fn conf(&self) -> &FileAccount { |
207 | @@ -276,7 +281,7 @@ impl From<FileAccount> for AccountConf { |
208 | .map(|(k, v)| (k.clone(), v.mailbox_conf.clone())) |
209 | .collect(); |
210 | |
211 | - let acc = AccountSettings { |
212 | + let acc = melib::AccountSettings { |
213 | name: String::new(), |
214 | root_mailbox, |
215 | format, |
216 | @@ -294,6 +299,7 @@ impl From<FileAccount> for AccountConf { |
217 | let mailbox_confs = x.mailboxes.clone(); |
218 | Self { |
219 | account: acc, |
220 | + send_mail: x.send_mail.clone(), |
221 | default_mailbox: None, |
222 | sent_mailbox: None, |
223 | conf_override: x.conf_override.clone(), |
224 | @@ -439,7 +445,7 @@ impl FileSettings { |
225 | .set_kind(ErrorKind::Configuration)); |
226 | } |
227 | #[cfg(not(test))] |
228 | - let ask = Ask { |
229 | + let ask = crate::terminal::Ask { |
230 | message: format!( |
231 | "No configuration found. Would you like to generate one in {}?", |
232 | path_string |
233 | @@ -461,46 +467,26 @@ impl FileSettings { |
234 | ); |
235 | } |
236 | |
237 | - Self::validate(config_path, true, false) |
238 | + Self::validate(config_path, false) |
239 | } |
240 | |
241 | /// Validate configuration from `input` string. |
242 | - pub fn validate_string(s: String, interactive: bool, clear_extras: bool) -> Result<Self> { |
243 | - let map: toml::value::Table = toml::from_str(&s).map_err(|err| { |
244 | + pub fn validate_string(s: String, clear_extras: bool) -> Result<Self> { |
245 | + let _: toml::value::Table = melib::serde_path_to_error::deserialize( |
246 | + toml::Deserializer::new(&s), |
247 | + ) |
248 | + .map_err(|err| { |
249 | Error::new("Config file is invalid TOML") |
250 | .set_source(Some(Arc::new(err))) |
251 | .set_kind(ErrorKind::ValueError) |
252 | })?; |
253 | |
254 | - // Check that a global composing option is set and return a user-friendly |
255 | - // error message because the default serde one is confusing. |
256 | - if !map.contains_key("composing") { |
257 | - let err_msg = r#"You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows: |
258 | - |
259 | - [composing] |
260 | - send_mail = 'false' |
261 | - |
262 | - This is required so that you don't accidentally start meli and find out later that you can't send emails."#; |
263 | - if interactive { |
264 | - println!("{}", err_msg); |
265 | - let ask = Ask { |
266 | - message: "Would you like to append this dummy value in your configuration \ |
267 | - input and continue?" |
268 | - .to_string(), |
269 | - }; |
270 | - if ask.run() { |
271 | - let mut s = s; |
272 | - s.push_str("[composing]\nsend_mail = 'false'\n"); |
273 | - return Self::validate_string(s, interactive, clear_extras); |
274 | - } |
275 | - } |
276 | - return Err(Error::new(err_msg).set_kind(ErrorKind::Configuration)); |
277 | - } |
278 | - let mut s: Self = toml::from_str(&s).map_err(|err| { |
279 | - Error::new("Input contains errors") |
280 | - .set_source(Some(Arc::new(err))) |
281 | - .set_kind(ErrorKind::Configuration) |
282 | - })?; |
283 | + let mut s: Self = melib::serde_path_to_error::deserialize(toml::Deserializer::new(&s)) |
284 | + .map_err(|err| { |
285 | + Error::new("Input contains errors") |
286 | + .set_source(Some(Arc::new(err))) |
287 | + .set_kind(ErrorKind::Configuration) |
288 | + })?; |
289 | let backends = melib::backends::Backends::new(); |
290 | let Themes { |
291 | light: default_light, |
292 | @@ -538,6 +524,7 @@ This is required so that you don't accidentally start meli and find out later th |
293 | let FileAccount { |
294 | root_mailbox, |
295 | format, |
296 | + send_mail: _, |
297 | identity, |
298 | extra_identities, |
299 | read_only, |
300 | @@ -554,7 +541,7 @@ This is required so that you don't accidentally start meli and find out later th |
301 | } = acc.clone(); |
302 | |
303 | let lowercase_format = format.to_lowercase(); |
304 | - let mut s = AccountSettings { |
305 | + let mut s = melib::AccountSettings { |
306 | name: name.to_string(), |
307 | root_mailbox, |
308 | format: format.clone(), |
309 | @@ -589,9 +576,9 @@ This is required so that you don't accidentally start meli and find out later th |
310 | } |
311 | |
312 | /// Validate `path` and print errors. |
313 | - pub fn validate(path: PathBuf, interactive: bool, clear_extras: bool) -> Result<Self> { |
314 | + pub fn validate(path: PathBuf, clear_extras: bool) -> Result<Self> { |
315 | let s = pp::pp(&path)?; |
316 | - let map: toml::value::Table = toml::from_str(&s).map_err(|err| { |
317 | + let _: toml::value::Table = toml::from_str(&s).map_err(|err| { |
318 | Error::new(format!( |
319 | "{}: Config file is invalid TOML; {}", |
320 | path.display(), |
321 | @@ -599,40 +586,6 @@ This is required so that you don't accidentally start meli and find out later th |
322 | )) |
323 | })?; |
324 | |
325 | - // Check that a global composing option is set and return a user-friendly |
326 | - // error message because the default serde one is confusing. |
327 | - if !map.contains_key("composing") { |
328 | - let err_msg = r#"You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows: |
329 | - |
330 | - [composing] |
331 | - send_mail = 'false' |
332 | - |
333 | - This is required so that you don't accidentally start meli and find out later that you can't send emails."#; |
334 | - if interactive { |
335 | - println!("{}", err_msg); |
336 | - let ask = Ask { |
337 | - message: format!( |
338 | - "Would you like to append this dummy value in your configuration file {} \ |
339 | - and continue?", |
340 | - path.display() |
341 | - ), |
342 | - }; |
343 | - if ask.run() { |
344 | - let mut file = OpenOptions::new().append(true).open(&path)?; |
345 | - file.write_all(b"[composing]\nsend_mail = 'false'\n") |
346 | - .map_err(|err| { |
347 | - Error::new(format!("Could not append to {}: {}", path.display(), err)) |
348 | - })?; |
349 | - return Self::validate(path, interactive, clear_extras); |
350 | - } |
351 | - } |
352 | - return Err(Error::new(format!( |
353 | - "{}\n\nEdit the {} and relaunch meli.", |
354 | - if interactive { "" } else { err_msg }, |
355 | - path.display() |
356 | - )) |
357 | - .set_kind(ErrorKind::Configuration)); |
358 | - } |
359 | let mut s: Self = toml::from_str(&s).map_err(|err| { |
360 | Error::new(format!("{}: Config file contains errors", path.display())) |
361 | .set_source(Some(Arc::new(err))) |
362 | @@ -675,6 +628,7 @@ This is required so that you don't accidentally start meli and find out later th |
363 | let FileAccount { |
364 | root_mailbox, |
365 | format, |
366 | + send_mail: _, |
367 | identity, |
368 | extra_identities, |
369 | read_only, |
370 | @@ -691,7 +645,7 @@ This is required so that you don't accidentally start meli and find out later th |
371 | } = acc.clone(); |
372 | |
373 | let lowercase_format = format.to_lowercase(); |
374 | - let mut s = AccountSettings { |
375 | + let mut s = melib::AccountSettings { |
376 | name: name.to_string(), |
377 | root_mailbox, |
378 | format: format.clone(), |
379 | @@ -1492,6 +1446,7 @@ pub mod tests { |
380 | [accounts.account-name] |
381 | root_mailbox = "/path/to/root/mailbox" |
382 | format = "Maildir" |
383 | + send_mail = 'false' |
384 | listing.index_style = "Conversations" # or [plain, threaded, compact] |
385 | identity="email@example.com" |
386 | display_name = "Name" |
387 | @@ -1507,6 +1462,7 @@ subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"] |
388 | [accounts.mbox] |
389 | root_mailbox = "/var/mail/username" |
390 | format = "mbox" |
391 | + send_mail = 'false' |
392 | listing.index_style = "Compact" |
393 | identity="username@hostname.local" |
394 | "#; |
395 | @@ -1515,39 +1471,26 @@ identity="username@hostname.local" |
396 | [accounts.mbox] |
397 | root_mailbox = "/" |
398 | format = "mbox" |
399 | + send_mail = 'false' |
400 | index_style = "Compact" |
401 | identity="username@hostname.local" |
402 | - |
403 | - [composing] |
404 | - send_mail = 'false' |
405 | "#; |
406 | pub const IMAP_CONFIG: &str = r#" |
407 | [accounts.imap] |
408 | root_mailbox = "INBOX" |
409 | format = "imap" |
410 | + send_mail = 'false' |
411 | identity="username@example.com" |
412 | server_username = "null" |
413 | server_hostname = "example.com" |
414 | server_password_command = "false" |
415 | - |
416 | - [composing] |
417 | - send_mail = 'false' |
418 | "#; |
419 | |
420 | #[test] |
421 | fn test_config_parse() { |
422 | let tempdir = tempfile::tempdir().unwrap(); |
423 | - let mut new_file = ConfigFile::new(TEST_CONFIG, &tempdir).unwrap(); |
424 | - let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err(); |
425 | - assert!(err.summary.as_ref().starts_with( |
426 | - "You must set a global `composing` option. If you override `composing` in each \ |
427 | - account, you can use a dummy global like follows" |
428 | - )); |
429 | - new_file |
430 | - .file |
431 | - .write_all(b"[composing]\nsend_mail = 'false'\n") |
432 | - .unwrap(); |
433 | - let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err(); |
434 | + let new_file = ConfigFile::new(TEST_CONFIG, &tempdir).unwrap(); |
435 | + let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err(); |
436 | assert_eq!( |
437 | err.summary.as_ref(), |
438 | "Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a \ |
439 | @@ -1557,7 +1500,7 @@ send_mail = 'false' |
440 | /* Test unrecognised configuration entries error */ |
441 | |
442 | let new_file = ConfigFile::new(EXTRA_CONFIG, &tempdir).unwrap(); |
443 | - let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err(); |
444 | + let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err(); |
445 | assert_eq!( |
446 | err.summary.as_ref(), |
447 | "Unrecognised configuration values: {\"index_style\": \"Compact\"}" |
448 | @@ -1566,8 +1509,7 @@ send_mail = 'false' |
449 | /* Test IMAP config */ |
450 | |
451 | let new_file = ConfigFile::new(IMAP_CONFIG, &tempdir).unwrap(); |
452 | - FileSettings::validate(new_file.path.clone(), false, true) |
453 | - .expect("could not parse IMAP config"); |
454 | + FileSettings::validate(new_file.path.clone(), true).expect("could not parse IMAP config"); |
455 | |
456 | /* Test sample config */ |
457 | |
458 | @@ -1579,7 +1521,7 @@ send_mail = 'false' |
459 | ); |
460 | |
461 | let new_file = ConfigFile::new(&example_config, &tempdir).unwrap(); |
462 | - let config = FileSettings::validate(new_file.path.clone(), false, true) |
463 | + let config = FileSettings::validate(new_file.path.clone(), true) |
464 | .expect("Could not parse example config!"); |
465 | for (accname, acc) in config.accounts.iter() { |
466 | if !acc.extra.is_empty() { |
467 | @@ -1589,5 +1531,8 @@ send_mail = 'false' |
468 | ); |
469 | } |
470 | } |
471 | + if let Err(err) = tempdir.close() { |
472 | + eprintln!("Could not cleanup tempdir: {}", err); |
473 | + } |
474 | } |
475 | } |
476 | diff --git a/meli/src/conf/composing.rs b/meli/src/conf/composing.rs |
477 | index 1e5044a..e87ee48 100644 |
478 | --- a/meli/src/conf/composing.rs |
479 | +++ b/meli/src/conf/composing.rs |
480 | @@ -34,9 +34,6 @@ use super::{ |
481 | #[derive(Clone, Debug, Deserialize, Serialize)] |
482 | #[serde(deny_unknown_fields)] |
483 | pub struct ComposingSettings { |
484 | - /// A command to pipe new emails to |
485 | - /// Required |
486 | - pub send_mail: SendMail, |
487 | /// Command to launch editor. Can have arguments. Draft filename is given as |
488 | /// the last argument. If it's missing, the environment variable $EDITOR is |
489 | /// looked up. |
490 | @@ -116,7 +113,6 @@ pub struct ComposingSettings { |
491 | impl Default for ComposingSettings { |
492 | fn default() -> Self { |
493 | Self { |
494 | - send_mail: SendMail::ShellCommand("false".into()), |
495 | editor_command: None, |
496 | embedded_pty: false, |
497 | format_flowed: true, |
498 | @@ -187,6 +183,14 @@ pub enum SendMail { |
499 | ShellCommand(String), |
500 | } |
501 | |
502 | + impl Default for SendMail { |
503 | + /// Returns the `false` POSIX shell utility, in order to return an error |
504 | + /// when called. |
505 | + fn default() -> Self { |
506 | + Self::ShellCommand("false".into()) |
507 | + } |
508 | + } |
509 | + |
510 | /// Shell command compose hooks (See |
511 | /// [`crate::mail::compose::hooks::Hook`]) |
512 | #[derive(Clone, Debug, Deserialize, Serialize)] |
513 | @@ -220,9 +224,10 @@ Using a shell script |
514 | Direct SMTP connection |
515 | ====================== |
516 | |
517 | + [accounts.account-name] |
518 | send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } } |
519 | |
520 | - [composing.send_mail] |
521 | + [accounts.account-name.send_mail] |
522 | hostname = "mail.example.com" |
523 | port = 587 |
524 | auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } } |
525 | @@ -284,6 +289,8 @@ impl<'de> Deserialize<'de> for SendMail { |
526 | where |
527 | D: Deserializer<'de>, |
528 | { |
529 | + use serde::de::Error; |
530 | + |
531 | #[derive(Deserialize)] |
532 | #[serde(untagged)] |
533 | enum SendMailInner { |
534 | @@ -294,11 +301,17 @@ impl<'de> Deserialize<'de> for SendMail { |
535 | ShellCommand(String), |
536 | } |
537 | |
538 | - match <SendMailInner>::deserialize(deserializer) { |
539 | + match melib::serde_path_to_error::deserialize(deserializer) { |
540 | #[cfg(feature = "smtp")] |
541 | Ok(SendMailInner::Smtp(v)) => Ok(Self::Smtp(v)), |
542 | Ok(SendMailInner::ServerSubmission) => Ok(Self::ServerSubmission), |
543 | Ok(SendMailInner::ShellCommand(v)) => Ok(Self::ShellCommand(v)), |
544 | + Err(err) |
545 | + if err.inner().to_string() == D::Error::missing_field("send_mail").to_string() => |
546 | + { |
547 | + // Surely there should be a better way to do this... |
548 | + Err(err.into_inner()) |
549 | + } |
550 | Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)), |
551 | } |
552 | } |
553 | diff --git a/meli/src/conf/overrides.rs b/meli/src/conf/overrides.rs |
554 | index bef748e..964b208 100644 |
555 | --- a/meli/src/conf/overrides.rs |
556 | +++ b/meli/src/conf/overrides.rs |
557 | @@ -35,7 +35,7 @@ use melib::HeaderName; |
558 | |
559 | # [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 } } } |
560 | |
561 | - # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [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 < HashMap < 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 appears above the quoted reply text."] # [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 = " 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 { send_mail : None , 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 } } } |
562 | + # [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 < HashMap < 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 appears above the quoted reply text."] # [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 = " 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 } } } |
563 | |
564 | # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { Self { colors : None , ignore_tags : None } } } |
565 | |
566 | diff --git a/meli/src/mail/compose.rs b/meli/src/mail/compose.rs |
567 | index abcf6d3..50ae4a4 100644 |
568 | --- a/meli/src/mail/compose.rs |
569 | +++ b/meli/src/mail/compose.rs |
570 | @@ -649,23 +649,22 @@ To: {} |
571 | .values() |
572 | .map(|acc| { |
573 | let addr = acc.settings.account.main_identity_address(); |
574 | - let desc = |
575 | - match account_settings!(c[acc.hash()].composing.send_mail) { |
576 | - crate::conf::composing::SendMail::ShellCommand(ref cmd) => { |
577 | - let mut cmd = cmd.as_str(); |
578 | - cmd.truncate_at_boundary(10); |
579 | - format!("{} [exec: {}]", acc.name(), cmd) |
580 | - } |
581 | - #[cfg(feature = "smtp")] |
582 | - crate::conf::composing::SendMail::Smtp(ref inner) => { |
583 | - let mut hostname = inner.hostname.as_str(); |
584 | - hostname.truncate_at_boundary(10); |
585 | - format!("{} [smtp: {}]", acc.name(), hostname) |
586 | - } |
587 | - crate::conf::composing::SendMail::ServerSubmission => { |
588 | - format!("{} [server submission]", acc.name()) |
589 | - } |
590 | - }; |
591 | + let desc = match account_settings!(c[acc.hash()].send_mail) { |
592 | + crate::conf::composing::SendMail::ShellCommand(ref cmd) => { |
593 | + let mut cmd = cmd.as_str(); |
594 | + cmd.truncate_at_boundary(10); |
595 | + format!("{} [exec: {}]", acc.name(), cmd) |
596 | + } |
597 | + #[cfg(feature = "smtp")] |
598 | + crate::conf::composing::SendMail::Smtp(ref inner) => { |
599 | + let mut hostname = inner.hostname.as_str(); |
600 | + hostname.truncate_at_boundary(10); |
601 | + format!("{} [smtp: {}]", acc.name(), hostname) |
602 | + } |
603 | + crate::conf::composing::SendMail::ServerSubmission => { |
604 | + format!("{} [server submission]", acc.name()) |
605 | + } |
606 | + }; |
607 | |
608 | (addr.to_string(), desc) |
609 | }) |
610 | @@ -2489,7 +2488,7 @@ pub fn send_draft( |
611 | } |
612 | } |
613 | let bytes = draft.finalise().unwrap(); |
614 | - let send_mail = account_settings!(context[account_hash].composing.send_mail).clone(); |
615 | + let send_mail = account_settings!(context[account_hash].send_mail).clone(); |
616 | let ret = |
617 | context.accounts[&account_hash].send(bytes.clone(), send_mail, complete_in_background); |
618 | save_draft(bytes.as_bytes(), context, mailbox_type, flags, account_hash); |
619 | @@ -2578,7 +2577,7 @@ pub fn send_draft_async( |
620 | gpg_state.encrypt_keys, |
621 | )?)); |
622 | } |
623 | - let send_mail = account_settings!(context[account_hash].composing.send_mail).clone(); |
624 | + let send_mail = account_settings!(context[account_hash].send_mail).clone(); |
625 | let send_cb = context.accounts[&account_hash].send_async(send_mail); |
626 | let mut content_type = ContentType::default(); |
627 | if format_flowed { |
628 | diff --git a/meli/src/subcommands.rs b/meli/src/subcommands.rs |
629 | index 7096111..cc62a97 100644 |
630 | --- a/meli/src/subcommands.rs |
631 | +++ b/meli/src/subcommands.rs |
632 | @@ -133,13 +133,13 @@ pub fn test_config(path: Option<PathOrStdio>) -> Result<()> { |
633 | if input.trim().is_empty() { |
634 | return Err(Error::new("Input was empty.").set_kind(ErrorKind::ValueError)); |
635 | } |
636 | - conf::FileSettings::validate_string(input, true, false)?; |
637 | + conf::FileSettings::validate_string(input, false)?; |
638 | return Ok(()); |
639 | } |
640 | Some(PathOrStdio::Path(path)) => path.expand(), |
641 | None => conf::get_config_file()?, |
642 | }; |
643 | - conf::FileSettings::validate(config_path, true, false)?; |
644 | + conf::FileSettings::validate(config_path, false)?; |
645 | Ok(()) |
646 | } |
647 | |
648 | @@ -183,7 +183,7 @@ pub fn tool(path: Option<PathBuf>, opt: ToolOpt) -> Result<()> { |
649 | } else { |
650 | conf::get_config_file()? |
651 | }; |
652 | - let conf = conf::FileSettings::validate(config_path, true, false)?; |
653 | + let conf = conf::FileSettings::validate(config_path, false)?; |
654 | let account = match opt { |
655 | ToolOpt::ImapShell { ref account } => account, |
656 | #[cfg(feature = "smtp")] |
657 | @@ -208,13 +208,7 @@ pub fn tool(path: Option<PathBuf>, opt: ToolOpt) -> Result<()> { |
658 | ToolOpt::SmtpShell { .. } => { |
659 | use crate::conf::composing::SendMail; |
660 | |
661 | - let send_mail = file_account_conf |
662 | - .conf_override |
663 | - .composing |
664 | - .send_mail |
665 | - .as_ref() |
666 | - .unwrap_or(&conf.composing.send_mail) |
667 | - .clone(); |
668 | + let send_mail = file_account_conf.send_mail; |
669 | let SendMail::Smtp(smtp_conf) = send_mail else { |
670 | panic!( |
671 | "smtp shell requires an smtp configuration for account {}", |
672 | diff --git a/meli/tests/test_cli_subcommands.rs b/meli/tests/test_cli_subcommands.rs |
673 | index 144e2cd..4e6d3d0 100644 |
674 | --- a/meli/tests/test_cli_subcommands.rs |
675 | +++ b/meli/tests/test_cli_subcommands.rs |
676 | @@ -130,13 +130,11 @@ fn test_cli_subcommands() { |
677 | [accounts.imap] |
678 | root_mailbox = "INBOX" |
679 | format = "imap" |
680 | + send_mail = 'false' |
681 | identity="username@example.com" |
682 | server_username = "null" |
683 | server_hostname = "example.com" |
684 | server_password_command = "false" |
685 | - |
686 | - [composing] |
687 | - send_mail = 'false' |
688 | "# |
689 | .as_slice(), |
690 | ) |
691 | @@ -258,13 +256,11 @@ send_mail = 'false' |
692 | [accounts.imap] |
693 | root_mailbox = "INBOX" |
694 | format = "imap" |
695 | + send_mail = 'false' |
696 | identity="username@example.com" |
697 | server_username = "null" |
698 | server_hostname = "example.com" |
699 | server_password_command = "false" |
700 | - |
701 | - [composing] |
702 | - send_mail = 'false' |
703 | "#, |
704 | ) |
705 | .unwrap(); |