replace dialoguer with inquire

In order to reduce our dependencies, we are replacing the dependencies
that use console_rs with those that use crossterm.

This commit will completely replace dialoguer with inquire.

Signed-off-by: Perma Alesheikh <me@prma.dev>
This commit is contained in:
Perma Alesheikh 2024-05-06 15:49:58 +03:30 committed by Clément DOUIN
parent d54dd6429e
commit 1e448e56eb
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
17 changed files with 441 additions and 543 deletions

19
Cargo.lock generated
View file

@ -1217,18 +1217,6 @@ dependencies = [
"cipher 0.4.4", "cipher 0.4.4",
] ]
[[package]]
name = "dialoguer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87"
dependencies = [
"console",
"shell-words",
"tempfile",
"zeroize",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -2049,7 +2037,6 @@ dependencies = [
"clap_mangen", "clap_mangen",
"color-eyre", "color-eyre",
"console", "console",
"dialoguer",
"dirs 4.0.0", "dirs 4.0.0",
"email-lib", "email-lib",
"email_address", "email_address",
@ -4297,12 +4284,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]] [[package]]
name = "shellexpand" name = "shellexpand"
version = "3.1.0" version = "3.1.0"

View file

@ -53,7 +53,6 @@ clap_complete = "4.4"
clap_mangen = "0.2" clap_mangen = "0.2"
color-eyre = "0.6.3" color-eyre = "0.6.3"
console = "0.15.2" console = "0.15.2"
dialoguer = "0.10.2"
dirs = "4" dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] } email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
email_address = "0.2.4" email_address = "0.2.4"

View file

