pimalaya-cli 0.0.1

Collection of CLI tools for Pimalaya
use secrecy::SecretString;

use crate::prompt::{self, PromptResult};

#[derive(Clone, Debug)]
pub struct WizardJmapConfig {
    pub server: String,
    pub auth: JmapAuth,
}

#[derive(Clone, Debug)]
pub enum JmapAuth {
    Basic { login: String, secret: JmapSecret },
    Bearer { secret: JmapSecret },
}

#[derive(Clone, Debug)]
pub enum JmapSecret {
    Raw(SecretString),
    Command(String),
}

const BASIC: &str = "Basic (username + password)";
const BEARER: &str = "Bearer (OAuth access token)";
const AUTHS: [&str; 2] = [BASIC, BEARER];

const CMD: &str = "Use a shell command to retrieve my secret (recommended)";
const RAW: &str = "Save secret in the configuration file (plaintext, NOT recommended)";
const SECRETS: [&str; 2] = [CMD, RAW];

pub fn run(
    account_name: impl AsRef<str>,
    local_part: impl AsRef<str>,
    domain: impl AsRef<str>,
    defaults: Option<&WizardJmapConfig>,
) -> PromptResult<WizardJmapConfig> {
    let account_name = account_name.as_ref();
    let local_part = local_part.as_ref();
    let domain = domain.as_ref();

    let default_server = defaults
        .map(|c| c.server.clone())
        .unwrap_or_else(|| domain.to_string());

    let server = prompt::text(
        "JMAP server (bare authority or full URL):",
        Some(default_server.as_str()),
    )?;

    let default_strategy = match defaults.map(|c| &c.auth) {
        Some(JmapAuth::Basic { .. }) => Some(BASIC),
        Some(JmapAuth::Bearer { .. }) => Some(BEARER),
        None => None,
    };

    let strategy = prompt::item("JMAP authentication strategy:", AUTHS, default_strategy)?;

    let auth = match strategy {
        BASIC => {
            let default_login = defaults
                .and_then(|c| match &c.auth {
                    JmapAuth::Basic { login, .. } if !login.is_empty() => Some(login.clone()),
                    _ => None,
                })
                .unwrap_or_else(|| format!("{local_part}@{domain}"));

            let login = prompt::text("JMAP login:", Some(default_login.as_str()))?;
            let secret = prompt_secret(account_name, "password")?;

            JmapAuth::Basic { login, secret }
        }
        BEARER => {
            let secret = prompt_secret(account_name, "token")?;
            JmapAuth::Bearer { secret }
        }
        _ => unreachable!(),
    };

    Ok(WizardJmapConfig { server, auth })
}

fn prompt_secret(account_name: &str, label: &str) -> PromptResult<JmapSecret> {
    let strategy = prompt::item("JMAP secret strategy:", SECRETS, None)?;

    match strategy {
        CMD => {
            let default_cmd = default_secret_cmd(account_name);
            let cmd = prompt::text("Shell command:", Some(default_cmd.as_str()))?;
            Ok(JmapSecret::Command(cmd))
        }
        RAW => {
            let secret =
                prompt::password(format!("JMAP {label}:"), format!("Confirm JMAP {label}:"))?;
            Ok(JmapSecret::Raw(secret))
        }
        _ => unreachable!(),
    }
}

fn default_secret_cmd(account_name: &str) -> String {
    if cfg!(target_os = "macos") {
        format!(
            "security find-generic-password \
	     -a '{account_name}' \
	     -s 'himalaya-{account_name}-jmap' \
	     -w"
        )
    } else if cfg!(target_os = "linux") {
        format!("secret-tool lookup account {account_name} service himalaya-jmap")
    } else {
        String::new()
    }
}