mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-25 04:20:22 +00:00
fix cargo features issues
This commit is contained in:
parent
556949a684
commit
04982a4644
7 changed files with 519 additions and 32 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1217,7 +1217,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "email-lib"
|
name = "email-lib"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
source = "git+https://git.sr.ht/~soywod/pimalaya#f413917f9110ef4eafe4a8423626b22e4391317e"
|
source = "git+https://git.sr.ht/~soywod/pimalaya#01f7e96b55da8d46be00c2face31ee437e1c5c5a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"advisory-lock",
|
"advisory-lock",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
@ -16,11 +16,16 @@ signature-delim = "-- \n"
|
||||||
# Enable the synchronization for this account. Running the command
|
# Enable the synchronization for this account. Running the command
|
||||||
# `account sync example` will synchronize all folders and all emails
|
# `account sync example` will synchronize all folders and all emails
|
||||||
# to a local Maildir at `$XDG_DATA_HOME/himalaya/example`.
|
# to a local Maildir at `$XDG_DATA_HOME/himalaya/example`.
|
||||||
sync.enable = true
|
sync.enable = false
|
||||||
|
|
||||||
# Override the default Maildir path for synchronization.
|
# Override the default Maildir path for synchronization.
|
||||||
sync.dir = "/tmp/himalaya-sync-example"
|
sync.dir = "/tmp/himalaya-sync-example"
|
||||||
|
|
||||||
|
# Filter folders to sync
|
||||||
|
folder.sync.filter.include = ["INBOX"]
|
||||||
|
# folder.sync.filter.exclude = ["All mails"]
|
||||||
|
# folder.sync.filter = "all"
|
||||||
|
|
||||||
# Define main folder aliases
|
# Define main folder aliases
|
||||||
folder.alias.inbox = "INBOX"
|
folder.alias.inbox = "INBOX"
|
||||||
folder.alias.sent = "Sent"
|
folder.alias.sent = "Sent"
|
||||||
|
@ -57,7 +62,7 @@ envelope.watch.received.notify.body = "{subject}"
|
||||||
message.send.backend = "smtp"
|
message.send.backend = "smtp"
|
||||||
|
|
||||||
# Save a copy of sent messages to the sent folder.
|
# Save a copy of sent messages to the sent folder.
|
||||||
message.send.save-copy = true
|
message.send.save-copy = false
|
||||||
|
|
||||||
# IMAP config
|
# IMAP config
|
||||||
imap.host = "localhost"
|
imap.host = "localhost"
|
||||||
|
|
|
@ -26,8 +26,11 @@ impl AccountCheckUpCommand {
|
||||||
|
|
||||||
printer.print_log("Checking configuration integrity…")?;
|
printer.print_log("Checking configuration integrity…")?;
|
||||||
|
|
||||||
let (toml_account_config, account_config) =
|
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||||
config.clone().into_account_configs(account, true)?;
|
account,
|
||||||
|
#[cfg(feature = "account-sync")]
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
let used_backends = toml_account_config.get_used_backends();
|
let used_backends = toml_account_config.get_used_backends();
|
||||||
|
|
||||||
printer.print_log("Checking backend context integrity…")?;
|
printer.print_log("Checking backend context integrity…")?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
use dialoguer::{Confirm, Input};
|
use dialoguer::Confirm;
|
||||||
use email::account;
|
use dialoguer::Input;
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
use email::account::sync::config::SyncConfig;
|
use email::account::sync::config::SyncConfig;
|
||||||
use email_address::EmailAddress;
|
use email_address::EmailAddress;
|
||||||
|
@ -9,11 +9,12 @@ use std::str::FromStr;
|
||||||
|
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
use crate::wizard_prompt;
|
use crate::wizard_prompt;
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
use crate::wizard_warn;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{self, config::BackendConfig, BackendKind},
|
backend::{self, config::BackendConfig, BackendKind},
|
||||||
message::config::{MessageConfig, MessageSendConfig},
|
message::config::{MessageConfig, MessageSendConfig},
|
||||||
ui::THEME,
|
ui::THEME,
|
||||||
wizard_warn,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::TomlAccountConfig;
|
use super::TomlAccountConfig;
|
||||||
|
@ -34,9 +35,14 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||||
|
|
||||||
let addr = EmailAddress::from_str(&config.email).unwrap();
|
let addr = EmailAddress::from_str(&config.email).unwrap();
|
||||||
|
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
let autoconfig_email = config.email.to_owned();
|
let autoconfig_email = config.email.to_owned();
|
||||||
let autoconfig =
|
#[cfg(feature = "account-discovery")]
|
||||||
tokio::spawn(async move { account::discover::from_addr(&autoconfig_email).await.ok() });
|
let autoconfig = tokio::spawn(async move {
|
||||||
|
email::account::discover::from_addr(&autoconfig_email)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
let account_name = Input::with_theme(&*THEME)
|
let account_name = Input::with_theme(&*THEME)
|
||||||
.with_prompt("Account name")
|
.with_prompt("Account name")
|
||||||
|
@ -59,9 +65,12 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||||
);
|
);
|
||||||
|
|
||||||
let email = &config.email;
|
let email = &config.email;
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
let autoconfig = autoconfig.await?;
|
let autoconfig = autoconfig.await?;
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
let autoconfig = autoconfig.as_ref();
|
let autoconfig = autoconfig.as_ref();
|
||||||
|
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
if let Some(config) = autoconfig {
|
if let Some(config) = autoconfig {
|
||||||
if config.is_gmail() {
|
if config.is_gmail() {
|
||||||
println!();
|
println!();
|
||||||
|
@ -71,7 +80,14 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match backend::wizard::configure(&account_name, email, autoconfig).await? {
|
match backend::wizard::configure(
|
||||||
|
&account_name,
|
||||||
|
email,
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
autoconfig,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
Some(BackendConfig::Imap(imap_config)) => {
|
Some(BackendConfig::Imap(imap_config)) => {
|
||||||
config.imap = Some(imap_config);
|
config.imap = Some(imap_config);
|
||||||
|
@ -90,7 +106,14 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
match backend::wizard::configure_sender(&account_name, email, autoconfig).await? {
|
match backend::wizard::configure_sender(
|
||||||
|
&account_name,
|
||||||
|
email,
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
autoconfig,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
#[cfg(feature = "smtp")]
|
#[cfg(feature = "smtp")]
|
||||||
Some(BackendConfig::Smtp(smtp_config)) => {
|
Some(BackendConfig::Smtp(smtp_config)) => {
|
||||||
config.smtp = Some(smtp_config);
|
config.smtp = Some(smtp_config);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dialoguer::Select;
|
use dialoguer::Select;
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
use email::account::discover::config::AutoConfig;
|
use email::account::discover::config::AutoConfig;
|
||||||
|
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
|
@ -35,7 +36,7 @@ const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
|
||||||
pub(crate) async fn configure(
|
pub(crate) async fn configure(
|
||||||
account_name: &str,
|
account_name: &str,
|
||||||
email: &str,
|
email: &str,
|
||||||
autoconfig: Option<&AutoConfig>,
|
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
|
||||||
) -> Result<Option<BackendConfig>> {
|
) -> Result<Option<BackendConfig>> {
|
||||||
let kind = Select::with_theme(&*THEME)
|
let kind = Select::with_theme(&*THEME)
|
||||||
.with_prompt("Default email backend")
|
.with_prompt("Default email backend")
|
||||||
|
@ -46,9 +47,15 @@ pub(crate) async fn configure(
|
||||||
|
|
||||||
let config = match kind {
|
let config = match kind {
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
Some(kind) if kind == BackendKind::Imap => {
|
Some(kind) if kind == BackendKind::Imap => Some(
|
||||||
Some(imap::wizard::configure(account_name, email, autoconfig).await?)
|
imap::wizard::configure(
|
||||||
}
|
account_name,
|
||||||
|
email,
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
autoconfig,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
#[cfg(feature = "maildir")]
|
#[cfg(feature = "maildir")]
|
||||||
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
|
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
|
||||||
#[cfg(feature = "notmuch")]
|
#[cfg(feature = "notmuch")]
|
||||||
|
@ -62,7 +69,7 @@ pub(crate) async fn configure(
|
||||||
pub(crate) async fn configure_sender(
|
pub(crate) async fn configure_sender(
|
||||||
account_name: &str,
|
account_name: &str,
|
||||||
email: &str,
|
email: &str,
|
||||||
autoconfig: Option<&AutoConfig>,
|
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
|
||||||
) -> Result<Option<BackendConfig>> {
|
) -> Result<Option<BackendConfig>> {
|
||||||
let kind = Select::with_theme(&*THEME)
|
let kind = Select::with_theme(&*THEME)
|
||||||
.with_prompt("Backend for sending messages")
|
.with_prompt("Backend for sending messages")
|
||||||
|
@ -73,9 +80,15 @@ pub(crate) async fn configure_sender(
|
||||||
|
|
||||||
let config = match kind {
|
let config = match kind {
|
||||||
#[cfg(feature = "smtp")]
|
#[cfg(feature = "smtp")]
|
||||||
Some(kind) if kind == BackendKind::Smtp => {
|
Some(kind) if kind == BackendKind::Smtp => Some(
|
||||||
Some(smtp::wizard::configure(account_name, email, autoconfig).await?)
|
smtp::wizard::configure(
|
||||||
}
|
account_name,
|
||||||
|
email,
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
autoconfig,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
#[cfg(feature = "sendmail")]
|
#[cfg(feature = "sendmail")]
|
||||||
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
|
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dialoguer::{Confirm, Input, Password, Select};
|
use dialoguer::{Confirm, Input, Password, Select};
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
|
||||||
use email::{
|
use email::{
|
||||||
account::{
|
account::config::{
|
||||||
config::{
|
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
||||||
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
passwd::PasswdConfig,
|
||||||
passwd::PasswdConfig,
|
|
||||||
},
|
|
||||||
discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType},
|
|
||||||
},
|
},
|
||||||
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
|
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
|
||||||
};
|
};
|
||||||
|
@ -34,6 +33,7 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri
|
||||||
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
|
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
|
||||||
const CMD: &str = "Ask me a shell command that exposes my password";
|
const CMD: &str = "Ask me a shell command that exposes my password";
|
||||||
|
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
pub(crate) async fn configure(
|
pub(crate) async fn configure(
|
||||||
account_name: &str,
|
account_name: &str,
|
||||||
email: &str,
|
email: &str,
|
||||||
|
@ -333,3 +333,225 @@ pub(crate) async fn configure(
|
||||||
|
|
||||||
Ok(BackendConfig::Imap(config))
|
Ok(BackendConfig::Imap(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "account-discovery"))]
|
||||||
|
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||||
|
let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1);
|
||||||
|
|
||||||
|
let host = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP hostname")
|
||||||
|
.default(default_host)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let encryption_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP encryption")
|
||||||
|
.items(ENCRYPTIONS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
let (encryption, default_port) = match encryption_idx {
|
||||||
|
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
|
||||||
|
(Some(ImapEncryptionKind::Tls), 993)
|
||||||
|
}
|
||||||
|
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
|
||||||
|
(Some(ImapEncryptionKind::StartTls), 143)
|
||||||
|
}
|
||||||
|
_ => (Some(ImapEncryptionKind::None), 143),
|
||||||
|
};
|
||||||
|
|
||||||
|
let port = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP port")
|
||||||
|
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||||
|
.default(default_port.to_string())
|
||||||
|
.interact()
|
||||||
|
.map(|input| input.parse::<u16>().unwrap())?;
|
||||||
|
|
||||||
|
let default_login = email.to_owned();
|
||||||
|
|
||||||
|
let login = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP login")
|
||||||
|
.default(default_login)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let oauth2_enabled = Confirm::new()
|
||||||
|
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
|
||||||
|
.default(false)
|
||||||
|
.interact_opt()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let auth = if oauth2_enabled {
|
||||||
|
let mut config = OAuth2Config::default();
|
||||||
|
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
|
||||||
|
let redirect_port = OAuth2Config::get_first_available_port()?;
|
||||||
|
|
||||||
|
let method_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP OAuth 2.0 mechanism")
|
||||||
|
.items(OAUTH2_MECHANISMS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
config.method = match method_idx {
|
||||||
|
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||||
|
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||||
|
_ => OAuth2Method::XOAuth2,
|
||||||
|
};
|
||||||
|
|
||||||
|
config.client_id = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP OAuth 2.0 client id")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let client_secret: String = Password::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP OAuth 2.0 client secret")
|
||||||
|
.interact()?;
|
||||||
|
config.client_secret =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"));
|
||||||
|
config
|
||||||
|
.client_secret
|
||||||
|
.set_keyring_entry_secret(&client_secret)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
config.auth_url = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP OAuth 2.0 authorization URL")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
config.token_url = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP OAuth 2.0 token URL")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
|
||||||
|
Ok(Some(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.default(String::default())
|
||||||
|
.interact()?
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.filter(|scope| !scope.is_empty()))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
|
||||||
|
config.scopes = OAuth2Scopes::Scope(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirm_additional_scope = || -> Result<bool> {
|
||||||
|
let confirm = Confirm::new()
|
||||||
|
.with_prompt(wizard_prompt!(
|
||||||
|
"Would you like to add more IMAP OAuth 2.0 scopes?"
|
||||||
|
))
|
||||||
|
.default(false)
|
||||||
|
.interact_opt()?
|
||||||
|
.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()
|
||||||
|
.with_prompt(wizard_prompt!(
|
||||||
|
"Would you like to enable PKCE verification?"
|
||||||
|
))
|
||||||
|
.default(true)
|
||||||
|
.interact_opt()?
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
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::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"));
|
||||||
|
config
|
||||||
|
.access_token
|
||||||
|
.set_keyring_entry_secret(access_token)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(refresh_token) = &refresh_token {
|
||||||
|
config.refresh_token =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"));
|
||||||
|
config
|
||||||
|
.refresh_token
|
||||||
|
.set_keyring_entry_secret(refresh_token)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImapAuthConfig::OAuth2(config)
|
||||||
|
} else {
|
||||||
|
let secret_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP authentication strategy")
|
||||||
|
.items(SECRETS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
let secret = match secret_idx {
|
||||||
|
Some(idx) if SECRETS[idx] == KEYRING => {
|
||||||
|
let secret = Secret::new_keyring_entry(format!("{account_name}-imap-passwd"));
|
||||||
|
secret
|
||||||
|
.set_keyring_entry_secret(prompt::passwd("IMAP password")?)
|
||||||
|
.await?;
|
||||||
|
secret
|
||||||
|
}
|
||||||
|
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
|
||||||
|
Some(idx) if SECRETS[idx] == CMD => Secret::new_cmd(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("Shell command")
|
||||||
|
.default(format!("pass show {account_name}-imap-passwd"))
|
||||||
|
.interact()?,
|
||||||
|
),
|
||||||
|
_ => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ImapAuthConfig::Passwd(PasswdConfig(secret))
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = ImapConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
encryption,
|
||||||
|
login,
|
||||||
|
auth,
|
||||||
|
watch: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(BackendConfig::Imap(config))
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dialoguer::{Confirm, Input, Password, Select};
|
use dialoguer::{Confirm, Input, Password, Select};
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
|
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
|
||||||
use email::{
|
use email::{
|
||||||
account::{
|
account::config::{
|
||||||
config::{
|
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
||||||
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
passwd::PasswdConfig,
|
||||||
passwd::PasswdConfig,
|
|
||||||
},
|
|
||||||
discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType},
|
|
||||||
},
|
},
|
||||||
smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind},
|
smtp::config::{SmtpAuthConfig, SmtpConfig, SmtpEncryptionKind},
|
||||||
};
|
};
|
||||||
|
@ -34,6 +33,7 @@ const KEYRING: &str = "Ask my password, then save it in my system's global keyri
|
||||||
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
|
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
|
||||||
const CMD: &str = "Ask me a shell command that exposes my password";
|
const CMD: &str = "Ask me a shell command that exposes my password";
|
||||||
|
|
||||||
|
#[cfg(feature = "account-discovery")]
|
||||||
pub(crate) async fn configure(
|
pub(crate) async fn configure(
|
||||||
account_name: &str,
|
account_name: &str,
|
||||||
email: &str,
|
email: &str,
|
||||||
|
@ -332,3 +332,224 @@ pub(crate) async fn configure(
|
||||||
|
|
||||||
Ok(BackendConfig::Smtp(config))
|
Ok(BackendConfig::Smtp(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "account-discovery"))]
|
||||||
|
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||||
|
let default_host = format!("smtp.{}", email.rsplit_once('@').unwrap().1);
|
||||||
|
|
||||||
|
let host = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP hostname")
|
||||||
|
.default(default_host)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let encryption_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP encryption")
|
||||||
|
.items(ENCRYPTIONS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
let (encryption, default_port) = match encryption_idx {
|
||||||
|
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::Tls => {
|
||||||
|
(Some(SmtpEncryptionKind::Tls), 465)
|
||||||
|
}
|
||||||
|
Some(idx) if ENCRYPTIONS[idx] == SmtpEncryptionKind::StartTls => {
|
||||||
|
(Some(SmtpEncryptionKind::StartTls), 587)
|
||||||
|
}
|
||||||
|
_ => (Some(SmtpEncryptionKind::None), 25),
|
||||||
|
};
|
||||||
|
|
||||||
|
let port = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP port")
|
||||||
|
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||||
|
.default(default_port.to_string())
|
||||||
|
.interact()
|
||||||
|
.map(|input| input.parse::<u16>().unwrap())?;
|
||||||
|
|
||||||
|
let default_login = email.to_owned();
|
||||||
|
|
||||||
|
let login = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP login")
|
||||||
|
.default(default_login)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let oauth2_enabled = Confirm::new()
|
||||||
|
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
|
||||||
|
.default(false)
|
||||||
|
.interact_opt()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let auth = if oauth2_enabled {
|
||||||
|
let mut config = OAuth2Config::default();
|
||||||
|
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
|
||||||
|
let redirect_port = OAuth2Config::get_first_available_port()?;
|
||||||
|
|
||||||
|
let method_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP OAuth 2.0 mechanism")
|
||||||
|
.items(OAUTH2_MECHANISMS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
config.method = match method_idx {
|
||||||
|
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||||
|
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||||
|
_ => OAuth2Method::XOAuth2,
|
||||||
|
};
|
||||||
|
|
||||||
|
config.client_id = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP OAuth 2.0 client id")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let client_secret: String = Password::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP OAuth 2.0 client secret")
|
||||||
|
.interact()?;
|
||||||
|
config.client_secret =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"));
|
||||||
|
config
|
||||||
|
.client_secret
|
||||||
|
.set_keyring_entry_secret(&client_secret)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
config.auth_url = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP OAuth 2.0 authorization URL")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
config.token_url = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP OAuth 2.0 token URL")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
|
||||||
|
Ok(Some(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.default(String::default())
|
||||||
|
.interact()?
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.filter(|scope| !scope.is_empty()))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(scope) = prompt_scope("SMTP OAuth 2.0 main scope")? {
|
||||||
|
config.scopes = OAuth2Scopes::Scope(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirm_additional_scope = || -> Result<bool> {
|
||||||
|
let confirm = Confirm::new()
|
||||||
|
.with_prompt(wizard_prompt!(
|
||||||
|
"Would you like to add more SMTP OAuth 2.0 scopes?"
|
||||||
|
))
|
||||||
|
.default(false)
|
||||||
|
.interact_opt()?
|
||||||
|
.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()
|
||||||
|
.with_prompt(wizard_prompt!(
|
||||||
|
"Would you like to enable PKCE verification?"
|
||||||
|
))
|
||||||
|
.default(true)
|
||||||
|
.interact_opt()?
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
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::new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"));
|
||||||
|
config
|
||||||
|
.access_token
|
||||||
|
.set_keyring_entry_secret(access_token)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(refresh_token) = &refresh_token {
|
||||||
|
config.refresh_token =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-refresh-token"));
|
||||||
|
config
|
||||||
|
.refresh_token
|
||||||
|
.set_keyring_entry_secret(refresh_token)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
SmtpAuthConfig::OAuth2(config)
|
||||||
|
} else {
|
||||||
|
let secret_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP authentication strategy")
|
||||||
|
.items(SECRETS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
let secret = match secret_idx {
|
||||||
|
Some(idx) if SECRETS[idx] == KEYRING => {
|
||||||
|
let secret = Secret::new_keyring_entry(format!("{account_name}-smtp-passwd"));
|
||||||
|
secret
|
||||||
|
.set_keyring_entry_secret(prompt::passwd("SMTP password")?)
|
||||||
|
.await?;
|
||||||
|
secret
|
||||||
|
}
|
||||||
|
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("SMTP password")?),
|
||||||
|
Some(idx) if SECRETS[idx] == CMD => Secret::new_cmd(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("Shell command")
|
||||||
|
.default(format!("pass show {account_name}-smtp-passwd"))
|
||||||
|
.interact()?,
|
||||||
|
),
|
||||||
|
_ => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
SmtpAuthConfig::Passwd(PasswdConfig(secret))
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = SmtpConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
encryption,
|
||||||
|
login,
|
||||||
|
auth,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(BackendConfig::Smtp(config))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue