fix wizard serialization issues

This commit is contained in:
Clément DOUIN 2024-01-12 10:16:43 +01:00
parent a15e2c0442
commit 1246be8a5b
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
8 changed files with 383 additions and 37 deletions

8
Cargo.lock generated
View file

@ -1463,8 +1463,6 @@ dependencies = [
[[package]] [[package]]
name = "email-lib" name = "email-lib"
version = "0.20.0" version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b77f4c2aee25fe234194b651bfadc3b5a3e3c817ca8ba1cd0044f1dd462723"
dependencies = [ dependencies = [
"advisory-lock", "advisory-lock",
"anyhow", "anyhow",
@ -2960,8 +2958,6 @@ dependencies = [
[[package]] [[package]]
name = "mml-lib" name = "mml-lib"
version = "1.0.6" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3767365c25258656f689a63f645fff963c94716caba0dc079bf19c2119ea1df7"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"chumsky", "chumsky",
@ -3714,8 +3710,6 @@ dependencies = [
[[package]] [[package]]
name = "process-lib" name = "process-lib"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cb01de71b99c9d36dacf51218b950e03f80a86ee5f910ff723a1693796bad3"
dependencies = [ dependencies = [
"log", "log",
"once_cell", "once_cell",
@ -4252,8 +4246,6 @@ dependencies = [
[[package]] [[package]]
name = "secret-lib" name = "secret-lib"
version = "0.3.2" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545285fca1cf00676621557d25814d589264d5635e8a767c603b3ff89364370e"
dependencies = [ dependencies = [
"keyring-lib", "keyring-lib",
"process-lib", "process-lib",

View file

@ -35,8 +35,8 @@ default = [
"attachment", "attachment",
"template", "template",
# "pgp-commands", # enable PGP based on shell commands # "pgp-commands",
# "pgp-gpg", # enable # "pgp-gpg",
# "pgp-native", # "pgp-native",
] ]
@ -116,7 +116,7 @@ clap_mangen = "0.2"
console = "0.15.2" console = "0.15.2"
dialoguer = "0.10.2" dialoguer = "0.10.2"
dirs = "4.0" dirs = "4.0"
email-lib = { version = "=0.20.0", default-features = false } email-lib = { version = "=0.20.1", default-features = false }
email_address = "0.2.4" email_address = "0.2.4"
env_logger = "0.8" env_logger = "0.8"
erased-serde = "0.3" erased-serde = "0.3"
@ -125,11 +125,11 @@ keyring-lib = "=0.3.2"
log = "0.4" log = "0.4"
mail-builder = "0.3" mail-builder = "0.3"
md5 = "0.7.0" md5 = "0.7.0"
mml-lib = { version = "=1.0.6", default-features = false } mml-lib = { version = "=1.0.7", default-features = false }
oauth-lib = "=0.1.0" oauth-lib = "=0.1.0"
once_cell = "1.16" once_cell = "1.16"
process-lib = "=0.3.0" process-lib = "=0.3.1"
secret-lib = "=0.3.2" secret-lib = "=0.3.3"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
shellexpand-utils = "=0.2.0" shellexpand-utils = "=0.2.0"

View file

@ -88,7 +88,7 @@ If you just want to **discuss** about the project, feel free to join the [Matrix
Special thanks to the [NLnet foundation](https://nlnet.nl/project/Himalaya/index.html) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from: Special thanks to the [NLnet foundation](https://nlnet.nl/project/Himalaya/index.html) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from:
- [NGI Assure](https://nlnet.nl/assure/) in 2022 - [NGI Assure](https://nlnet.nl/assure/) in 2022
- [NGI Zero Untrust](https://nlnet.nl/entrust/) in 2023 - [NGI Zero Entrust](https://nlnet.nl/entrust/) in 2023
If you appreciate the project, feel free to donate using one of the following providers: If you appreciate the project, feel free to donate using one of the following providers:

View file

@ -45,6 +45,7 @@
# Rust # Rust
rust-toolchain rust-toolchain
cargo-watch
# OpenSSL # OpenSSL
openssl.dev openssl.dev

View file

@ -45,10 +45,10 @@ pub struct TomlAccountConfig {
pub flag: Option<FlagConfig>, pub flag: Option<FlagConfig>,
pub message: Option<MessageConfig>, pub message: Option<MessageConfig>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirConfig>,
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
pub imap: Option<ImapConfig>, pub imap: Option<ImapConfig>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirConfig>,
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
pub notmuch: Option<NotmuchConfig>, pub notmuch: Option<NotmuchConfig>,
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]

View file

@ -94,16 +94,21 @@ pub(crate) async fn configure(path: PathBuf) -> Result<TomlConfig> {
let path = expand::path(path); let path = expand::path(path);
println!("Writing the configuration to {path:?}"); println!("Writing the configuration to {path:?}");
let toml = pretty_serialize(&config)?;
fs::create_dir_all(path.parent().unwrap_or(&path))?;
fs::write(path, toml)?;
let mut doc = toml::to_string(&config)?.parse::<Document>()?; println!("Exiting the wizard…");
Ok(config)
}
fn pretty_serialize(config: &TomlConfig) -> Result<String> {
let mut doc: Document = toml::to_string(&config)?.parse()?;
doc.iter_mut().for_each(|(_, item)| { doc.iter_mut().for_each(|(_, item)| {
set_table_dotted(item, "folder-aliases");
set_table_dotted(item, "sync-folders-strategy");
set_table_dotted(item, "folder"); set_table_dotted(item, "folder");
if let Some(item) = get_table_mut(item, "folder") { if let Some(item) = get_table_mut(item, "folder") {
set_tables_dotted(item, ["add", "list", "expunge", "purge", "delete"]); set_tables_dotted(item, ["alias", "add", "list", "expunge", "purge", "delete"]);
} }
set_table_dotted(item, "envelope"); set_table_dotted(item, "envelope");
@ -124,7 +129,9 @@ pub(crate) async fn configure(path: PathBuf) -> Result<TomlConfig> {
); );
} }
#[cfg(feature = "maildir")]
set_table_dotted(item, "maildir"); set_table_dotted(item, "maildir");
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
{ {
set_table_dotted(item, "imap"); set_table_dotted(item, "imap");
@ -132,9 +139,10 @@ pub(crate) async fn configure(path: PathBuf) -> Result<TomlConfig> {
set_tables_dotted(item, ["passwd", "oauth2"]); set_tables_dotted(item, ["passwd", "oauth2"]);
} }
} }
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
set_table_dotted(item, "notmuch"); set_table_dotted(item, "notmuch");
set_table_dotted(item, "sendmail");
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
{ {
set_table_dotted(item, "smtp"); set_table_dotted(item, "smtp");
@ -143,14 +151,22 @@ pub(crate) async fn configure(path: PathBuf) -> Result<TomlConfig> {
} }
} }
#[cfg(feature = "sendmail")]
set_table_dotted(item, "sendmail");
#[cfg(feature = "account-sync")]
{
set_table_dotted(item, "sync");
if let Some(item) = get_table_mut(item, "sync") {
set_tables_dotted(item, ["strategy"]);
}
}
#[cfg(feature = "pgp")] #[cfg(feature = "pgp")]
set_table_dotted(item, "pgp"); set_table_dotted(item, "pgp");
}); });
fs::create_dir_all(path.parent().unwrap_or(&path))?; Ok(doc.to_string())
fs::write(path, doc.to_string())?;
Ok(config)
} }
fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> { fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> {
@ -168,3 +184,336 @@ fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a
set_table_dotted(item, key) set_table_dotted(item, key)
} }
} }
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::{account::config::TomlAccountConfig, config::TomlConfig};
fn assert_eq(config: TomlAccountConfig, expected_toml: &str) {
let config = TomlConfig {
accounts: HashMap::from_iter([("test".into(), config)]),
..Default::default()
};
let toml = super::pretty_serialize(&config).expect("serialize error");
assert_eq!(toml, expected_toml);
let expected_config = toml::from_str(&toml).expect("deserialize error");
assert_eq!(config, expected_config);
}
#[test]
fn pretty_serialize_default() {
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
..Default::default()
},
r#"[test]
email = "test@localhost"
"#,
)
}
#[cfg(feature = "account-sync")]
#[test]
fn pretty_serialize_sync_all() {
use email::{account::sync::config::SyncConfig, folder::sync::FolderSyncStrategy};
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
sync: Some(SyncConfig {
enable: Some(false),
dir: Some("/tmp/test".into()),
strategy: Some(FolderSyncStrategy::All),
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
sync.enable = false
sync.dir = "/tmp/test"
sync.strategy = "all"
"#,
);
}
#[cfg(feature = "account-sync")]
#[test]
fn pretty_serialize_sync_include() {
use std::collections::HashSet;
use email::{account::sync::config::SyncConfig, folder::sync::FolderSyncStrategy};
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
sync: Some(SyncConfig {
enable: Some(true),
dir: Some("/tmp/test".into()),
strategy: Some(FolderSyncStrategy::Include(HashSet::from_iter([
"test".into()
]))),
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
sync.enable = true
sync.dir = "/tmp/test"
sync.strategy.include = ["test"]
"#,
);
}
#[cfg(feature = "imap")]
#[test]
fn pretty_serialize_imap_passwd_cmd() {
use email::{
account::config::passwd::PasswdConfig,
imap::config::{ImapAuthConfig, ImapConfig},
};
use secret::Secret;
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
imap: Some(ImapConfig {
host: "localhost".into(),
port: 143,
login: "test@localhost".into(),
auth: ImapAuthConfig::Passwd(PasswdConfig(Secret::new_cmd("pass show test"))),
..Default::default()
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
imap.host = "localhost"
imap.port = 143
imap.login = "test@localhost"
imap.passwd.cmd = "pass show test"
"#,
);
}
#[cfg(feature = "imap")]
#[test]
fn pretty_serialize_imap_passwd_cmds() {
use email::{
account::config::passwd::PasswdConfig,
imap::config::{ImapAuthConfig, ImapConfig},
};
use secret::Secret;
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
imap: Some(ImapConfig {
host: "localhost".into(),
port: 143,
login: "test@localhost".into(),
auth: ImapAuthConfig::Passwd(PasswdConfig(Secret::new_cmd(vec![
"pass show test",
"tr -d '[:blank:]'",
]))),
..Default::default()
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
imap.host = "localhost"
imap.port = 143
imap.login = "test@localhost"
imap.passwd.cmd = ["pass show test", "tr -d '[:blank:]'"]
"#,
);
}
#[cfg(feature = "imap")]
#[test]
fn pretty_serialize_imap_oauth2() {
use email::{
account::config::oauth2::OAuth2Config,
imap::config::{ImapAuthConfig, ImapConfig},
};
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
imap: Some(ImapConfig {
host: "localhost".into(),
port: 143,
login: "test@localhost".into(),
auth: ImapAuthConfig::OAuth2(OAuth2Config {
client_id: "client-id".into(),
auth_url: "auth-url".into(),
token_url: "token-url".into(),
..Default::default()
}),
..Default::default()
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
imap.host = "localhost"
imap.port = 143
imap.login = "test@localhost"
imap.oauth2.method = "xoauth2"
imap.oauth2.client-id = "client-id"
imap.oauth2.auth-url = "auth-url"
imap.oauth2.token-url = "token-url"
imap.oauth2.pkce = false
imap.oauth2.scopes = []
"#,
);
}
#[cfg(feature = "maildir")]
#[test]
fn pretty_serialize_maildir() {
use email::maildir::config::MaildirConfig;
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
maildir: Some(MaildirConfig {
root_dir: "/tmp/test".into(),
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
maildir.root-dir = "/tmp/test"
"#,
);
}
#[cfg(feature = "smtp")]
#[test]
fn pretty_serialize_smtp_passwd_cmd() {
use email::{
account::config::passwd::PasswdConfig,
smtp::config::{SmtpAuthConfig, SmtpConfig},
};
use secret::Secret;
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
smtp: Some(SmtpConfig {
host: "localhost".into(),
port: 143,
login: "test@localhost".into(),
auth: SmtpAuthConfig::Passwd(PasswdConfig(Secret::new_cmd("pass show test"))),
..Default::default()
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
smtp.host = "localhost"
smtp.port = 143
smtp.login = "test@localhost"
smtp.passwd.cmd = "pass show test"
"#,
);
}
#[cfg(feature = "smtp")]
#[test]
fn pretty_serialize_smtp_passwd_cmds() {
use email::{
account::config::passwd::PasswdConfig,
smtp::config::{SmtpAuthConfig, SmtpConfig},
};
use secret::Secret;
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
smtp: Some(SmtpConfig {
host: "localhost".into(),
port: 143,
login: "test@localhost".into(),
auth: SmtpAuthConfig::Passwd(PasswdConfig(Secret::new_cmd(vec![
"pass show test",
"tr -d '[:blank:]'",
]))),
..Default::default()
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
smtp.host = "localhost"
smtp.port = 143
smtp.login = "test@localhost"
smtp.passwd.cmd = ["pass show test", "tr -d '[:blank:]'"]
"#,
);
}
#[cfg(feature = "smtp")]
#[test]
fn pretty_serialize_smtp_oauth2() {
use email::{
account::config::oauth2::OAuth2Config,
smtp::config::{SmtpAuthConfig, SmtpConfig},
};
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
smtp: Some(SmtpConfig {
host: "localhost".into(),
port: 143,
login: "test@localhost".into(),
auth: SmtpAuthConfig::OAuth2(OAuth2Config {
client_id: "client-id".into(),
auth_url: "auth-url".into(),
token_url: "token-url".into(),
..Default::default()
}),
..Default::default()
}),
..Default::default()
},
r#"[test]
email = "test@localhost"
smtp.host = "localhost"
smtp.port = 143
smtp.login = "test@localhost"
smtp.oauth2.method = "xoauth2"
smtp.oauth2.client-id = "client-id"
smtp.oauth2.auth-url = "auth-url"
smtp.oauth2.token-url = "token-url"
smtp.oauth2.pkce = false
smtp.oauth2.scopes = []
"#,
);
}
#[cfg(feature = "pgp")]
#[test]
fn pretty_serialize_pgp_cmds() {
use email::account::config::pgp::PgpConfig;
assert_eq(
TomlAccountConfig {
email: "test@localhost".into(),
pgp: Some(PgpConfig::Cmds(Default::default())),
..Default::default()
},
r#"[test]
email = "test@localhost"
pgp.backend = "cmds"
"#,
);
}
}

View file

@ -135,6 +135,8 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
let auth = if oauth2_enabled { let auth = if oauth2_enabled {
let mut config = OAuth2Config::default(); let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME) let method_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 mechanism") .with_prompt("IMAP OAuth 2.0 mechanism")
@ -245,13 +247,13 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
config.auth_url.clone(), config.auth_url.clone(),
config.token_url.clone(), config.token_url.clone(),
)? )?
.with_redirect_host(config.redirect_host.clone()) .with_redirect_host(redirect_host.to_owned())
.with_redirect_port(config.redirect_port) .with_redirect_port(redirect_port)
.build()?; .build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new() let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(config.redirect_host.clone()) .with_redirect_host(redirect_host.to_owned())
.with_redirect_port(config.redirect_port); .with_redirect_port(redirect_port);
if config.pkce { if config.pkce {
auth_code_grant = auth_code_grant.with_pkce(); auth_code_grant = auth_code_grant.with_pkce();
@ -312,7 +314,7 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
_ => Default::default(), _ => Default::default(),
}; };
ImapAuthConfig::Passwd(PasswdConfig { passwd: secret }) ImapAuthConfig::Passwd(PasswdConfig(secret))
}; };
let config = ImapConfig { let config = ImapConfig {

View file

@ -135,6 +135,8 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
let auth = if oauth2_enabled { let auth = if oauth2_enabled {
let mut config = OAuth2Config::default(); let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME) let method_idx = Select::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 mechanism") .with_prompt("SMTP OAuth 2.0 mechanism")
@ -245,13 +247,13 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
config.auth_url.clone(), config.auth_url.clone(),
config.token_url.clone(), config.token_url.clone(),
)? )?
.with_redirect_host(config.redirect_host.clone()) .with_redirect_host(redirect_host.to_owned())
.with_redirect_port(config.redirect_port) .with_redirect_port(redirect_port)
.build()?; .build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new() let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(config.redirect_host.clone()) .with_redirect_host(redirect_host.to_owned())
.with_redirect_port(config.redirect_port); .with_redirect_port(redirect_port);
if config.pkce { if config.pkce {
auth_code_grant = auth_code_grant.with_pkce(); auth_code_grant = auth_code_grant.with_pkce();
@ -312,7 +314,7 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
_ => Default::default(), _ => Default::default(),
}; };
SmtpAuthConfig::Passwd(PasswdConfig { passwd: secret }) SmtpAuthConfig::Passwd(PasswdConfig(secret))
}; };
let config = SmtpConfig { let config = SmtpConfig {