@ -2,13 +2,10 @@
use crate::account::config::SyncConfig; use crate::account::config::SyncConfig;
use color_eyre::{eyre::OptionExt, Result}; use color_eyre::{eyre::OptionExt, Result};
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
use dialoguer::Confirm;
use email_address::EmailAddress; use email_address::EmailAddress;
use inquire::validator::{ErrorMessage, Validation}; use inquire::validator::{ErrorMessage, Validation};
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr};
#[cfg(feature = "account-sync")]
use crate::wizard_prompt;
#[cfg(feature = "account-discovery")] #[cfg(feature = "account-discovery")]
use crate::wizard_warn; use crate::wizard_warn;
use crate::{ use crate::{
@ -144,13 +141,11 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
{ {
let should_configure_sync = Confirm::new() let should_configure_sync =
.with_prompt(wizard_prompt!( inquire::Confirm::new("Do you need offline access for your account?")
"Do you need offline access for your account?" .with_default(false)
)) .prompt_skippable()?
.default(false) .unwrap_or_default();
.interact_opt()?
.unwrap_or_default();
if should_configure_sync { if should_configure_sync {
config.sync = Some(SyncConfig { config.sync = Some(SyncConfig {

View file

@ -1,9 +1,9 @@
pub mod config; pub mod config;
pub(crate) mod wizard; pub(crate) mod wizard;
use color_eyre::Result;
use async_trait::async_trait; use async_trait::async_trait;
use std::{ops::Deref, sync::Arc}; use color_eyre::Result;
use std::{fmt::Display, ops::Deref, sync::Arc};
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
use email::imap::{ImapContextBuilder, ImapContextSync}; use email::imap::{ImapContextBuilder, ImapContextSync};
@ -70,30 +70,32 @@ pub enum BackendKind {
Sendmail, Sendmail,
} }
impl ToString for BackendKind { impl Display for BackendKind {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let kind = match self { write!(
Self::None => "None", f,
"{}",
match self {
Self::None => "None",
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
Self::Imap => "IMAP", Self::Imap => "IMAP",
#[cfg(all(feature = "imap", feature = "account-sync"))] #[cfg(all(feature = "imap", feature = "account-sync"))]
Self::ImapCache => "IMAP cache", Self::ImapCache => "IMAP cache",
#[cfg(feature = "maildir")] #[cfg(feature = "maildir")]
Self::Maildir => "Maildir", Self::Maildir => "Maildir",
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
Self::Notmuch => "Notmuch", Self::Notmuch => "Notmuch",
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
Self::Smtp => "SMTP", Self::Smtp => "SMTP",
#[cfg(feature = "sendmail")] #[cfg(feature = "sendmail")]
Self::Sendmail => "Sendmail", Self::Sendmail => "Sendmail",
}; }
)
kind.to_string()
} }
} }

View file

@ -1,7 +1,7 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::Select;
#[cfg(feature = "account-discovery")] #[cfg(feature = "account-discovery")]
use email::account::discover::config::AutoConfig; use email::account::discover::config::AutoConfig;
use inquire::Select;
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
use crate::imap; use crate::imap;
@ -13,7 +13,6 @@ use crate::notmuch;
use crate::sendmail; use crate::sendmail;
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
use crate::smtp; use crate::smtp;
use crate::ui::THEME;
use super::{config::BackendConfig, BackendKind}; use super::{config::BackendConfig, BackendKind};
@ -38,12 +37,9 @@ pub(crate) async fn configure(
email: &str, email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>, #[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> { ) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME) let kind = Select::new("Default email backend", DEFAULT_BACKEND_KINDS.to_vec())
.with_prompt("Default email backend") .with_starting_cursor(0)
.items(DEFAULT_BACKEND_KINDS) .prompt_skippable()?;
.default(0)
.interact_opt()?
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));
let config = match kind { let config = match kind {
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
@ -71,12 +67,12 @@ pub(crate) async fn configure_sender(
email: &str, email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>, #[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> { ) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME) let kind = Select::new(
.with_prompt("Backend for sending messages") "Backend for sending messages",
.items(SEND_MESSAGE_BACKEND_KINDS) SEND_MESSAGE_BACKEND_KINDS.to_vec(),
.default(0) )
.interact_opt()? .with_starting_cursor(0)
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone)); .prompt_skippable()?;
let config = match kind { let config = match kind {
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]

View file

@ -18,7 +18,7 @@ use tracing::debug;
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
use crate::backend::BackendKind; use crate::backend::BackendKind;
use crate::{account::config::TomlAccountConfig, wizard_prompt, wizard_warn}; use crate::{account::config::TomlAccountConfig, wizard_warn};
/// Represents the user config file. /// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
@ -46,8 +46,8 @@ impl TomlConfig {
1 => { 1 => {
let path = &paths[0]; let path = &paths[0];
let ref content = fs::read_to_string(path) let content = &(fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?; .context(format!("cannot read config file at {path:?}"))?);
toml::from_str(content).context(format!("cannot parse config file at {path:?}")) toml::from_str(content).context(format!("cannot parse config file at {path:?}"))
} }
@ -85,17 +85,13 @@ impl TomlConfig {
/// ///
/// NOTE: the wizard can only be used with interactive shells. /// NOTE: the wizard can only be used with interactive shells.
async fn from_wizard(path: &PathBuf) -> Result<Self> { async fn from_wizard(path: &PathBuf) -> Result<Self> {
use dialoguer::Confirm;
use std::process; use std::process;
wizard_warn!("Cannot find existing configuration at {path:?}."); wizard_warn!("Cannot find existing configuration at {path:?}.");
let confirm = Confirm::new() let confirm = inquire::Confirm::new("Would you like to create one with the wizard? ")
.with_prompt(wizard_prompt!( .with_default(true)
"Would you like to create one with the wizard?" .prompt_skippable()?
))
.default(true)
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
if !confirm { if !confirm {

View file

@ -1,10 +1,10 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::{Confirm, Input, Select}; use inquire::{Confirm, Select, Text};
use shellexpand_utils::expand; use shellexpand_utils::expand;
use std::{fs, path::PathBuf, process}; use std::{fs, path::Path, process};
use toml_edit::{DocumentMut, Item}; use toml_edit::{DocumentMut, Item};
use crate::{account, ui::THEME}; use crate::account;
use super::TomlConfig; use super::TomlConfig;
@ -31,7 +31,7 @@ macro_rules! wizard_log {
}; };
} }
pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> { pub(crate) async fn configure(path: &Path) -> Result<TomlConfig> {
wizard_log!("Configuring your first account:"); wizard_log!("Configuring your first account:");
let mut config = TomlConfig::default(); let mut config = TomlConfig::default();
@ -39,12 +39,9 @@ pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
while let Some((name, account_config)) = account::wizard::configure().await? { while let Some((name, account_config)) = account::wizard::configure().await? {
config.accounts.insert(name, account_config); config.accounts.insert(name, account_config);
if !Confirm::new() if !Confirm::new("Would you like to configure another account?")
.with_prompt(wizard_prompt!( .with_default(false)
"Would you like to configure another account?" .prompt_skippable()?
))
.default(false)
.interact_opt()?
.unwrap_or_default() .unwrap_or_default()
{ {
break; break;
@ -68,14 +65,13 @@ pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
println!("{} accounts have been configured.", accounts.len()); println!("{} accounts have been configured.", accounts.len());
Select::with_theme(&*THEME) Select::new(
.with_prompt(wizard_prompt!( "Which account would you like to set as your default?",
"Which account would you like to set as your default?" accounts,
)) )
.items(&accounts) .with_starting_cursor(0)
.default(0) .prompt_skippable()?
.interact_opt()? .and_then(|input| config.accounts.get_mut(input))
.and_then(|idx| config.accounts.get_mut(accounts[idx]))
} }
}; };
@ -85,12 +81,9 @@ pub(crate) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
process::exit(0) process::exit(0)
} }
let path = Input::with_theme(&*THEME) let path = Text::new("Where would you like to save your configuration?")
.with_prompt(wizard_prompt!( .with_default(&path.to_string_lossy())
"Where would you like to save your configuration?" .prompt()?;
))
.default(path.to_string_lossy().to_string())
.interact()?;
let path = expand::path(path); let path = expand::path(path);
println!("Writing the configuration to {path:?}"); println!("Writing the configuration to {path:?}");

View file

@ -1,7 +1,7 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use dialoguer::Confirm;
use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder}; use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder};
use inquire::Confirm;
use std::process; use std::process;
use tracing::info; use tracing::info;
@ -35,12 +35,9 @@ impl FolderDeleteCommand {
let folder = &self.folder.name; let folder = &self.folder.name;
let confirm_msg = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted."); let confirm = Confirm::new(&format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted."))
let confirm = Confirm::new() .with_default(false).prompt_skippable()?;
.with_prompt(confirm_msg)
.default(false)
.report(false)
.interact_opt()?;
if let Some(false) | None = confirm { if let Some(false) | None = confirm {
process::exit(0); process::exit(0);
}; };

View file

@ -1,6 +1,5 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use dialoguer::Confirm;
use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder}; use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
use std::process; use std::process;
use tracing::info; use tracing::info;
@ -35,12 +34,10 @@ impl FolderPurgeCommand {
let folder = &self.folder.name; let folder = &self.folder.name;
let confirm_msg = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted."); let confirm = inquire::Confirm::new(&format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted."))
let confirm = Confirm::new() .with_default(false)
.with_prompt(confirm_msg) .prompt_skippable()?;
.default(false)
.report(false)
.interact_opt()?;
if let Some(false) | None = confirm { if let Some(false) | None = confirm {
process::exit(0); process::exit(0);
}; };

View file

@ -1,5 +1,4 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::{Confirm, Input, Password, Select};
#[cfg(feature = "account-discovery")] #[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType}; use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::{ use email::{
@ -9,14 +8,11 @@ use email::{
}, },
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind}, imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
}; };
use inquire::validator::{ErrorMessage, StringValidator, Validation};
use oauth::v2_0::{AuthorizationCodeGrant, Client}; use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret; use secret::Secret;
use crate::{ use crate::{backend::config::BackendConfig, ui::prompt, wizard_log};
backend::config::BackendConfig,
ui::{prompt, THEME},
wizard_log, wizard_prompt,
};
const ENCRYPTIONS: &[ImapEncryptionKind] = &[ const ENCRYPTIONS: &[ImapEncryptionKind] = &[
ImapEncryptionKind::Tls, ImapEncryptionKind::Tls,
@ -33,12 +29,35 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)"; 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"; 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<Validation, inquire::CustomUserError> {
if input.parse::<u16>().is_ok() {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid(ErrorMessage::Custom(format!(
"you should enter a number between {} and {}",
u16::MIN,
u16::MAX
))))
}
}
}
#[cfg(feature = "account-discovery")] #[cfg(feature = "account-discovery")]
pub(crate) async fn configure( pub(crate) async fn configure(
account_name: &str, account_name: &str,
email: &str, email: &str,
autoconfig: Option<&AutoConfig>, autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> { ) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text};
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2()); let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| { let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider() c.email_provider()
@ -54,10 +73,9 @@ pub(crate) async fn configure(
let default_host = let default_host =
autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1)); autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1));
let host = Input::with_theme(&*THEME) let host = Text::new("IMAP hostname")
.with_prompt("IMAP hostname") .with_default(&default_host)
.default(default_host) .prompt()?;
.interact()?;
let autoconfig_encryption = autoconfig_server let autoconfig_encryption = autoconfig_server
.and_then(|imap| { .and_then(|imap| {
@ -75,11 +93,9 @@ pub(crate) async fn configure(
ImapEncryptionKind::None => 2, ImapEncryptionKind::None => 2,
}; };
let encryption_idx = Select::with_theme(&*THEME) let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
.with_prompt("IMAP encryption") .with_starting_cursor(default_encryption_idx)
.items(ENCRYPTIONS) .prompt_skippable()?;
.default(default_encryption_idx)
.interact_opt()?;
let autoconfig_port = autoconfig_server let autoconfig_port = autoconfig_server
.and_then(|s| s.port()) .and_then(|s| s.port())
@ -91,23 +107,26 @@ pub(crate) async fn configure(
}); });
let (encryption, default_port) = match encryption_idx { let (encryption, default_port) = match encryption_idx {
Some(idx) if idx == default_encryption_idx => { Some(enc_kind)
if &enc_kind
== ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre(
"something impossible happened while selecting the encryption of imap.",
)? =>
{
(Some(autoconfig_encryption), autoconfig_port) (Some(autoconfig_encryption), autoconfig_port)
} }
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => { Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993),
(Some(ImapEncryptionKind::Tls), 993) Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143),
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
(Some(ImapEncryptionKind::StartTls), 143)
}
_ => (Some(ImapEncryptionKind::None), 143), _ => (Some(ImapEncryptionKind::None), 143),
}; };
let port = Input::with_theme(&*THEME) let port = Text::new("IMAP port")
.with_prompt("IMAP port") .with_validators(&[
.validate_with(|input: &String| input.parse::<u16>().map(|_| ())) Box::new(MinLengthValidator::new(1)),
.default(default_port.to_string()) Box::new(U16Validator {}),
.interact() ])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?; .map(|input| input.parse::<u16>().unwrap())?;
let autoconfig_login = autoconfig_server.map(|imap| match imap.username() { let autoconfig_login = autoconfig_server.map(|imap| match imap.username() {
@ -118,10 +137,9 @@ pub(crate) async fn configure(
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned()); let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Input::with_theme(&*THEME) let login = Text::new("IMAP login")
.with_prompt("IMAP login") .with_default(&default_login)
.default(default_login) .prompt()?;
.interact()?;
let default_oauth2_enabled = autoconfig_server let default_oauth2_enabled = autoconfig_server
.and_then(|imap| { .and_then(|imap| {
@ -132,10 +150,9 @@ pub(crate) async fn configure(
.filter(|_| autoconfig_oauth2.is_some()) .filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default(); .unwrap_or_default();
let oauth2_enabled = Confirm::new() let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) .with_default(default_oauth2_enabled)
.default(default_oauth2_enabled) .prompt_skippable()?
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
let auth = if oauth2_enabled { let auth = if oauth2_enabled {
@ -143,25 +160,19 @@ pub(crate) async fn configure(
let redirect_host = OAuth2Config::LOCALHOST.to_owned(); let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?; let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME) let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_prompt("IMAP OAuth 2.0 mechanism") .with_starting_cursor(0)
.items(OAUTH2_MECHANISMS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
config.method = match method_idx { config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2, Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2, _ => OAuth2Method::XOAuth2,
}; };
config.client_id = Input::with_theme(&*THEME) config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
.with_prompt("IMAP OAuth 2.0 client id")
.interact()?;
let client_secret: String = Password::with_theme(&*THEME) let client_secret: String = Password::new("IMAP OAuth 2.0 client secret").prompt()?;
.with_prompt("IMAP OAuth 2.0 client secret")
.interact()?;
config.client_secret = config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?; Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config config
@ -172,38 +183,28 @@ pub(crate) async fn configure(
let default_auth_url = autoconfig_oauth2 let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned()) .map(|o| o.auth_url().to_owned())
.unwrap_or_default(); .unwrap_or_default();
config.auth_url = Input::with_theme(&*THEME) config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL")
.with_prompt("IMAP OAuth 2.0 authorization URL") .with_default(&default_auth_url)
.default(default_auth_url) .prompt()?;
.interact()?;
let default_token_url = autoconfig_oauth2 let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned()) .map(|o| o.token_url().to_owned())
.unwrap_or_default(); .unwrap_or_default();
config.token_url = Input::with_theme(&*THEME) config.token_url = Text::new("IMAP OAuth 2.0 token URL")
.with_prompt("IMAP OAuth 2.0 token URL") .with_default(&default_token_url)
.default(default_token_url) .prompt()?;
.interact()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope()); let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> { let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes { Ok(match &autoconfig_scopes {
Some(scopes) => Select::with_theme(&*THEME) Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_prompt(prompt) .with_starting_cursor(0)
.items(scopes) .prompt_skippable()?
.default(0) .map(ToOwned::to_owned),
.interact_opt()? None => {
.and_then(|idx| scopes.get(idx)) Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty())
.map(|scope| scope.to_string()), }
None => Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()),
}) })
}; };
@ -212,12 +213,9 @@ pub(crate) async fn configure(
} }
let confirm_additional_scope = || -> Result<bool> { let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new() let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_prompt(wizard_prompt!( .with_default(false)
"Would you like to add more IMAP OAuth 2.0 scopes?" .prompt_skippable()?
))
.default(false)
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
Ok(confirm) Ok(confirm)
@ -236,12 +234,9 @@ pub(crate) async fn configure(
config.scopes = OAuth2Scopes::Scopes(scopes); config.scopes = OAuth2Scopes::Scopes(scopes);
} }
config.pkce = Confirm::new() config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_prompt(wizard_prompt!( .with_default(true)
"Would you like to enable PKCE verification?" .prompt_skippable()?
))
.default(true)
.interact_opt()?
.unwrap_or(true); .unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -289,26 +284,23 @@ pub(crate) async fn configure(
ImapAuthConfig::OAuth2(config) ImapAuthConfig::OAuth2(config)
} else { } else {
let secret_idx = Select::with_theme(&*THEME) let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_prompt("IMAP authentication strategy") .with_starting_cursor(0)
.items(SECRETS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
let secret = match secret_idx { let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => { Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?; let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret secret
.set_only_keyring(prompt::passwd("IMAP password")?) .set_only_keyring(prompt::passwd("IMAP password")?)
.await?; .await?;
secret secret
} }
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?), Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command( Some(CMD) => Secret::new_command(
Input::with_theme(&*THEME) Text::new("Shell command")
.with_prompt("Shell command") .with_default(&format!("pass show {account_name}-imap-passwd"))
.default(format!("pass show {account_name}-imap-passwd")) .prompt()?,
.interact()?,
), ),
_ => Default::default(), _ => Default::default(),
}; };
@ -330,47 +322,44 @@ pub(crate) async fn configure(
#[cfg(not(feature = "account-discovery"))] #[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> { pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
use inquire::{
validator::MinLengthValidator, Confirm, Password, PasswordDisplayMode, Select, Text,
};
let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1); let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1);
let host = Input::with_theme(&*THEME) let host = Text::new("IMAP hostname")
.with_prompt("IMAP hostname") .with_default(&default_host)
.default(default_host) .prompt()?;
.interact()?;
let encryption_idx = Select::with_theme(&*THEME) let encryption_idx = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
.with_prompt("IMAP encryption") .with_starting_cursor(0)
.items(ENCRYPTIONS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
let (encryption, default_port) = match encryption_idx { let (encryption, default_port) = match encryption_idx {
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => { Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 993),
(Some(ImapEncryptionKind::Tls), 993) Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 143),
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
(Some(ImapEncryptionKind::StartTls), 143)
}
_ => (Some(ImapEncryptionKind::None), 143), _ => (Some(ImapEncryptionKind::None), 143),
}; };
let port = Input::with_theme(&*THEME) let port = Text::new("IMAP port")
.with_prompt("IMAP port") .with_validators(&[
.validate_with(|input: &String| input.parse::<u16>().map(|_| ())) Box::new(MinLengthValidator::new(1)),
.default(default_port.to_string()) Box::new(U16Validator {}),
.interact() ])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?; .map(|input| input.parse::<u16>().unwrap())?;
let default_login = email.to_owned(); let default_login = email.to_owned();
let login = Input::with_theme(&*THEME) let login = Text::new("IMAP login")
.with_prompt("IMAP login") .with_default(&default_login)
.default(default_login) .prompt()?;
.interact()?;
let oauth2_enabled = Confirm::new() let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) .with_default(false)
.default(false) .prompt_skippable()?
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
let auth = if oauth2_enabled { let auth = if oauth2_enabled {
@ -378,25 +367,21 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
let redirect_host = OAuth2Config::LOCALHOST.to_owned(); let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?; let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME) let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_prompt("IMAP OAuth 2.0 mechanism") .with_starting_cursor(0)
.items(OAUTH2_MECHANISMS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
config.method = match method_idx { config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2, Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2, _ => OAuth2Method::XOAuth2,
}; };
config.client_id = Input::with_theme(&*THEME) config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
.with_prompt("IMAP OAuth 2.0 client id")
.interact()?;
let client_secret: String = Password::with_theme(&*THEME) let client_secret: String = Password::new("IMAP OAuth 2.0 client secret")
.with_prompt("IMAP OAuth 2.0 client secret") .with_display_mode(PasswordDisplayMode::Masked)
.interact()?; .prompt()?;
config.client_secret = config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?; Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config config
@ -404,23 +389,12 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
.set_only_keyring(&client_secret) .set_only_keyring(&client_secret)
.await?; .await?;
config.auth_url = Input::with_theme(&*THEME) config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL").prompt()?;
.with_prompt("IMAP OAuth 2.0 authorization URL")
.interact()?;
config.token_url = Input::with_theme(&*THEME) config.token_url = Text::new("IMAP OAuth 2.0 token URL").prompt()?;
.with_prompt("IMAP OAuth 2.0 token URL")
.interact()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> { let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some( Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()))
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()))
}; };
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? { if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
@ -428,12 +402,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
} }
let confirm_additional_scope = || -> Result<bool> { let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new() let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_prompt(wizard_prompt!( .with_default(false)
"Would you like to add more IMAP OAuth 2.0 scopes?" .prompt_skippable()?
))
.default(false)
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
Ok(confirm) Ok(confirm)
@ -452,12 +423,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
config.scopes = OAuth2Scopes::Scopes(scopes); config.scopes = OAuth2Scopes::Scopes(scopes);
} }
config.pkce = Confirm::new() config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_prompt(wizard_prompt!( .with_default(true)
"Would you like to enable PKCE verification?" .prompt_skippable()?
))
.default(true)
.interact_opt()?
.unwrap_or(true); .unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -505,26 +473,23 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
ImapAuthConfig::OAuth2(config) ImapAuthConfig::OAuth2(config)
} else { } else {
let secret_idx = Select::with_theme(&*THEME) let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_prompt("IMAP authentication strategy") .with_starting_cursor(0)
.items(SECRETS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
let secret = match secret_idx { let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => { Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?; let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret secret
.set_only_keyring(prompt::passwd("IMAP password")?) .set_only_keyring(prompt::passwd("IMAP password")?)
.await?; .await?;
secret secret
} }
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?), Some(RAW) => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command( Some(CMD) => Secret::new_command(
Input::with_theme(&*THEME) Text::new("Shell command")
.with_prompt("Shell command") .with_default(&format!("pass show {account_name}-imap-passwd"))
.default(format!("pass show {account_name}-imap-passwd")) .prompt()?,
.interact()?,
), ),
_ => Default::default(), _ => Default::default(),
}; };

