use std::io::{self, BufRead, Write}; use std::path::PathBuf; use crate::config::{BridgeConfig, Config, ImapConfig, Provider, ProtonConfig, SmtpConfig, TlsMode}; use crate::credentials::{self, CredentialStore}; // ── helpers ────────────────────────────────────────────────────────────────── fn prompt(label: &str, default: Option<&str>) -> String { let hint = default.map(|d| format!(" [{d}]")).unwrap_or_default(); print!("{label}{hint}: "); io::stdout().flush().unwrap(); let mut line = String::new(); io::stdin().lock().read_line(&mut line).unwrap(); let trimmed = line.trim().to_string(); if trimmed.is_empty() { default.unwrap_or("").to_string() } else { trimmed } } /// Optional password prompt — returns None (keep existing) when Enter is pressed with no input. /// Use for --configure where a stored value may already exist. fn prompt_password_optional(label: &str) -> Option { print!("{label} [stored, Enter to keep]: "); io::stdout().flush().unwrap(); let mut line = String::new(); io::stdin().lock().read_line(&mut line).unwrap(); let trimmed = line.trim().to_string(); if trimmed.is_empty() { None } else { Some(trimmed) } } /// Required password prompt — keeps asking until the user types something. fn prompt_password_required(label: &str) -> String { loop { print!("{label}: "); io::stdout().flush().unwrap(); let mut line = String::new(); io::stdin().lock().read_line(&mut line).unwrap(); let trimmed = line.trim().to_string(); if !trimmed.is_empty() { return trimmed; } println!("Password cannot be empty."); } } fn prompt_bool(label: &str, default: bool) -> bool { let hint = if default { "Y/n" } else { "y/N" }; print!("{label} [{hint}]: "); io::stdout().flush().unwrap(); let mut line = String::new(); io::stdin().lock().read_line(&mut line).unwrap(); match line.trim().to_lowercase().as_str() { "y" | "yes" => true, "n" | "no" => false, _ => default, } } fn prompt_tls_mode(default: Option<&TlsMode>) -> TlsMode { let default_str = default.map(|m| match m { TlsMode::None => "none", TlsMode::Starttls => "starttls", TlsMode::Smtps => "smtps", }); loop { let val = prompt("SMTP TLS mode (none/starttls/smtps)", default_str); match val.to_lowercase().as_str() { "none" => return TlsMode::None, "starttls" => return TlsMode::Starttls, "smtps" => return TlsMode::Smtps, _ => println!("Please enter: none, starttls, or smtps"), } } } fn random_hex(bytes: usize) -> String { use rand::RngCore; let mut buf = vec![0u8; bytes]; rand::thread_rng().fill_bytes(&mut buf); buf.iter().map(|b| format!("{b:02x}")).collect() } fn write_config(config: &Config) -> Result<(), String> { let path = Config::config_path(); let fallback = PathBuf::from("."); let dir = path.parent().unwrap_or(&fallback); std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; let content = build_toml(config); std::fs::write(&path, content).map_err(|e| e.to_string())?; println!("Config written to {}", path.display()); Ok(()) } fn build_toml(config: &Config) -> String { let mut out = String::new(); match config.provider { Provider::Imap => { // provider line only needed when non-default, but write it for clarity out.push_str("provider = \"imap\"\n\n"); if let Some(imap) = &config.imap { out.push_str("[imap]\n"); out.push_str(&format!("host = {:?}\n", imap.host)); out.push_str(&format!("port = {}\n", imap.port)); out.push_str(&format!("username = {:?}\n", imap.username)); out.push_str(&format!("use_tls = {}\n", imap.use_tls)); out.push('\n'); } if let Some(smtp) = &config.smtp { out.push_str("[smtp]\n"); out.push_str(&format!("host = {:?}\n", smtp.host)); out.push_str(&format!("port = {}\n", smtp.port)); out.push_str(&format!("username = {:?}\n", smtp.username)); let tls = match &smtp.tls_mode { TlsMode::None => "none", TlsMode::Starttls => "starttls", TlsMode::Smtps => "smtps", }; out.push_str(&format!("tls_mode = {:?}\n", tls)); out.push_str(&format!("from = {:?}\n", smtp.from)); } } Provider::Proton => { out.push_str("provider = \"proton\"\n\n"); if let Some(proton) = &config.proton { out.push_str("[proton]\n"); out.push_str(&format!("username = {:?}\n", proton.username)); } out.push('\n'); if let Some(bridge) = &config.bridge { out.push_str("[bridge]\n"); out.push_str(&format!("imap_port = {}\n", bridge.imap_port)); out.push_str(&format!("smtp_port = {}\n", bridge.smtp_port)); } } } out } // ── public API ─────────────────────────────────────────────────────────────── /// First-time setup wizard. Runs before raw mode on plain stdout. pub fn run_first_time_setup() -> Result { println!("=== tuimail first-time setup ==="); println!("Passwords will be stored in the OS keychain.\n"); let provider_str = loop { let v = prompt("Provider (imap/proton)", Some("imap")); match v.to_lowercase().as_str() { "imap" | "proton" => break v.to_lowercase(), _ => println!("Please enter 'imap' or 'proton'"), } }; let config = if provider_str == "imap" { setup_imap(None) } else { setup_proton(None) }?; write_config(&config)?; Ok(config) } /// Re-run the wizard with existing values pre-filled (--configure). pub fn run_configure(existing: &Config) -> Result<(), String> { println!("=== tuimail configure ==="); println!("Press Enter to keep the current value. Passwords: Enter to keep stored credential.\n"); let config = match existing.provider { Provider::Imap => setup_imap(Some(existing)), Provider::Proton => setup_proton(Some(existing)), }?; write_config(&config)?; Ok(()) } // ── provider-specific flows ─────────────────────────────────────────────────── fn setup_imap(existing: Option<&Config>) -> Result { let ex_imap = existing.and_then(|c| c.imap.as_ref()); let ex_smtp = existing.and_then(|c| c.smtp.as_ref()); // Load once; all existence checks and updates happen in memory. let mut store = CredentialStore::load(); println!("--- IMAP settings ---"); let imap_host = prompt("IMAP host", ex_imap.map(|i| i.host.as_str())); let imap_port_default = ex_imap.map(|i| i.port.to_string()); let imap_port: u16 = prompt("IMAP port", imap_port_default.as_deref().or(Some("993"))) .parse() .unwrap_or(993); let imap_user = prompt("IMAP username (email)", ex_imap.map(|i| i.username.as_str())); let imap_tls = prompt_bool("Use TLS for IMAP?", ex_imap.map(|i| i.use_tls).unwrap_or(true)); let imap_pass = if store.contains(credentials::IMAP_PASSWORD) { prompt_password_optional("IMAP password") } else { Some(prompt_password_required("IMAP password")) }; if let Some(ref pw) = imap_pass { store.set(credentials::IMAP_PASSWORD, pw); } println!("\n--- SMTP settings ---"); let smtp_host = prompt("SMTP host", ex_smtp.map(|s| s.host.as_str())); let smtp_port_default = ex_smtp.map(|s| s.port.to_string()); let smtp_port: u16 = prompt("SMTP port", smtp_port_default.as_deref().or(Some("465"))) .parse() .unwrap_or(465); let smtp_user = prompt("SMTP username (email)", ex_smtp.map(|s| s.username.as_str())); let smtp_tls = prompt_tls_mode(ex_smtp.map(|s| &s.tls_mode)); let smtp_from = prompt("From address (e.g. Name )", ex_smtp.map(|s| s.from.as_str())); let smtp_pass = if store.contains(credentials::SMTP_PASSWORD) { prompt_password_optional("SMTP password") } else { Some(prompt_password_required("SMTP password")) }; if let Some(ref pw) = smtp_pass { store.set(credentials::SMTP_PASSWORD, pw); } // One write for all credentials. store.save()?; Ok(Config { provider: Provider::Imap, imap: Some(ImapConfig { host: imap_host, port: imap_port, username: imap_user, password: None, // stored in keychain use_tls: imap_tls, tls_cert_der: None, }), smtp: Some(SmtpConfig { host: smtp_host, port: smtp_port, username: smtp_user, password: None, // stored in keychain tls_mode: smtp_tls, from: smtp_from, tls_cert_der: None, }), proton: None, bridge: None, }) } fn setup_proton(existing: Option<&Config>) -> Result { let ex_proton = existing.and_then(|c| c.proton.as_ref()); let ex_bridge = existing.and_then(|c| c.bridge.as_ref()); // Load once; all existence checks and updates happen in memory. let mut store = CredentialStore::load(); println!("--- ProtonMail settings ---"); let username = prompt("Proton account email", ex_proton.map(|p| p.username.as_str())); let proton_pass = if store.contains(credentials::PROTON_PASSWORD) { prompt_password_optional("Proton login password") } else { Some(prompt_password_required("Proton login password")) }; if let Some(ref pw) = proton_pass { store.set(credentials::PROTON_PASSWORD, pw); } let two_pw = prompt_bool( "Use two-password mode?", ex_proton.and_then(|p| p.mailbox_password.as_ref()).is_some(), ); if two_pw { let mbx_pass = if store.contains(credentials::PROTON_MAILBOX_PASSWORD) { prompt_password_optional("Mailbox password") } else { Some(prompt_password_required("Mailbox password")) }; if let Some(ref pw) = mbx_pass { store.set(credentials::PROTON_MAILBOX_PASSWORD, pw); } } else { store.delete(credentials::PROTON_MAILBOX_PASSWORD); } let bridge_imap_default = ex_bridge.map(|b| b.imap_port.to_string()); let imap_port: u16 = prompt( "Bridge IMAP port", bridge_imap_default.as_deref().or(Some("1143")), ) .parse() .unwrap_or(1143); let bridge_smtp_default = ex_bridge.map(|b| b.smtp_port.to_string()); let smtp_port: u16 = prompt( "Bridge SMTP port", bridge_smtp_default.as_deref().or(Some("1025")), ) .parse() .unwrap_or(1025); // Generate a new local bridge password only if one isn't already stored. if !store.contains(credentials::BRIDGE_LOCAL_PASSWORD) { store.set(credentials::BRIDGE_LOCAL_PASSWORD, &random_hex(16)); println!("Generated bridge local password (stored in keychain)."); } // One write for all credentials. store.save()?; Ok(Config { provider: Provider::Proton, imap: None, smtp: None, proton: Some(ProtonConfig { username, password: None, mailbox_password: None, user_key_passphrase: ex_proton.and_then(|p| p.user_key_passphrase.clone()), }), bridge: Some(BridgeConfig { imap_port, smtp_port, local_password: None, }), }) }