pimalaya_cli/wizard/
caldav.rs1use core::fmt;
4
5use secrecy::SecretString;
6
7use crate::prompt::{self, PromptResult};
8
9#[derive(Clone, Debug)]
11pub struct WizardCaldavConfig {
12 pub host: String,
14 pub port: u16,
16 pub encryption: Encryption,
18 pub home_url: Option<String>,
21 pub auth: CaldavAuth,
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27pub enum Encryption {
28 #[default]
30 Tls,
31 None,
33}
34
35impl fmt::Display for Encryption {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::Tls => f.write_str("Always (TLS)"),
39 Self::None => f.write_str("None (insecure)"),
40 }
41 }
42}
43
44#[derive(Clone, Debug)]
46pub enum CaldavAuth {
47 Basic {
49 username: String,
51 secret: CaldavSecret,
53 },
54 Bearer {
56 secret: CaldavSecret,
58 },
59}
60
61#[derive(Clone, Debug)]
63pub enum CaldavSecret {
64 Raw(SecretString),
66 Command(String),
68}
69
70const ENCRYPTIONS: [Encryption; 2] = [Encryption::Tls, Encryption::None];
71
72const CMD: &str = "Use a shell command to retrieve my secret (recommended)";
73const RAW: &str = "Save secret in the configuration file (plaintext, NOT recommended)";
74const SECRETS: [&str; 2] = [CMD, RAW];
75
76const BASIC: &str = "HTTP Basic (username + password)";
77const BEARER: &str = "HTTP Bearer (token)";
78const AUTHS: [&str; 2] = [BASIC, BEARER];
79
80pub fn run(
83 account_name: impl AsRef<str>,
84 local_part: impl AsRef<str>,
85 domain: impl AsRef<str>,
86 defaults: Option<&WizardCaldavConfig>,
87) -> PromptResult<WizardCaldavConfig> {
88 let account_name = account_name.as_ref();
89 let local_part = local_part.as_ref();
90 let domain = domain.as_ref();
91
92 let default_host = defaults
93 .map(|c| c.host.clone())
94 .unwrap_or_else(|| format!("caldav.{domain}"));
95
96 let host = prompt::text("CalDAV hostname:", Some(&default_host))?;
97
98 let default_encryption = defaults.map(|c| c.encryption).unwrap_or_default();
99
100 let encryption = prompt::item("CalDAV encryption:", ENCRYPTIONS, Some(default_encryption))?;
101
102 let default_port = if encryption == default_encryption {
103 defaults
104 .map(|c| c.port)
105 .unwrap_or_else(|| default_port(encryption))
106 } else {
107 default_port(encryption)
108 };
109
110 let port = prompt::u16("CalDAV port:", Some(default_port))?;
111
112 let default_home_url = defaults
113 .and_then(|c| c.home_url.clone())
114 .unwrap_or_default();
115
116 let home_url = prompt::text(
117 "CalDAV home URL (leave blank to auto-discover):",
118 Some(&default_home_url),
119 )?;
120
121 let home_url = if home_url.trim().is_empty() {
122 None
123 } else {
124 Some(home_url)
125 };
126
127 let default_strategy = match defaults.map(|c| &c.auth) {
128 Some(CaldavAuth::Basic { .. }) => Some(BASIC),
129 Some(CaldavAuth::Bearer { .. }) => Some(BEARER),
130 None => None,
131 };
132
133 let strategy = prompt::item("CalDAV authentication strategy:", AUTHS, default_strategy)?;
134
135 let auth = match strategy {
136 BASIC => {
137 let default_username = defaults
138 .and_then(|c| match &c.auth {
139 CaldavAuth::Basic { username, .. } if !username.is_empty() => {
140 Some(username.clone())
141 }
142 _ => None,
143 })
144 .unwrap_or_else(|| format!("{local_part}@{domain}"));
145
146 let username = prompt::text("CalDAV username:", Some(&default_username))?;
147 let secret = prompt_secret(account_name, "password")?;
148
149 CaldavAuth::Basic { username, secret }
150 }
151 BEARER => {
152 let secret = prompt_secret(account_name, "token")?;
153 CaldavAuth::Bearer { secret }
154 }
155 _ => unreachable!(),
156 };
157
158 Ok(WizardCaldavConfig {
159 host,
160 port,
161 encryption,
162 home_url,
163 auth,
164 })
165}
166
167fn prompt_secret(account_name: &str, label: &str) -> PromptResult<CaldavSecret> {
168 let strategy = prompt::item("CalDAV secret strategy:", SECRETS, None)?;
169
170 match strategy {
171 CMD => {
172 let default_cmd = default_secret_cmd(account_name, "caldav");
173 let cmd = prompt::text("Shell command:", Some(&default_cmd))?;
174 Ok(CaldavSecret::Command(cmd))
175 }
176 RAW => {
177 let secret = prompt::password(
178 format!("CalDAV {label}:"),
179 format!("Confirm CalDAV {label}:"),
180 )?;
181 Ok(CaldavSecret::Raw(secret))
182 }
183 _ => unreachable!(),
184 }
185}
186
187fn default_secret_cmd(account_name: &str, protocol: &str) -> String {
188 if cfg!(target_os = "macos") {
189 format!(
190 "security find-generic-password \
191 -a '{account_name}' \
192 -s 'himalaya-{account_name}-{protocol}' \
193 -w"
194 )
195 } else if cfg!(target_os = "linux") {
196 format!("secret-tool lookup account {account_name} service himalaya-{protocol}")
197 } else {
198 String::new()
199 }
200}
201
202fn default_port(encryption: Encryption) -> u16 {
203 match encryption {
204 Encryption::Tls => 443,
205 Encryption::None => 80,
206 }
207}