Skip to main content

pimalaya_cli/wizard/
imap.rs

1//! Interactive IMAP account setup wizard.
2
3use core::fmt;
4
5use secrecy::SecretString;
6
7use crate::prompt::{self, PromptResult};
8
9/// IMAP account settings collected by the wizard.
10#[derive(Clone, Debug)]
11pub struct WizardImapConfig {
12    /// The IMAP server hostname.
13    pub host: String,
14    /// The IMAP server port.
15    pub port: u16,
16    /// The connection encryption scheme.
17    pub encryption: Encryption,
18    /// The login (username) sent during authentication.
19    pub login: String,
20    /// The authentication method and its secret.
21    pub auth: ImapAuth,
22}
23
24/// Connection encryption scheme offered by the wizard.
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub enum Encryption {
27    /// Implicit TLS negotiated on connection (the default).
28    #[default]
29    Tls,
30    /// Opportunistic upgrade to TLS through STARTTLS.
31    StartTls,
32    /// No encryption (insecure).
33    None,
34}
35
36impl fmt::Display for Encryption {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Self::Tls => f.write_str("Always (TLS)"),
40            Self::StartTls => f.write_str("Opportunistic (STARTTLS)"),
41            Self::None => f.write_str("None (insecure)"),
42        }
43    }
44}
45
46/// IMAP authentication method.
47#[derive(Clone, Debug)]
48pub enum ImapAuth {
49    /// Password authentication carrying the password secret.
50    Password(ImapSecret),
51}
52
53/// Source of an IMAP password.
54#[derive(Clone, Debug)]
55pub enum ImapSecret {
56    /// The password stored in plaintext in the configuration.
57    Raw(SecretString),
58    /// A shell command whose output is the password.
59    Command(String),
60}
61
62const ENCRYPTIONS: [Encryption; 3] = [Encryption::Tls, Encryption::StartTls, Encryption::None];
63
64const CMD: &str = "Use a shell command to retrieve my password (recommended)";
65const RAW: &str = "Save password in the configuration file (plaintext, NOT recommended)";
66
67const SECRETS: [&str; 2] = [CMD, RAW];
68
69/// Runs the interactive IMAP account wizard, returning the collected
70/// settings.
71pub fn run(
72    account_name: impl AsRef<str>,
73    local_part: impl AsRef<str>,
74    domain: impl AsRef<str>,
75    defaults: Option<&WizardImapConfig>,
76) -> PromptResult<WizardImapConfig> {
77    let account_name = account_name.as_ref();
78    let local_part = local_part.as_ref();
79    let domain = domain.as_ref();
80
81    let default_host = defaults
82        .map(|c| c.host.clone())
83        .unwrap_or_else(|| format!("imap.{domain}"));
84
85    let host = prompt::text("IMAP hostname:", Some(&default_host))?;
86
87    let default_encryption = defaults.map(|c| c.encryption).unwrap_or_default();
88
89    let encryption = prompt::item("IMAP encryption:", ENCRYPTIONS, Some(default_encryption))?;
90
91    let default_port = if encryption == default_encryption {
92        defaults
93            .map(|c| c.port)
94            .unwrap_or_else(|| default_port(encryption))
95    } else {
96        default_port(encryption)
97    };
98
99    let port = prompt::u16("IMAP port:", Some(default_port))?;
100
101    let default_login = defaults
102        .map(|c| c.login.clone())
103        .unwrap_or_else(|| format!("{local_part}@{domain}"));
104
105    let login = prompt::text("IMAP login:", Some(&default_login))?;
106
107    let auth = {
108        let strategy = prompt::item("IMAP authentication strategy:", SECRETS, None)?;
109        let secret = match strategy {
110            CMD => {
111                let default_cmd = default_secret_cmd(account_name, "imap");
112                ImapSecret::Command(prompt::text("Shell command:", Some(&default_cmd))?)
113            }
114            RAW => ImapSecret::Raw(prompt::password(
115                "IMAP password:",
116                "Confirm IMAP password:",
117            )?),
118            _ => unreachable!(),
119        };
120
121        ImapAuth::Password(secret)
122    };
123
124    Ok(WizardImapConfig {
125        host,
126        port,
127        encryption,
128        login,
129        auth,
130    })
131}
132
133fn default_secret_cmd(account_name: &str, protocol: &str) -> String {
134    if cfg!(target_os = "macos") {
135        format!(
136            "security find-generic-password \
137	     -a '{account_name}' \
138	     -s 'himalaya-{account_name}-{protocol}' \
139	     -w"
140        )
141    } else if cfg!(target_os = "linux") {
142        format!("secret-tool lookup account {account_name} service himalaya-{protocol}")
143    } else {
144        String::new()
145    }
146}
147
148fn default_port(encryption: Encryption) -> u16 {
149    match encryption {
150        Encryption::Tls => 993,
151        Encryption::StartTls | Encryption::None => 143,
152    }
153}