use serde::Deserialize; use std::fs; use std::path::PathBuf; #[derive(Debug, Deserialize, Clone, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Provider { #[default] Imap, Proton, } #[derive(Debug, Deserialize, Clone)] pub struct Config { #[serde(default)] pub provider: Provider, // Standard IMAP/SMTP providers — absent when provider = "proton" pub imap: Option, pub smtp: Option, // ProtonMail provider pub proton: Option, pub bridge: Option, } /// How the SMTP connection should be secured. /// /// - `none` — plain text (port 25 or 587 without TLS) /// - `starttls` — upgrades to TLS mid-session (port 587, most providers) /// - `smtps` — TLS from the first byte (port 465, Gmail "SSL") #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum TlsMode { None, Starttls, Smtps, } #[derive(Debug, Deserialize, Clone)] pub struct SmtpConfig { pub host: String, pub port: u16, pub username: String, #[serde(default)] pub password: Option, pub tls_mode: TlsMode, pub from: String, } #[derive(Debug, Deserialize, Clone)] pub struct ImapConfig { pub host: String, pub port: u16, pub username: String, #[serde(default)] pub password: Option, pub use_tls: bool, } /// Mirror of `proton_bridge::config::ProtonConfig` — used for config parsing /// without requiring the `proton` feature to be active. #[derive(Debug, Deserialize, Clone)] pub struct ProtonConfig { pub username: String, #[serde(default)] pub password: Option, pub mailbox_password: Option, pub user_key_passphrase: Option, } /// Mirror of `proton_bridge::config::BridgeConfig`. #[derive(Debug, Deserialize, Clone)] pub struct BridgeConfig { pub imap_port: u16, pub smtp_port: u16, #[serde(default)] pub local_password: Option, } impl Config { /// Return the path to the config file: `{config_dir}/tuimail/config.toml`. pub fn config_path() -> PathBuf { dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("tuimail") .join("config.toml") } pub fn load() -> Result> { let path = Self::config_path(); let content = fs::read_to_string(&path) .map_err(|e| format!("Cannot read {}: {e}", path.display()))?; let config: Config = toml::from_str(&content)?; Ok(config) } /// Fill in password fields from the OS keychain (with env-var fallback). /// Must be called after `load()` and before any provider operations. pub fn inject_credentials(&mut self) -> Result<(), String> { use crate::credentials; match self.provider { Provider::Imap => { if let Some(ref mut imap) = self.imap && imap.password.is_none() { imap.password = Some(credentials::get(credentials::IMAP_PASSWORD)?); } if let Some(ref mut smtp) = self.smtp && smtp.password.is_none(){ smtp.password = Some(credentials::get(credentials::SMTP_PASSWORD)?); } } Provider::Proton => { if let Some(ref mut proton) = self.proton { if proton.password.is_none() { proton.password = Some(credentials::get(credentials::PROTON_PASSWORD)?); } if proton.mailbox_password.is_none() { proton.mailbox_password = credentials::get(credentials::PROTON_MAILBOX_PASSWORD).ok(); } } if let Some(ref mut bridge) = self.bridge && bridge.local_password.is_none() { bridge.local_password = Some(credentials::get(credentials::BRIDGE_LOCAL_PASSWORD)?); } } } Ok(()) } /// Returns the effective IMAP config regardless of provider. /// For `provider = "imap"` this is the `[imap]` section. /// For `provider = "proton"` this is derived from `[bridge]`. pub fn effective_imap(&self) -> Result { match self.provider { Provider::Imap => self .imap .clone() .ok_or_else(|| "[imap] section missing from config".to_string()), Provider::Proton => { let b = self .bridge .as_ref() .ok_or_else(|| "[bridge] section missing from config".to_string())?; Ok(ImapConfig { host: "127.0.0.1".into(), port: b.imap_port, username: "bridge".into(), password: b.local_password.clone(), use_tls: false, }) } } } /// Returns the effective SMTP config regardless of provider. pub fn effective_smtp(&self) -> Result { match self.provider { Provider::Imap => self .smtp .clone() .ok_or_else(|| "[smtp] section missing from config".to_string()), Provider::Proton => { let b = self .bridge .as_ref() .ok_or_else(|| "[bridge] section missing from config".to_string())?; let from = self .proton .as_ref() .map(|p| p.username.clone()) .unwrap_or_default(); Ok(SmtpConfig { host: "127.0.0.1".into(), port: b.smtp_port, username: "bridge".into(), password: b.local_password.clone(), tls_mode: TlsMode::None, from, }) } } } /// Convert to the `proton-bridge` crate's `Config` type. /// Only valid when `provider = "proton"`. pub fn as_bridge_config(&self) -> Result { let p = self.proton.as_ref().ok_or("[proton] section missing")?; let b = self.bridge.as_ref().ok_or("[bridge] section missing")?; Ok(proton_bridge::config::Config { proton: proton_bridge::config::ProtonConfig { username: p.username.clone(), password: p.password.clone().unwrap_or_default(), mailbox_password: p.mailbox_password.clone(), user_key_passphrase: p.user_key_passphrase.clone(), }, bridge: proton_bridge::config::BridgeConfig { imap_port: b.imap_port, smtp_port: b.smtp_port, local_password: b.local_password.clone().unwrap_or_default(), }, }) } }