View file

@ -1,23 +1,25 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::Input;
use dirs::home_dir; use dirs::home_dir;
use email::maildir::config::MaildirConfig; use email::maildir::config::MaildirConfig;
use inquire::Text;
use crate::{backend::config::BackendConfig, ui::THEME}; use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> { pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = MaildirConfig::default(); let mut config = MaildirConfig::default();
let mut input = Input::with_theme(&*THEME); let mut input = Text::new("Maildir directory");
if let Some(home) = home_dir() { let Some(home) = home_dir() else {
input.default(home.join("Mail").display().to_string()); config.root_dir = input.prompt()?.into();
return Ok(BackendConfig::Maildir(config));
}; };
config.root_dir = input let def = home.join("Mail").display().to_string();
.with_prompt("Maildir directory") input = input.with_default(&def);
.interact_text()?
.into(); config.root_dir = input.prompt()?.into();
Ok(BackendConfig::Maildir(config)) Ok(BackendConfig::Maildir(config))
} }

View file

@ -1,24 +1,23 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::Input;
use email::notmuch::config::NotmuchConfig; use email::notmuch::config::NotmuchConfig;
use inquire::Text;
use crate::{backend::config::BackendConfig, ui::THEME}; use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> { pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = NotmuchConfig::default(); let config = NotmuchConfig {
database_path: Some(
let default_database_path = NotmuchConfig::get_default_database_path() Text::new("Notmuch database path")
.unwrap_or_default() .with_default(
.to_string_lossy() &NotmuchConfig::get_default_database_path()
.to_string(); .unwrap_or_default()
.to_string_lossy(),
config.database_path = Some( )
Input::with_theme(&*THEME) .prompt()?
.with_prompt("Notmuch database path") .into(),
.default(default_database_path) ),
.interact_text()? ..Default::default()
.into(), };
);
Ok(BackendConfig::Notmuch(config)) Ok(BackendConfig::Notmuch(config))
} }

