pimalaya_cli/wizard/
jmap.rs1use secrecy::SecretString;
4
5use crate::prompt::{self, PromptResult};
6
7#[derive(Clone, Debug)]
9pub struct WizardJmapConfig {
10 pub server: String,
12 pub auth: JmapAuth,
14}
15
16#[derive(Clone, Debug)]
18pub enum JmapAuth {
19 Basic {
21 login: String,
23 secret: JmapSecret,
25 },
26 Bearer {
28 secret: JmapSecret,
30 },
31}
32
33#[derive(Clone, Debug)]
35pub enum JmapSecret {
36 Raw(SecretString),
38 Command(String),
40}
41
42const BASIC: &str = "Basic (username + password)";
43const BEARER: &str = "Bearer (OAuth access token)";
44const AUTHS: [&str; 2] = [BASIC, BEARER];
45
46const CMD: &str = "Use a shell command to retrieve my secret (recommended)";
47const RAW: &str = "Save secret in the configuration file (plaintext, NOT recommended)";
48const SECRETS: [&str; 2] = [CMD, RAW];
49
50pub fn run(
53 account_name: impl AsRef<str>,
54 local_part: impl AsRef<str>,
55 domain: impl AsRef<str>,
56 defaults: Option<&WizardJmapConfig>,
57) -> PromptResult<WizardJmapConfig> {
58 let account_name = account_name.as_ref();
59 let local_part = local_part.as_ref();
60 let domain = domain.as_ref();
61
62 let default_server = defaults
63 .map(|c| c.server.clone())
64 .unwrap_or_else(|| domain.to_string());
65
66 let server = prompt::text(
67 "JMAP server (bare authority or full URL):",
68 Some(default_server.as_str()),
69 )?;
70
71 let default_strategy = match defaults.map(|c| &c.auth) {
72 Some(JmapAuth::Basic { .. }) => Some(BASIC),
73 Some(JmapAuth::Bearer { .. }) => Some(BEARER),
74 None => None,
75 };
76
77 let strategy = prompt::item("JMAP authentication strategy:", AUTHS, default_strategy)?;
78
79 let auth = match strategy {
80 BASIC => {
81 let default_login = defaults
82 .and_then(|c| match &c.auth {
83 JmapAuth::Basic { login, .. } if !login.is_empty() => Some(login.clone()),
84 _ => None,
85 })
86 .unwrap_or_else(|| format!("{local_part}@{domain}"));
87
88 let login = prompt::text("JMAP login:", Some(default_login.as_str()))?;
89 let secret = prompt_secret(account_name, "password")?;
90
91 JmapAuth::Basic { login, secret }
92 }
93 BEARER => {
94 let secret = prompt_secret(account_name, "token")?;
95 JmapAuth::Bearer { secret }
96 }
97 _ => unreachable!(),
98 };
99
100 Ok(WizardJmapConfig { server, auth })
101}
102
103fn prompt_secret(account_name: &str, label: &str) -> PromptResult<JmapSecret> {
104 let strategy = prompt::item("JMAP secret strategy:", SECRETS, None)?;
105
106 match strategy {
107 CMD => {
108 let default_cmd = default_secret_cmd(account_name);
109 let cmd = prompt::text("Shell command:", Some(default_cmd.as_str()))?;
110 Ok(JmapSecret::Command(cmd))
111 }
112 RAW => {
113 let secret =
114 prompt::password(format!("JMAP {label}:"), format!("Confirm JMAP {label}:"))?;
115 Ok(JmapSecret::Raw(secret))
116 }
117 _ => unreachable!(),
118 }
119}
120
121fn default_secret_cmd(account_name: &str) -> String {
122 if cfg!(target_os = "macos") {
123 format!(
124 "security find-generic-password \
125 -a '{account_name}' \
126 -s 'himalaya-{account_name}-jmap' \
127 -w"
128 )
129 } else if cfg!(target_os = "linux") {
130 format!("secret-tool lookup account {account_name} service himalaya-jmap")
131 } else {
132 String::new()
133 }
134}