mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-22 11:00:19 +00:00
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:
parent
d54dd6429e
commit
1e448e56eb
17 changed files with 441 additions and 543 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,12 +141,10 @@ 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)
|
|
||||||
.interact_opt()?
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if should_configure_sync {
|
if should_configure_sync {
|
||||||
|
|
|
@ -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,9 +70,12 @@ 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!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
Self::None => "None",
|
Self::None => "None",
|
||||||
|
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
|
@ -91,9 +94,8 @@ impl ToString for BackendKind {
|
||||||
|
|
||||||
#[cfg(feature = "sendmail")]
|
#[cfg(feature = "sendmail")]
|
||||||
Self::Sendmail => "Sendmail",
|
Self::Sendmail => "Sendmail",
|
||||||
};
|
}
|
||||||
|
)
|
||||||
kind.to_string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:?}…");
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
.with_default(
|
||||||
|
&NotmuchConfig::get_default_database_path()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy(),
|
||||||
.to_string();
|
)
|
||||||
|
.prompt()?
|
||||||
config.database_path = Some(
|
|
||||||
Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("Notmuch database path")
|
|
||||||
.default(default_database_path)
|
|
||||||
.interact_text()?
|
|
||||||
.into(),
|
.into(),
|
||||||
);
|
),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(BackendConfig::Notmuch(config))
|
Ok(BackendConfig::Notmuch(config))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
match self {
|
match self {
|
||||||
Self::Edit => "Edit it".into(),
|
Self::Edit => "Edit it",
|
||||||
Self::Discard => "Discard it".into(),
|
Self::Discard => "Discard it",
|
||||||
Self::Quit => "Quit".into(),
|
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 {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
match self {
|
match self {
|
||||||
Self::Send => "Send it".into(),
|
Self::Send => "Send it",
|
||||||
Self::Edit => "Edit it again".into(),
|
Self::Edit => "Edit it again",
|
||||||
Self::LocalDraft => "Save it as local draft".into(),
|
Self::LocalDraft => "Save it as local draft",
|
||||||
Self::RemoteDraft => "Save it as remote draft".into(),
|
Self::RemoteDraft => "Save it as remote draft",
|
||||||
Self::Discard => "Discard it".into(),
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
|
@ -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| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Interrupted,
|
||||||
|
format!("failed to get password: {e}"),
|
||||||
)
|
)
|
||||||
.interact()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue