diff --git a/Cargo.lock b/Cargo.lock index 874e505..2244aab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,7 +211,7 @@ dependencies = [ "futures-lite 2.3.0", "parking", "polling 3.7.3", - "rustix 0.38.34", + "rustix 0.38.35", "slab", "tracing", "windows-sys 0.59.0", @@ -250,7 +250,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.48.0", ] @@ -277,7 +277,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix 0.38.35", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -560,9 +560,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.14" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "jobserver", "libc", @@ -671,14 +671,14 @@ dependencies = [ "anstyle", "clap_lex", "strsim 0.11.1", - "terminal_size 0.3.0", + "terminal_size", ] [[package]] name = "clap_complete" -version = "4.5.23" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531d7959c5bbb6e266cecdd0f20213639c3a5c3e4d615f97db87661745f781ff" +checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b" dependencies = [ "clap", ] @@ -774,19 +774,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1309,12 +1296,6 @@ dependencies = [ "serde", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoding_rs" version = "0.8.34" @@ -1463,9 +1444,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", @@ -1910,13 +1891,11 @@ dependencies = [ "clap_mangen", "color-eyre", "comfy-table", - "console", "crossterm 0.27.0", "dirs 4.0.0", "email-lib", "email_address", "erased-serde", - "indicatif", "inquire", "mail-builder", "md5", @@ -1924,6 +1903,7 @@ dependencies = [ "oauth-lib", "once_cell", "petgraph", + "pimalaya-tui", "process-lib", "secret-lib", "serde", @@ -1931,14 +1911,12 @@ dependencies = [ "serde_json", "shellexpand-utils", "sled", - "terminal_size 0.1.17", "tokio", "toml", "toml_edit 0.22.20", "tracing", "tracing-error", "tracing-subscriber", - "unicode-width", "url", "uuid", ] @@ -2121,7 +2099,7 @@ dependencies = [ "hyper-util", "log", "rustls 0.23.12", - "rustls-native-certs 0.7.2", + "rustls-native-certs 0.7.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -2213,7 +2191,7 @@ source = "git+https://github.com/pimalaya/imap-client#02d6bce5513c8ec6ac3aff0e7b dependencies = [ "imap-next", "once_cell", - "rustls-native-certs 0.7.2", + "rustls-native-certs 0.7.3", "thiserror", "tokio", "tokio-rustls 0.26.0", @@ -2223,7 +2201,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0-alpha.4" -source = "git+https://github.com/duesee/imap-codec#fff8355ad0f7133be9e58919be5a6f05f684d421" +source = "git+https://github.com/duesee/imap-codec#95de04494f89464a59c114859217e6119a18d426" dependencies = [ "abnf-core", "base64 0.22.1", @@ -2250,7 +2228,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0-alpha.3" -source = "git+https://github.com/duesee/imap-codec#fff8355ad0f7133be9e58919be5a6f05f684d421" +source = "git+https://github.com/duesee/imap-codec#95de04494f89464a59c114859217e6119a18d426" dependencies = [ "base64 0.22.1", "bounded-static", @@ -2276,19 +2254,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indicatif" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - [[package]] name = "inotify" version = "0.9.6" @@ -2938,12 +2903,6 @@ dependencies = [ "libm", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "oauth-lib" version = "0.1.1" @@ -3240,6 +3199,22 @@ dependencies = [ "z-base-32", ] +[[package]] +name = "pimalaya-tui" +version = "0.1.0" +source = "git+https://github.com/pimalaya/tui#80660dfaf9daafbaa716c711e510bf3cfd04cd69" +dependencies = [ + "crossterm 0.25.0", + "dirs 4.0.0", + "email-lib", + "email_address", + "inquire", + "oauth-lib", + "secret-lib", + "shellexpand-utils", + "thiserror", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -3336,17 +3311,11 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix 0.38.35", "tracing", "windows-sys 0.59.0", ] -[[package]] -name = "portable-atomic" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" - [[package]] name = "ppv-lite86" version = "0.2.20" @@ -3358,9 +3327,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a909e6e8053fa1a5ad670f5816c7d93029ee1fa8898718490544a6b0d5d38b3e" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", "syn 2.0.76", @@ -3710,9 +3679,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -3733,9 +3702,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", @@ -3766,7 +3735,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.6", + "rustls-webpki 0.102.7", "subtle", "zeroize", ] @@ -3785,9 +3754,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04182dffc9091a404e0fc069ea5cd60e5b866c3adf881eff99a32d048242dffa" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", "rustls-pemfile 2.1.3", @@ -3833,9 +3802,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" dependencies = [ "aws-lc-rs", "ring", @@ -4367,27 +4336,17 @@ dependencies = [ "cfg-if", "fastrand 2.1.1", "once_cell", - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.59.0", ] -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.48.0", ] @@ -4438,9 +4397,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -4921,7 +4880,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.34", + "rustix 0.38.35", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d4878b4..32d309b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,24 +23,24 @@ default = [ "smtp", "sendmail", - "wizard", # "keyring", # "oauth2", + "wizard", # "pgp-commands", # "pgp-gpg", # "pgp-native", ] -imap = ["email-lib/imap"] -maildir = ["email-lib/maildir"] -notmuch = ["email-lib/notmuch"] -smtp = ["email-lib/smtp"] -sendmail = ["email-lib/sendmail"] +imap = ["email-lib/imap", "pimalaya-tui/imap"] +maildir = ["email-lib/maildir", "pimalaya-tui/maildir"] +notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"] +smtp = ["email-lib/smtp", "pimalaya-tui/smtp"] +sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"] -keyring = ["email-lib/keyring", "secret-lib?/keyring-tokio"] -oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "keyring"] -wizard = ["dep:secret-lib", "dep:toml_edit", "email-lib/autoconfig"] +keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib?/keyring-tokio"] +oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"] +wizard = ["dep:email_address", "dep:secret-lib", "dep:toml_edit", "email-lib/autoconfig"] pgp = [] pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"] @@ -55,13 +55,11 @@ clap_complete = "4.4" clap_mangen = "0.2" color-eyre = "0.6.3" comfy-table = "7.1.1" -console = "0.15.2" crossterm = { version = "0.27", features = ["serde"] } dirs = "4" email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] } -email_address = "0.2.4" +email_address = { version = "0.2", optional = true } erased-serde = "0.3" -indicatif = "0.17" inquire = "0.7.4" mail-builder = "0.3" md5 = "0.7" @@ -69,6 +67,7 @@ mml-lib = { version = "=1.0.14", default-features = false, features = ["derive"] oauth-lib = { version = "=0.1.1", optional = true } once_cell = "1.16" petgraph = "0.6" +pimalaya-tui = { version = "=0.1.0", default-features = false, features = ["email", "path"] } process-lib = { version = "=0.4.2", features = ["derive"] } secret-lib = { version = "=0.4.6", default-features = false, features = ["command", "derive"], optional = true } serde = { version = "1", features = ["derive"] } @@ -76,20 +75,19 @@ serde-toml-merge = "0.3" serde_json = "1" shellexpand-utils = "=0.2.1" sled = "=0.34.7" -terminal_size = "0.1" tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } toml = "0.8" toml_edit = { version = "0.22", optional = true } tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -unicode-width = "0.1" url = "2.2" uuid = { version = "0.8", features = ["v4"] } [patch.crates-io] -oauth-lib = { git = "https://github.com/pimalaya/core" } imap-codec = { git = "https://github.com/duesee/imap-codec" } imap-next = { git = "https://github.com/duesee/imap-next", branch = "jakoschiko_poison-message-with-fragmentizer" } imap-client = { git = "https://github.com/pimalaya/imap-client" } +oauth-lib = { git = "https://github.com/pimalaya/core" } email-lib = { git = "https://github.com/pimalaya/core" } +pimalaya-tui = { git = "https://github.com/pimalaya/tui" } diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index 6a3fb0a..4e18780 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -4,12 +4,12 @@ use color_eyre::Result; use email::imap::config::ImapAuthConfig; #[cfg(feature = "smtp")] use email::smtp::config::SmtpAuthConfig; +#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))] +use pimalaya_tui::prompt; use tracing::info; #[cfg(any(feature = "imap", feature = "smtp"))] use tracing::{debug, warn}; -#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))] -use crate::ui::prompt; use crate::{account::arg::name::AccountNameArg, config::TomlConfig, printer::Printer}; /// Configure an account. @@ -74,12 +74,14 @@ impl AccountConfigureCommand { if let Some(ref config) = account_config.imap { match &config.auth { ImapAuthConfig::Passwd(config) => { - config.configure(|| prompt::passwd("IMAP password")).await + config + .configure(|| Ok(prompt::password("IMAP password")?)) + .await } #[cfg(feature = "oauth2")] ImapAuthConfig::OAuth2(config) => { config - .configure(|| prompt::secret("IMAP OAuth 2.0 client secret")) + .configure(|| Ok(prompt::secret("IMAP OAuth 2.0 clientsecret")?)) .await } }?; @@ -89,12 +91,14 @@ impl AccountConfigureCommand { if let Some(ref config) = account_config.smtp { match &config.auth { SmtpAuthConfig::Passwd(config) => { - config.configure(|| prompt::passwd("SMTP password")).await + config + .configure(|| Ok(prompt::password("SMTP password")?)) + .await } #[cfg(feature = "oauth2")] SmtpAuthConfig::OAuth2(config) => { config - .configure(|| prompt::secret("SMTP OAuth 2.0 client secret")) + .configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?)) .await } }?; @@ -104,7 +108,7 @@ impl AccountConfigureCommand { if let Some(ref config) = account_config.pgp { config .configure(&account_config.email, || { - prompt::passwd("PGP secret key password") + Ok(prompt::password("PGP secret key password")?) }) .await?; } diff --git a/src/account/wizard.rs b/src/account/wizard.rs index 1562643..d1b3112 100644 --- a/src/account/wizard.rs +++ b/src/account/wizard.rs @@ -1,9 +1,6 @@ -use color_eyre::{eyre::OptionExt, Result}; -use email_address::EmailAddress; -use inquire::validator::{ErrorMessage, Validation}; -use std::{path::PathBuf, str::FromStr}; +use color_eyre::Result; +use pimalaya_tui::{print, prompt}; -use crate::wizard_warn; use crate::{ backend::{self, config::BackendConfig, BackendKind}, message::config::{MessageConfig, MessageSendConfig}, @@ -11,104 +8,66 @@ use crate::{ use super::TomlAccountConfig; -pub(crate) async fn configure() -> Result> { - let mut config = TomlAccountConfig { - email: inquire::Text::new("Email address: ") - .with_validator(|email: &_| { - if EmailAddress::is_valid(email) { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid(ErrorMessage::Custom(format!( - "Invalid email address: {email}" - )))) - } - }) - .prompt()?, +pub async fn configure() -> Result<(String, TomlAccountConfig)> { + let email = prompt::email("Email address:", None)?; + let mut config = TomlAccountConfig { + email: email.to_string(), ..Default::default() }; - let addr = EmailAddress::from_str(&config.email).unwrap(); - - #[cfg(feature = "wizard")] let autoconfig_email = config.email.to_owned(); - #[cfg(feature = "wizard")] let autoconfig = tokio::spawn(async move { email::autoconfig::from_addr(&autoconfig_email).await.ok() }); - let account_name = inquire::Text::new("Account name: ") - .with_default( - addr.domain() - .split_once('.') - .ok_or_eyre("not a valid domain, without any .")? - .0, - ) - .prompt()?; + let default_account_name = email + .domain() + .split_once('.') + .map(|domain| domain.0) + .unwrap_or(email.domain()); + let account_name = prompt::text("Account name:", Some(default_account_name))?; - config.display_name = Some( - inquire::Text::new("Full display name: ") - .with_default(addr.local_part()) - .prompt()?, - ); + config.display_name = Some(prompt::text( + "Full display name:", + Some(email.local_part()), + )?); - config.downloads_dir = Some(PathBuf::from( - inquire::Text::new("Downloads directory: ") - .with_default("~/Downloads") - .prompt()?, - )); + config.downloads_dir = Some(prompt::path("Downloads directory:", Some("~/Downloads"))?); - let email = &config.email; - #[cfg(feature = "wizard")] let autoconfig = autoconfig.await?; - #[cfg(feature = "wizard")] let autoconfig = autoconfig.as_ref(); - #[cfg(feature = "wizard")] if let Some(config) = autoconfig { if config.is_gmail() { println!(); - wizard_warn!("Warning: Google passwords cannot be used directly, see:"); - wizard_warn!("https://pimalaya.org/himalaya/cli/latest/configuration/gmail.html"); + print::warn("Warning: Google passwords cannot be used directly, see:"); + print::warn("https://github.com/pimalaya/himalaya?tab=readme-ov-file#configuration"); println!(); } } - match backend::wizard::configure( - &account_name, - email, - #[cfg(feature = "wizard")] - autoconfig, - ) - .await? - { + match backend::wizard::configure(&account_name, &email, autoconfig).await? { #[cfg(feature = "imap")] - Some(BackendConfig::Imap(imap_config)) => { + BackendConfig::Imap(imap_config) => { config.imap = Some(imap_config); config.backend = Some(BackendKind::Imap); } #[cfg(feature = "maildir")] - Some(BackendConfig::Maildir(mdir_config)) => { + BackendConfig::Maildir(mdir_config) => { config.maildir = Some(mdir_config); config.backend = Some(BackendKind::Maildir); } #[cfg(feature = "notmuch")] - Some(BackendConfig::Notmuch(notmuch_config)) => { + BackendConfig::Notmuch(notmuch_config) => { config.notmuch = Some(notmuch_config); config.backend = Some(BackendKind::Notmuch); } - _ => (), + _ => unreachable!(), }; - match backend::wizard::configure_sender( - &account_name, - email, - #[cfg(feature = "wizard")] - autoconfig, - ) - .await? - { + match backend::wizard::configure_sender(&account_name, &email, autoconfig).await? { #[cfg(feature = "smtp")] - Some(BackendConfig::Smtp(smtp_config)) => { + BackendConfig::Smtp(smtp_config) => { config.smtp = Some(smtp_config); config.message = Some(MessageConfig { send: Some(MessageSendConfig { @@ -119,7 +78,7 @@ pub(crate) async fn configure() -> Result> { }); } #[cfg(feature = "sendmail")] - Some(BackendConfig::Sendmail(sendmail_config)) => { + BackendConfig::Sendmail(sendmail_config) => { config.sendmail = Some(sendmail_config); config.message = Some(MessageConfig { send: Some(MessageSendConfig { @@ -129,8 +88,8 @@ pub(crate) async fn configure() -> Result> { ..Default::default() }); } - _ => (), + _ => unreachable!(), }; - Ok(Some((account_name, config))) + Ok((account_name, config)) } diff --git a/src/backend/wizard.rs b/src/backend/wizard.rs index 35285ec..f03dfaf 100644 --- a/src/backend/wizard.rs +++ b/src/backend/wizard.rs @@ -1,17 +1,7 @@ use color_eyre::Result; use email::autoconfig::config::AutoConfig; -use inquire::Select; - -#[cfg(feature = "imap")] -use crate::imap; -#[cfg(feature = "maildir")] -use crate::maildir; -#[cfg(feature = "notmuch")] -use crate::notmuch; -#[cfg(feature = "sendmail")] -use crate::sendmail; -#[cfg(feature = "smtp")] -use crate::smtp; +use email_address::EmailAddress; +use pimalaya_tui::{prompt, wizard}; use super::{config::BackendConfig, BackendKind}; @@ -24,6 +14,34 @@ const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[ BackendKind::Notmuch, ]; +pub async fn configure( + account_name: &str, + email: &EmailAddress, + autoconfig: Option<&AutoConfig>, +) -> Result { + let backend = prompt::item("Default backend:", &*DEFAULT_BACKEND_KINDS, None)?; + + match backend { + #[cfg(feature = "imap")] + BackendKind::Imap => { + let config = wizard::imap::start(account_name, email, autoconfig).await?; + Ok(BackendConfig::Imap(config)) + } + #[cfg(feature = "maildir")] + BackendKind::Maildir => { + let config = wizard::maildir::start(account_name)?; + Ok(BackendConfig::Maildir(config)) + } + // TODO + // #[cfg(feature = "notmuch")] + // BackendKind::Notmuch => { + // let config = wizard::notmuch::start()?; + // Ok(BackendConfig::Notmuch(config)) + // } + _ => unreachable!(), + } +} + const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[ #[cfg(feature = "smtp")] BackendKind::Smtp, @@ -31,63 +49,30 @@ const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[ BackendKind::Sendmail, ]; -pub(crate) async fn configure( +pub async fn configure_sender( account_name: &str, - email: &str, + email: &EmailAddress, autoconfig: Option<&AutoConfig>, -) -> Result> { - let kind = Select::new("Default email backend", DEFAULT_BACKEND_KINDS.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()?; +) -> Result { + let backend = prompt::item( + "Backend for sending messages:", + &*SEND_MESSAGE_BACKEND_KINDS, + None, + )?; - let config = match kind { - #[cfg(feature = "imap")] - Some(kind) if kind == BackendKind::Imap => Some( - imap::wizard::configure( - account_name, - email, - #[cfg(feature = "wizard")] - autoconfig, - ) - .await?, - ), - #[cfg(feature = "maildir")] - Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?), - #[cfg(feature = "notmuch")] - Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?), - _ => None, - }; - - Ok(config) -} - -pub(crate) async fn configure_sender( - account_name: &str, - email: &str, - autoconfig: Option<&AutoConfig>, -) -> Result> { - let kind = Select::new( - "Backend for sending messages", - SEND_MESSAGE_BACKEND_KINDS.to_vec(), - ) - .with_starting_cursor(0) - .prompt_skippable()?; - - let config = match kind { + match backend { + // TODO #[cfg(feature = "smtp")] - Some(kind) if kind == BackendKind::Smtp => Some( - smtp::wizard::configure( - account_name, - email, - #[cfg(feature = "wizard")] - autoconfig, - ) - .await?, - ), + BackendKind::Smtp => { + let config = wizard::smtp::start(account_name, email, autoconfig).await?; + Ok(BackendConfig::Smtp(config)) + } + // TODO #[cfg(feature = "sendmail")] - Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?), - _ => None, - }; - - Ok(config) + BackendKind::Sendmail => { + let config = wizard::sendmail::start()?; + Ok(BackendConfig::Sendmail(config)) + } + _ => unreachable!(), + } } diff --git a/src/config/mod.rs b/src/config/mod.rs index bced744..69a1b11 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,6 +1,8 @@ #[cfg(feature = "wizard")] pub mod wizard; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; + use color_eyre::{ eyre::{bail, eyre, Context}, Result, @@ -11,16 +13,15 @@ use email::{ account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig, folder::config::FolderConfig, message::config::MessageConfig, }; +#[cfg(feature = "wizard")] +use pimalaya_tui::print; use serde::{Deserialize, Serialize}; use serde_toml_merge::merge; use shellexpand_utils::{canonicalize, expand}; -use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; use toml::{self, Value}; use tracing::debug; use crate::account::config::{ListAccountsTableConfig, TomlAccountConfig}; -#[cfg(feature = "wizard")] -use crate::wizard_warn; /// Represents the user config file. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -121,7 +122,7 @@ impl TomlConfig { /// NOTE: the wizard can only be used with interactive shells. #[cfg(feature = "wizard")] async fn from_wizard(path: &PathBuf) -> Result { - wizard_warn!("Cannot find existing configuration at {path:?}."); + print::warn(format!("Cannot find existing configuration at {path:?}.")); let confirm = inquire::Confirm::new("Would you like to create one with the wizard? ") .with_default(true) diff --git a/src/config/wizard.rs b/src/config/wizard.rs index 3a9071f..1629ec6 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -1,97 +1,29 @@ +use std::{fs, path::PathBuf}; + use color_eyre::Result; -use inquire::{Confirm, Select, Text}; -use shellexpand_utils::expand; -use std::{fs, path::Path, process}; -use toml_edit::{DocumentMut, Item}; +use pimalaya_tui::{print, prompt}; +use toml_edit::{DocumentMut, Table}; use crate::account; use super::TomlConfig; -#[macro_export] -macro_rules! wizard_warn { - ($($arg:tt)*) => { - println!("{}", console::style(format!($($arg)*)).yellow().bold()); - }; -} - -#[macro_export] -macro_rules! wizard_prompt { - ($($arg:tt)*) => { - format!("{}", console::style(format!($($arg)*)).italic()) - }; -} - -#[macro_export] -macro_rules! wizard_log { - ($($arg:tt)*) => { - println!(); - println!("{}", console::style(format!($($arg)*)).underlined()); - println!(); - }; -} - -pub(crate) async fn configure(path: &Path) -> Result { - wizard_log!("Configuring your first account:"); +pub async fn configure(path: &PathBuf) -> Result { + print::section("Configuring your default account"); let mut config = TomlConfig::default(); - while let Some((name, account_config)) = account::wizard::configure().await? { - config.accounts.insert(name, account_config); + let (account_name, account_config) = account::wizard::configure().await?; + config.accounts.insert(account_name, account_config); - if !Confirm::new("Would you like to configure another account?") - .with_default(false) - .prompt_skippable()? - .unwrap_or_default() - { - break; - } + let path = prompt::path("Where to save the configuration?", Some(path))?; + println!("Writing the configuration to {}…", path.display()); - wizard_log!("Configuring another account:"); - } - - // If one account is setup, make it the default. If multiple - // accounts are setup, decide which will be the default. If no - // accounts are setup, exit the process. - let default_account = match config.accounts.len() { - 0 => { - wizard_warn!("No account configured, exiting."); - process::exit(0); - } - 1 => Some(config.accounts.values_mut().next().unwrap()), - _ => { - let accounts = config.accounts.clone(); - let accounts: Vec<&String> = accounts.keys().collect(); - - println!("{} accounts have been configured.", accounts.len()); - - Select::new( - "Which account would you like to set as your default?", - accounts, - ) - .with_starting_cursor(0) - .prompt_skippable()? - .and_then(|input| config.accounts.get_mut(input)) - } - }; - - if let Some(account) = default_account { - account.default = Some(true); - } else { - process::exit(0) - } - - let path = Text::new("Where would you like to save your configuration?") - .with_default(&path.to_string_lossy()) - .prompt()?; - let path = expand::path(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)?; - println!("Exiting the wizard…"); + println!("Done! Exiting the wizard…"); Ok(config) } @@ -99,61 +31,11 @@ fn pretty_serialize(config: &TomlConfig) -> Result { let mut doc: DocumentMut = toml::to_string(&config)?.parse()?; doc.iter_mut().for_each(|(_, item)| { - if let Some(item) = item.as_table_mut() { - item.iter_mut().for_each(|(_, item)| { - set_table_dotted(item, "folder"); - if let Some(item) = get_table_mut(item, "folder") { - let keys = ["alias", "add", "list", "expunge", "purge", "delete", "sync"]; - set_tables_dotted(item, keys); - - if let Some(item) = get_table_mut(item, "sync") { - set_tables_dotted(item, ["filter", "permissions"]); - } + if let Some(table) = item.as_table_mut() { + table.iter_mut().for_each(|(_, item)| { + if let Some(table) = item.as_table_mut() { + set_table_dotted(table); } - - set_table_dotted(item, "envelope"); - if let Some(item) = get_table_mut(item, "envelope") { - set_tables_dotted(item, ["list", "get"]); - } - - set_table_dotted(item, "flag"); - if let Some(item) = get_table_mut(item, "flag") { - set_tables_dotted(item, ["add", "set", "remove"]); - } - - set_table_dotted(item, "message"); - if let Some(item) = get_table_mut(item, "message") { - let keys = ["add", "send", "peek", "get", "copy", "move", "delete"]; - set_tables_dotted(item, keys); - } - - #[cfg(feature = "maildir")] - set_table_dotted(item, "maildir"); - - #[cfg(feature = "imap")] - { - set_table_dotted(item, "imap"); - if let Some(item) = get_table_mut(item, "imap") { - set_tables_dotted(item, ["passwd", "oauth2"]); - } - } - - #[cfg(feature = "notmuch")] - set_table_dotted(item, "notmuch"); - - #[cfg(feature = "smtp")] - { - set_table_dotted(item, "smtp"); - if let Some(item) = get_table_mut(item, "smtp") { - set_tables_dotted(item, ["passwd", "oauth2"]); - } - } - - #[cfg(feature = "sendmail")] - set_table_dotted(item, "sendmail"); - - #[cfg(feature = "pgp")] - set_table_dotted(item, "pgp"); }) } }); @@ -161,19 +43,13 @@ fn pretty_serialize(config: &TomlConfig) -> Result { Ok(doc.to_string()) } -fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> { - item.get_mut(key).filter(|item| item.is_table()) -} - -fn set_table_dotted(item: &mut Item, key: &str) { - if let Some(table) = get_table_mut(item, key).and_then(|item| item.as_table_mut()) { - table.set_dotted(true) - } -} - -fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator) { - for key in keys { - set_table_dotted(item, key) +fn set_table_dotted(table: &mut Table) { + let keys: Vec = table.iter().map(|(key, _)| key.to_string()).collect(); + for ref key in keys { + if let Some(table) = table.get_mut(key).unwrap().as_table_mut() { + table.set_dotted(true); + set_table_dotted(table) + } } } diff --git a/src/imap/mod.rs b/src/imap/mod.rs deleted file mode 100644 index 7df0104..0000000 --- a/src/imap/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "wizard")] -pub(crate) mod wizard; diff --git a/src/imap/wizard.rs b/src/imap/wizard.rs deleted file mode 100644 index 1efcf81..0000000 --- a/src/imap/wizard.rs +++ /dev/null @@ -1,346 +0,0 @@ -use color_eyre::Result; -use email::autoconfig::config::{AutoConfig, SecurityType, ServerType}; -#[cfg(feature = "oauth2")] -use email::{ - account::config::oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes}, - autoconfig::config::AuthenticationType, -}; -use email::{ - account::config::passwd::PasswdConfig, - imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind}, -}; -use inquire::validator::{ErrorMessage, StringValidator, Validation}; -#[cfg(feature = "oauth2")] -use oauth::v2_0::{AuthorizationCodeGrant, Client}; -use secret::Secret; - -use crate::{backend::config::BackendConfig, ui::prompt}; - -const ENCRYPTIONS: &[ImapEncryptionKind] = &[ - ImapEncryptionKind::Tls, - ImapEncryptionKind::StartTls, - ImapEncryptionKind::None, -]; - -const SECRETS: &[&str] = &[ - #[cfg(feature = "keyring")] - KEYRING, - RAW, - CMD, -]; -#[cfg(feature = "keyring")] -const KEYRING: &str = "Ask my password, then save it in my system's global keyring"; -const RAW: &str = "Ask my password, then save it in the configuration file (not safe)"; -const CMD: &str = "Ask me a shell command that exposes my password"; - -#[derive(Clone, Copy)] -struct U16Validator; - -impl StringValidator for U16Validator { - fn validate( - &self, - input: &str, - ) -> std::prelude::v1::Result { - if input.parse::().is_ok() { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid(ErrorMessage::Custom(format!( - "you should enter a number between {} and {}", - u16::MIN, - u16::MAX - )))) - } - } -} - -pub(crate) async fn configure( - account_name: &str, - email: &str, - autoconfig: Option<&AutoConfig>, -) -> Result { - use color_eyre::eyre::OptionExt as _; - use inquire::{validator, Select, Text}; - - let autoconfig_server = autoconfig.and_then(|c| { - c.email_provider() - .incoming_servers() - .into_iter() - .find(|server| matches!(server.server_type(), ServerType::Imap)) - }); - - let autoconfig_host = autoconfig_server - .and_then(|s| s.hostname()) - .map(ToOwned::to_owned); - - let default_host = - autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1)); - - let host = Text::new("IMAP hostname") - .with_default(&default_host) - .prompt()?; - - let autoconfig_encryption = autoconfig_server - .and_then(|imap| { - imap.security_type().map(|encryption| match encryption { - SecurityType::Plain => ImapEncryptionKind::None, - SecurityType::Starttls => ImapEncryptionKind::StartTls, - SecurityType::Tls => ImapEncryptionKind::Tls, - }) - }) - .unwrap_or_default(); - - let default_encryption_idx = match &autoconfig_encryption { - ImapEncryptionKind::Tls => 0, - ImapEncryptionKind::StartTls => 1, - ImapEncryptionKind::None => 2, - }; - - let encryption_kind = Select::new("IMAP encryption", ENCRYPTIONS.to_vec()) - .with_starting_cursor(default_encryption_idx) - .prompt_skippable()?; - - let autoconfig_port = autoconfig_server - .and_then(|s| s.port()) - .map(ToOwned::to_owned) - .unwrap_or_else(|| match &autoconfig_encryption { - ImapEncryptionKind::Tls => 465, - ImapEncryptionKind::StartTls => 587, - ImapEncryptionKind::None => 25, - }); - - let (encryption, default_port) = match encryption_kind { - Some(idx) - if &idx - == ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre( - "something impossible happened during finding default match for encryption.", - )? => - { - (Some(autoconfig_encryption), autoconfig_port) - } - Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 465), - Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 587), - _ => (Some(ImapEncryptionKind::None), 25), - }; - - let port = Text::new("IMAP port") - .with_validators(&[ - Box::new(validator::MinLengthValidator::new(1)), - Box::new(U16Validator {}), - ]) - .with_default(&default_port.to_string()) - .prompt() - .map(|input| input.parse::().unwrap())?; - - let autoconfig_login = autoconfig_server.map(|imap| match imap.username() { - Some("%EMAILLOCALPART%") => email.rsplit_once('@').unwrap().0.to_owned(), - Some("%EMAILADDRESS%") => email.to_owned(), - _ => email.to_owned(), - }); - - let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned()); - - let login = Text::new("IMAP login") - .with_default(&default_login) - .prompt()?; - - #[cfg(feature = "oauth2")] - let auth = { - use inquire::{Confirm, Password}; - - const XOAUTH2: &str = "XOAUTH2"; - const OAUTHBEARER: &str = "OAUTHBEARER"; - const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER]; - - let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2()); - - let default_oauth2_enabled = autoconfig_server - .and_then(|imap| { - imap.authentication_type() - .into_iter() - .find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2))) - }) - .filter(|_| autoconfig_oauth2.is_some()) - .unwrap_or_default(); - - let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?") - .with_default(default_oauth2_enabled) - .prompt_skippable()? - .unwrap_or_default(); - - if oauth2_enabled { - let mut config = OAuth2Config::default(); - let redirect_host = OAuth2Config::LOCALHOST; - let redirect_port = OAuth2Config::get_first_available_port()?; - - let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()?; - - config.method = match method_idx { - Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2, - Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer, - _ => OAuth2Method::XOAuth2, - }; - - config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?; - - let client_secret: String = Password::new("IMAP OAuth 2.0 client secret") - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt()?; - config.client_secret = - Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?; - config - .client_secret - .set_only_keyring(&client_secret) - .await?; - - let default_auth_url = autoconfig_oauth2 - .map(|o| o.auth_url().to_owned()) - .unwrap_or_default(); - config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL") - .with_default(&default_auth_url) - .prompt()?; - - let default_token_url = autoconfig_oauth2 - .map(|o| o.token_url().to_owned()) - .unwrap_or_default(); - config.token_url = Text::new("IMAP OAuth 2.0 token URL") - .with_default(&default_token_url) - .prompt()?; - - let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope()); - - let prompt_scope = |prompt: &str| -> Result> { - Ok(match &autoconfig_scopes { - Some(scopes) => Select::new(prompt, scopes.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()? - .map(ToOwned::to_owned), - None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()), - }) - }; - - if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? { - config.scopes = OAuth2Scopes::Scope(scope); - } - - let confirm_additional_scope = || -> Result { - let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?") - .with_default(false) - .prompt_skippable()? - .unwrap_or_default(); - - Ok(confirm) - }; - - while confirm_additional_scope()? { - let mut scopes = match config.scopes { - OAuth2Scopes::Scope(scope) => vec![scope], - OAuth2Scopes::Scopes(scopes) => scopes, - }; - - if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? { - scopes.push(scope) - } - - config.scopes = OAuth2Scopes::Scopes(scopes); - } - - config.pkce = Confirm::new("Would you like to enable PKCE verification?") - .with_default(true) - .prompt_skippable()? - .unwrap_or(true); - - crate::wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); - - let client = Client::new( - config.client_id.clone(), - client_secret, - config.auth_url.clone(), - config.token_url.clone(), - )? - .with_redirect_host(redirect_host.to_owned()) - .with_redirect_port(redirect_port) - .build()?; - - let mut auth_code_grant = AuthorizationCodeGrant::new() - .with_redirect_host(redirect_host.to_owned()) - .with_redirect_port(redirect_port); - - if config.pkce { - auth_code_grant = auth_code_grant.with_pkce(); - } - - for scope in config.scopes.clone() { - auth_code_grant = auth_code_grant.with_scope(scope); - } - - let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client); - - println!("{redirect_url}"); - println!(); - - let (access_token, refresh_token) = auth_code_grant - .wait_for_redirection(&client, csrf_token) - .await?; - - config.access_token = - Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?; - config.access_token.set_only_keyring(access_token).await?; - - if let Some(refresh_token) = &refresh_token { - config.refresh_token = Secret::try_new_keyring_entry(format!( - "{account_name}-imap-oauth2-refresh-token" - ))?; - config.refresh_token.set_only_keyring(refresh_token).await?; - } - - ImapAuthConfig::OAuth2(config) - } else { - configure_passwd(account_name).await? - } - }; - - #[cfg(not(feature = "oauth2"))] - let auth = configure_passwd(account_name).await?; - - let config = ImapConfig { - host, - port, - encryption, - login, - auth, - extensions: None, - watch: None, - }; - - Ok(BackendConfig::Imap(config)) -} - -pub(crate) async fn configure_passwd(account_name: &str) -> Result { - use inquire::{Select, Text}; - - let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()?; - - let secret = match secret_idx { - #[cfg(feature = "keyring")] - Some(sec) if sec == KEYRING => { - let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?; - secret - .set_only_keyring(prompt::passwd("IMAP password")?) - .await?; - secret - } - Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("IMAP password")?), - Some(sec) if sec == CMD => Secret::new_command( - Text::new("Shell command") - .with_default(&format!("pass show {account_name}-imap-passwd")) - .prompt()?, - ), - _ => Default::default(), - }; - - Ok(ImapAuthConfig::Passwd(PasswdConfig(secret))) -} diff --git a/src/lib.rs b/src/lib.rs index 429283e..4d9232c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,19 +6,9 @@ pub mod completion; pub mod config; pub mod email; pub mod folder; -#[cfg(feature = "imap")] -pub mod imap; -#[cfg(feature = "maildir")] -pub mod maildir; pub mod manual; -#[cfg(feature = "notmuch")] -pub mod notmuch; pub mod output; pub mod printer; -#[cfg(feature = "sendmail")] -pub mod sendmail; -#[cfg(feature = "smtp")] -pub mod smtp; pub mod tracing; pub mod ui; diff --git a/src/maildir/mod.rs b/src/maildir/mod.rs deleted file mode 100644 index 7df0104..0000000 --- a/src/maildir/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "wizard")] -pub(crate) mod wizard; diff --git a/src/maildir/wizard.rs b/src/maildir/wizard.rs deleted file mode 100644 index a21112b..0000000 --- a/src/maildir/wizard.rs +++ /dev/null @@ -1,25 +0,0 @@ -use color_eyre::Result; -use dirs::home_dir; -use email::maildir::config::MaildirConfig; -use inquire::Text; - -use crate::backend::config::BackendConfig; - -pub(crate) fn configure() -> Result { - let mut config = MaildirConfig::default(); - - let mut input = Text::new("Maildir directory"); - - let Some(home) = home_dir() else { - config.root_dir = input.prompt()?.into(); - - return Ok(BackendConfig::Maildir(config)); - }; - - let def = home.join("Mail").display().to_string(); - input = input.with_default(&def); - - config.root_dir = input.prompt()?.into(); - - Ok(BackendConfig::Maildir(config)) -} diff --git a/src/notmuch/mod.rs b/src/notmuch/mod.rs deleted file mode 100644 index 73818b4..0000000 --- a/src/notmuch/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod wizard; diff --git a/src/notmuch/wizard.rs b/src/notmuch/wizard.rs deleted file mode 100644 index 4c3ba54..0000000 --- a/src/notmuch/wizard.rs +++ /dev/null @@ -1,23 +0,0 @@ -use color_eyre::Result; -use email::notmuch::config::NotmuchConfig; -use inquire::Text; - -use crate::backend::config::BackendConfig; - -pub(crate) fn configure() -> Result { - let config = NotmuchConfig { - database_path: Some( - Text::new("Notmuch database path") - .with_default( - &NotmuchConfig::get_default_database_path() - .unwrap_or_default() - .to_string_lossy(), - ) - .prompt()? - .into(), - ), - ..Default::default() - }; - - Ok(BackendConfig::Notmuch(config)) -} diff --git a/src/sendmail/mod.rs b/src/sendmail/mod.rs deleted file mode 100644 index 73818b4..0000000 --- a/src/sendmail/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod wizard; diff --git a/src/sendmail/wizard.rs b/src/sendmail/wizard.rs deleted file mode 100644 index bd7df9d..0000000 --- a/src/sendmail/wizard.rs +++ /dev/null @@ -1,16 +0,0 @@ -use color_eyre::Result; -use email::sendmail::config::SendmailConfig; -use inquire::Text; - -use crate::backend::config::BackendConfig; - -pub(crate) fn configure() -> Result { - let config = SendmailConfig { - cmd: Text::new("Sendmail-compatible shell command to send emails") - .with_default("/usr/bin/msmtp") - .prompt()? - .into(), - }; - - Ok(BackendConfig::Sendmail(config)) -} diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs deleted file mode 100644 index 7df0104..0000000 --- a/src/smtp/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "wizard")] -pub(crate) mod wizard; diff --git a/src/smtp/wizard.rs b/src/smtp/wizard.rs deleted file mode 100644 index 9879bdb..0000000 --- a/src/smtp/wizard.rs +++ /dev/null @@ -1,344 +0,0 @@ -use color_eyre::Result; -use email::autoconfig::config::{AutoConfig, SecurityType, ServerType}; -#[cfg(feature = "oauth2")] -use email::{ - account::config::oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes}, - autoconfig::config::AuthenticationType, -}; -use email::{ - account::config::passwd::PasswdConfig, - smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind}, -}; -use inquire::validator::{ErrorMessage, StringValidator, Validation}; -#[cfg(feature = "oauth2")] -use oauth::v2_0::{AuthorizationCodeGrant, Client}; -use secret::Secret; - -use crate::{backend::config::BackendConfig, ui::prompt}; - -const ENCRYPTIONS: &[SmtpEncryptionKind] = &[ - SmtpEncryptionKind::Tls, - SmtpEncryptionKind::StartTls, - SmtpEncryptionKind::None, -]; - -const SECRETS: &[&str] = &[ - #[cfg(feature = "keyring")] - KEYRING, - RAW, - CMD, -]; -#[cfg(feature = "keyring")] -const KEYRING: &str = "Ask my password, then save it in my system's global keyring"; -const RAW: &str = "Ask my password, then save it in the configuration file (not safe)"; -const CMD: &str = "Ask me a shell command that exposes my password"; - -#[derive(Clone, Copy)] -struct U16Validator; - -impl StringValidator for U16Validator { - fn validate( - &self, - input: &str, - ) -> std::prelude::v1::Result { - if input.parse::().is_ok() { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid(ErrorMessage::Custom(format!( - "you should enter a number between {} and {}", - u16::MIN, - u16::MAX - )))) - } - } -} - -pub(crate) async fn configure( - account_name: &str, - email: &str, - autoconfig: Option<&AutoConfig>, -) -> Result { - use color_eyre::eyre::OptionExt as _; - use inquire::{validator, Select, Text}; - - let autoconfig_server = autoconfig.and_then(|c| { - c.email_provider() - .outgoing_servers() - .into_iter() - .find(|server| matches!(server.server_type(), ServerType::Smtp)) - }); - - let autoconfig_host = autoconfig_server - .and_then(|s| s.hostname()) - .map(ToOwned::to_owned); - - let default_host = - autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1)); - - let host = Text::new("SMTP hostname") - .with_default(&default_host) - .prompt()?; - - let autoconfig_encryption = autoconfig_server - .and_then(|smtp| { - smtp.security_type().map(|encryption| match encryption { - SecurityType::Plain => SmtpEncryptionKind::None, - SecurityType::Starttls => SmtpEncryptionKind::StartTls, - SecurityType::Tls => SmtpEncryptionKind::Tls, - }) - }) - .unwrap_or_default(); - - let default_encryption_idx = match &autoconfig_encryption { - SmtpEncryptionKind::Tls => 0, - SmtpEncryptionKind::StartTls => 1, - SmtpEncryptionKind::None => 2, - }; - - let encryption_kind = Select::new("SMTP encryption", ENCRYPTIONS.to_vec()) - .with_starting_cursor(default_encryption_idx) - .prompt_skippable()?; - - let autoconfig_port = autoconfig_server - .and_then(|s| s.port()) - .map(ToOwned::to_owned) - .unwrap_or_else(|| match &autoconfig_encryption { - SmtpEncryptionKind::Tls => 465, - SmtpEncryptionKind::StartTls => 587, - SmtpEncryptionKind::None => 25, - }); - - let (encryption, default_port) = match encryption_kind { - Some(idx) - if &idx - == ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre( - "something impossible happened during finding default match for encryption.", - )? => - { - (Some(autoconfig_encryption), autoconfig_port) - } - Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465), - Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587), - _ => (Some(SmtpEncryptionKind::None), 25), - }; - - let port = Text::new("SMTP port") - .with_validators(&[ - Box::new(validator::MinLengthValidator::new(1)), - Box::new(U16Validator {}), - ]) - .with_default(&default_port.to_string()) - .prompt() - .map(|input| input.parse::().unwrap())?; - - let autoconfig_login = autoconfig_server.map(|smtp| match smtp.username() { - Some("%EMAILLOCALPART%") => email.rsplit_once('@').unwrap().0.to_owned(), - Some("%EMAILADDRESS%") => email.to_owned(), - _ => email.to_owned(), - }); - - let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned()); - - let login = Text::new("SMTP login") - .with_default(&default_login) - .prompt()?; - - #[cfg(feature = "oauth2")] - let auth = { - use inquire::{Confirm, Password}; - - const XOAUTH2: &str = "XOAUTH2"; - const OAUTHBEARER: &str = "OAUTHBEARER"; - const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER]; - - let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2()); - - let default_oauth2_enabled = autoconfig_server - .and_then(|smtp| { - smtp.authentication_type() - .into_iter() - .find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2))) - }) - .filter(|_| autoconfig_oauth2.is_some()) - .unwrap_or_default(); - - let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?") - .with_default(default_oauth2_enabled) - .prompt_skippable()? - .unwrap_or_default(); - - if oauth2_enabled { - let mut config = OAuth2Config::default(); - let redirect_host = OAuth2Config::LOCALHOST; - let redirect_port = OAuth2Config::get_first_available_port()?; - - let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()?; - - config.method = match method_idx { - Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2, - Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer, - _ => OAuth2Method::XOAuth2, - }; - - config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?; - - let client_secret: String = Password::new("SMTP OAuth 2.0 client secret") - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt()?; - config.client_secret = - Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?; - config - .client_secret - .set_only_keyring(&client_secret) - .await?; - - let default_auth_url = autoconfig_oauth2 - .map(|o| o.auth_url().to_owned()) - .unwrap_or_default(); - config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL") - .with_default(&default_auth_url) - .prompt()?; - - let default_token_url = autoconfig_oauth2 - .map(|o| o.token_url().to_owned()) - .unwrap_or_default(); - config.token_url = Text::new("SMTP OAuth 2.0 token URL") - .with_default(&default_token_url) - .prompt()?; - - let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope()); - - let prompt_scope = |prompt: &str| -> Result> { - Ok(match &autoconfig_scopes { - Some(scopes) => Select::new(prompt, scopes.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()? - .map(ToOwned::to_owned), - None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()), - }) - }; - - if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? { - config.scopes = OAuth2Scopes::Scope(scope); - } - - let confirm_additional_scope = || -> Result { - let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?") - .with_default(false) - .prompt_skippable()? - .unwrap_or_default(); - - Ok(confirm) - }; - - while confirm_additional_scope()? { - let mut scopes = match config.scopes { - OAuth2Scopes::Scope(scope) => vec![scope], - OAuth2Scopes::Scopes(scopes) => scopes, - }; - - if let Some(scope) = prompt_scope("Additional SMTP OAuth 2.0 scope")? { - scopes.push(scope) - } - - config.scopes = OAuth2Scopes::Scopes(scopes); - } - - config.pkce = Confirm::new("Would you like to enable PKCE verification?") - .with_default(true) - .prompt_skippable()? - .unwrap_or(true); - - crate::wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); - - let client = Client::new( - config.client_id.clone(), - client_secret, - config.auth_url.clone(), - config.token_url.clone(), - )? - .with_redirect_host(redirect_host.to_owned()) - .with_redirect_port(redirect_port) - .build()?; - - let mut auth_code_grant = AuthorizationCodeGrant::new() - .with_redirect_host(redirect_host.to_owned()) - .with_redirect_port(redirect_port); - - if config.pkce { - auth_code_grant = auth_code_grant.with_pkce(); - } - - for scope in config.scopes.clone() { - auth_code_grant = auth_code_grant.with_scope(scope); - } - - let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client); - - println!("{redirect_url}"); - println!(); - - let (access_token, refresh_token) = auth_code_grant - .wait_for_redirection(&client, csrf_token) - .await?; - - config.access_token = - Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))?; - config.access_token.set_only_keyring(access_token).await?; - - if let Some(refresh_token) = &refresh_token { - config.refresh_token = Secret::try_new_keyring_entry(format!( - "{account_name}-smtp-oauth2-refresh-token" - ))?; - config.refresh_token.set_only_keyring(refresh_token).await?; - } - - SmtpAuthConfig::OAuth2(config) - } else { - configure_passwd(account_name).await? - } - }; - - #[cfg(not(feature = "oauth2"))] - let auth = configure_passwd(account_name).await?; - - let config = SmtpConfig { - host, - port, - encryption, - login, - auth, - }; - - Ok(BackendConfig::Smtp(config)) -} - -pub(crate) async fn configure_passwd(account_name: &str) -> Result { - use inquire::{Select, Text}; - - let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec()) - .with_starting_cursor(0) - .prompt_skippable()?; - - let secret = match secret_idx { - #[cfg(feature = "keyring")] - Some(sec) if sec == KEYRING => { - let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?; - secret - .set_only_keyring(prompt::passwd("SMTP password")?) - .await?; - secret - } - Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?), - Some(sec) if sec == CMD => Secret::new_command( - Text::new("Shell command") - .with_default(&format!("pass show {account_name}-smtp-passwd")) - .prompt()?, - ), - _ => Default::default(), - }; - - Ok(SmtpAuthConfig::Passwd(PasswdConfig(secret))) -} diff --git a/src/ui/choice.rs b/src/ui/choice.rs index bd3a4fd..9b06db5 100644 --- a/src/ui/choice.rs +++ b/src/ui/choice.rs @@ -1,17 +1,17 @@ -use std::fmt::Display; +use std::fmt; use color_eyre::Result; -use inquire::Select; +use pimalaya_tui::prompt; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum PreEditChoice { Edit, Discard, Quit, } -impl Display for PreEditChoice { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for PreEditChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", @@ -24,22 +24,20 @@ impl Display for PreEditChoice { } } +static PRE_EDIT_CHOICES: [PreEditChoice; 3] = [ + PreEditChoice::Edit, + PreEditChoice::Discard, + PreEditChoice::Quit, +]; + pub fn pre_edit() -> Result { - let choices = [ - PreEditChoice::Edit, - PreEditChoice::Discard, - PreEditChoice::Quit, - ]; - - let user_choice = Select::new( + let user_choice = prompt::item( "A draft was found, what would you like to do with it?", - choices.to_vec(), - ) - .with_starting_cursor(0) - .with_vim_mode(true) - .prompt()?; + &PRE_EDIT_CHOICES, + None, + )?; - Ok(user_choice) + Ok(user_choice.clone()) } #[derive(Clone, Debug, Eq, PartialEq)] @@ -51,7 +49,7 @@ pub enum PostEditChoice { Discard, } -impl Display for PostEditChoice { +impl fmt::Display for PostEditChoice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -67,22 +65,20 @@ impl Display for PostEditChoice { } } +static POST_EDIT_CHOICES: [PostEditChoice; 5] = [ + PostEditChoice::Send, + PostEditChoice::Edit, + PostEditChoice::LocalDraft, + PostEditChoice::RemoteDraft, + PostEditChoice::Discard, +]; + pub fn post_edit() -> Result { - let choices = [ - PostEditChoice::Send, - PostEditChoice::Edit, - PostEditChoice::LocalDraft, - PostEditChoice::RemoteDraft, - PostEditChoice::Discard, - ]; - - let user_choice = inquire::Select::new( + let user_choice = prompt::item( "What would you like to do with this message?", - choices.to_vec(), - ) - .with_starting_cursor(0) - .with_vim_mode(true) - .prompt()?; + &POST_EDIT_CHOICES, + None, + )?; - Ok(user_choice) + Ok(user_choice.clone()) } diff --git a/src/ui/editor.rs b/src/ui/editor.rs index 91b1419..f9b28b8 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,3 +1,5 @@ +use std::{env, fs, sync::Arc}; + use color_eyre::{eyre::Context, Result}; use email::{ account::config::AccountConfig, @@ -8,7 +10,6 @@ use email::{ }; use mml::MmlCompilerBuilder; use process::SingleCommand; -use std::{env, fs, sync::Arc}; use tracing::debug; use crate::{ diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a9a4f69..6079f9d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,7 +2,6 @@ use crossterm::style::Color; pub mod choice; pub mod editor; -pub(crate) mod prompt; pub(crate) fn map_color(color: Color) -> comfy_table::Color { match color { diff --git a/src/ui/prompt.rs b/src/ui/prompt.rs deleted file mode 100644 index e4b67cb..0000000 --- a/src/ui/prompt.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::io; - -pub(crate) fn passwd(prompt: &str) -> io::Result { - inquire::Password::new(prompt) - .with_custom_confirmation_message("Confirm password") - .with_custom_confirmation_error_message("Passwords do not match, please try again.") - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt() - .map_err(|e| { - io::Error::new( - io::ErrorKind::Interrupted, - format!("failed to get password: {e}"), - ) - }) -} - -#[cfg(feature = "oauth2")] -pub(crate) fn secret(prompt: &str) -> io::Result { - inquire::Password::new(prompt) - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .without_confirmation() - .prompt() - .map_err(|e| { - io::Error::new( - io::ErrorKind::Interrupted, - format!("failed to get secret: {e}"), - ) - }) -}