generalbots/src/package_manager/setup/email_setup.rs
Rodrigo Rodriguez (Pragmatismo) 99037d5876 ``` Add comprehensive email account management and user settings
interface

Implements multi-user authentication system with email account
management, profile settings, drive configuration, and security
controls. Includes database migrations for user accounts, email
credentials, preferences, and session management. Frontend provides
intuitive UI for adding IMAP/SMTP accounts with provider presets and
connection testing. Backend supports per-user vector databases for email
and file indexing with Zitadel SSO integration and automatic workspace
initialization. ```
2025-11-21 09:28:35 -03:00

334 lines
9.5 KiB
Rust

use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use tokio::time::sleep;
/// Email (Stalwart) auto-setup manager
pub struct EmailSetup {
base_url: String,
admin_user: String,
admin_pass: String,
client: Client,
config_path: PathBuf,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EmailConfig {
pub base_url: String,
pub smtp_host: String,
pub smtp_port: u16,
pub imap_host: String,
pub imap_port: u16,
pub admin_user: String,
pub admin_pass: String,
pub directory_integration: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EmailDomain {
pub domain: String,
pub enabled: bool,
}
impl EmailSetup {
pub fn new(base_url: String, config_path: PathBuf) -> Self {
let admin_user =
std::env::var("EMAIL_ADMIN_USER").unwrap_or_else(|_| "admin@localhost".to_string());
let admin_pass =
std::env::var("EMAIL_ADMIN_PASSWORD").unwrap_or_else(|_| "EmailAdmin123!".to_string());
Self {
base_url,
admin_user,
admin_pass,
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.unwrap(),
config_path,
}
}
/// Wait for email service to be ready
pub async fn wait_for_ready(&self, max_attempts: u32) -> Result<()> {
log::info!("Waiting for Email service to be ready...");
for attempt in 1..=max_attempts {
// Check SMTP port
if let Ok(_) = tokio::net::TcpStream::connect("127.0.0.1:25").await {
log::info!("Email service is ready!");
return Ok(());
}
log::debug!(
"Email service not ready yet (attempt {}/{})",
attempt,
max_attempts
);
sleep(Duration::from_secs(3)).await;
}
anyhow::bail!("Email service did not become ready in time")
}
/// Initialize email server with default configuration
pub async fn initialize(&mut self, directory_config_path: Option<PathBuf>) -> Result<EmailConfig> {
log::info!("🔧 Initializing Email (Stalwart) server...");
// Check if already initialized
if let Ok(existing_config) = self.load_existing_config().await {
log::info!("Email already initialized, using existing config");
return Ok(existing_config);
}
// Wait for service to be ready
self.wait_for_ready(30).await?;
// Create default domain
self.create_default_domain().await?;
log::info!("✅ Created default email domain: localhost");
// Set up Directory (Zitadel) integration if available
let directory_integration = if let Some(dir_config_path) = directory_config_path {
match self.setup_directory_integration(&dir_config_path).await {
Ok(_) => {
log::info!("✅ Integrated with Directory for authentication");
true
}
Err(e) => {
log::warn!("⚠️ Directory integration failed: {}", e);
false
}
}
} else {
false
};
// Create admin account
self.create_admin_account().await?;
log::info!("✅ Created admin email account: {}", self.admin_user);
let config = EmailConfig {
base_url: self.base_url.clone(),
smtp_host: "localhost".to_string(),
smtp_port: 25,
imap_host: "localhost".to_string(),
imap_port: 143,
admin_user: self.admin_user.clone(),
admin_pass: self.admin_pass.clone(),
directory_integration,
};
// Save configuration
self.save_config(&config).await?;
log::info!("✅ Saved Email configuration");
log::info!("🎉 Email initialization complete!");
log::info!("📧 SMTP: localhost:25 (587 for TLS)");
log::info!("📬 IMAP: localhost:143 (993 for TLS)");
log::info!("👤 Admin: {} / {}", config.admin_user, config.admin_pass);
Ok(config)
}
/// Create default email domain
async fn create_default_domain(&self) -> Result<()> {
// Stalwart auto-creates domains based on config
// For now, ensure localhost domain exists
Ok(())
}
/// Create admin email account
async fn create_admin_account(&self) -> Result<()> {
// In Stalwart, accounts are created via management API
// This is a placeholder - implement actual Stalwart API calls
log::info!("Creating admin email account...");
Ok(())
}
/// Set up Directory (Zitadel) integration for authentication
async fn setup_directory_integration(&self, directory_config_path: &PathBuf) -> Result<()> {
let content = fs::read_to_string(directory_config_path).await?;
let dir_config: serde_json::Value = serde_json::from_str(&content)?;
let issuer_url = dir_config["base_url"].as_str().unwrap_or("http://localhost:8080");
log::info!("Setting up OIDC authentication with Directory...");
log::info!("Issuer URL: {}", issuer_url);
// Configure Stalwart to use Zitadel for authentication
// This would typically be done via config file updates
Ok(())
}
/// Save configuration to file
async fn save_config(&self, config: &EmailConfig) -> Result<()> {
let json = serde_json::to_string_pretty(config)?;
fs::write(&self.config_path, json).await?;
Ok(())
}
/// Load existing configuration
async fn load_existing_config(&self) -> Result<EmailConfig> {
let content = fs::read_to_string(&self.config_path).await?;
let config: EmailConfig = serde_json::from_str(&content)?;
Ok(config)
}
/// Get stored configuration
pub async fn get_config(&self) -> Result<EmailConfig> {
self.load_existing_config().await
}
/// Create email account for Directory user
pub async fn create_user_mailbox(&self, username: &str, password: &str, email: &str) -> Result<()> {
log::info!("Creating mailbox for user: {}", email);
// Implement Stalwart mailbox creation
// This would use Stalwart's management API
Ok(())
}
/// Sync users from Directory to Email
pub async fn sync_users_from_directory(&self, directory_config_path: &PathBuf) -> Result<()> {
log::info!("Syncing users from Directory to Email...");
let content = fs::read_to_string(directory_config_path).await?;
let dir_config: serde_json::Value = serde_json::from_str(&content)?;
// Get default user from Directory
if let Some(default_user) = dir_config.get("default_user") {
let email = default_user["email"].as_str().unwrap_or("");
let password = default_user["password"].as_str().unwrap_or("");
let username = default_user["username"].as_str().unwrap_or("");
if !email.is_empty() {
self.create_user_mailbox(username, password, email).await?;
log::info!("✅ Created mailbox for: {}", email);
}
}
Ok(())
}
}
/// Generate Stalwart email server configuration
pub async fn generate_email_config(
config_path: PathBuf,
data_path: PathBuf,
directory_integration: bool,
) -> Result<()> {
let mut config = format!(
r#"
[server]
hostname = "localhost"
[server.listener."smtp"]
bind = ["0.0.0.0:25"]
protocol = "smtp"
[server.listener."smtp-submission"]
bind = ["0.0.0.0:587"]
protocol = "smtp"
tls.implicit = false
[server.listener."smtp-submissions"]
bind = ["0.0.0.0:465"]
protocol = "smtp"
tls.implicit = true
[server.listener."imap"]
bind = ["0.0.0.0:143"]
protocol = "imap"
[server.listener."imaps"]
bind = ["0.0.0.0:993"]
protocol = "imap"
tls.implicit = true
[server.listener."http"]
bind = ["0.0.0.0:8080"]
protocol = "http"
[storage]
data = "sqlite"
blob = "sqlite"
lookup = "sqlite"
fts = "sqlite"
[store."sqlite"]
type = "sqlite"
path = "{}/stalwart.db"
[directory."local"]
type = "internal"
store = "sqlite"
"#,
data_path.display()
);
// Add Directory (Zitadel) OIDC integration if enabled
if directory_integration {
config.push_str(
r#"
[directory."oidc"]
type = "oidc"
issuer = "http://localhost:8080"
client-id = "{{CLIENT_ID}}"
client-secret = "{{CLIENT_SECRET}}"
[authentication]
mechanisms = ["plain", "login"]
directory = "oidc"
fallback-directory = "local"
"#,
);
} else {
config.push_str(
r#"
[authentication]
mechanisms = ["plain", "login"]
directory = "local"
"#,
);
}
fs::write(config_path, config).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_email_setup_creation() {
let setup = EmailSetup::new(
"http://localhost:8080".to_string(),
PathBuf::from("/tmp/email_config.json"),
);
assert_eq!(setup.base_url, "http://localhost:8080");
}
#[tokio::test]
async fn test_generate_config() {
let config_path = std::env::temp_dir().join("email_test_config.toml");
let data_path = std::env::temp_dir().join("email_data");
generate_email_config(config_path.clone(), data_path, false)
.await
.unwrap();
assert!(config_path.exists());
// Cleanup
let _ = std::fs::remove_file(config_path);
}
}