conf.rs: check that all conf flags are recognized in validation

This commit adds logic in configuration file validation that checks that
each account "extra" field is empty after getting it back from the
backend validation. This is to ensure the user doesn't set options that
are invalidly stated in the documentation or by accident.

Closes #135

Configuration error (xxx): the following flags are set but are not recognized: ["index_style"] https://git.meli.delivery/meli/meli/issues/135
pull/144/head
Manos Pitsidianakis 2 years ago
parent d0de04854e
commit f5dc25ae0d
No known key found for this signature in database
GPG Key ID: 73627C2F690DF710

27
Cargo.lock generated

@ -14,6 +14,15 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]]
name = "aho-corasick"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
dependencies = [
"memchr",
]
[[package]]
name = "arrayref"
version = "0.3.6"
@ -1117,6 +1126,7 @@ dependencies = [
"pcre2",
"proc-macro2",
"quote 1.0.8",
"regex",
"serde",
"serde_derive",
"serde_json",
@ -1673,6 +1683,23 @@ dependencies = [
"rust-argon2",
]
[[package]]
name = "regex"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
version = "0.5.3"

@ -54,7 +54,7 @@ futures = "0.3.5"
async-task = "3.0.0"
num_cpus = "1.12.0"
flate2 = { version = "1.0.16", optional = true }
[target.'cfg(target_os="linux")'.dependencies]
notify-rust = { version = "^4", optional = true }
@ -64,6 +64,9 @@ quote = "^1.0"
proc-macro2 = "1.0.18"
flate2 = { version = "1.0.16", optional = true }
[dev-dependencies]
regex = "1"
[profile.release]
lto = "fat"
codegen-units = 1

@ -77,7 +77,7 @@ example configuration
[accounts.account-name]
root_mailbox = "/path/to/root/folder"
format = "Maildir"
index_style = "Compact"
listing.index_style = "Compact"
identity="email@example.com"
subscribed_mailboxes = ["folder", "folder/Sent"] # or [ "*", ] for all mailboxes
display_name = "Name"
@ -93,7 +93,7 @@ display_name = "Name"
[accounts.mbox]
root_mailbox = "/var/mail/username"
format = "mbox"
index_style = "Compact"
listing.index_style = "Compact"
identity="username@hostname.local"
composing.send_mail = { hostname = "localhost", port = 25, auth = { type = "none" }, security = { type = "none" } }
@ -138,8 +138,6 @@ The glob wildcard
can be used to match every mailbox name and path.
.It Ic identity Ar String
Your e-mail address that is inserted in the From: headers of outgoing mail.
.It Ic index_style Ar String
Sets the way mailboxes are displayed.
.El
.TS
allbox tab(:);

@ -11,7 +11,7 @@
#[accounts.account-name]
#root_mailbox = "/path/to/root/mailbox"
#format = "Maildir"
#index_style = "Conversations" # or [plain, threaded, compact]
#listing.index_style = "Conversations" # or [plain, threaded, compact]
#identity="email@example.com"
#display_name = "Name"
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
@ -26,7 +26,7 @@
#[accounts.mbox]
#root_mailbox = "/var/mail/username"
#format = "mbox"
#index_style = "Compact"
#listing.index_style = "Compact"
#identity="username@hostname.local"
#
## Setting up an IMAP account
@ -36,10 +36,10 @@
#server_hostname="mail.example.com"
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
#server_username="username@example.com"
#server_port="993" # imaps
##server_port="993" # imaps
#server_port="143" # STARTTLS
#use_starttls=true #optional
#index_style = "Conversations"
#listing.index_style = "Conversations"
#identity = "username@example.com"
#display_name = "Name Name"
### match every mailbox:
@ -51,7 +51,7 @@
#[accounts.notmuch]
#root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
#format = "notmuch"
#index_style = "conversations"
#listing.index_style = "conversations"
#identity="username@example.com"
#display_name = "Name Name"
# # notmuch mailboxes are virtual, they are defined by their alias and the notmuch query that corresponds to their content.
@ -68,7 +68,7 @@
#server_password="password"
#server_username="username@gmail.com"
#server_port="993"
#index_style = "Conversations"
#listing.index_style = "Conversations"
#identity = "username@gmail.com"
#display_name = "Name Name"
### match every mailbox:

@ -97,7 +97,7 @@ pub struct Backends {
pub struct Backend {
pub create_fn: Box<dyn Fn() -> BackendCreator>,
pub validate_conf_fn: Box<dyn Fn(&AccountSettings) -> Result<()>>,
pub validate_conf_fn: Box<dyn Fn(&mut AccountSettings) -> Result<()>>,
}
impl Default for Backends {
@ -200,7 +200,7 @@ impl Backends {
self.map.insert(key, backend);
}
pub fn validate_config(&self, key: &str, s: &AccountSettings) -> Result<()> {
pub fn validate_config(&self, key: &str, s: &mut AccountSettings) -> Result<()> {
(self
.map
.get(key)

@ -1500,12 +1500,12 @@ impl ImapType {
Ok(mailboxes)
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let mut keys: HashSet<&'static str> = Default::default();
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {{
keys.insert($var);
$s.extra.get($var).ok_or_else(|| {
$s.extra.remove($var).ok_or_else(|| {
MeliError::new(format!(
"Configuration error ({}): IMAP connection requires the field `{}` set",
$s.name.as_str(),
@ -1516,9 +1516,9 @@ impl ImapType {
($s:ident[$var:literal], $default:expr) => {{
keys.insert($var);
$s.extra
.get($var)
.remove($var)
.map(|v| {
<_>::from_str(v).map_err(|e| {
<_>::from_str(&v).map_err(|e| {
MeliError::new(format!(
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),

@ -874,7 +874,34 @@ impl JmapType {
}))
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {
$s.extra.remove($var).ok_or_else(|| {
MeliError::new(format!(
"Configuration error ({}): JMAP connection requires the field `{}` set",
$s.name.as_str(),
$var
))
})
};
($s:ident[$var:literal], $default:expr) => {
$s.extra
.remove($var)
.map(|v| {
<_>::from_str(&v).map_err(|e| {
MeliError::new(format!(
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),
$var,
v,
e
))
})
})
.unwrap_or_else(|| Ok($default))
};
}
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_username"])?;
get_conf_val!(s["server_password"])?;

@ -1316,7 +1316,7 @@ impl MaildirType {
Ok(())
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let root_path = PathBuf::from(s.root_mailbox()).expand();
if !root_path.exists() {
return Err(MeliError::new(format!(

@ -1339,7 +1339,34 @@ impl MboxType {
Ok(Box::new(ret))
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {
$s.extra.remove($var).ok_or_else(|| {
MeliError::new(format!(
"Configuration error ({}): mbox backend requires the field `{}` set",
$s.name.as_str(),
$var
))
})
};
($s:ident[$var:literal], $default:expr) => {
$s.extra
.remove($var)
.map(|v| {
<_>::from_str(&v).map_err(|e| {
MeliError::new(format!(
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),
$var,
v,
e
))
})
})
.unwrap_or_else(|| Ok($default))
};
}
let path = Path::new(s.root_mailbox.as_str()).expand();
if !path.exists() {
return Err(MeliError::new(format!(

@ -654,12 +654,12 @@ impl NntpType {
Ok(())
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let mut keys: HashSet<&'static str> = Default::default();
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {{
keys.insert($var);
$s.extra.get($var).ok_or_else(|| {
$s.extra.remove($var).ok_or_else(|| {
MeliError::new(format!(
"Configuration error ({}): NNTP connection requires the field `{}` set",
$s.name.as_str(),
@ -670,9 +670,9 @@ impl NntpType {
($s:ident[$var:literal], $default:expr) => {{
keys.insert($var);
$s.extra
.get($var)
.remove($var)
.map(|v| {
<_>::from_str(v).map_err(|e| {
<_>::from_str(&v).map_err(|e| {
MeliError::new(format!(
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),

@ -372,7 +372,7 @@ impl NotmuchDb {
}))
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let path = Path::new(s.root_mailbox.as_str()).expand();
if !path.exists() {
return Err(MeliError::new(format!(
@ -381,8 +381,8 @@ impl NotmuchDb {
s.name()
)));
}
for (k, f) in s.mailboxes.iter() {
if f.extra.get("query").is_none() {
for (k, f) in s.mailboxes.iter_mut() {
if f.extra.remove("query").is_none() {
return Err(MeliError::new(format!(
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
k

@ -223,7 +223,7 @@ fn run_app(opt: Opt) -> Result<()> {
} else {
crate::conf::get_config_file()?
};
conf::FileSettings::validate(config_path, true)?; // TODO: test for tty/interaction
conf::FileSettings::validate(config_path, true, false)?; // TODO: test for tty/interaction
return Ok(());
}
Some(SubCommand::CreateConfig { path }) => {

@ -350,10 +350,10 @@ impl FileSettings {
return Err(MeliError::new("No configuration file found."));
}
FileSettings::validate(config_path, true)
FileSettings::validate(config_path, true, false)
}
pub fn validate(path: PathBuf, interactive: bool) -> Result<Self> {
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(|e| {
MeliError::new(format!(
@ -391,7 +391,7 @@ This is required so that you don't accidentally start meli and find out later th
err
))
})?;
return FileSettings::validate(path, interactive);
return FileSettings::validate(path, interactive, clear_extras);
}
}
return Err(MeliError::new(format!(
@ -439,7 +439,7 @@ This is required so that you don't accidentally start meli and find out later th
}
s.terminal.themes.validate()?;
for (name, acc) in &s.accounts {
for (name, acc) in s.accounts.iter_mut() {
let FileAccount {
root_mailbox,
format,
@ -456,7 +456,7 @@ This is required so that you don't accidentally start meli and find out later th
} = acc.clone();
let lowercase_format = format.to_lowercase();
let s = AccountSettings {
let mut s = AccountSettings {
name: name.to_string(),
root_mailbox,
format: format.clone(),
@ -471,7 +471,16 @@ This is required so that you don't accidentally start meli and find out later th
.collect(),
extra: extra.into_iter().collect(),
};
backends.validate_config(&lowercase_format, &s)?;
backends.validate_config(&lowercase_format, &mut s)?;
if !s.extra.is_empty() {
return Err(MeliError::new(format!(
"Unrecognised configuration values: {:?}",
s.extra
)));
}
if clear_extras {
acc.extra.clear();
}
}
Ok(s)
@ -1136,7 +1145,7 @@ fn test_config_parse() {
[accounts.account-name]
root_mailbox = "/path/to/root/mailbox"
format = "Maildir"
index_style = "Conversations" # or [plain, threaded, compact]
listing.index_style = "Conversations" # or [plain, threaded, compact]
identity="email@example.com"
display_name = "Name"
subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
@ -1151,11 +1160,25 @@ subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
[accounts.mbox]
root_mailbox = "/var/mail/username"
format = "mbox"
listing.index_style = "Compact"
identity="username@hostname.local"
"#;
const EXTRA_CONFIG: &str = r#"
[accounts.mbox]
root_mailbox = "/"
format = "mbox"
index_style = "Compact"
identity="username@hostname.local"
[composing]
send_mail = '/bin/false'
"#;
const EXAMPLE_CONFIG: &str = include_str!("../docs/samples/sample-config.toml");
impl ConfigFile {
fn new() -> std::result::Result<Self, std::io::Error> {
fn new(content: &str) -> std::result::Result<Self, std::io::Error> {
let mut f = fs::File::open("/dev/urandom")?;
let mut buf = [0u8; 16];
f.read_exact(&mut buf)?;
@ -1169,7 +1192,7 @@ identity="username@hostname.local"
.create_new(true)
.append(true)
.open(&path)?;
file.write_all(TEST_CONFIG.as_bytes())?;
file.write_all(content.as_bytes())?;
Ok(ConfigFile { path, file })
}
}
@ -1180,13 +1203,46 @@ identity="username@hostname.local"
}
}
let mut new_file = ConfigFile::new().unwrap();
let err = FileSettings::validate(new_file.path.clone(), false).unwrap_err();
let mut new_file = ConfigFile::new(TEST_CONFIG).unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert!(err.details.as_ref().starts_with("You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows"));
new_file
.file
.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
.unwrap();
let err = FileSettings::validate(new_file.path.clone(), false).unwrap_err();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(err.details.as_ref(), "Configuration error (account-name): root_path `/path/to/root/mailbox` is not a valid directory.");
/* Test unrecognised configuration entries error */
let new_file = ConfigFile::new(EXTRA_CONFIG).unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(
err.details.as_ref(),
"Unrecognised configuration values: {\"index_style\": \"Compact\"}"
);
/* Test sample config */
let example_config = EXAMPLE_CONFIG.replace("\n#", "\n");
let re = regex::Regex::new(r#"root_mailbox\s*=\s*"[^"]*""#).unwrap();
let example_config = re.replace_all(
&example_config,
&format!(
r#"root_mailbox = "{}""#,
std::env::temp_dir().to_str().unwrap()
),
);
let new_file = ConfigFile::new(&example_config).unwrap();
let config = FileSettings::validate(new_file.path.clone(), false, true)
.expect("Could not parse example config!");
for (accname, acc) in config.accounts.iter() {
if !acc.extra.is_empty() {
panic!(
"In example config, account `{}` has unrecognised configuration entries: {:?}",
accname, acc.extra
);
}
}
}

Loading…
Cancel
Save