pimalaya_cli/wizard/
smtp.rs1use core::fmt;
4
5use secrecy::SecretString;
6
7use crate::prompt::{self, PromptResult};
8
9#[derive(Clone, Debug)]
11pub struct WizardSmtpConfig {
12 pub host: String,
14 pub port: u16,
16 pub encryption: Encryption,
18 pub login: String,
20 pub auth: SmtpAuth,
22}
23
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub enum Encryption {
27 #[default]
29 Tls,
30 StartTls,
32 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#[derive(Clone, Debug)]
48pub enum SmtpAuth {
49 Password(SmtpSecret),
51}
52
53#[derive(Clone, Debug)]
55pub enum SmtpSecret {
56 Raw(SecretString),
58 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
69pub fn run(
72 account_name: impl AsRef<str>,
73 local_part: impl AsRef<str>,
74 domain: impl AsRef<str>,
75 defaults: Option<&WizardSmtpConfig>,
76) -> PromptResult<WizardSmtpConfig> {
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!("smtp.{domain}"));
84
85 let host = prompt::text("SMTP hostname:", Some(&default_host))?;
86
87 let default_encryption = defaults.map(|c| c.encryption).unwrap_or_default();
88
89 let encryption = prompt::item("SMTP 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("SMTP 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("SMTP login:", Some(&default_login))?;
106
107 let auth = {
108 let strategy = prompt::item("SMTP authentication strategy:", SECRETS, None)?;
109 let secret = match strategy {
110 CMD => {
111 let default_cmd = default_secret_cmd(account_name, "smtp");
112 SmtpSecret::Command(prompt::text("Shell command:", Some(&default_cmd))?)
113 }
114 RAW => SmtpSecret::Raw(prompt::password(
115 "SMTP password:",
116 "Confirm SMTP password:",
117 )?),
118 _ => unreachable!(),
119 };
120
121 SmtpAuth::Password(secret)
122 };
123
124 Ok(WizardSmtpConfig {
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 => 465,
151 Encryption::StartTls => 587,
152 Encryption::None => 25,
153 }
154}