View file

@ -1,15 +1,14 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::Input;
use email::sendmail::config::SendmailConfig; use email::sendmail::config::SendmailConfig;
use inquire::Text;
use crate::{backend::config::BackendConfig, ui::THEME}; use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> { pub(crate) fn configure() -> Result<BackendConfig> {
let config = SendmailConfig { let config = SendmailConfig {
cmd: Input::with_theme(&*THEME) cmd: Text::new("Sendmail-compatible shell command to send emails")
.with_prompt("Sendmail-compatible shell command to send emails") .with_default("/usr/bin/msmtp")
.default(String::from("/usr/bin/msmtp")) .prompt()?
.interact()?
.into(), .into(),
}; };

View file

@ -1,5 +1,4 @@
use color_eyre::Result; use color_eyre::Result;
use dialoguer::{Confirm, Input, Password, Select};
#[cfg(feature = "account-discovery")] #[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType}; use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::{ use email::{
@ -9,14 +8,11 @@ use email::{
}, },
smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind}, smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind},
}; };
use inquire::validator::{ErrorMessage, StringValidator, Validation};
use oauth::v2_0::{AuthorizationCodeGrant, Client}; use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret; use secret::Secret;
use crate::{ use crate::{backend::config::BackendConfig, ui::prompt, wizard_log};
backend::config::BackendConfig,
ui::{prompt, THEME},
wizard_log, wizard_prompt,
};
const ENCRYPTIONS: &[SmtpEncryptionKind] = &[ const ENCRYPTIONS: &[SmtpEncryptionKind] = &[
SmtpEncryptionKind::Tls, SmtpEncryptionKind::Tls,
@ -33,12 +29,35 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)"; 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"; 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<Validation, inquire::CustomUserError> {
if input.parse::<u16>().is_ok() {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid(ErrorMessage::Custom(format!(
"you should enter a number between {} and {}",
u16::MIN,
u16::MAX
))))
}
}
}
#[cfg(feature = "account-discovery")] #[cfg(feature = "account-discovery")]
pub(crate) async fn configure( pub(crate) async fn configure(
account_name: &str, account_name: &str,
email: &str, email: &str,
autoconfig: Option<&AutoConfig>, autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> { ) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator, Confirm, Password, Select, Text};
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2()); let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| { let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider() c.email_provider()
@ -54,10 +73,9 @@ pub(crate) async fn configure(
let default_host = let default_host =
autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1)); autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1));
let host = Input::with_theme(&*THEME) let host = Text::new("SMTP hostname")
.with_prompt("SMTP hostname") .with_default(&default_host)
.default(default_host) .prompt()?;
.interact()?;
let autoconfig_encryption = autoconfig_server let autoconfig_encryption = autoconfig_server
.and_then(|smtp| { .and_then(|smtp| {
@ -75,11 +93,9 @@ pub(crate) async fn configure(
SmtpEncryptionKind::None => 2, SmtpEncryptionKind::None => 2,
}; };
let encryption_idx = Select::with_theme(&*THEME) let encryption_kind = Select::new("SMTP encryption", ENCRYPTIONS.to_vec())
.with_prompt("SMTP encryption") .with_starting_cursor(default_encryption_idx)
.items(ENCRYPTIONS) .prompt_skippable()?;
.default(default_encryption_idx)
.interact_opt()?;
let autoconfig_port = autoconfig_server let autoconfig_port = autoconfig_server
.and_then(|s| s.port()) .and_then(|s| s.port())
@ -90,24 +106,27 @@ pub(crate) async fn configure(
SmtpEncryptionKind::None => 25, SmtpEncryptionKind::None => 25,
}); });
let (encryption, default_port) = match encryption_idx { let (encryption, default_port) = match encryption_kind {
Some(idx) if idx == default_encryption_idx => { 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(autoconfig_encryption), autoconfig_port)
} }
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => { Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465),
(Some(SmtpEncryptionKind::Tls), 465) Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587),
}
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => {
(Some(SmtpEncryptionKind::StartTls), 587)
}
_ => (Some(SmtpEncryptionKind::None), 25), _ => (Some(SmtpEncryptionKind::None), 25),
}; };
let port = Input::with_theme(&*THEME) let port = Text::new("SMTP port")
.with_prompt("SMTP port") .with_validators(&[
.validate_with(|input: &String| input.parse::<u16>().map(|_| ())) Box::new(validator::MinLengthValidator::new(1)),
.default(default_port.to_string()) Box::new(U16Validator {}),
.interact() ])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?; .map(|input| input.parse::<u16>().unwrap())?;
let autoconfig_login = autoconfig_server.map(|smtp| match smtp.username() { let autoconfig_login = autoconfig_server.map(|smtp| match smtp.username() {
@ -118,10 +137,9 @@ pub(crate) async fn configure(
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned()); let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Input::with_theme(&*THEME) let login = Text::new("SMTP login")
.with_prompt("SMTP login") .with_default(&default_login)
.default(default_login) .prompt()?;
.interact()?;
let default_oauth2_enabled = autoconfig_server let default_oauth2_enabled = autoconfig_server
.and_then(|smtp| { .and_then(|smtp| {
@ -132,10 +150,9 @@ pub(crate) async fn configure(
.filter(|_| autoconfig_oauth2.is_some()) .filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default(); .unwrap_or_default();
let oauth2_enabled = Confirm::new() let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) .with_default(default_oauth2_enabled)
.default(default_oauth2_enabled) .prompt_skippable()?
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
let auth = if oauth2_enabled { let auth = if oauth2_enabled {
@ -143,25 +160,21 @@ pub(crate) async fn configure(
let redirect_host = OAuth2Config::LOCALHOST; let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?; let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME) let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_prompt("SMTP OAuth 2.0 mechanism") .with_starting_cursor(0)
.items(OAUTH2_MECHANISMS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
config.method = match method_idx { config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2, Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2, _ => OAuth2Method::XOAuth2,
}; };
config.client_id = Input::with_theme(&*THEME) config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
.with_prompt("SMTP OAuth 2.0 client id")
.interact()?;
let client_secret: String = Password::with_theme(&*THEME) let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_prompt("SMTP OAuth 2.0 client secret") .with_display_mode(inquire::PasswordDisplayMode::Masked)
.interact()?; .prompt()?;
config.client_secret = config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?; Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config config
@ -172,38 +185,26 @@ pub(crate) async fn configure(
let default_auth_url = autoconfig_oauth2 let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned()) .map(|o| o.auth_url().to_owned())
.unwrap_or_default(); .unwrap_or_default();
config.auth_url = Input::with_theme(&*THEME) config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL")
.with_prompt("SMTP OAuth 2.0 authorization URL") .with_default(&default_auth_url)
.default(default_auth_url) .prompt()?;
.interact()?;
let default_token_url = autoconfig_oauth2 let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned()) .map(|o| o.token_url().to_owned())
.unwrap_or_default(); .unwrap_or_default();
config.token_url = Input::with_theme(&*THEME) config.token_url = Text::new("SMTP OAuth 2.0 token URL")
.with_prompt("SMTP OAuth 2.0 token URL") .with_default(&default_token_url)
.default(default_token_url) .prompt()?;
.interact()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope()); let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> { let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes { Ok(match &autoconfig_scopes {
Some(scopes) => Select::with_theme(&*THEME) Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_prompt(prompt) .with_starting_cursor(0)
.items(scopes) .prompt_skippable()?
.default(0) .map(ToOwned::to_owned),
.interact_opt()? None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
.and_then(|idx| scopes.get(idx))
.map(|scope| scope.to_string()),
None => Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()),
}) })
}; };
@ -212,12 +213,9 @@ pub(crate) async fn configure(
} }
let confirm_additional_scope = || -> Result<bool> { let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new() let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_prompt(wizard_prompt!( .with_default(false)
"Would you like to add more SMTP OAuth 2.0 scopes?" .prompt_skippable()?
))
.default(false)
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
Ok(confirm) Ok(confirm)
@ -236,12 +234,9 @@ pub(crate) async fn configure(
config.scopes = OAuth2Scopes::Scopes(scopes); config.scopes = OAuth2Scopes::Scopes(scopes);
} }
config.pkce = Confirm::new() config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_prompt(wizard_prompt!( .with_default(true)
"Would you like to enable PKCE verification?" .prompt_skippable()?
))
.default(true)
.interact_opt()?
.unwrap_or(true); .unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -289,26 +284,23 @@ pub(crate) async fn configure(
SmtpAuthConfig::OAuth2(config) SmtpAuthConfig::OAuth2(config)
} else { } else {
let secret_idx = Select::with_theme(&*THEME) let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_prompt("SMTP authentication strategy") .with_starting_cursor(0)
.items(SECRETS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
let secret = match secret_idx { let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => { Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?; let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret secret
.set_only_keyring(prompt::passwd("SMTP password")?) .set_only_keyring(prompt::passwd("SMTP password")?)
.await?; .await?;
secret secret
} }
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?), Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command( Some(sec) if sec == CMD => Secret::new_command(
Input::with_theme(&*THEME) Text::new("Shell command")
.with_prompt("Shell command") .with_default(&format!("pass show {account_name}-smtp-passwd"))
.default(format!("pass show {account_name}-smtp-passwd")) .prompt()?,
.interact()?,
), ),
_ => Default::default(), _ => Default::default(),
}; };
@ -329,47 +321,42 @@ pub(crate) async fn configure(
#[cfg(not(feature = "account-discovery"))] #[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> { pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
use inquire::{validator::MinLengthValidator, Confirm, Password, Select, Text};
let default_host = format!("smtp.{}", email.rsplit_once('@').unwrap().1); let default_host = format!("smtp.{}", email.rsplit_once('@').unwrap().1);
let host = Input::with_theme(&*THEME) let host = Text::new("SMTP hostname")
.with_prompt("SMTP hostname") .with_default(&default_host)
.default(default_host) .prompt()?;
.interact()?;
let encryption_idx = Select::with_theme(&*THEME) let encryption_idx = Select::new("SMTP encryption", ENCRYPTIONS.to_vec())
.with_prompt("SMTP encryption") .with_starting_cursor(0)
.items(ENCRYPTIONS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
let (encryption, default_port) = match encryption_idx { let (encryption, default_port) = match encryption_idx {
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => { Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465),
(Some(SmtpEncryptionKind::Tls), 465) Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587),
}
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => {
(Some(SmtpEncryptionKind::StartTls), 587)
}
_ => (Some(SmtpEncryptionKind::None), 25), _ => (Some(SmtpEncryptionKind::None), 25),
}; };
let port = Input::with_theme(&*THEME) let port = Text::new("SMTP port")
.with_prompt("SMTP port") .with_validators(&[
.validate_with(|input: &String| input.parse::<u16>().map(|_| ())) Box::new(MinLengthValidator::new(1)),
.default(default_port.to_string()) Box::new(U16Validator {}),
.interact() ])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?; .map(|input| input.parse::<u16>().unwrap())?;
let default_login = email.to_owned(); let default_login = email.to_owned();
let login = Input::with_theme(&*THEME) let login = Text::new("SMTP login")
.with_prompt("SMTP login") .with_default(&default_login)
.default(default_login) .prompt()?;
.interact()?;
let oauth2_enabled = Confirm::new() let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?")) .with_default(false)
.default(false) .prompt_skippable()?
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
let auth = if oauth2_enabled { let auth = if oauth2_enabled {
@ -377,25 +364,21 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
let redirect_host = OAuth2Config::LOCALHOST.to_owned(); let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?; let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME) let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_prompt("SMTP OAuth 2.0 mechanism") .with_starting_cursor(0)
.items(OAUTH2_MECHANISMS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
config.method = match method_idx { config.method = match method_idx {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2, Some(XOAUTH2) => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer, Some(OAUTHBEARER) => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2, _ => OAuth2Method::XOAuth2,
}; };
config.client_id = Input::with_theme(&*THEME) config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
.with_prompt("SMTP OAuth 2.0 client id")
.interact()?;
let client_secret: String = Password::with_theme(&*THEME) let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_prompt("SMTP OAuth 2.0 client secret") .with_display_mode(inquire::PasswordDisplayMode::Masked)
.interact()?; .prompt()?;
config.client_secret = config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?; Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config config
@ -403,23 +386,12 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
.set_only_keyring(&client_secret) .set_only_keyring(&client_secret)
.await?; .await?;
config.auth_url = Input::with_theme(&*THEME) config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL").prompt()?;
.with_prompt("SMTP OAuth 2.0 authorization URL")
.interact()?;
config.token_url = Input::with_theme(&*THEME) config.token_url = Text::new("SMTP OAuth 2.0 token URL").prompt()?;
.with_prompt("SMTP OAuth 2.0 token URL")
.interact()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> { let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some( Ok(Some(Text::new(prompt).prompt()?.to_owned()).filter(|scope| !scope.is_empty()))
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()))
}; };
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? { if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
@ -427,12 +399,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
} }
let confirm_additional_scope = || -> Result<bool> { let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new() let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_prompt(wizard_prompt!( .with_default(false)
"Would you like to add more SMTP OAuth 2.0 scopes?" .prompt_skippable()?
))
.default(false)
.interact_opt()?
.unwrap_or_default(); .unwrap_or_default();
Ok(confirm) Ok(confirm)
@ -451,12 +420,9 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
config.scopes = OAuth2Scopes::Scopes(scopes); config.scopes = OAuth2Scopes::Scopes(scopes);
} }
config.pkce = Confirm::new() config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_prompt(wizard_prompt!( .with_default(true)
"Would you like to enable PKCE verification?" .prompt_skippable()?
))
.default(true)
.interact_opt()?
.unwrap_or(true); .unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:"); wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
@ -504,26 +470,23 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
SmtpAuthConfig::OAuth2(config) SmtpAuthConfig::OAuth2(config)
} else { } else {
let secret_idx = Select::with_theme(&*THEME) let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_prompt("SMTP authentication strategy") .with_starting_cursor(0)
.items(SECRETS) .prompt_skippable()?;
.default(0)
.interact_opt()?;
let secret = match secret_idx { let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => { Some(KEYRING) => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?; let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret secret
.set_only_keyring(prompt::passwd("SMTP password")?) .set_only_keyring(prompt::passwd("SMTP password")?)
.await?; .await?;
secret secret
} }
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?), Some(RAW) => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command( Some(CMD) => Secret::new_command(
Input::with_theme(&*THEME) Text::new("Shell command")
.with_prompt("Shell command") .with_default(&format!("pass show {account_name}-smtp-passwd"))
.default(format!("pass show {account_name}-smtp-passwd")) .prompt()?,
.interact()?,
), ),
_ => Default::default(), _ => Default::default(),
}; };

