207 lines
7 KiB
Rust
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(),
|
|
},
|
|
})
|
|
}
|
|
}
|