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::{
8    prompt::{self, PromptResult},
9    wizard::keyring::{self, SecretChoice},
10};
11
12/// IMAP account settings collected by the wizard.
13#[derive(Clone, Debug)]
14pub struct WizardImapConfig {
15    /// The IMAP server hostname.
16    pub host: String,
17    /// The IMAP server port.
18    pub port: u16,
19    /// The connection encryption scheme.
20    pub encryption: Encryption,
21    /// The login (username) sent during authentication.
22    pub login: String,
23    /// The authentication method and its secret.
24    pub auth: ImapAuth,
25}
26
27/// Connection encryption scheme offered by the wizard.
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29pub enum Encryption {
30    /// Implicit TLS negotiated on connection (the default).
31    #[default]
32    Tls,
33    /// Opportunistic upgrade to TLS through STARTTLS.
34    StartTls,
35    /// No encryption (insecure).
36    None,
37}
38
39impl fmt::Display for Encryption {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::Tls => f.write_str("Always (TLS)"),
43            Self::StartTls => f.write_str("Opportunistic (STARTTLS)"),
44            Self::None => f.write_str("None (insecure)"),
45        }
46    }
47}
48
49/// IMAP authentication method.
50#[derive(Clone, Debug)]
51pub enum ImapAuth {
52    /// Password authentication carrying the password secret.
53    Password(ImapSecret),
54}
55
56/// Source of an IMAP password.
57#[derive(Clone, Debug)]
58pub enum ImapSecret {
59    /// The password stored in plaintext in the configuration.
60    Raw(SecretString),
61    /// An argv command (program + arguments, no shell) whose output is
62    /// the password. The preferred form.
63    Command(Vec<String>),
64    /// A shell command line whose output is the password — the fallback,
65    /// run through the platform shell.
66    Shell(String),
67}
68
69const ENCRYPTIONS: [Encryption; 3] = [Encryption::Tls, Encryption::StartTls, Encryption::None];
70
71/// Runs the interactive IMAP account wizard, returning the collected
72/// settings.
73pub fn run(
74    account_name: impl AsRef<str>,
75    local_part: impl AsRef<str>,
76    domain: impl AsRef<str>,
77    defaults: Option<&WizardImapConfig>,
78) -> PromptResult<WizardImapConfig> {
79    let account_name = account_name.as_ref();
80    let local_part = local_part.as_ref();
81    let domain = domain.as_ref();
82
83    let default_host = defaults
84        .map(|c| c.host.clone())
85        .unwrap_or_else(|| format!("imap.{domain}"));
86
87    let host = prompt::text("IMAP hostname:", Some(&default_host))?;
88
89    let default_encryption = defaults.map(|c| c.encryption).unwrap_or_default();
90
91    let encryption = prompt::item("IMAP encryption:", ENCRYPTIONS, Some(default_encryption))?;
92
93    let default_port = if encryption == default_encryption {
94        defaults
95            .map(|c| c.port)
96            .unwrap_or_else(|| default_port(encryption))
97    } else {
98        default_port(encryption)
99    };
100
101    let port = prompt::u16("IMAP port:", Some(default_port))?;
102
103    let default_login = defaults
104        .map(|c| c.login.clone())
105        .unwrap_or_else(|| format!("{local_part}@{domain}"));
106
107    let login = prompt::text("IMAP login:", Some(&default_login))?;
108
109    let auth = {
110        let key = format!("{account_name}-imap");
111        let secret = keyring::prompt_secret("IMAP password", &key)?;
112        ImapAuth::Password(match secret {
113            SecretChoice::Command(argv) => ImapSecret::Command(argv),
114            SecretChoice::Shell(line) => ImapSecret::Shell(line),
115            SecretChoice::Raw(secret) => ImapSecret::Raw(secret),
116        })
117    };
118
119    Ok(WizardImapConfig {
120        host,
121        port,
122        encryption,
123        login,
124        auth,
125    })
126}
127
128fn default_port(encryption: Encryption) -> u16 {
129    match encryption {
130        Encryption::Tls => 993,
131        Encryption::StartTls | Encryption::None => 143,
132    }
133}