tuimail/src/config.rs
2026-03-02 14:58:44 +01:00

207 lines
7 KiB
Rust

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<ImapConfig>,
pub smtp: Option<SmtpConfig>,
// ProtonMail provider
pub proton: Option<ProtonConfig>,
pub bridge: Option<BridgeConfig>,
}
/// 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<String>,
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<String>,
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<String>,
pub mailbox_password: Option<String>,
pub user_key_passphrase: Option<String>,
}
/// 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<String>,
}
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<Self, Box<dyn std::error::Error>> {
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<ImapConfig, String> {
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<SmtpConfig, String> {
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<proton_bridge::config::Config, String> {
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(),
},
})
}
}