mirror of
https://github.com/soywod/himalaya.git
synced 2024-09-28 20:21:13 +00:00
plug autoconfig to imap and smtp wizards
This commit is contained in:
parent
b0d7e773dc
commit
6f9f75cfd2
|
@ -21,7 +21,7 @@ pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||||
|
|
||||||
let account_name = Input::with_theme(&*THEME)
|
let account_name = Input::with_theme(&*THEME)
|
||||||
.with_prompt("Account name")
|
.with_prompt("Account name")
|
||||||
.default(String::from("Personal"))
|
.default(String::from("personal"))
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
config.email = Input::with_theme(&*THEME)
|
config.email = Input::with_theme(&*THEME)
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use autoconfig::config::Config as AutoConfig;
|
||||||
use dialoguer::Select;
|
use dialoguer::Select;
|
||||||
|
use log::{debug, warn};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
use crate::imap;
|
use crate::imap;
|
||||||
|
@ -31,6 +34,23 @@ const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
|
||||||
BackendKind::Sendmail,
|
BackendKind::Sendmail,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static AUTOCONFIG: OnceLock<AutoConfig> = OnceLock::new();
|
||||||
|
|
||||||
|
#[cfg(any(feature = "imap", feature = "smtp"))]
|
||||||
|
pub(crate) async fn get_or_init_autoconfig(email: &str) -> Option<&AutoConfig> {
|
||||||
|
match AUTOCONFIG.get() {
|
||||||
|
Some(autoconfig) => Some(autoconfig),
|
||||||
|
None => match autoconfig::from_addr(email).await {
|
||||||
|
Ok(autoconfig) => Some(AUTOCONFIG.get_or_init(|| autoconfig)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("cannot discover SMTP configuration from {email}: {err}");
|
||||||
|
debug!("{err:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn configure(
|
pub(crate) async fn configure(
|
||||||
#[allow(unused)] account_name: &str,
|
#[allow(unused)] account_name: &str,
|
||||||
#[allow(unused)] email: &str,
|
#[allow(unused)] email: &str,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use autoconfig::config::{AuthenticationType, SecurityType, ServerType};
|
||||||
use dialoguer::{Confirm, Input, Password, Select};
|
use dialoguer::{Confirm, Input, Password, Select};
|
||||||
use email::{
|
use email::{
|
||||||
account::config::{
|
account::config::{
|
||||||
|
@ -11,21 +12,17 @@ use oauth::v2_0::{AuthorizationCodeGrant, Client};
|
||||||
use secret::Secret;
|
use secret::Secret;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::config::BackendConfig,
|
backend::{config::BackendConfig, wizard::get_or_init_autoconfig},
|
||||||
ui::{prompt, THEME},
|
ui::{prompt, THEME},
|
||||||
wizard_log, wizard_prompt,
|
wizard_log, wizard_prompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROTOCOLS: &[ImapEncryptionKind] = &[
|
const ENCRYPTIONS: &[ImapEncryptionKind] = &[
|
||||||
ImapEncryptionKind::Tls,
|
ImapEncryptionKind::Tls,
|
||||||
ImapEncryptionKind::StartTls,
|
ImapEncryptionKind::StartTls,
|
||||||
ImapEncryptionKind::None,
|
ImapEncryptionKind::None,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PASSWD: &str = "Password";
|
|
||||||
const OAUTH2: &str = "OAuth 2.0";
|
|
||||||
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];
|
|
||||||
|
|
||||||
const XOAUTH2: &str = "XOAUTH2";
|
const XOAUTH2: &str = "XOAUTH2";
|
||||||
const OAUTHBEARER: &str = "OAUTHBEARER";
|
const OAUTHBEARER: &str = "OAUTHBEARER";
|
||||||
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
||||||
|
@ -36,92 +33,116 @@ const RAW: &str = "Ask my password, then save it in the configuration file (not
|
||||||
const CMD: &str = "Ask me a shell command that exposes my password";
|
const CMD: &str = "Ask me a shell command that exposes my password";
|
||||||
|
|
||||||
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||||
let mut config = ImapConfig::default();
|
let autoconfig = get_or_init_autoconfig(email).await;
|
||||||
|
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
|
||||||
|
let autoconfig_server = autoconfig.and_then(|c| {
|
||||||
|
c.email_provider()
|
||||||
|
.incoming_servers()
|
||||||
|
.into_iter()
|
||||||
|
.find(|server| matches!(server.server_type(), ServerType::Imap))
|
||||||
|
});
|
||||||
|
|
||||||
config.host = Input::with_theme(&*THEME)
|
let autoconfig_host = autoconfig_server
|
||||||
.with_prompt("IMAP host")
|
.and_then(|s| s.hostname())
|
||||||
.default(format!("imap.{}", email.rsplit_once('@').unwrap().1))
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
let default_host =
|
||||||
|
autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1));
|
||||||
|
|
||||||
|
let host = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP hostname")
|
||||||
|
.default(default_host)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
let protocol = Select::with_theme(&*THEME)
|
let autoconfig_encryption = autoconfig_server
|
||||||
.with_prompt("IMAP security protocol")
|
.and_then(|imap| {
|
||||||
.items(PROTOCOLS)
|
imap.security_type().map(|encryption| match encryption {
|
||||||
.default(0)
|
SecurityType::Plain => ImapEncryptionKind::None,
|
||||||
.interact_opt()?;
|
SecurityType::Starttls => ImapEncryptionKind::StartTls,
|
||||||
|
SecurityType::Tls => ImapEncryptionKind::Tls,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let default_port = match protocol {
|
let default_encryption_idx = match &autoconfig_encryption {
|
||||||
Some(idx) if PROTOCOLS[idx] == ImapEncryptionKind::Tls => {
|
ImapEncryptionKind::Tls => 0,
|
||||||
config.encryption = Some(ImapEncryptionKind::Tls);
|
ImapEncryptionKind::StartTls => 1,
|
||||||
993
|
ImapEncryptionKind::None => 2,
|
||||||
}
|
|
||||||
Some(idx) if PROTOCOLS[idx] == ImapEncryptionKind::StartTls => {
|
|
||||||
config.encryption = Some(ImapEncryptionKind::StartTls);
|
|
||||||
143
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
config.encryption = Some(ImapEncryptionKind::None);
|
|
||||||
143
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config.port = Input::with_theme(&*THEME)
|
let encryption_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("IMAP encryption")
|
||||||
|
.items(ENCRYPTIONS)
|
||||||
|
.default(default_encryption_idx)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
let autoconfig_port = autoconfig_server
|
||||||
|
.and_then(|s| s.port())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.unwrap_or_else(|| match &autoconfig_encryption {
|
||||||
|
ImapEncryptionKind::Tls => 993,
|
||||||
|
ImapEncryptionKind::StartTls => 143,
|
||||||
|
ImapEncryptionKind::None => 143,
|
||||||
|
});
|
||||||
|
|
||||||
|
let (encryption, default_port) = match encryption_idx {
|
||||||
|
Some(idx) if idx == default_encryption_idx => {
|
||||||
|
(Some(autoconfig_encryption), autoconfig_port)
|
||||||
|
}
|
||||||
|
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")
|
.with_prompt("IMAP port")
|
||||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||||
.default(default_port.to_string())
|
.default(default_port.to_string())
|
||||||
.interact()
|
.interact()
|
||||||
.map(|input| input.parse::<u16>().unwrap())?;
|
.map(|input| input.parse::<u16>().unwrap())?;
|
||||||
|
|
||||||
config.login = Input::with_theme(&*THEME)
|
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 = Input::with_theme(&*THEME)
|
||||||
.with_prompt("IMAP login")
|
.with_prompt("IMAP login")
|
||||||
.default(email.to_owned())
|
.default(default_login)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
let auth = Select::with_theme(&*THEME)
|
let default_oauth2_enabled = autoconfig_server
|
||||||
.with_prompt("IMAP authentication mechanism")
|
.and_then(|imap| {
|
||||||
.items(AUTH_MECHANISMS)
|
imap.authentication_type()
|
||||||
.default(0)
|
.into_iter()
|
||||||
.interact_opt()?;
|
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
|
||||||
|
})
|
||||||
|
.filter(|_| autoconfig_oauth2.is_some())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
config.auth = match auth {
|
let oauth2_enabled = Confirm::new()
|
||||||
Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
|
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
|
||||||
let secret = Select::with_theme(&*THEME)
|
.default(default_oauth2_enabled)
|
||||||
.with_prompt("IMAP authentication strategy")
|
.interact_opt()?
|
||||||
.items(SECRETS)
|
.unwrap_or_default();
|
||||||
.default(0)
|
|
||||||
.interact_opt()?;
|
|
||||||
|
|
||||||
let config = match secret {
|
let auth = if oauth2_enabled {
|
||||||
Some(idx) if SECRETS[idx] == KEYRING => {
|
let mut config = OAuth2Config::default();
|
||||||
Secret::new_keyring_entry(format!("{account_name}-imap-passwd"))
|
|
||||||
.set_keyring_entry_secret(prompt::passwd("IMAP password")?)
|
|
||||||
.await?;
|
|
||||||
PasswdConfig::default()
|
|
||||||
}
|
|
||||||
Some(idx) if SECRETS[idx] == RAW => PasswdConfig {
|
|
||||||
passwd: Secret::Raw(prompt::passwd("IMAP password")?),
|
|
||||||
},
|
|
||||||
Some(idx) if SECRETS[idx] == CMD => PasswdConfig {
|
|
||||||
passwd: Secret::new_cmd(
|
|
||||||
Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("Shell command")
|
|
||||||
.default(format!("pass show {account_name}-imap-passwd"))
|
|
||||||
.interact()?,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
_ => PasswdConfig::default(),
|
|
||||||
};
|
|
||||||
ImapAuthConfig::Passwd(config)
|
|
||||||
}
|
|
||||||
Some(idx) if AUTH_MECHANISMS[idx] == OAUTH2 => {
|
|
||||||
let mut config = OAuth2Config::new()?;
|
|
||||||
|
|
||||||
let method = Select::with_theme(&*THEME)
|
let method_idx = Select::with_theme(&*THEME)
|
||||||
.with_prompt("IMAP OAuth 2.0 mechanism")
|
.with_prompt("IMAP OAuth 2.0 mechanism")
|
||||||
.items(OAUTH2_MECHANISMS)
|
.items(OAUTH2_MECHANISMS)
|
||||||
.default(0)
|
.default(0)
|
||||||
.interact_opt()?;
|
.interact_opt()?;
|
||||||
|
|
||||||
config.method = match method {
|
config.method = match method_idx {
|
||||||
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||||
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||||
_ => OAuth2Method::XOAuth2,
|
_ => OAuth2Method::XOAuth2,
|
||||||
|
@ -134,42 +155,76 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
|
||||||
let client_secret: String = Password::with_theme(&*THEME)
|
let client_secret: String = Password::with_theme(&*THEME)
|
||||||
.with_prompt("IMAP OAuth 2.0 client secret")
|
.with_prompt("IMAP OAuth 2.0 client secret")
|
||||||
.interact()?;
|
.interact()?;
|
||||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))
|
config.client_secret =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"));
|
||||||
|
config
|
||||||
|
.client_secret
|
||||||
.set_keyring_entry_secret(&client_secret)
|
.set_keyring_entry_secret(&client_secret)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let default_auth_url = autoconfig_oauth2
|
||||||
|
.map(|o| o.auth_url().to_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
config.auth_url = Input::with_theme(&*THEME)
|
config.auth_url = Input::with_theme(&*THEME)
|
||||||
.with_prompt("IMAP OAuth 2.0 authorization URL")
|
.with_prompt("IMAP OAuth 2.0 authorization URL")
|
||||||
|
.default(default_auth_url)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
|
let default_token_url = autoconfig_oauth2
|
||||||
|
.map(|o| o.token_url().to_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
config.token_url = Input::with_theme(&*THEME)
|
config.token_url = Input::with_theme(&*THEME)
|
||||||
.with_prompt("IMAP OAuth 2.0 token URL")
|
.with_prompt("IMAP OAuth 2.0 token URL")
|
||||||
|
.default(default_token_url)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
config.scopes = OAuth2Scopes::Scope(
|
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
|
||||||
Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("IMAP OAuth 2.0 main scope")
|
|
||||||
.interact()?,
|
|
||||||
);
|
|
||||||
|
|
||||||
while Confirm::new()
|
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
|
||||||
|
Ok(match &autoconfig_scopes {
|
||||||
|
Some(scopes) => Select::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.items(scopes)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?
|
||||||
|
.and_then(|idx| scopes.get(idx))
|
||||||
|
.map(|scope| scope.to_string()),
|
||||||
|
None => Some(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.default(String::default())
|
||||||
|
.interact()?
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.filter(|scope| !scope.is_empty()),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
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!(
|
.with_prompt(wizard_prompt!(
|
||||||
"Would you like to add more IMAP OAuth 2.0 scopes?"
|
"Would you like to add more IMAP OAuth 2.0 scopes?"
|
||||||
))
|
))
|
||||||
.default(false)
|
.default(false)
|
||||||
.interact_opt()?
|
.interact_opt()?
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
{
|
|
||||||
|
Ok(confirm)
|
||||||
|
};
|
||||||
|
|
||||||
|
while confirm_additional_scope()? {
|
||||||
let mut scopes = match config.scopes {
|
let mut scopes = match config.scopes {
|
||||||
OAuth2Scopes::Scope(scope) => vec![scope],
|
OAuth2Scopes::Scope(scope) => vec![scope],
|
||||||
OAuth2Scopes::Scopes(scopes) => scopes,
|
OAuth2Scopes::Scopes(scopes) => scopes,
|
||||||
};
|
};
|
||||||
|
|
||||||
scopes.push(
|
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
|
||||||
Input::with_theme(&*THEME)
|
scopes.push(scope)
|
||||||
.with_prompt("Additional IMAP OAuth 2.0 scope")
|
}
|
||||||
.interact()?,
|
|
||||||
);
|
|
||||||
|
|
||||||
config.scopes = OAuth2Scopes::Scopes(scopes);
|
config.scopes = OAuth2Scopes::Scopes(scopes);
|
||||||
}
|
}
|
||||||
|
@ -215,19 +270,58 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
|
||||||
.wait_for_redirection(&client, csrf_token)
|
.wait_for_redirection(&client, csrf_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))
|
config.access_token =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"));
|
||||||
|
config
|
||||||
|
.access_token
|
||||||
.set_keyring_entry_secret(access_token)
|
.set_keyring_entry_secret(access_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(refresh_token) = &refresh_token {
|
if let Some(refresh_token) = &refresh_token {
|
||||||
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-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)
|
.set_keyring_entry_secret(refresh_token)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImapAuthConfig::OAuth2(config)
|
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
|
||||||
}
|
}
|
||||||
_ => ImapAuthConfig::default(),
|
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 { passwd: secret })
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = ImapConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
encryption,
|
||||||
|
login,
|
||||||
|
auth,
|
||||||
|
watch: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BackendConfig::Imap(config))
|
Ok(BackendConfig::Imap(config))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use autoconfig::config::{AuthenticationType, SecurityType, ServerType};
|
||||||
|
use dialoguer::{Confirm, Input, Password, Select};
|
||||||
use email::{
|
use email::{
|
||||||
account::config::{
|
account::config::{
|
||||||
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
||||||
|
@ -11,117 +12,137 @@ use oauth::v2_0::{AuthorizationCodeGrant, Client};
|
||||||
use secret::Secret;
|
use secret::Secret;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::config::BackendConfig,
|
backend::{config::BackendConfig, wizard::get_or_init_autoconfig},
|
||||||
ui::{prompt, THEME},
|
ui::{prompt, THEME},
|
||||||
wizard_log, wizard_prompt,
|
wizard_log, wizard_prompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROTOCOLS: &[SmtpEncryptionKind] = &[
|
const ENCRYPTIONS: &[SmtpEncryptionKind] = &[
|
||||||
SmtpEncryptionKind::Tls,
|
SmtpEncryptionKind::Tls,
|
||||||
SmtpEncryptionKind::StartTls,
|
SmtpEncryptionKind::StartTls,
|
||||||
SmtpEncryptionKind::None,
|
SmtpEncryptionKind::None,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PASSWD: &str = "Password";
|
|
||||||
const OAUTH2: &str = "OAuth 2.0";
|
|
||||||
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];
|
|
||||||
|
|
||||||
const XOAUTH2: &str = "XOAUTH2";
|
const XOAUTH2: &str = "XOAUTH2";
|
||||||
const OAUTHBEARER: &str = "OAUTHBEARER";
|
const OAUTHBEARER: &str = "OAUTHBEARER";
|
||||||
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
|
||||||
|
|
||||||
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
|
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
|
||||||
const KEYRING: &str = "Ask the password, then save it in my system's global keyring";
|
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
|
||||||
const RAW: &str = "Ask the 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 = "Use a shell command that exposes the password";
|
const CMD: &str = "Ask me a shell command that exposes my password";
|
||||||
|
|
||||||
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
||||||
let mut config = SmtpConfig::default();
|
let autoconfig = get_or_init_autoconfig(email).await;
|
||||||
|
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
|
||||||
|
let autoconfig_server = autoconfig.and_then(|c| {
|
||||||
|
c.email_provider()
|
||||||
|
.incoming_servers()
|
||||||
|
.into_iter()
|
||||||
|
.find(|server| matches!(server.server_type(), ServerType::Smtp))
|
||||||
|
});
|
||||||
|
|
||||||
config.host = Input::with_theme(&*THEME)
|
let autoconfig_host = autoconfig_server
|
||||||
.with_prompt("SMTP host")
|
.and_then(|s| s.hostname())
|
||||||
.default(format!("smtp.{}", email.rsplit_once('@').unwrap().1))
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
let default_host =
|
||||||
|
autoconfig_host.unwrap_or_else(|| format!("smtp.{}", email.rsplit_once('@').unwrap().1));
|
||||||
|
|
||||||
|
let host = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP hostname")
|
||||||
|
.default(default_host)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
let protocol = Select::with_theme(&*THEME)
|
let autoconfig_encryption = autoconfig_server
|
||||||
.with_prompt("SMTP security protocol")
|
.and_then(|smtp| {
|
||||||
.items(PROTOCOLS)
|
smtp.security_type().map(|encryption| match encryption {
|
||||||
.default(0)
|
SecurityType::Plain => SmtpEncryptionKind::None,
|
||||||
.interact_opt()?;
|
SecurityType::Starttls => SmtpEncryptionKind::StartTls,
|
||||||
|
SecurityType::Tls => SmtpEncryptionKind::Tls,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let default_port = match protocol {
|
let default_encryption_idx = match &autoconfig_encryption {
|
||||||
Some(idx) if PROTOCOLS[idx] == SmtpEncryptionKind::Tls => {
|
SmtpEncryptionKind::Tls => 0,
|
||||||
config.encryption = Some(SmtpEncryptionKind::Tls);
|
SmtpEncryptionKind::StartTls => 1,
|
||||||
465
|
SmtpEncryptionKind::None => 2,
|
||||||
}
|
|
||||||
Some(idx) if PROTOCOLS[idx] == SmtpEncryptionKind::StartTls => {
|
|
||||||
config.encryption = Some(SmtpEncryptionKind::StartTls);
|
|
||||||
587
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
config.encryption = Some(SmtpEncryptionKind::None);
|
|
||||||
25
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config.port = Input::with_theme(&*THEME)
|
let encryption_idx = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("SMTP encryption")
|
||||||
|
.items(ENCRYPTIONS)
|
||||||
|
.default(default_encryption_idx)
|
||||||
|
.interact_opt()?;
|
||||||
|
|
||||||
|
let autoconfig_port = autoconfig_server
|
||||||
|
.and_then(|s| s.port())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.unwrap_or_else(|| match &autoconfig_encryption {
|
||||||
|
SmtpEncryptionKind::Tls => 993,
|
||||||
|
SmtpEncryptionKind::StartTls => 143,
|
||||||
|
SmtpEncryptionKind::None => 143,
|
||||||
|
});
|
||||||
|
|
||||||
|
let (encryption, default_port) = match encryption_idx {
|
||||||
|
Some(idx) if idx == default_encryption_idx => {
|
||||||
|
(Some(autoconfig_encryption), autoconfig_port)
|
||||||
|
}
|
||||||
|
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")
|
.with_prompt("SMTP port")
|
||||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||||
.default(default_port.to_string())
|
.default(default_port.to_string())
|
||||||
.interact()
|
.interact()
|
||||||
.map(|input| input.parse::<u16>().unwrap())?;
|
.map(|input| input.parse::<u16>().unwrap())?;
|
||||||
|
|
||||||
config.login = Input::with_theme(&*THEME)
|
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 = Input::with_theme(&*THEME)
|
||||||
.with_prompt("SMTP login")
|
.with_prompt("SMTP login")
|
||||||
.default(email.to_owned())
|
.default(default_login)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
let auth = Select::with_theme(&*THEME)
|
let default_oauth2_enabled = autoconfig_server
|
||||||
.with_prompt("SMTP authentication mechanism")
|
.and_then(|smtp| {
|
||||||
.items(AUTH_MECHANISMS)
|
smtp.authentication_type()
|
||||||
.default(0)
|
.into_iter()
|
||||||
.interact_opt()?;
|
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
|
||||||
|
})
|
||||||
|
.filter(|_| autoconfig_oauth2.is_some())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
config.auth = match auth {
|
let oauth2_enabled = Confirm::new()
|
||||||
Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
|
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
|
||||||
let secret = Select::with_theme(&*THEME)
|
.default(default_oauth2_enabled)
|
||||||
.with_prompt("SMTP authentication strategy")
|
.interact_opt()?
|
||||||
.items(SECRETS)
|
.unwrap_or_default();
|
||||||
.default(0)
|
|
||||||
.interact_opt()?;
|
|
||||||
|
|
||||||
let config = match secret {
|
let auth = if oauth2_enabled {
|
||||||
Some(idx) if SECRETS[idx] == KEYRING => {
|
|
||||||
Secret::new_keyring_entry(format!("{account_name}-smtp-passwd"))
|
|
||||||
.set_keyring_entry_secret(prompt::passwd("SMTP password")?)
|
|
||||||
.await?;
|
|
||||||
PasswdConfig::default()
|
|
||||||
}
|
|
||||||
Some(idx) if SECRETS[idx] == RAW => PasswdConfig {
|
|
||||||
passwd: Secret::Raw(prompt::passwd("SMTP password")?),
|
|
||||||
},
|
|
||||||
Some(idx) if SECRETS[idx] == CMD => PasswdConfig {
|
|
||||||
passwd: Secret::new_cmd(
|
|
||||||
Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("Shell command")
|
|
||||||
.default(format!("pass show {account_name}-smtp-passwd"))
|
|
||||||
.interact()?,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
_ => PasswdConfig::default(),
|
|
||||||
};
|
|
||||||
SmtpAuthConfig::Passwd(config)
|
|
||||||
}
|
|
||||||
Some(idx) if AUTH_MECHANISMS[idx] == OAUTH2 => {
|
|
||||||
let mut config = OAuth2Config::default();
|
let mut config = OAuth2Config::default();
|
||||||
|
|
||||||
let method = Select::with_theme(&*THEME)
|
let method_idx = Select::with_theme(&*THEME)
|
||||||
.with_prompt("SMTP OAuth 2.0 mechanism")
|
.with_prompt("SMTP OAuth 2.0 mechanism")
|
||||||
.items(OAUTH2_MECHANISMS)
|
.items(OAUTH2_MECHANISMS)
|
||||||
.default(0)
|
.default(0)
|
||||||
.interact_opt()?;
|
.interact_opt()?;
|
||||||
|
|
||||||
config.method = match method {
|
config.method = match method_idx {
|
||||||
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
|
||||||
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
|
||||||
_ => OAuth2Method::XOAuth2,
|
_ => OAuth2Method::XOAuth2,
|
||||||
|
@ -131,45 +152,79 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
|
||||||
.with_prompt("SMTP OAuth 2.0 client id")
|
.with_prompt("SMTP OAuth 2.0 client id")
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
let client_secret: String = Input::with_theme(&*THEME)
|
let client_secret: String = Password::with_theme(&*THEME)
|
||||||
.with_prompt("SMTP OAuth 2.0 client secret")
|
.with_prompt("SMTP OAuth 2.0 client secret")
|
||||||
.interact()?;
|
.interact()?;
|
||||||
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))
|
config.client_secret =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"));
|
||||||
|
config
|
||||||
|
.client_secret
|
||||||
.set_keyring_entry_secret(&client_secret)
|
.set_keyring_entry_secret(&client_secret)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let default_auth_url = autoconfig_oauth2
|
||||||
|
.map(|o| o.auth_url().to_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
config.auth_url = Input::with_theme(&*THEME)
|
config.auth_url = Input::with_theme(&*THEME)
|
||||||
.with_prompt("SMTP OAuth 2.0 authorization URL")
|
.with_prompt("SMTP OAuth 2.0 authorization URL")
|
||||||
|
.default(default_auth_url)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
|
let default_token_url = autoconfig_oauth2
|
||||||
|
.map(|o| o.token_url().to_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
config.token_url = Input::with_theme(&*THEME)
|
config.token_url = Input::with_theme(&*THEME)
|
||||||
.with_prompt("SMTP OAuth 2.0 token URL")
|
.with_prompt("SMTP OAuth 2.0 token URL")
|
||||||
|
.default(default_token_url)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
config.scopes = OAuth2Scopes::Scope(
|
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
|
||||||
Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("SMTP OAuth 2.0 main scope")
|
|
||||||
.interact()?,
|
|
||||||
);
|
|
||||||
|
|
||||||
while Confirm::new()
|
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
|
||||||
|
Ok(match &autoconfig_scopes {
|
||||||
|
Some(scopes) => Select::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.items(scopes)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?
|
||||||
|
.and_then(|idx| scopes.get(idx))
|
||||||
|
.map(|scope| scope.to_string()),
|
||||||
|
None => Some(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.default(String::default())
|
||||||
|
.interact()?
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.filter(|scope| !scope.is_empty()),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
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!(
|
.with_prompt(wizard_prompt!(
|
||||||
"Would you like to add more SMTP OAuth 2.0 scopes?"
|
"Would you like to add more SMTP OAuth 2.0 scopes?"
|
||||||
))
|
))
|
||||||
.default(false)
|
.default(false)
|
||||||
.interact_opt()?
|
.interact_opt()?
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
{
|
|
||||||
|
Ok(confirm)
|
||||||
|
};
|
||||||
|
|
||||||
|
while confirm_additional_scope()? {
|
||||||
let mut scopes = match config.scopes {
|
let mut scopes = match config.scopes {
|
||||||
OAuth2Scopes::Scope(scope) => vec![scope],
|
OAuth2Scopes::Scope(scope) => vec![scope],
|
||||||
OAuth2Scopes::Scopes(scopes) => scopes,
|
OAuth2Scopes::Scopes(scopes) => scopes,
|
||||||
};
|
};
|
||||||
|
|
||||||
scopes.push(
|
if let Some(scope) = prompt_scope("Additional SMTP OAuth 2.0 scope")? {
|
||||||
Input::with_theme(&*THEME)
|
scopes.push(scope)
|
||||||
.with_prompt("Additional SMTP OAuth 2.0 scope")
|
}
|
||||||
.interact()?,
|
|
||||||
);
|
|
||||||
|
|
||||||
config.scopes = OAuth2Scopes::Scopes(scopes);
|
config.scopes = OAuth2Scopes::Scopes(scopes);
|
||||||
}
|
}
|
||||||
|
@ -209,25 +264,63 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Backend
|
||||||
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
|
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
|
||||||
|
|
||||||
println!("{}", redirect_url.to_string());
|
println!("{}", redirect_url.to_string());
|
||||||
println!();
|
println!("");
|
||||||
|
|
||||||
let (access_token, refresh_token) = auth_code_grant
|
let (access_token, refresh_token) = auth_code_grant
|
||||||
.wait_for_redirection(&client, csrf_token)
|
.wait_for_redirection(&client, csrf_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))
|
config.access_token =
|
||||||
|
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"));
|
||||||
|
config
|
||||||
|
.access_token
|
||||||
.set_keyring_entry_secret(access_token)
|
.set_keyring_entry_secret(access_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(refresh_token) = &refresh_token {
|
if let Some(refresh_token) = &refresh_token {
|
||||||
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-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)
|
.set_keyring_entry_secret(refresh_token)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
SmtpAuthConfig::OAuth2(config)
|
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
|
||||||
}
|
}
|
||||||
_ => SmtpAuthConfig::default(),
|
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 { passwd: secret })
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = SmtpConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
encryption,
|
||||||
|
login,
|
||||||
|
auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BackendConfig::Smtp(config))
|
Ok(BackendConfig::Smtp(config))
|
||||||
|
|
Loading…
Reference in a new issue