meli: make config error more user-friendly

If `send_mail` is incorrect, display a long-ish list of valid examples.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/335/head
Manos Pitsidianakis 2023-12-29 15:54:06 +02:00
parent f63774fa6d
commit f0866a3965
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
8 changed files with 213 additions and 30 deletions

69
Cargo.lock generated
View File

@ -616,6 +616,12 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.2"
@ -1002,6 +1008,16 @@ dependencies = [
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
name = "inotify"
version = "0.7.1"
@ -1271,7 +1287,7 @@ dependencies = [
"crossbeam",
"flate2",
"futures",
"indexmap",
"indexmap 1.9.3",
"libc",
"libz-sys",
"linkify",
@ -1312,7 +1328,7 @@ dependencies = [
"flate2",
"futures",
"imap-codec",
"indexmap",
"indexmap 1.9.3",
"isahc",
"libc",
"libloading",
@ -2021,6 +2037,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
@ -2307,12 +2332,37 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.5.11"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"indexmap",
"indexmap 2.0.1",
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap 2.0.1",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
@ -2688,6 +2738,15 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5"
dependencies = [
"memchr",
]
[[package]]
name = "ws2_32-sys"
version = "0.2.1"

View File

@ -44,7 +44,7 @@ smallvec = { version = "^1.5.0", features = ["serde"] }
structopt = { version = "0.3.14", default-features = false }
svg_crate = { version = "^0.13", optional = true, package = "svg" }
termion = { version = "1.5.1", default-features = false }
toml = { version = "0.5.6", default-features = false, features = ["preserve_order"] }
toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] }
xdg = "2.1.0"
[dependencies.pcre2]

View File

@ -439,18 +439,16 @@ impl FileSettings {
pub fn validate(path: PathBuf, interactive: bool, clear_extras: bool) -> Result<Self> {
let s = pp::pp(&path)?;
let map: toml::map::Map<String, toml::value::Value> =
toml::from_str(&s).map_err(|err| {
Error::new(format!(
"{}:\nConfig file is invalid TOML: {}",
path.display(),
err
))
})?;
/*
* Check that a global composing option is set and return a user-friendly
* error message because the default serde one is confusing.
*/
let map: toml::value::Table = toml::from_str(&s).map_err(|err| {
Error::new(format!(
"{}: Config file is invalid TOML; {}",
path.display(),
err
))
})?;
// Check that a global composing option is set and return a user-friendly
// error message because the default serde one is confusing.
if !map.contains_key("composing") {
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:
@ -484,7 +482,7 @@ This is required so that you don't accidentally start meli and find out later th
}
let mut s: FileSettings = toml::from_str(&s).map_err(|err| {
Error::new(format!(
"{}:\nConfig file contains errors: {}",
"{}: Config file contains errors; {}",
path.display(),
err
))

View File

@ -23,6 +23,7 @@
use std::collections::HashMap;
use melib::{email::HeaderName, ToggleFlag};
use serde::{de, Deserialize, Deserializer};
use super::{
default_vals::{ask, false_val, none, true_val},
@ -176,7 +177,7 @@ pub mod strings {
named_unit_variant!(server_submission);
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum SendMail {
#[cfg(feature = "smtp")]
@ -202,3 +203,103 @@ impl From<ComposeHook> for crate::mail::hooks::Hook {
Self::new_shell_command(c.name.into(), c.command)
}
}
const SENDMAIL_ERR_HELP: &str = r#"Invalid `send_mail` value.
Here are some valid examples:
Use server submission in protocols that support it (JMAP, NNTP)
===============================================================
send_mail = "server_submission"
Using a shell script
====================
send_mail = "msmtp --read-recipients --read-envelope-from"
Direct SMTP connection
======================
send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } }
[composing.send_mail]
hostname = "mail.example.com"
port = 587
auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } }
security = { type = "TLS", danger_accept_invalid_certs = true } }
`send_mail` direct SMTP connection fields:
- hostname: text
- port: valid port number
- envelope_from: text (optional, default is empty),
- auth: ...
- security: ... (optional, default is "auto")
- extensions: ... (optional, default is PIPELINING, CHUNKING, PRDR, 8BITMIME, BINARYMIME, SMTPUTF8, AUTH and DSN_NOTIFY)
Possible values for `send_mail.auth`:
No authentication:
auth = { type = "none" }
Regular authentication:
Note: `require_auth` and `auth_type` are optional and can be skipped.
auth = { type = "auto", username = "...", password = "...", require_auth = true, auth_type = ... }
password can be:
password = { type = "raw", value = "..." }
password = { type = "command_eval", value = "/path/to/password_script.sh" }
XOAuth2 authentication:
Note: `require_auth` is optional and can be skipped.
auth = { type = "xoauth2", token_command = "...", require_auth = true }
Possible values for `send_mail.auth.auth_type` when `auth.type` is "auto":
auth_type = { plain = false, login = true }
Possible values for `send_mail.security`:
Note that in all cases field `danger_accept_invalid_certs` is optional and its default value is false.
security = "none"
security = { type = "auto", danger_accept_invalid_certs = false }
security = { type = "STARTTLS", danger_accept_invalid_certs = false }
security = { type = "TLS", danger_accept_invalid_certs = false }
Possible values for `send_mail.extensions` (All optional and have default values `true`:
pipelining
chunking
8bitmime
prdr
binarymime
smtputf8
auth
dsn_notify: Array of options e.g. ["FAILURE"]
"#;
impl<'de> Deserialize<'de> for SendMail {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SendMailInner {
#[cfg(feature = "smtp")]
Smtp(melib::smtp::SmtpServerConf),
#[serde(with = "strings::server_submission")]
ServerSubmission,
ShellCommand(String),
}
match <SendMailInner>::deserialize(deserializer) {
#[cfg(feature = "smtp")]
Ok(SendMailInner::Smtp(v)) => Ok(SendMail::Smtp(v)),
Ok(SendMailInner::ServerSubmission) => Ok(SendMail::ServerSubmission),
Ok(SendMailInner::ShellCommand(v)) => Ok(SendMail::ShellCommand(v)),
Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)),
}
}
}

View File

@ -460,20 +460,32 @@ fn test_color_de() {
);
};
($s:literal, err $v:literal) => {
test_color!($s, err $v, "")
};
($s:literal, err $v:literal, $extra:literal) => {
assert_eq!(
toml::from_str::<V>(std::concat!("k = \"", $s, "\""))
.unwrap_err()
.to_string(),
$v.to_string()
std::concat!(
"TOML parse error at line 1, column 5\n |\n1 | k = \"",
$s,
"\"\n | ",
$extra,
"^^^^^^\n",
$v,
'\n',
)
.to_string()
);
};
}
test_color!("#Ff6600", ok Color::Rgb(255, 102, 0));
test_color!("#2E3440", ok Color::Rgb(46, 52, 64));
test_color!("#f60", ok Color::Rgb(255, 102, 0));
test_color!("#gb0", err "invalid `color` value for key `k` at line 1 column 1");
test_color!("#gb0", err "invalid `color` value");
test_color!("Olive", ok Color::Byte(3));
test_color!("Oafahifdave", err "invalid `color` value for key `k` at line 1 column 1");
test_color!("Oafahifdave", err "invalid `color` value", "^^^^^^^");
}
impl Serialize for Color {

View File

@ -439,11 +439,23 @@ fn test_key_serde() {
);
};
($s:literal, err $v:literal) => {
test_key!($s, err $v, "^")
};
($s:literal, err $v:literal, $extra:literal) => {
assert_eq!(
toml::from_str::<V>(std::concat!("k = \"", $s, "\""))
.unwrap_err()
.to_string(),
$v.to_string()
std::concat!(
"TOML parse error at line 1, column 5\n |\n1 | k = \"",
$s,
"\"\n | ",
$extra,
"^^^^\n",
$v,
'\n',
)
.to_string()
);
};
}
@ -468,9 +480,9 @@ fn test_key_serde() {
test_key!("M-a", ok Key::Alt('a') );
test_key!("F1", ok Key::F(1) );
test_key!("F12", ok Key::F(12) );
test_key!("C-V", err "`V` should be a lowercase and alphanumeric character instead. for key `k` at line 1 column 5");
test_key!("M-V", err "`V` should be a lowercase and alphanumeric character instead. for key `k` at line 1 column 5");
test_key!("F13", err "`13` should be a number 1 <= n <= 12 instead. for key `k` at line 1 column 5");
test_key!("Fc", err "`c` should be a number 1 <= n <= 12 instead. for key `k` at line 1 column 5");
test_key!("adsfsf", err "Cannot derive shortcut from `adsfsf`. Please consult the manual for valid key inputs. for key `k` at line 1 column 5");
test_key!("C-V", err "`V` should be a lowercase and alphanumeric character instead.");
test_key!("M-V", err "`V` should be a lowercase and alphanumeric character instead.");
test_key!("F13", err "`13` should be a number 1 <= n <= 12 instead.");
test_key!("Fc", err "`c` should be a number 1 <= n <= 12 instead.", "");
test_key!("adsfsf", err "Cannot derive shortcut from `adsfsf`. Please consult the manual for valid key inputs.", "^^^^");
}

View File

@ -196,6 +196,7 @@ pub extern crate futures;
#[allow(unused_imports)]
#[macro_use]
pub extern crate indexmap;
pub extern crate serde_path_to_error;
pub extern crate smallvec;
pub extern crate smol;
pub extern crate uuid;

View File

@ -191,7 +191,7 @@ pub struct SmtpExtensionSupport {
#[serde(default = "crate::conf::true_val")]
chunking: bool,
/// [RFC 6152: SMTP Service Extension for 8-bit MIME Transport](https://www.rfc-editor.org/rfc/rfc6152)
#[serde(default = "crate::conf::true_val")]
#[serde(alias = "8bitmime", default = "crate::conf::true_val")]
_8bitmime: bool,
/// Essentially, the PRDR extension to SMTP allows (but does not require) an
/// SMTP server to issue multiple responses after a message has been