Initial commit: Complete CalDAV calendar synchronizer

- Rust-based CLI tool for Zoho to Nextcloud calendar sync
- Selective calendar import from Zoho to single Nextcloud calendar
- Timezone-aware event handling for next-week synchronization
- Comprehensive configuration system with TOML support
- CLI interface with debug, list, and sync operations
- Complete documentation and example configurations
This commit is contained in:
Alvaro Soliverez 2025-10-04 11:57:44 -03:00
commit 8362ebe44b
16 changed files with 6192 additions and 0 deletions

204
src/config.rs Normal file
View file

@ -0,0 +1,204 @@
//! Configuration management for CalDAV synchronizer
use serde::{Deserialize, Serialize};
use std::path::Path;
use anyhow::Result;
/// Main configuration structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Server configuration
pub server: ServerConfig,
/// Calendar configuration
pub calendar: CalendarConfig,
/// Filter configuration
pub filters: Option<FilterConfig>,
/// Sync configuration
pub sync: SyncConfig,
}
/// Server connection configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
/// CalDAV server URL
pub url: String,
/// Username for authentication
pub username: String,
/// Password for authentication
pub password: String,
/// Whether to use HTTPS
pub use_https: bool,
/// Timeout in seconds
pub timeout: u64,
/// Custom headers to send with requests
pub headers: Option<std::collections::HashMap<String, String>>,
}
/// Calendar-specific configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarConfig {
/// Calendar name/path
pub name: String,
/// Calendar display name
pub display_name: Option<String>,
/// Calendar color
pub color: Option<String>,
/// Calendar timezone
pub timezone: String,
/// Whether to sync this calendar
pub enabled: bool,
}
/// Filter configuration for events
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterConfig {
/// Start date filter (ISO 8601)
pub start_date: Option<String>,
/// End date filter (ISO 8601)
pub end_date: Option<String>,
/// Event types to include
pub event_types: Option<Vec<String>>,
/// Keywords to filter by
pub keywords: Option<Vec<String>>,
/// Exclude keywords
pub exclude_keywords: Option<Vec<String>>,
}
/// Synchronization configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConfig {
/// Sync interval in seconds
pub interval: u64,
/// Whether to sync on startup
pub sync_on_startup: bool,
/// Maximum number of retries
pub max_retries: u32,
/// Retry delay in seconds
pub retry_delay: u64,
/// Whether to delete events not found on server
pub delete_missing: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
server: ServerConfig::default(),
calendar: CalendarConfig::default(),
filters: None,
sync: SyncConfig::default(),
}
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
url: "https://caldav.example.com".to_string(),
username: String::new(),
password: String::new(),
use_https: true,
timeout: 30,
headers: None,
}
}
}
impl Default for CalendarConfig {
fn default() -> Self {
Self {
name: "calendar".to_string(),
display_name: None,
color: None,
timezone: "UTC".to_string(),
enabled: true,
}
}
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
interval: 300, // 5 minutes
sync_on_startup: true,
max_retries: 3,
retry_delay: 5,
delete_missing: false,
}
}
}
impl Config {
/// Load configuration from a TOML file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
/// Save configuration to a TOML file
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
/// Load configuration from environment variables
pub fn from_env() -> Result<Self> {
let mut config = Config::default();
if let Ok(url) = std::env::var("CALDAV_URL") {
config.server.url = url;
}
if let Ok(username) = std::env::var("CALDAV_USERNAME") {
config.server.username = username;
}
if let Ok(password) = std::env::var("CALDAV_PASSWORD") {
config.server.password = password;
}
if let Ok(calendar) = std::env::var("CALDAV_CALENDAR") {
config.calendar.name = calendar;
}
Ok(config)
}
/// Validate configuration
pub fn validate(&self) -> Result<()> {
if self.server.url.is_empty() {
anyhow::bail!("Server URL cannot be empty");
}
if self.server.username.is_empty() {
anyhow::bail!("Username cannot be empty");
}
if self.server.password.is_empty() {
anyhow::bail!("Password cannot be empty");
}
if self.calendar.name.is_empty() {
anyhow::bail!("Calendar name cannot be empty");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.server.url, "https://caldav.example.com");
assert_eq!(config.calendar.name, "calendar");
assert_eq!(config.sync.interval, 300);
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
assert!(config.validate().is_err()); // Empty username/password
config.server.username = "test".to_string();
config.server.password = "test".to_string();
assert!(config.validate().is_ok());
}
}