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. ```
334 lines
9.5 KiB
Rust
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);
|
|
}
|
|
}
|