make use of pimalaya-tui

This commit is contained in:
Clément DOUIN 2024-08-30 12:13:06 +02:00
parent c5b33b9623
commit 5a22cab781
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
22 changed files with 211 additions and 1234 deletions

137
Cargo.lock generated
View file

@ -211,7 +211,7 @@ dependencies = [
"futures-lite 2.3.0",
"parking",
"polling 3.7.3",
"rustix 0.38.34",
"rustix 0.38.35",
"slab",
"tracing",
"windows-sys 0.59.0",
@ -250,7 +250,7 @@ dependencies = [
"cfg-if",
"event-listener 3.1.0",
"futures-lite 1.13.0",
"rustix 0.38.34",
"rustix 0.38.35",
"windows-sys 0.48.0",
]
@ -277,7 +277,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
"rustix 0.38.34",
"rustix 0.38.35",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
@ -560,9 +560,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.14"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"jobserver",
"libc",
@ -671,14 +671,14 @@ dependencies = [
"anstyle",
"clap_lex",
"strsim 0.11.1",
"terminal_size 0.3.0",
"terminal_size",
]
[[package]]
name = "clap_complete"
version = "4.5.23"
version = "4.5.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531d7959c5bbb6e266cecdd0f20213639c3a5c3e4d615f97db87661745f781ff"
checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b"
dependencies = [
"clap",
]
@ -774,19 +774,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"unicode-width",
"windows-sys 0.52.0",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@ -1309,12 +1296,6 @@ dependencies = [
"serde",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.34"
@ -1463,9 +1444,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.24"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
@ -1910,13 +1891,11 @@ dependencies = [
"clap_mangen",
"color-eyre",
"comfy-table",
"console",
"crossterm 0.27.0",
"dirs 4.0.0",
"email-lib",
"email_address",
"erased-serde",
"indicatif",
"inquire",
"mail-builder",
"md5",
@ -1924,6 +1903,7 @@ dependencies = [
"oauth-lib",
"once_cell",
"petgraph",
"pimalaya-tui",
"process-lib",
"secret-lib",
"serde",
@ -1931,14 +1911,12 @@ dependencies = [
"serde_json",
"shellexpand-utils",
"sled",
"terminal_size 0.1.17",
"tokio",
"toml",
"toml_edit 0.22.20",
"tracing",
"tracing-error",
"tracing-subscriber",
"unicode-width",
"url",
"uuid",
]
@ -2121,7 +2099,7 @@ dependencies = [
"hyper-util",
"log",
"rustls 0.23.12",
"rustls-native-certs 0.7.2",
"rustls-native-certs 0.7.3",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
@ -2213,7 +2191,7 @@ source = "git+https://github.com/pimalaya/imap-client#02d6bce5513c8ec6ac3aff0e7b
dependencies = [
"imap-next",
"once_cell",
"rustls-native-certs 0.7.2",
"rustls-native-certs 0.7.3",
"thiserror",
"tokio",
"tokio-rustls 0.26.0",
@ -2223,7 +2201,7 @@ dependencies = [
[[package]]
name = "imap-codec"
version = "2.0.0-alpha.4"
source = "git+https://github.com/duesee/imap-codec#fff8355ad0f7133be9e58919be5a6f05f684d421"
source = "git+https://github.com/duesee/imap-codec#95de04494f89464a59c114859217e6119a18d426"
dependencies = [
"abnf-core",
"base64 0.22.1",
@ -2250,7 +2228,7 @@ dependencies = [
[[package]]
name = "imap-types"
version = "2.0.0-alpha.3"
source = "git+https://github.com/duesee/imap-codec#fff8355ad0f7133be9e58919be5a6f05f684d421"
source = "git+https://github.com/duesee/imap-codec#95de04494f89464a59c114859217e6119a18d426"
dependencies = [
"base64 0.22.1",
"bounded-static",
@ -2276,19 +2254,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "indicatif"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
dependencies = [
"console",
"instant",
"number_prefix",
"portable-atomic",
"unicode-width",
]
[[package]]
name = "inotify"
version = "0.9.6"
@ -2938,12 +2903,6 @@ dependencies = [
"libm",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "oauth-lib"
version = "0.1.1"
@ -3240,6 +3199,22 @@ dependencies = [
"z-base-32",
]
[[package]]
name = "pimalaya-tui"
version = "0.1.0"
source = "git+https://github.com/pimalaya/tui#80660dfaf9daafbaa716c711e510bf3cfd04cd69"
dependencies = [
"crossterm 0.25.0",
"dirs 4.0.0",
"email-lib",
"email_address",
"inquire",
"oauth-lib",
"secret-lib",
"shellexpand-utils",
"thiserror",
]
[[package]]
name = "pin-project"
version = "1.1.5"
@ -3336,17 +3311,11 @@ dependencies = [
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix 0.38.34",
"rustix 0.38.35",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "portable-atomic"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -3358,9 +3327,9 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.21"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a909e6e8053fa1a5ad670f5816c7d93029ee1fa8898718490544a6b0d5d38b3e"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn 2.0.76",
@ -3710,9 +3679,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
@ -3733,9 +3702,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.34"
version = "0.38.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
dependencies = [
"bitflags 2.6.0",
"errno",
@ -3766,7 +3735,7 @@ dependencies = [
"log",
"once_cell",
"rustls-pki-types",
"rustls-webpki 0.102.6",
"rustls-webpki 0.102.7",
"subtle",
"zeroize",
]
@ -3785,9 +3754,9 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04182dffc9091a404e0fc069ea5cd60e5b866c3adf881eff99a32d048242dffa"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.3",
@ -3833,9 +3802,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.102.6"
version = "0.102.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56"
dependencies = [
"aws-lc-rs",
"ring",
@ -4367,27 +4336,17 @@ dependencies = [
"cfg-if",
"fastrand 2.1.1",
"once_cell",
"rustix 0.38.34",
"rustix 0.38.35",
"windows-sys 0.59.0",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix 0.38.34",
"rustix 0.38.35",
"windows-sys 0.48.0",
]
@ -4438,9 +4397,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.39.3"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"bytes",
@ -4921,7 +4880,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.34",
"rustix 0.38.35",
]
[[package]]

View file

@ -23,24 +23,24 @@ default = [
"smtp",
"sendmail",
"wizard",
# "keyring",
# "oauth2",
"wizard",
# "pgp-commands",
# "pgp-gpg",
# "pgp-native",
]
imap = ["email-lib/imap"]
maildir = ["email-lib/maildir"]
notmuch = ["email-lib/notmuch"]
smtp = ["email-lib/smtp"]
sendmail = ["email-lib/sendmail"]
imap = ["email-lib/imap", "pimalaya-tui/imap"]
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
keyring = ["email-lib/keyring", "secret-lib?/keyring-tokio"]
oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "keyring"]
wizard = ["dep:secret-lib", "dep:toml_edit", "email-lib/autoconfig"]
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib?/keyring-tokio"]
oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
wizard = ["dep:email_address", "dep:secret-lib", "dep:toml_edit", "email-lib/autoconfig"]
pgp = []
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"]
@ -55,13 +55,11 @@ clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6.3"
comfy-table = "7.1.1"
console = "0.15.2"
crossterm = { version = "0.27", features = ["serde"] }
dirs = "4"
email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] }
email_address = "0.2.4"
email_address = { version = "0.2", optional = true }
erased-serde = "0.3"
indicatif = "0.17"
inquire = "0.7.4"
mail-builder = "0.3"
md5 = "0.7"
@ -69,6 +67,7 @@ mml-lib = { version = "=1.0.14", default-features = false, features = ["derive"]
oauth-lib = { version = "=0.1.1", optional = true }
once_cell = "1.16"
petgraph = "0.6"
pimalaya-tui = { version = "=0.1.0", default-features = false, features = ["email", "path"] }
process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.6", default-features = false, features = ["command", "derive"], optional = true }
serde = { version = "1", features = ["derive"] }
@ -76,20 +75,19 @@ serde-toml-merge = "0.3"
serde_json = "1"
shellexpand-utils = "=0.2.1"
sled = "=0.34.7"
terminal_size = "0.1"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8"
toml_edit = { version = "0.22", optional = true }
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-width = "0.1"
url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io]
oauth-lib = { git = "https://github.com/pimalaya/core" }
imap-codec = { git = "https://github.com/duesee/imap-codec" }
imap-next = { git = "https://github.com/duesee/imap-next", branch = "jakoschiko_poison-message-with-fragmentizer" }
imap-client = { git = "https://github.com/pimalaya/imap-client" }
oauth-lib = { git = "https://github.com/pimalaya/core" }
email-lib = { git = "https://github.com/pimalaya/core" }
pimalaya-tui = { git = "https://github.com/pimalaya/tui" }

View file

@ -4,12 +4,12 @@ use color_eyre::Result;
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpAuthConfig;
#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))]
use pimalaya_tui::prompt;
use tracing::info;
#[cfg(any(feature = "imap", feature = "smtp"))]
use tracing::{debug, warn};
#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))]
use crate::ui::prompt;
use crate::{account::arg::name::AccountNameArg, config::TomlConfig, printer::Printer};
/// Configure an account.
@ -74,12 +74,14 @@ impl AccountConfigureCommand {
if let Some(ref config) = account_config.imap {
match &config.auth {
ImapAuthConfig::Passwd(config) => {
config.configure(|| prompt::passwd("IMAP password")).await
config
.configure(|| Ok(prompt::password("IMAP password")?))
.await
}
#[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => {
config
.configure(|| prompt::secret("IMAP OAuth 2.0 client secret"))
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 clientsecret")?))
.await
}
}?;
@ -89,12 +91,14 @@ impl AccountConfigureCommand {
if let Some(ref config) = account_config.smtp {
match &config.auth {
SmtpAuthConfig::Passwd(config) => {
config.configure(|| prompt::passwd("SMTP password")).await
config
.configure(|| Ok(prompt::password("SMTP password")?))
.await
}
#[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => {
config
.configure(|| prompt::secret("SMTP OAuth 2.0 client secret"))
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
.await
}
}?;
@ -104,7 +108,7 @@ impl AccountConfigureCommand {
if let Some(ref config) = account_config.pgp {
config
.configure(&account_config.email, || {
prompt::passwd("PGP secret key password")
Ok(prompt::password("PGP secret key password")?)
})
.await?;
}

View file

@ -1,9 +1,6 @@
use color_eyre::{eyre::OptionExt, Result};
use email_address::EmailAddress;
use inquire::validator::{ErrorMessage, Validation};
use std::{path::PathBuf, str::FromStr};
use color_eyre::Result;
use pimalaya_tui::{print, prompt};
use crate::wizard_warn;
use crate::{
backend::{self, config::BackendConfig, BackendKind},
message::config::{MessageConfig, MessageSendConfig},
@ -11,104 +8,66 @@ use crate::{
use super::TomlAccountConfig;
pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
let mut config = TomlAccountConfig {
email: inquire::Text::new("Email address: ")
.with_validator(|email: &_| {
if EmailAddress::is_valid(email) {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid(ErrorMessage::Custom(format!(
"Invalid email address: {email}"
))))
}
})
.prompt()?,
pub async fn configure() -> Result<(String, TomlAccountConfig)> {
let email = prompt::email("Email address:", None)?;
let mut config = TomlAccountConfig {
email: email.to_string(),
..Default::default()
};
let addr = EmailAddress::from_str(&config.email).unwrap();
#[cfg(feature = "wizard")]
let autoconfig_email = config.email.to_owned();
#[cfg(feature = "wizard")]
let autoconfig =
tokio::spawn(async move { email::autoconfig::from_addr(&autoconfig_email).await.ok() });
let account_name = inquire::Text::new("Account name: ")
.with_default(
addr.domain()
.split_once('.')
.ok_or_eyre("not a valid domain, without any .")?
.0,
)
.prompt()?;
let default_account_name = email
.domain()
.split_once('.')
.map(|domain| domain.0)
.unwrap_or(email.domain());
let account_name = prompt::text("Account name:", Some(default_account_name))?;
config.display_name = Some(
inquire::Text::new("Full display name: ")
.with_default(addr.local_part())
.prompt()?,
);
config.display_name = Some(prompt::text(
"Full display name:",
Some(email.local_part()),
)?);
config.downloads_dir = Some(PathBuf::from(
inquire::Text::new("Downloads directory: ")
.with_default("~/Downloads")
.prompt()?,
));
config.downloads_dir = Some(prompt::path("Downloads directory:", Some("~/Downloads"))?);
let email = &config.email;
#[cfg(feature = "wizard")]
let autoconfig = autoconfig.await?;
#[cfg(feature = "wizard")]
let autoconfig = autoconfig.as_ref();
#[cfg(feature = "wizard")]
if let Some(config) = autoconfig {
if config.is_gmail() {
println!();
wizard_warn!("Warning: Google passwords cannot be used directly, see:");
wizard_warn!("https://pimalaya.org/himalaya/cli/latest/configuration/gmail.html");
print::warn("Warning: Google passwords cannot be used directly, see:");
print::warn("https://github.com/pimalaya/himalaya?tab=readme-ov-file#configuration");
println!();
}
}
match backend::wizard::configure(
&account_name,
email,
#[cfg(feature = "wizard")]
autoconfig,
)
.await?
{
match backend::wizard::configure(&account_name, &email, autoconfig).await? {
#[cfg(feature = "imap")]
Some(BackendConfig::Imap(imap_config)) => {
BackendConfig::Imap(imap_config) => {
config.imap = Some(imap_config);
config.backend = Some(BackendKind::Imap);
}
#[cfg(feature = "maildir")]
Some(BackendConfig::Maildir(mdir_config)) => {
BackendConfig::Maildir(mdir_config) => {
config.maildir = Some(mdir_config);
config.backend = Some(BackendKind::Maildir);
}
#[cfg(feature = "notmuch")]
Some(BackendConfig::Notmuch(notmuch_config)) => {
BackendConfig::Notmuch(notmuch_config) => {
config.notmuch = Some(notmuch_config);
config.backend = Some(BackendKind::Notmuch);
}
_ => (),
_ => unreachable!(),
};
match backend::wizard::configure_sender(
&account_name,
email,
#[cfg(feature = "wizard")]
autoconfig,
)
.await?
{
match backend::wizard::configure_sender(&account_name, &email, autoconfig).await? {
#[cfg(feature = "smtp")]
Some(BackendConfig::Smtp(smtp_config)) => {
BackendConfig::Smtp(smtp_config) => {
config.smtp = Some(smtp_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
@ -119,7 +78,7 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
});
}
#[cfg(feature = "sendmail")]
Some(BackendConfig::Sendmail(sendmail_config)) => {
BackendConfig::Sendmail(sendmail_config) => {
config.sendmail = Some(sendmail_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
@ -129,8 +88,8 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
..Default::default()
});
}
_ => (),
_ => unreachable!(),
};
Ok(Some((account_name, config)))
Ok((account_name, config))
}

View file

@ -1,17 +1,7 @@
use color_eyre::Result;
use email::autoconfig::config::AutoConfig;
use inquire::Select;
#[cfg(feature = "imap")]
use crate::imap;
#[cfg(feature = "maildir")]
use crate::maildir;
#[cfg(feature = "notmuch")]
use crate::notmuch;
#[cfg(feature = "sendmail")]
use crate::sendmail;
#[cfg(feature = "smtp")]
use crate::smtp;
use email_address::EmailAddress;
use pimalaya_tui::{prompt, wizard};
use super::{config::BackendConfig, BackendKind};
@ -24,6 +14,34 @@ const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
BackendKind::Notmuch,
];
pub async fn configure(
account_name: &str,
email: &EmailAddress,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
let backend = prompt::item("Default backend:", &*DEFAULT_BACKEND_KINDS, None)?;
match backend {
#[cfg(feature = "imap")]
BackendKind::Imap => {
let config = wizard::imap::start(account_name, email, autoconfig).await?;
Ok(BackendConfig::Imap(config))
}
#[cfg(feature = "maildir")]
BackendKind::Maildir => {
let config = wizard::maildir::start(account_name)?;
Ok(BackendConfig::Maildir(config))
}
// TODO
// #[cfg(feature = "notmuch")]
// BackendKind::Notmuch => {
// let config = wizard::notmuch::start()?;
// Ok(BackendConfig::Notmuch(config))
// }
_ => unreachable!(),
}
}
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
#[cfg(feature = "smtp")]
BackendKind::Smtp,
@ -31,63 +49,30 @@ const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
BackendKind::Sendmail,
];
pub(crate) async fn configure(
pub async fn configure_sender(
account_name: &str,
email: &str,
email: &EmailAddress,
autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::new("Default email backend", DEFAULT_BACKEND_KINDS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
) -> Result<BackendConfig> {
let backend = prompt::item(
"Backend for sending messages:",
&*SEND_MESSAGE_BACKEND_KINDS,
None,
)?;
let config = match kind {
#[cfg(feature = "imap")]
Some(kind) if kind == BackendKind::Imap => Some(
imap::wizard::configure(
account_name,
email,
#[cfg(feature = "wizard")]
autoconfig,
)
.await?,
),
#[cfg(feature = "maildir")]
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
#[cfg(feature = "notmuch")]
Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?),
_ => None,
};
Ok(config)
}
pub(crate) async fn configure_sender(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::new(
"Backend for sending messages",
SEND_MESSAGE_BACKEND_KINDS.to_vec(),
)
.with_starting_cursor(0)
.prompt_skippable()?;
let config = match kind {
match backend {
// TODO
#[cfg(feature = "smtp")]
Some(kind) if kind == BackendKind::Smtp => Some(
smtp::wizard::configure(
account_name,
email,
#[cfg(feature = "wizard")]
autoconfig,
)
.await?,
),
BackendKind::Smtp => {
let config = wizard::smtp::start(account_name, email, autoconfig).await?;
Ok(BackendConfig::Smtp(config))
}
// TODO
#[cfg(feature = "sendmail")]
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
_ => None,
};
Ok(config)
BackendKind::Sendmail => {
let config = wizard::sendmail::start()?;
Ok(BackendConfig::Sendmail(config))
}
_ => unreachable!(),
}
}

View file

@ -1,6 +1,8 @@
#[cfg(feature = "wizard")]
pub mod wizard;
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use color_eyre::{
eyre::{bail, eyre, Context},
Result,
@ -11,16 +13,15 @@ use email::{
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
folder::config::FolderConfig, message::config::MessageConfig,
};
#[cfg(feature = "wizard")]
use pimalaya_tui::print;
use serde::{Deserialize, Serialize};
use serde_toml_merge::merge;
use shellexpand_utils::{canonicalize, expand};
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use toml::{self, Value};
use tracing::debug;
use crate::account::config::{ListAccountsTableConfig, TomlAccountConfig};
#[cfg(feature = "wizard")]
use crate::wizard_warn;
/// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
@ -121,7 +122,7 @@ impl TomlConfig {
/// NOTE: the wizard can only be used with interactive shells.
#[cfg(feature = "wizard")]
async fn from_wizard(path: &PathBuf) -> Result<Self> {
wizard_warn!("Cannot find existing configuration at {path:?}.");
print::warn(format!("Cannot find existing configuration at {path:?}."));
let confirm = inquire::Confirm::new("Would you like to create one with the wizard? ")
.with_default(true)

View file

@ -1,97 +1,29 @@
use std::{fs, path::PathBuf};
use color_eyre::Result;
use inquire::{Confirm, Select, Text};
use shellexpand_utils::expand;
use std::{fs, path::Path, process};
use toml_edit::{DocumentMut, Item};
use pimalaya_tui::{print, prompt};
use toml_edit::{DocumentMut, Table};
use crate::account;
use super::TomlConfig;
#[macro_export]
macro_rules! wizard_warn {
($($arg:tt)*) => {
println!("{}", console::style(format!($($arg)*)).yellow().bold());
};
}
#[macro_export]
macro_rules! wizard_prompt {
($($arg:tt)*) => {
format!("{}", console::style(format!($($arg)*)).italic())
};
}
#[macro_export]
macro_rules! wizard_log {
($($arg:tt)*) => {
println!();
println!("{}", console::style(format!($($arg)*)).underlined());
println!();
};
}
pub(crate) async fn configure(path: &Path) -> Result<TomlConfig> {
wizard_log!("Configuring your first account:");
pub async fn configure(path: &PathBuf) -> Result<TomlConfig> {
print::section("Configuring your default account");
let mut config = TomlConfig::default();
while let Some((name, account_config)) = account::wizard::configure().await? {
config.accounts.insert(name, account_config);
let (account_name, account_config) = account::wizard::configure().await?;
config.accounts.insert(account_name, account_config);
if !Confirm::new("Would you like to configure another account?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default()
{
break;
}
let path = prompt::path("Where to save the configuration?", Some(path))?;
println!("Writing the configuration to {}", path.display());
wizard_log!("Configuring another account:");
}
// If one account is setup, make it the default. If multiple
// accounts are setup, decide which will be the default. If no
// accounts are setup, exit the process.
let default_account = match config.accounts.len() {
0 => {
wizard_warn!("No account configured, exiting.");
process::exit(0);
}
1 => Some(config.accounts.values_mut().next().unwrap()),
_ => {
let accounts = config.accounts.clone();
let accounts: Vec<&String> = accounts.keys().collect();
println!("{} accounts have been configured.", accounts.len());
Select::new(
"Which account would you like to set as your default?",
accounts,
)
.with_starting_cursor(0)
.prompt_skippable()?
.and_then(|input| config.accounts.get_mut(input))
}
};
if let Some(account) = default_account {
account.default = Some(true);
} else {
process::exit(0)
}
let path = Text::new("Where would you like to save your configuration?")
.with_default(&path.to_string_lossy())
.prompt()?;
let path = expand::path(path);
println!("Writing the configuration to {path:?}");
let toml = pretty_serialize(&config)?;
fs::create_dir_all(path.parent().unwrap_or(&path))?;
fs::write(path, toml)?;
println!("Exiting the wizard…");
println!("Done! Exiting the wizard…");
Ok(config)
}
@ -99,61 +31,11 @@ fn pretty_serialize(config: &TomlConfig) -> Result<String> {
let mut doc: DocumentMut = toml::to_string(&config)?.parse()?;
doc.iter_mut().for_each(|(_, item)| {
if let Some(item) = item.as_table_mut() {
item.iter_mut().for_each(|(_, item)| {
set_table_dotted(item, "folder");
if let Some(item) = get_table_mut(item, "folder") {
let keys = ["alias", "add", "list", "expunge", "purge", "delete", "sync"];
set_tables_dotted(item, keys);
if let Some(item) = get_table_mut(item, "sync") {
set_tables_dotted(item, ["filter", "permissions"]);
}
if let Some(table) = item.as_table_mut() {
table.iter_mut().for_each(|(_, item)| {
if let Some(table) = item.as_table_mut() {
set_table_dotted(table);
}
set_table_dotted(item, "envelope");
if let Some(item) = get_table_mut(item, "envelope") {
set_tables_dotted(item, ["list", "get"]);
}
set_table_dotted(item, "flag");
if let Some(item) = get_table_mut(item, "flag") {
set_tables_dotted(item, ["add", "set", "remove"]);
}
set_table_dotted(item, "message");
if let Some(item) = get_table_mut(item, "message") {
let keys = ["add", "send", "peek", "get", "copy", "move", "delete"];
set_tables_dotted(item, keys);
}
#[cfg(feature = "maildir")]
set_table_dotted(item, "maildir");
#[cfg(feature = "imap")]
{
set_table_dotted(item, "imap");
if let Some(item) = get_table_mut(item, "imap") {
set_tables_dotted(item, ["passwd", "oauth2"]);
}
}
#[cfg(feature = "notmuch")]
set_table_dotted(item, "notmuch");
#[cfg(feature = "smtp")]
{
set_table_dotted(item, "smtp");
if let Some(item) = get_table_mut(item, "smtp") {
set_tables_dotted(item, ["passwd", "oauth2"]);
}
}
#[cfg(feature = "sendmail")]
set_table_dotted(item, "sendmail");
#[cfg(feature = "pgp")]
set_table_dotted(item, "pgp");
})
}
});
@ -161,19 +43,13 @@ fn pretty_serialize(config: &TomlConfig) -> Result<String> {
Ok(doc.to_string())
}
fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> {
item.get_mut(key).filter(|item| item.is_table())
}
fn set_table_dotted(item: &mut Item, key: &str) {
if let Some(table) = get_table_mut(item, key).and_then(|item| item.as_table_mut()) {
table.set_dotted(true)
}
}
fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a str>) {
for key in keys {
set_table_dotted(item, key)
fn set_table_dotted(table: &mut Table) {
let keys: Vec<String> = table.iter().map(|(key, _)| key.to_string()).collect();
for ref key in keys {
if let Some(table) = table.get_mut(key).unwrap().as_table_mut() {
table.set_dotted(true);
set_table_dotted(table)
}
}
}

View file

@ -1,2 +0,0 @@
#[cfg(feature = "wizard")]
pub(crate) mod wizard;

View file

@ -1,346 +0,0 @@
use color_eyre::Result;
use email::autoconfig::config::{AutoConfig, SecurityType, ServerType};
#[cfg(feature = "oauth2")]
use email::{
account::config::oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
autoconfig::config::AuthenticationType,
};
use email::{
account::config::passwd::PasswdConfig,
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
};
use inquire::validator::{ErrorMessage, StringValidator, Validation};
#[cfg(feature = "oauth2")]
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{backend::config::BackendConfig, ui::prompt};
const ENCRYPTIONS: &[ImapEncryptionKind] = &[
ImapEncryptionKind::Tls,
ImapEncryptionKind::StartTls,
ImapEncryptionKind::None,
];
const SECRETS: &[&str] = &[
#[cfg(feature = "keyring")]
KEYRING,
RAW,
CMD,
];
#[cfg(feature = "keyring")]
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
#[derive(Clone, Copy)]
struct U16Validator;
impl StringValidator for U16Validator {
fn validate(
&self,
input: &str,
) -> std::prelude::v1::Result<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
))))
}
}
}
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator, Select, Text};
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
.incoming_servers()
.into_iter()
.find(|server| matches!(server.server_type(), ServerType::Imap))
});
let autoconfig_host = autoconfig_server
.and_then(|s| s.hostname())
.map(ToOwned::to_owned);
let default_host =
autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1));
let host = Text::new("IMAP hostname")
.with_default(&default_host)
.prompt()?;
let autoconfig_encryption = autoconfig_server
.and_then(|imap| {
imap.security_type().map(|encryption| match encryption {
SecurityType::Plain => ImapEncryptionKind::None,
SecurityType::Starttls => ImapEncryptionKind::StartTls,
SecurityType::Tls => ImapEncryptionKind::Tls,
})
})
.unwrap_or_default();
let default_encryption_idx = match &autoconfig_encryption {
ImapEncryptionKind::Tls => 0,
ImapEncryptionKind::StartTls => 1,
ImapEncryptionKind::None => 2,
};
let encryption_kind = Select::new("IMAP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(default_encryption_idx)
.prompt_skippable()?;
let autoconfig_port = autoconfig_server
.and_then(|s| s.port())
.map(ToOwned::to_owned)
.unwrap_or_else(|| match &autoconfig_encryption {
ImapEncryptionKind::Tls => 465,
ImapEncryptionKind::StartTls => 587,
ImapEncryptionKind::None => 25,
});
let (encryption, default_port) = match encryption_kind {
Some(idx)
if &idx
== ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre(
"something impossible happened during finding default match for encryption.",
)? =>
{
(Some(autoconfig_encryption), autoconfig_port)
}
Some(ImapEncryptionKind::Tls) => (Some(ImapEncryptionKind::Tls), 465),
Some(ImapEncryptionKind::StartTls) => (Some(ImapEncryptionKind::StartTls), 587),
_ => (Some(ImapEncryptionKind::None), 25),
};
let port = Text::new("IMAP port")
.with_validators(&[
Box::new(validator::MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let autoconfig_login = autoconfig_server.map(|imap| match imap.username() {
Some("%EMAILLOCALPART%") => email.rsplit_once('@').unwrap().0.to_owned(),
Some("%EMAILADDRESS%") => email.to_owned(),
_ => email.to_owned(),
});
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Text::new("IMAP login")
.with_default(&default_login)
.prompt()?;
#[cfg(feature = "oauth2")]
let auth = {
use inquire::{Confirm, Password};
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let default_oauth2_enabled = autoconfig_server
.and_then(|imap| {
imap.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::new("IMAP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Text::new("IMAP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("IMAP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Text::new("IMAP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Text::new("IMAP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
})
};
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more IMAP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
};
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
scopes.push(scope)
}
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
crate::wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token = Secret::try_new_keyring_entry(format!(
"{account_name}-imap-oauth2-refresh-token"
))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
ImapAuthConfig::OAuth2(config)
} else {
configure_passwd(account_name).await?
}
};
#[cfg(not(feature = "oauth2"))]
let auth = configure_passwd(account_name).await?;
let config = ImapConfig {
host,
port,
encryption,
login,
auth,
extensions: None,
watch: None,
};
Ok(BackendConfig::Imap(config))
}
pub(crate) async fn configure_passwd(account_name: &str) -> Result<ImapAuthConfig> {
use inquire::{Select, Text};
let secret_idx = Select::new("IMAP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
#[cfg(feature = "keyring")]
Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(sec) if sec == CMD => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-imap-passwd"))
.prompt()?,
),
_ => Default::default(),
};
Ok(ImapAuthConfig::Passwd(PasswdConfig(secret)))
}

View file

@ -6,19 +6,9 @@ pub mod completion;
pub mod config;
pub mod email;
pub mod folder;
#[cfg(feature = "imap")]
pub mod imap;
#[cfg(feature = "maildir")]
pub mod maildir;
pub mod manual;
#[cfg(feature = "notmuch")]
pub mod notmuch;
pub mod output;
pub mod printer;
#[cfg(feature = "sendmail")]
pub mod sendmail;
#[cfg(feature = "smtp")]
pub mod smtp;
pub mod tracing;
pub mod ui;

View file

@ -1,2 +0,0 @@
#[cfg(feature = "wizard")]
pub(crate) mod wizard;

View file

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

View file

@ -1 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,23 +0,0 @@
use color_eyre::Result;
use email::notmuch::config::NotmuchConfig;
use inquire::Text;
use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> {
let config = NotmuchConfig {
database_path: Some(
Text::new("Notmuch database path")
.with_default(
&NotmuchConfig::get_default_database_path()
.unwrap_or_default()
.to_string_lossy(),
)
.prompt()?
.into(),
),
..Default::default()
};
Ok(BackendConfig::Notmuch(config))
}

View file

@ -1 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,16 +0,0 @@
use color_eyre::Result;
use email::sendmail::config::SendmailConfig;
use inquire::Text;
use crate::backend::config::BackendConfig;
pub(crate) fn configure() -> Result<BackendConfig> {
let config = SendmailConfig {
cmd: Text::new("Sendmail-compatible shell command to send emails")
.with_default("/usr/bin/msmtp")
.prompt()?
.into(),
};
Ok(BackendConfig::Sendmail(config))
}

View file

@ -1,2 +0,0 @@
#[cfg(feature = "wizard")]
pub(crate) mod wizard;

View file

@ -1,344 +0,0 @@
use color_eyre::Result;
use email::autoconfig::config::{AutoConfig, SecurityType, ServerType};
#[cfg(feature = "oauth2")]
use email::{
account::config::oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
autoconfig::config::AuthenticationType,
};
use email::{
account::config::passwd::PasswdConfig,
smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind},
};
use inquire::validator::{ErrorMessage, StringValidator, Validation};
#[cfg(feature = "oauth2")]
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{backend::config::BackendConfig, ui::prompt};
const ENCRYPTIONS: &[SmtpEncryptionKind] = &[
SmtpEncryptionKind::Tls,
SmtpEncryptionKind::StartTls,
SmtpEncryptionKind::None,
];
const SECRETS: &[&str] = &[
#[cfg(feature = "keyring")]
KEYRING,
RAW,
CMD,
];
#[cfg(feature = "keyring")]
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
#[derive(Clone, Copy)]
struct U16Validator;
impl StringValidator for U16Validator {
fn validate(
&self,
input: &str,
) -> std::prelude::v1::Result<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
))))
}
}
}
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
use color_eyre::eyre::OptionExt as _;
use inquire::{validator, Select, Text};
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
.outgoing_servers()
.into_iter()
.find(|server| matches!(server.server_type(), ServerType::Smtp))
});
let autoconfig_host = autoconfig_server
.and_then(|s| s.hostname())
.map(ToOwned::to_owned);
let default_host =
autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1));
let host = Text::new("SMTP hostname")
.with_default(&default_host)
.prompt()?;
let autoconfig_encryption = autoconfig_server
.and_then(|smtp| {
smtp.security_type().map(|encryption| match encryption {
SecurityType::Plain => SmtpEncryptionKind::None,
SecurityType::Starttls => SmtpEncryptionKind::StartTls,
SecurityType::Tls => SmtpEncryptionKind::Tls,
})
})
.unwrap_or_default();
let default_encryption_idx = match &autoconfig_encryption {
SmtpEncryptionKind::Tls => 0,
SmtpEncryptionKind::StartTls => 1,
SmtpEncryptionKind::None => 2,
};
let encryption_kind = Select::new("SMTP encryption", ENCRYPTIONS.to_vec())
.with_starting_cursor(default_encryption_idx)
.prompt_skippable()?;
let autoconfig_port = autoconfig_server
.and_then(|s| s.port())
.map(ToOwned::to_owned)
.unwrap_or_else(|| match &autoconfig_encryption {
SmtpEncryptionKind::Tls => 465,
SmtpEncryptionKind::StartTls => 587,
SmtpEncryptionKind::None => 25,
});
let (encryption, default_port) = match encryption_kind {
Some(idx)
if &idx
== ENCRYPTIONS.get(default_encryption_idx).ok_or_eyre(
"something impossible happened during finding default match for encryption.",
)? =>
{
(Some(autoconfig_encryption), autoconfig_port)
}
Some(SmtpEncryptionKind::Tls) => (Some(SmtpEncryptionKind::Tls), 465),
Some(SmtpEncryptionKind::StartTls) => (Some(SmtpEncryptionKind::StartTls), 587),
_ => (Some(SmtpEncryptionKind::None), 25),
};
let port = Text::new("SMTP port")
.with_validators(&[
Box::new(validator::MinLengthValidator::new(1)),
Box::new(U16Validator {}),
])
.with_default(&default_port.to_string())
.prompt()
.map(|input| input.parse::<u16>().unwrap())?;
let autoconfig_login = autoconfig_server.map(|smtp| match smtp.username() {
Some("%EMAILLOCALPART%") => email.rsplit_once('@').unwrap().0.to_owned(),
Some("%EMAILADDRESS%") => email.to_owned(),
_ => email.to_owned(),
});
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Text::new("SMTP login")
.with_default(&default_login)
.prompt()?;
#[cfg(feature = "oauth2")]
let auth = {
use inquire::{Confirm, Password};
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let default_oauth2_enabled = autoconfig_server
.and_then(|smtp| {
smtp.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
let oauth2_enabled = Confirm::new("Would you like to enable OAuth 2.0?")
.with_default(default_oauth2_enabled)
.prompt_skippable()?
.unwrap_or_default();
if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST;
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::new("SMTP OAuth 2.0 mechanism", OAUTH2_MECHANISMS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
config.method = match method_idx {
Some(choice) if choice == XOAUTH2 => OAuth2Method::XOAuth2,
Some(choice) if choice == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Text::new("SMTP OAuth 2.0 client id").prompt()?;
let client_secret: String = Password::new("SMTP OAuth 2.0 client secret")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Text::new("SMTP OAuth 2.0 authorization URL")
.with_default(&default_auth_url)
.prompt()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Text::new("SMTP OAuth 2.0 token URL")
.with_default(&default_token_url)
.prompt()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::new(prompt, scopes.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?
.map(ToOwned::to_owned),
None => Some(Text::new(prompt).prompt()?).filter(|scope| !scope.is_empty()),
})
};
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new("Would you like to add more SMTP OAuth 2.0 scopes?")
.with_default(false)
.prompt_skippable()?
.unwrap_or_default();
Ok(confirm)
};
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
if let Some(scope) = prompt_scope("Additional SMTP OAuth 2.0 scope")? {
scopes.push(scope)
}
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new("Would you like to enable PKCE verification?")
.with_default(true)
.prompt_skippable()?
.unwrap_or(true);
crate::wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(redirect_port);
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{redirect_url}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token = Secret::try_new_keyring_entry(format!(
"{account_name}-smtp-oauth2-refresh-token"
))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
SmtpAuthConfig::OAuth2(config)
} else {
configure_passwd(account_name).await?
}
};
#[cfg(not(feature = "oauth2"))]
let auth = configure_passwd(account_name).await?;
let config = SmtpConfig {
host,
port,
encryption,
login,
auth,
};
Ok(BackendConfig::Smtp(config))
}
pub(crate) async fn configure_passwd(account_name: &str) -> Result<SmtpAuthConfig> {
use inquire::{Select, Text};
let secret_idx = Select::new("SMTP authentication strategy", SECRETS.to_vec())
.with_starting_cursor(0)
.prompt_skippable()?;
let secret = match secret_idx {
#[cfg(feature = "keyring")]
Some(sec) if sec == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-smtp-passwd"))?;
secret
.set_only_keyring(prompt::passwd("SMTP password")?)
.await?;
secret
}
Some(sec) if sec == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
Some(sec) if sec == CMD => Secret::new_command(
Text::new("Shell command")
.with_default(&format!("pass show {account_name}-smtp-passwd"))
.prompt()?,
),
_ => Default::default(),
};
Ok(SmtpAuthConfig::Passwd(PasswdConfig(secret)))
}

View file

@ -1,17 +1,17 @@
use std::fmt::Display;
use std::fmt;
use color_eyre::Result;
use inquire::Select;
use pimalaya_tui::prompt;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PreEditChoice {
Edit,
Discard,
Quit,
}
impl Display for PreEditChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for PreEditChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
@ -24,22 +24,20 @@ impl Display for PreEditChoice {
}
}
static PRE_EDIT_CHOICES: [PreEditChoice; 3] = [
PreEditChoice::Edit,
PreEditChoice::Discard,
PreEditChoice::Quit,
];
pub fn pre_edit() -> Result<PreEditChoice> {
let choices = [
PreEditChoice::Edit,
PreEditChoice::Discard,
PreEditChoice::Quit,
];
let user_choice = Select::new(
let user_choice = prompt::item(
"A draft was found, what would you like to do with it?",
choices.to_vec(),
)
.with_starting_cursor(0)
.with_vim_mode(true)
.prompt()?;
&PRE_EDIT_CHOICES,
None,
)?;
Ok(user_choice)
Ok(user_choice.clone())
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -51,7 +49,7 @@ pub enum PostEditChoice {
Discard,
}
impl Display for PostEditChoice {
impl fmt::Display for PostEditChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
@ -67,22 +65,20 @@ impl Display for PostEditChoice {
}
}
static POST_EDIT_CHOICES: [PostEditChoice; 5] = [
PostEditChoice::Send,
PostEditChoice::Edit,
PostEditChoice::LocalDraft,
PostEditChoice::RemoteDraft,
PostEditChoice::Discard,
];
pub fn post_edit() -> Result<PostEditChoice> {
let choices = [
PostEditChoice::Send,
PostEditChoice::Edit,
PostEditChoice::LocalDraft,
PostEditChoice::RemoteDraft,
PostEditChoice::Discard,
];
let user_choice = inquire::Select::new(
let user_choice = prompt::item(
"What would you like to do with this message?",
choices.to_vec(),
)
.with_starting_cursor(0)
.with_vim_mode(true)
.prompt()?;
&POST_EDIT_CHOICES,
None,
)?;
Ok(user_choice)
Ok(user_choice.clone())
}

View file

@ -1,3 +1,5 @@
use std::{env, fs, sync::Arc};
use color_eyre::{eyre::Context, Result};
use email::{
account::config::AccountConfig,
@ -8,7 +10,6 @@ use email::{
};
use mml::MmlCompilerBuilder;
use process::SingleCommand;
use std::{env, fs, sync::Arc};
use tracing::debug;
use crate::{

View file

@ -2,7 +2,6 @@ use crossterm::style::Color;
pub mod choice;
pub mod editor;
pub(crate) mod prompt;
pub(crate) fn map_color(color: Color) -> comfy_table::Color {
match color {

View file

@ -1,29 +0,0 @@
use std::io;
pub(crate) fn passwd(prompt: &str) -> io::Result<String> {
inquire::Password::new(prompt)
.with_custom_confirmation_message("Confirm password")
.with_custom_confirmation_error_message("Passwords do not match, please try again.")
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.prompt()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Interrupted,
format!("failed to get password: {e}"),
)
})
}
#[cfg(feature = "oauth2")]
pub(crate) fn secret(prompt: &str) -> io::Result<String> {
inquire::Password::new(prompt)
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.without_confirmation()
.prompt()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Interrupted,
format!("failed to get secret: {e}"),
)
})
}