use crate::types::{EmailConfig, SmtpConfig, ImapConfig};
use crate::error::{ConfigError, Result};
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub entity: EntityConfig,
pub email: EmailConfig,
pub router: RouterConfig,
pub security: SecurityConfig,
pub logging: LoggingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityConfig {
pub local_name: String,
pub entity_type: String,
pub domain: String,
pub capabilities: Vec<String>,
pub display_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouterConfig {
pub max_connections: usize,
pub queue_size: usize,
pub connection_timeout: u64,
pub max_retries: u32,
pub enable_realtime: bool,
pub idle_timeout: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub private_key_path: Option<String>,
pub public_key_path: Option<String>,
pub auto_generate_keys: bool,
pub default_security_level: String,
pub trusted_domains: Vec<String>,
pub require_encryption_for: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub file: Option<String>,
pub log_message_content: bool,
}
impl Config {
#[cfg(not(target_arch = "wasm32"))]
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| ConfigError::FileNotFound(e.to_string()))?;
toml::from_str(&content)
.map_err(|e| ConfigError::InvalidFormat(e.to_string()).into())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self)
.map_err(|e| ConfigError::InvalidFormat(e.to_string()))?;
std::fs::write(path.as_ref(), content)
.map_err(|e| ConfigError::FileNotFound(e.to_string()).into())
}
pub fn default_for_entity(local_name: impl Into<String>, entity_type: impl Into<String>) -> Self {
let local_name = local_name.into();
let entity_type = entity_type.into();
Self {
entity: EntityConfig {
local_name: local_name.clone(),
entity_type: entity_type.clone(),
domain: "synapse.local".to_string(),
capabilities: Self::default_capabilities_for_type(&entity_type),
display_name: Some(format!("{} ({})", local_name, entity_type)),
},
email: EmailConfig {
smtp: SmtpConfig {
host: "localhost".to_string(),
port: 587,
username: format!("{}@synapse.local", local_name.to_lowercase()),
password: "changeme".to_string(),
use_tls: true,
use_ssl: false,
},
imap: ImapConfig {
host: "localhost".to_string(),
port: 993,
username: format!("{}@synapse.local", local_name.to_lowercase()),
password: "changeme".to_string(),
use_ssl: true,
},
},
router: RouterConfig {
max_connections: 100,
queue_size: 1000,
connection_timeout: 30,
max_retries: 3,
enable_realtime: true,
idle_timeout: 300,
},
security: SecurityConfig {
private_key_path: Some(format!("{}_private.pem", local_name.to_lowercase())),
public_key_path: Some(format!("{}_public.pem", local_name.to_lowercase())),
auto_generate_keys: true,
default_security_level: "private".to_string(),
trusted_domains: vec!["synapse.local".to_string()],
require_encryption_for: vec!["human".to_string()],
},
logging: LoggingConfig {
level: "info".to_string(),
format: "compact".to_string(),
file: None,
log_message_content: false,
},
}
}
fn default_capabilities_for_type(entity_type: &str) -> Vec<String> {
match entity_type {
"ai_model" => vec![
"conversation".to_string(),
"analysis".to_string(),
"reasoning".to_string(),
],
"human" => vec![
"conversation".to_string(),
"decision_making".to_string(),
"creativity".to_string(),
],
"tool" => vec![
"task_execution".to_string(),
"data_processing".to_string(),
],
"service" => vec![
"api_access".to_string(),
"data_storage".to_string(),
],
"router" => vec![
"message_routing".to_string(),
"identity_resolution".to_string(),
],
_ => vec!["communication".to_string()],
}
}
pub fn validate(&self) -> Result<()> {
if self.entity.local_name.trim().is_empty() {
return Err(ConfigError::ValidationFailed("Local name cannot be empty".to_string()).into());
}
if self.entity.domain.trim().is_empty() {
return Err(ConfigError::ValidationFailed("Domain cannot be empty".to_string()).into());
}
if self.email.smtp.host.trim().is_empty() {
return Err(ConfigError::ValidationFailed("SMTP host cannot be empty".to_string()).into());
}
if self.email.imap.host.trim().is_empty() {
return Err(ConfigError::ValidationFailed("IMAP host cannot be empty".to_string()).into());
}
if self.email.smtp.port == 0 {
return Err(ConfigError::ValidationFailed("Invalid SMTP port".to_string()).into());
}
if self.email.imap.port == 0 {
return Err(ConfigError::ValidationFailed("Invalid IMAP port".to_string()).into());
}
if !["public", "private", "authenticated", "secure"].contains(&self.security.default_security_level.as_str()) {
return Err(ConfigError::ValidationFailed("Invalid default security level".to_string()).into());
}
if !["trace", "debug", "info", "warn", "error"].contains(&self.logging.level.as_str()) {
return Err(ConfigError::ValidationFailed("Invalid log level".to_string()).into());
}
if !["compact", "pretty", "json"].contains(&self.logging.format.as_str()) {
return Err(ConfigError::ValidationFailed("Invalid log format".to_string()).into());
}
Ok(())
}
pub fn gmail_config(local_name: impl Into<String>, entity_type: impl Into<String>, email: impl Into<String>, password: impl Into<String>) -> Self {
let local_name = local_name.into();
let entity_type = entity_type.into();
let email = email.into();
let password = password.into();
let mut config = Self::default_for_entity(&local_name, &entity_type);
config.email.smtp = SmtpConfig {
host: "smtp.gmail.com".to_string(),
port: 587,
username: email.clone(),
password: password.clone(),
use_tls: true,
use_ssl: false,
};
config.email.imap = ImapConfig {
host: "imap.gmail.com".to_string(),
port: 993,
username: email,
password,
use_ssl: true,
};
config
}
pub fn outlook_config(local_name: impl Into<String>, entity_type: impl Into<String>, email: impl Into<String>, password: impl Into<String>) -> Self {
let local_name = local_name.into();
let entity_type = entity_type.into();
let email = email.into();
let password = password.into();
let mut config = Self::default_for_entity(&local_name, &entity_type);
config.email.smtp = SmtpConfig {
host: "smtp-mail.outlook.com".to_string(),
port: 587,
username: email.clone(),
password: password.clone(),
use_tls: true,
use_ssl: false,
};
config.email.imap = ImapConfig {
host: "outlook.office365.com".to_string(),
port: 993,
username: email,
password,
use_ssl: true,
};
config
}
pub fn for_testing() -> Self {
Self::default_for_entity("test_entity", "test")
}
}
impl Default for Config {
fn default() -> Self {
Self::default_for_entity("default", "ai_model")
}
}
pub struct ConfigTemplates;
impl ConfigTemplates {
pub fn available_providers() -> Vec<&'static str> {
vec!["gmail", "outlook", "yahoo", "custom"]
}
pub fn for_provider(
provider: &str,
local_name: impl Into<String>,
entity_type: impl Into<String>,
email: impl Into<String>,
password: impl Into<String>,
) -> Result<Config> {
match provider.to_lowercase().as_str() {
"gmail" => Ok(Config::gmail_config(local_name, entity_type, email, password)),
"outlook" => Ok(Config::outlook_config(local_name, entity_type, email, password)),
"yahoo" => {
let mut config = Config::outlook_config(local_name, entity_type, email, password);
config.email.smtp.host = "smtp.mail.yahoo.com".to_string();
config.email.imap.host = "imap.mail.yahoo.com".to_string();
Ok(config)
}
"custom" => Ok(Config::default_for_entity(local_name, entity_type)),
_ => Err(ConfigError::ValidationFailed(format!("Unknown provider: {}", provider)).into()),
}
}
pub fn provider_help(provider: &str) -> Option<&'static str> {
match provider.to_lowercase().as_str() {
"gmail" => Some(
"Gmail Configuration:\n\
- Enable 2-factor authentication\n\
- Generate an App Password\n\
- Use App Password instead of regular password\n\
- Enable IMAP in Gmail settings"
),
"outlook" => Some(
"Outlook Configuration:\n\
- Works with Microsoft 365 accounts\n\
- Use your regular email and password\n\
- May require app-specific password for some accounts"
),
"yahoo" => Some(
"Yahoo Configuration:\n\
- Generate an App Password in Yahoo Account Security\n\
- Use App Password instead of regular password\n\
- Enable IMAP access in Yahoo Mail settings"
),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_default_config_creation() {
let config = Config::default_for_entity("Claude", "ai_model");
assert_eq!(config.entity.local_name, "Claude");
assert_eq!(config.entity.entity_type, "ai_model");
assert!(config.validate().is_ok());
}
#[test]
fn test_gmail_config() {
let config = Config::gmail_config("Eric", "human", "eric@gmail.com", "app_password");
assert_eq!(config.email.smtp.host, "smtp.gmail.com");
assert_eq!(config.email.imap.host, "imap.gmail.com");
assert!(config.validate().is_ok());
}
#[test]
fn test_config_file_operations() {
let config = Config::default_for_entity("Test", "tool");
let temp_file = NamedTempFile::new().unwrap();
assert!(config.to_file(temp_file.path()).is_ok());
let loaded_config = Config::from_file(temp_file.path()).unwrap();
assert_eq!(config.entity.local_name, loaded_config.entity.local_name);
}
#[test]
fn test_config_validation() {
let mut config = Config::default_for_entity("Test", "ai_model");
assert!(config.validate().is_ok());
config.entity.local_name = "".to_string();
assert!(config.validate().is_err());
config.entity.local_name = "Test".to_string();
assert!(config.validate().is_ok());
let mut invalid_config = config.clone();
invalid_config.email.smtp.host = "".to_string(); assert!(invalid_config.validate().is_err());
}
#[test]
fn test_provider_templates() {
let providers = ConfigTemplates::available_providers();
assert!(providers.contains(&"gmail"));
assert!(providers.contains(&"outlook"));
let gmail_config = ConfigTemplates::for_provider("gmail", "Test", "ai_model", "test@gmail.com", "password");
assert!(gmail_config.is_ok());
let unknown_config = ConfigTemplates::for_provider("unknown", "Test", "ai_model", "test@example.com", "password");
assert!(unknown_config.is_err());
}
}