config: show explanation if `composing` field missing

pull/144/head
Manos Pitsidianakis 2021-09-04 18:50:34 +03:00
parent 09dc0a2409
commit 09f3edba76
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
2 changed files with 163 additions and 34 deletions

View File

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

View File

@ -293,6 +293,38 @@ pub fn get_config_file() -> Result<PathBuf> {
} }
} }
struct Ask {
message: String,
}
impl Ask {
fn run(self) -> bool {
let mut buffer = String::new();
let stdin = io::stdin();
let mut handle = stdin.lock();
print!("{} [Y/n] ", &self.message);
loop {
buffer.clear();
handle
.read_line(&mut buffer)
.expect("Could not read from stdin.");
match buffer.trim() {
"" | "Y" | "y" | "yes" | "YES" | "Yes" => {
return true;
}
"n" | "N" | "no" | "No" | "NO" => {
return false;
}
_ => {
print!("\n{} [Y/n] ", &self.message);
}
}
}
}
}
impl FileSettings { impl FileSettings {
pub fn new() -> Result<FileSettings> { pub fn new() -> Result<FileSettings> {
let config_path = get_config_file()?; let config_path = get_config_file()?;
@ -301,45 +333,71 @@ impl FileSettings {
if path_string.is_empty() { if path_string.is_empty() {
return Err(MeliError::new("No configuration found.")); return Err(MeliError::new("No configuration found."));
} }
println!( let ask = Ask {
"No configuration found. Would you like to generate one in {}? [Y/n]", message: format!(
path_string "No configuration found. Would you like to generate one in {}?",
); path_string
let mut buffer = String::new(); ),
let stdin = io::stdin(); };
let mut handle = stdin.lock(); if ask.run() {
create_config_file(&config_path)?;
loop { return Err(MeliError::new(
buffer.clear(); "Edit the sample configuration and relaunch meli.",
handle ));
.read_line(&mut buffer)
.expect("Could not read from stdin.");
match buffer.trim() {
"" | "Y" | "y" | "yes" | "YES" | "Yes" => {
create_config_file(&config_path)?;
return Err(MeliError::new(
"Edit the sample configuration and relaunch meli.",
));
}
"n" | "N" | "no" | "No" | "NO" => {
return Err(MeliError::new("No configuration file found."));
}
_ => {
println!(
"No configuration found. Would you like to generate one in {}? [Y/n]",
path_string
);
}
}
} }
return Err(MeliError::new("No configuration file found."));
} }
FileSettings::validate(config_path) FileSettings::validate(config_path, true)
} }
pub fn validate(path: PathBuf) -> Result<Self> { pub fn validate(path: PathBuf, interactive: bool) -> Result<Self> {
let s = pp::pp(&path)?; let s = pp::pp(&path)?;
let map: toml::map::Map<String, toml::value::Value> = toml::from_str(&s).map_err(|e| {
MeliError::new(format!(
"{}:\nConfig file is invalid TOML: {}",
path.display(),
e.to_string()
))
})?;
/*
* 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:
[composing]
send_mail = '/bin/false'
This is required so that you don't accidentally start meli and find out later that you can't send emails."#;
if interactive {
println!("{}", err_msg);
let ask = Ask {
message: format!(
"Would you like to append this dummy value in your configuration file {} and continue?",
path.display()
)
};
if ask.run() {
let mut file = OpenOptions::new().append(true).open(&path)?;
file.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
.map_err(|err| {
MeliError::new(format!(
"Could not append to {}: {}",
path.display(),
err
))
})?;
return FileSettings::validate(path, interactive);
}
}
return Err(MeliError::new(format!(
"{}\n\nEdit the {} and relaunch meli.",
if interactive { "" } else { err_msg },
path.display()
)));
}
let mut s: FileSettings = toml::from_str(&s).map_err(|e| { let mut s: FileSettings = toml::from_str(&s).map_err(|e| {
MeliError::new(format!( MeliError::new(format!(
"{}:\nConfig file contains errors: {}", "{}:\nConfig file contains errors: {}",
@ -1055,3 +1113,74 @@ mod dotaddressable {
} }
} }
} }
#[test]
fn test_config_parse() {
use std::fmt::Write;
use std::fs;
use std::io::prelude::*;
use std::path::PathBuf;
struct ConfigFile {
path: PathBuf,
file: fs::File,
}
const TEST_CONFIG: &str = r#"
[accounts.account-name]
root_mailbox = "/path/to/root/mailbox"
format = "Maildir"
index_style = "Conversations" # or [plain, threaded, compact]
identity="email@example.com"
display_name = "Name"
subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
# Set mailbox-specific settings
[accounts.account-name.mailboxes]
"INBOX" = { rename="Inbox" }
"drafts" = { rename="Drafts" }
"foobar-devel" = { ignore = true } # don't show notifications for this mailbox
# Setting up an mbox account
[accounts.mbox]
root_mailbox = "/var/mail/username"
format = "mbox"
index_style = "Compact"
identity="username@hostname.local"
"#;
impl ConfigFile {
fn new() -> 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)?;
let mut filename = String::with_capacity(2 * 16);
for byte in buf {
write!(&mut filename, "{:02X}", byte).unwrap();
}
let mut path = std::env::temp_dir();
path.push(&*filename);
let mut file = OpenOptions::new()
.create_new(true)
.append(true)
.open(&path)?;
file.write_all(TEST_CONFIG.as_bytes())?;
Ok(ConfigFile { path, file })
}
}
impl Drop for ConfigFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
let mut new_file = ConfigFile::new().unwrap();
let err = FileSettings::validate(new_file.path.clone(), false).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();
assert_eq!(err.details.as_ref(), "Configuration error (account-name): root_path `/path/to/root/mailbox` is not a valid directory.");
}