himalaya 1.0.0-beta

CLI to manage emails
use anyhow::Result;
use dialoguer::{Confirm, Input, Password, Select};
use email::{
    account::config::{
        oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
        passwd::PasswdConfig,
    },
    imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
};
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;

use crate::{
    backend::config::BackendConfig,
    config::wizard::{prompt_passwd, THEME},
    wizard_log, wizard_prompt,
};

const PROTOCOLS: &[ImapEncryptionKind] = &[
    ImapEncryptionKind::Tls,
    ImapEncryptionKind::StartTls,
    ImapEncryptionKind::None,
];

const PASSWD: &str = "Password";
const OAUTH2: &str = "OAuth 2.0";
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];

const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];

const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
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";

pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
    let mut config = ImapConfig::default();

    config.host = Input::with_theme(&*THEME)
        .with_prompt("IMAP host")
        .default(format!("imap.{}", email.rsplit_once('@').unwrap().1))
        .interact()?;

    let protocol = Select::with_theme(&*THEME)
        .with_prompt("IMAP security protocol")
        .items(PROTOCOLS)
        .default(0)
        .interact_opt()?;

    let default_port = match protocol {
        Some(idx) if PROTOCOLS[idx] == ImapEncryptionKind::Tls => {
            config.encryption = Some(ImapEncryptionKind::Tls);
            993
        }
        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)
        .with_prompt("IMAP port")
        .validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
        .default(default_port.to_string())
        .interact()
        .map(|input| input.parse::<u16>().unwrap())?;

    config.login = Input::with_theme(&*THEME)
        .with_prompt("IMAP login")
        .default(email.to_owned())
        .interact()?;

    let auth = Select::with_theme(&*THEME)
        .with_prompt("IMAP authentication mechanism")
        .items(AUTH_MECHANISMS)
        .default(0)
        .interact_opt()?;

    config.auth = match auth {
        Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
            let secret = Select::with_theme(&*THEME)
                .with_prompt("IMAP authentication strategy")
                .items(SECRETS)
                .default(0)
                .interact_opt()?;

            let config = match secret {
                Some(idx) if SECRETS[idx] == KEYRING => {
                    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)
                .with_prompt("IMAP OAuth 2.0 mechanism")
                .items(OAUTH2_MECHANISMS)
                .default(0)
                .interact_opt()?;

            config.method = match method {
                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()?;
            Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-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()?;

            config.scopes = OAuth2Scopes::Scope(
                Input::with_theme(&*THEME)
                    .with_prompt("IMAP OAuth 2.0 main scope")
                    .interact()?,
            );

            while Confirm::new()
                .with_prompt(wizard_prompt!(
                    "Would you like to add more IMAP OAuth 2.0 scopes?"
                ))
                .default(false)
                .interact_opt()?
                .unwrap_or_default()
            {
                let mut scopes = match config.scopes {
                    OAuth2Scopes::Scope(scope) => vec![scope],
                    OAuth2Scopes::Scopes(scopes) => scopes,
                };

                scopes.push(
                    Input::with_theme(&*THEME)
                        .with_prompt("Additional IMAP OAuth 2.0 scope")
                        .interact()?,
                );

                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(config.redirect_host.clone())
            .with_redirect_port(config.redirect_port)
            .build()?;

            let mut auth_code_grant = AuthorizationCodeGrant::new()
                .with_redirect_host(config.redirect_host.clone())
                .with_redirect_port(config.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.to_string());
            println!("");

            let (access_token, refresh_token) = auth_code_grant
                .wait_for_redirection(&client, csrf_token)
                .await?;

            Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))
                .set_keyring_entry_secret(access_token)
                .await?;

            if let Some(refresh_token) = &refresh_token {
                Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))
                    .set_keyring_entry_secret(refresh_token)
                    .await?;
            }

            ImapAuthConfig::OAuth2(config)
        }
        _ => ImapAuthConfig::default(),
    };

    Ok(BackendConfig::Imap(config))
}