View file

@ -1,7 +1,7 @@
use color_eyre::Result; use std::fmt::Display;
use dialoguer::Select;
use super::THEME; use color_eyre::Result;
use inquire::Select;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum PreEditChoice { pub enum PreEditChoice {
@ -10,13 +10,17 @@ pub enum PreEditChoice {
Quit, Quit,
} }
impl ToString for PreEditChoice { impl Display for PreEditChoice {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { write!(
Self::Edit => "Edit it".into(), f,
Self::Discard => "Discard it".into(), "{}",
Self::Quit => "Quit".into(), match self {
} Self::Edit => "Edit it",
Self::Discard => "Discard it",
Self::Quit => "Quit",
}
)
} }
} }
@ -27,13 +31,15 @@ pub fn pre_edit() -> Result<PreEditChoice> {
PreEditChoice::Quit, PreEditChoice::Quit,
]; ];
let choice_idx = Select::with_theme(&*THEME) let user_choice = Select::new(
.with_prompt("A draft was found, what would you like to do with it?") "A draft was found, what would you like to do with it?",
.items(&choices) choices.to_vec(),
.default(0) )
.interact()?; .with_starting_cursor(0)
.with_vim_mode(true)
.prompt()?;
Ok(choices[choice_idx].clone()) Ok(user_choice)
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -45,15 +51,19 @@ pub enum PostEditChoice {
Discard, Discard,
} }
impl ToString for PostEditChoice { impl Display for PostEditChoice {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { write!(
Self::Send => "Send it".into(), f,
Self::Edit => "Edit it again".into(), "{}",
Self::LocalDraft => "Save it as local draft".into(), match self {
Self::RemoteDraft => "Save it as remote draft".into(), Self::Send => "Send it",
Self::Discard => "Discard it".into(), Self::Edit => "Edit it again",
} Self::LocalDraft => "Save it as local draft",
Self::RemoteDraft => "Save it as remote draft",
Self::Discard => "Discard it",
}
)
} }
} }
@ -66,11 +76,13 @@ pub fn post_edit() -> Result<PostEditChoice> {
PostEditChoice::Discard, PostEditChoice::Discard,
]; ];
let choice_idx = Select::with_theme(&*THEME) let user_choice = inquire::Select::new(
.with_prompt("What would you like to do with this message?") "What would you like to do with this message?",
.items(&choices) choices.to_vec(),
.default(0) )
.interact()?; .with_starting_cursor(0)
.with_vim_mode(true)
.prompt()?;
Ok(choices[choice_idx].clone()) Ok(user_choice)
} }

View file

@ -3,9 +3,4 @@ pub mod editor;
pub(crate) mod prompt; pub(crate) mod prompt;
pub mod table; pub mod table;
use dialoguer::theme::ColorfulTheme;
use once_cell::sync::Lazy;
pub use self::table::*; pub use self::table::*;
pub(crate) static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);

View file

@ -1,21 +1,28 @@
use dialoguer::Password;
use std::io; use std::io;
use super::THEME;
pub(crate) fn passwd(prompt: &str) -> io::Result<String> { pub(crate) fn passwd(prompt: &str) -> io::Result<String> {
Password::with_theme(&*THEME) inquire::Password::new(prompt)
.with_prompt(prompt) .with_custom_confirmation_message("Confirm password")
.with_confirmation( .with_custom_confirmation_error_message("Passwords do not match, please try again.")
"Confirm password", .with_display_mode(inquire::PasswordDisplayMode::Masked)
"Passwords do not match, please try again.", .prompt()
) .map_err(|e| {
.interact() io::Error::new(
io::ErrorKind::Interrupted,
format!("failed to get password: {e}"),
)
})
} }
pub(crate) fn secret(prompt: &str) -> io::Result<String> { pub(crate) fn secret(prompt: &str) -> io::Result<String> {
Password::with_theme(&*THEME) inquire::Password::new(prompt)
.with_prompt(prompt) .with_display_mode(inquire::PasswordDisplayMode::Masked)
.report(false) .without_confirmation()
.interact() .prompt()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Interrupted,
format!("failed to get secret: {e}"),
)
})
} }