use hashbrown::HashMap;
use secret::Secret;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub default_account: Option<String>,
#[serde(default)]
pub accounts: HashMap<String, AccountConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AccountConfig {
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
pub username: String,
#[serde(default, deserialize_with = "deserialize_password_opt")]
pub password: Option<Secret>,
#[serde(default = "default_tls")]
pub tls: bool,
pub trash_mailbox: Option<String>,
pub drafts_mailbox: Option<String>,
#[serde(default)]
pub max_connections: Option<usize>,
}
impl AccountConfig {
pub fn new(host: impl Into<String>, username: impl Into<String>) -> Self {
let username = username.into();
let password = secret::keyring::KeyringEntry::try_new(&username)
.ok()
.map(Secret::new_keyring_entry);
Self {
host: host.into(),
port: 993,
username,
password,
tls: true,
trash_mailbox: None,
drafts_mailbox: None,
max_connections: None,
}
}
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn with_trash(mut self, mailbox: impl Into<String>) -> Self {
self.trash_mailbox = Some(mailbox.into());
self
}
pub fn with_drafts(mut self, mailbox: impl Into<String>) -> Self {
self.drafts_mailbox = Some(mailbox.into());
self
}
pub fn with_max_connections(mut self, n: usize) -> Self {
self.max_connections = Some(n);
self
}
}
fn default_port() -> u16 {
993
}
fn default_tls() -> bool {
true
}
fn deserialize_password_opt<'de, D>(deserializer: D) -> Result<Option<Secret>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum PasswordField {
Secret(Secret),
Plain(String),
}
let opt = Option::<PasswordField>::deserialize(deserializer)?;
Ok(opt.map(|pf| match pf {
PasswordField::Secret(s) => s,
PasswordField::Plain(s) => Secret::new_raw(s),
}))
}
impl Config {
pub fn load() -> crate::Result<Self> {
let path = Self::default_path();
Self::load_from(&path)
}
pub fn load_from(path: &std::path::Path) -> crate::Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| {
crate::AgentmailError::Config(format!(
"Failed to read config file '{}': {}. \
Create it with your IMAP account settings. See README for format.",
path.display(),
e
))
})?;
let config: Config = toml::from_str(&content).map_err(|e| {
crate::AgentmailError::Config(format!(
"Failed to parse config file '{}': {}",
path.display(),
e
))
})?;
if config.accounts.is_empty() {
return Err(crate::AgentmailError::Config(
"No accounts configured. Add at least one [accounts.<name>] section.".into(),
));
}
Ok(config)
}
pub fn default_account(&self) -> Option<&str> {
if let Some(ref name) = self.default_account
&& self.accounts.contains_key(name)
{
return Some(name);
}
if self.accounts.len() == 1 {
return self.accounts.keys().next().map(|s| s.as_str());
}
None
}
pub fn from_accounts(accounts: Vec<(String, AccountConfig)>) -> Self {
Self {
default_account: None,
accounts: accounts.into_iter().collect(),
}
}
pub fn empty() -> Self {
Self {
default_account: None,
accounts: HashMap::new(),
}
}
pub fn default_path() -> PathBuf {
if let Ok(p) = std::env::var("AGENTMAIL_CONFIG") {
return PathBuf::from(p);
}
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("agentmail")
.join("config.toml")
}
}