use std::{collections::HashMap, fmt, path::PathBuf, process::Command}; use anyhow::{bail, Result}; use pimalaya_toolbox::config::TomlConfig; use secrecy::SecretString; use serde::{de::Visitor, Deserialize, Deserializer}; use url::Url; #[derive(Clone, Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct Config { #[serde(alias = "name")] pub display_name: Option, pub signature: Option, pub signature_delim: Option, pub downloads_dir: Option, pub accounts: HashMap, // pub account: Option, } impl TomlConfig for Config { type Account = AccountConfig; fn project_name() -> &'static str { env!("CARGO_PKG_NAME") } fn find_default_account(&self) -> Option<(String, Self::Account)> { self.accounts .iter() .find(|(_, account)| account.default) .map(|(name, account)| (name.to_owned(), account.clone())) } fn find_account(&self, name: &str) -> Option<(String, Self::Account)> { self.accounts .get(name) .map(|account| (name.to_owned(), account.clone())) } } /// The account configuration. #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct AccountConfig { #[serde(default)] pub default: bool, pub imap: Option, #[serde(deserialize_with = "shell_expanded_string")] pub email: String, pub display_name: Option, pub signature: Option, pub signature_delim: Option, pub downloads_dir: Option, // pub backend: Option, // #[cfg(feature = "pgp")] // pub pgp: Option, // #[cfg(not(feature = "pgp"))] // #[serde(default)] // #[serde(skip_serializing, deserialize_with = "missing_pgp_feature")] // pub pgp: Option<()>, // pub folder: Option, // pub envelope: Option, // pub message: Option, // pub template: Option, } /// The account configuration. #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ImapConfig { pub url: Url, #[serde(default)] pub tls: TlsConfig, #[serde(default)] pub starttls: bool, #[serde(default)] pub sasl: SaslConfig, /// The IMAP extensions configuration. #[serde(default)] pub extensions: ImapExtensionsConfig, } /// The IMAP configuration dedicated to extensions. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ImapExtensionsConfig { #[serde(default)] id: ImapIdExtensionConfig, } /// The IMAP configuration dedicated to the ID extension. /// /// https://www.rfc-editor.org/rfc/rfc2971.html #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ImapIdExtensionConfig { /// Automatically sends the ID command straight after /// authentication. #[serde(default)] always_after_auth: bool, } #[derive(Clone, Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct TlsConfig { pub provider: Option, #[serde(default)] pub rustls: RustlsConfig, pub cert: Option, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum TlsProviderConfig { Rustls, NativeTls, } #[derive(Clone, Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct RustlsConfig { pub crypto: Option, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum RustlsCryptoConfig { Aws, Ring, } #[derive(Clone, Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslConfig { #[serde(default = "default_sasl_mechanisms")] pub mechanisms: Vec, pub login: Option, pub plain: Option, pub anonymous: Option, } fn default_sasl_mechanisms() -> Vec { vec![SaslMechanismConfig::Plain, SaslMechanismConfig::Login] } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum SaslMechanismConfig { Login, Plain, Anonymous, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum SecretConfig { Raw(SecretString), Command(Vec), } impl SecretConfig { pub fn get(&self) -> Result { match self { Self::Raw(secret) => Ok(secret.clone()), Self::Command(args) => { let Some((program, args)) = args.split_first() else { bail!("Secret command cannot be empty") }; let mut cmd = Command::new(program); cmd.args(args); let out = cmd.output()?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); bail!("Cannot read secret from command: {err}"); } let secret = String::from_utf8_lossy(&out.stdout); let secret = secret.trim_matches(['\r', '\n']); let secret = match secret.split_once('\n') { Some((secret, _)) => secret.trim_matches(['\r', '\n']), None => secret, }; Ok(SecretString::from(secret)) } } } } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslLoginConfig { pub username: String, pub password: SecretConfig, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslPlainConfig { pub authzid: Option, pub authcid: String, pub passwd: SecretConfig, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslAnonymousConfig { pub message: Option, } struct ShellExpandedStringVisitor; impl<'de> Visitor<'de> for ShellExpandedStringVisitor { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("an string containing environment variable(s)") } fn visit_string(self, v: String) -> Result { match shellexpand::full(&v) { Ok(v) => Ok(v.to_string()), Err(_) => Ok(v), } } } pub fn shell_expanded_string<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result { deserializer.deserialize_string(ShellExpandedStringVisitor) }