mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-24 20:10:23 +00:00
make use of pimalaya-tui
This commit is contained in:
parent
c5b33b9623
commit
5a22cab781
22 changed files with 211 additions and 1234 deletions
137
Cargo.lock
generated
137
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
28
Cargo.toml
28
Cargo.toml
|
@ -23,24 +23,24 @@ default = [
|
|||
"smtp",
|
||||
"sendmail",
|
||||
|
||||
"wizard",
|
||||
# "keyring",
|
||||
# "oauth2",
|
||||
"wizard",
|
||||
|
||||
# "pgp-commands",
|
||||
# "pgp-gpg",
|
||||
# "pgp-native",
|
||||
]
|
||||
|
||||
imap = ["email-lib/imap"]
|
||||
maildir = ["email-lib/maildir"]
|
||||
notmuch = ["email-lib/notmuch"]
|
||||
smtp = ["email-lib/smtp"]
|
||||
sendmail = ["email-lib/sendmail"]
|
||||
imap = ["email-lib/imap", "pimalaya-tui/imap"]
|
||||
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
|
||||
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
|
||||
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
|
||||
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
|
||||
|
||||
keyring = ["email-lib/keyring", "secret-lib?/keyring-tokio"]
|
||||
oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "keyring"]
|
||||
wizard = ["dep:secret-lib", "dep:toml_edit", "email-lib/autoconfig"]
|
||||
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib?/keyring-tokio"]
|
||||
oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
|
||||
wizard = ["dep:email_address", "dep:secret-lib", "dep:toml_edit", "email-lib/autoconfig"]
|
||||
|
||||
pgp = []
|
||||
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"]
|
||||
|
@ -55,13 +55,11 @@ clap_complete = "4.4"
|
|||
clap_mangen = "0.2"
|
||||
color-eyre = "0.6.3"
|
||||
comfy-table = "7.1.1"
|
||||
console = "0.15.2"
|
||||
crossterm = { version = "0.27", features = ["serde"] }
|
||||
dirs = "4"
|
||||
email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] }
|
||||
email_address = "0.2.4"
|
||||
email_address = { version = "0.2", optional = true }
|
||||
erased-serde = "0.3"
|
||||
indicatif = "0.17"
|
||||
inquire = "0.7.4"
|
||||
mail-builder = "0.3"
|
||||
md5 = "0.7"
|
||||
|
@ -69,6 +67,7 @@ mml-lib = { version = "=1.0.14", default-features = false, features = ["derive"]
|
|||
oauth-lib = { version = "=0.1.1", optional = true }
|
||||
once_cell = "1.16"
|
||||
petgraph = "0.6"
|
||||
pimalaya-tui = { version = "=0.1.0", default-features = false, features = ["email", "path"] }
|
||||
process-lib = { version = "=0.4.2", features = ["derive"] }
|
||||
secret-lib = { version = "=0.4.6", default-features = false, features = ["command", "derive"], optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
@ -76,20 +75,19 @@ serde-toml-merge = "0.3"
|
|||
serde_json = "1"
|
||||
shellexpand-utils = "=0.2.1"
|
||||
sled = "=0.34.7"
|
||||
terminal_size = "0.1"
|
||||
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.8"
|
||||
toml_edit = { version = "0.22", optional = true }
|
||||
tracing = "0.1.40"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
unicode-width = "0.1"
|
||||
url = "2.2"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
[patch.crates-io]
|
||||
oauth-lib = { git = "https://github.com/pimalaya/core" }
|
||||
imap-codec = { git = "https://github.com/duesee/imap-codec" }
|
||||
imap-next = { git = "https://github.com/duesee/imap-next", branch = "jakoschiko_poison-message-with-fragmentizer" }
|
||||
imap-client = { git = "https://github.com/pimalaya/imap-client" }
|
||||
oauth-lib = { git = "https://github.com/pimalaya/core" }
|
||||
email-lib = { git = "https://github.com/pimalaya/core" }
|
||||
pimalaya-tui = { git = "https://github.com/pimalaya/tui" }
|
||||
|
|
|
@ -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?;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
let default_account_name = email
|
||||
.domain()
|
||||
.split_once('.')
|
||||
.ok_or_eyre("not a valid domain, without any .")?
|
||||
.0,
|
||||
)
|
||||
.prompt()?;
|
||||
.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))
|
||||
}
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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_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)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a str>) {
|
||||
for key in keys {
|
||||
set_table_dotted(item, key)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#[cfg(feature = "wizard")]
|
||||
pub(crate) mod wizard;
|
|
@ -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)))
|
||||
}
|
10
src/lib.rs
10
src/lib.rs
|
@ -6,19 +6,9 @@ pub mod completion;
|
|||
pub mod config;
|
||||
pub mod email;
|
||||
pub mod folder;
|
||||
#[cfg(feature = "imap")]
|
||||
pub mod imap;
|
||||
#[cfg(feature = "maildir")]
|
||||
pub mod maildir;
|
||||
pub mod manual;
|
||||
#[cfg(feature = "notmuch")]
|
||||
pub mod notmuch;
|
||||
pub mod output;
|
||||
pub mod printer;
|
||||
#[cfg(feature = "sendmail")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp")]
|
||||
pub mod smtp;
|
||||
pub mod tracing;
|
||||
pub mod ui;
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#[cfg(feature = "wizard")]
|
||||
pub(crate) mod wizard;
|
|
@ -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))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -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))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub(crate) mod wizard;
|
|
@ -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))
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
#[cfg(feature = "wizard")]
|
||||
pub(crate) mod wizard;
|
|
@ -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)))
|
||||
}
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn pre_edit() -> Result<PreEditChoice> {
|
||||
let choices = [
|
||||
static PRE_EDIT_CHOICES: [PreEditChoice; 3] = [
|
||||
PreEditChoice::Edit,
|
||||
PreEditChoice::Discard,
|
||||
PreEditChoice::Quit,
|
||||
];
|
||||
];
|
||||
|
||||
let user_choice = Select::new(
|
||||
pub fn pre_edit() -> Result<PreEditChoice> {
|
||||
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 {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn post_edit() -> Result<PostEditChoice> {
|
||||
let choices = [
|
||||
static POST_EDIT_CHOICES: [PostEditChoice; 5] = [
|
||||
PostEditChoice::Send,
|
||||
PostEditChoice::Edit,
|
||||
PostEditChoice::LocalDraft,
|
||||
PostEditChoice::RemoteDraft,
|
||||
PostEditChoice::Discard,
|
||||
];
|
||||
];
|
||||
|
||||
let user_choice = inquire::Select::new(
|
||||
pub fn post_edit() -> Result<PostEditChoice> {
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}"),
|
||||
)
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue