# CalDAV Implementation - Developer Guide
This document provides detailed technical information about Tududi's CalDAV implementation for developers working on the codebase.
---
## Architecture Overview
### Protocol Stack
```
┌─────────────────────────────────────┐
│ CalDAV Clients (tasks.org, etc) │
└─────────────────┬───────────────────┘
│ HTTP/HTTPS
┌─────────────────┴───────────────────┐
│ WebDAV/CalDAV Protocol Layer │
│ (PROPFIND, REPORT, GET, PUT, etc) │
└─────────────────┬───────────────────┘
│
┌─────────────────┴───────────────────┐
│ iCalendar Transformation Layer │
│ (Task ↔ VTODO serialization) │
└─────────────────┬───────────────────┘
│
┌─────────────────┴───────────────────┐
│ Synchronization Engine │
│ (Pull → Merge → Push phases) │
└─────────────────┬───────────────────┘
│
┌─────────────────┴───────────────────┐
│ Repository & Database Layer │
│ (SQLite with CalDAV tables) │
└─────────────────────────────────────┘
```
### Module Structure
```
backend/modules/caldav/
├── index.js # Module exports
├── routes.js # WebDAV/CalDAV HTTP handlers
├── webdav/ # WebDAV protocol implementation
│ ├── propfind.js # PROPFIND method handler
│ ├── report.js # REPORT method handler (calendar-query)
│ ├── options.js # OPTIONS method handler
│ ├── task-handlers.js # GET/PUT/DELETE task operations
│ └── utils.js # WebDAV XML utilities
├── protocol/
│ ├── discovery.js # .well-known handler
│ ├── capabilities.js # CalDAV capabilities
│ └── sync-collection.js # RFC 6578 sync-token (future)
├── icalendar/ # iCalendar transformation
│ ├── vtodo-serializer.js # Task → VTODO
│ ├── vtodo-parser.js # VTODO → Task
│ ├── rrule-generator.js # Recurrence → RRULE
│ ├── rrule-parser.js # RRULE → Recurrence
│ └── field-mappings.js # Status, priority mappings
├── sync/ # Synchronization engine
│ ├── sync-engine.js # Main orchestrator
│ ├── pull-phase.js # Fetch from remote
│ ├── merge-phase.js # Conflict detection
│ ├── push-phase.js # Send to remote
│ └── conflict-resolver.js # Resolution strategies
├── repositories/ # Data access layer
│ ├── calendar-repository.js
│ ├── sync-state-repository.js
│ ├── override-repository.js
│ └── remote-calendar-repository.js
├── services/
│ ├── calendar-service.js
│ ├── sync-scheduler.js # Background sync (node-cron)
│ └── encryption-service.js # AES-256-GCM password encryption
├── middleware/
│ ├── caldav-auth.js # HTTP Basic Auth
│ └── xml-parser.js # Parse XML bodies
├── api/ # REST API endpoints
│ ├── routes.js # API route definitions
│ ├── calendar-controller.js # Calendar CRUD
│ ├── remote-calendar-controller.js
│ └── sync-controller.js # Manual sync, status
└── utils/
├── etag-generator.js
├── ctag-generator.js
└── validation.js
```
---
## Database Schema
### Tables
#### caldav_calendars
Local calendar configurations (per-user calendars served by Tududi).
```sql
CREATE TABLE caldav_calendars (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid STRING NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Calendar identity
name STRING NOT NULL,
description TEXT,
color STRING,
-- CalDAV metadata
ctag STRING, -- Collection tag (change detection)
sync_token STRING, -- RFC 6578 sync-token
-- Sync configuration
enabled BOOLEAN DEFAULT 1,
sync_direction STRING DEFAULT 'bidirectional',
sync_interval_minutes INTEGER DEFAULT 15,
last_sync_at DATETIME,
last_sync_status STRING,
conflict_resolution STRING DEFAULT 'last_write_wins',
created_at DATETIME,
updated_at DATETIME
);
```
**Indexes:**
- `idx_caldav_calendars_user_id` on `user_id`
- `idx_caldav_calendars_enabled` on `enabled`
- `idx_caldav_calendars_user_enabled` on `(user_id, enabled)`
#### caldav_sync_state
Per-task sync tracking metadata.
```sql
CREATE TABLE caldav_sync_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
calendar_id INTEGER NOT NULL REFERENCES caldav_calendars(id) ON DELETE CASCADE,
-- CalDAV metadata
etag STRING NOT NULL,
last_modified DATETIME NOT NULL,
-- Sync tracking
last_synced_at DATETIME,
sync_status STRING DEFAULT 'synced',
-- Conflict data
conflict_local_version JSON,
conflict_remote_version JSON,
conflict_detected_at DATETIME,
created_at DATETIME,
updated_at DATETIME,
UNIQUE(task_id, calendar_id)
);
```
**Indexes:**
- `idx_caldav_sync_state_task_id` on `task_id`
- `idx_caldav_sync_state_calendar_id` on `calendar_id`
- `idx_caldav_sync_state_status` on `sync_status`
- `idx_caldav_sync_state_modified` on `last_modified`
#### caldav_occurrence_overrides
Stores edited instances of recurring tasks.
```sql
CREATE TABLE caldav_occurrence_overrides (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
calendar_id INTEGER NOT NULL REFERENCES caldav_calendars(id) ON DELETE CASCADE,
recurrence_id DATETIME NOT NULL, -- Which instance (original due date)
-- Overridden fields (NULL = not overridden)
override_name TEXT,
override_due_date DATETIME,
override_status INTEGER,
override_priority INTEGER,
override_note TEXT,
created_at DATETIME,
updated_at DATETIME,
UNIQUE(parent_task_id, calendar_id, recurrence_id)
);
```
**Indexes:**
- `idx_caldav_overrides_parent` on `parent_task_id`
- `idx_caldav_overrides_calendar` on `calendar_id`
- `idx_caldav_overrides_recurrence` on `recurrence_id`
#### caldav_remote_calendars
External CalDAV server configurations.
```sql
CREATE TABLE caldav_remote_calendars (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
local_calendar_id INTEGER REFERENCES caldav_calendars(id) ON DELETE SET NULL,
-- Remote server
name STRING NOT NULL,
server_url STRING NOT NULL,
calendar_path STRING NOT NULL,
username STRING NOT NULL,
password_encrypted TEXT NOT NULL, -- AES-256-GCM
auth_type STRING DEFAULT 'basic',
-- Sync configuration
enabled BOOLEAN DEFAULT 1,
sync_direction STRING DEFAULT 'bidirectional',
last_sync_at DATETIME,
last_sync_status STRING,
last_sync_error TEXT,
server_ctag STRING,
server_sync_token STRING,
created_at DATETIME,
updated_at DATETIME
);
```
**Indexes:**
- `idx_caldav_remote_user_id` on `user_id`
- `idx_caldav_remote_enabled` on `enabled`
- `idx_caldav_remote_local_cal` on `local_calendar_id`
---
## WebDAV Protocol Implementation
### HTTP Method Registration
Express doesn't support WebDAV methods (PROPFIND, REPORT, etc.) by default. Register custom methods in [app.js](../../backend/app.js):
```javascript
['PROPFIND', 'REPORT', 'MKCALENDAR'].forEach(method => {
express.Router[method.toLowerCase()] = function(path, ...handlers) {
return this.route(path)[method.toLowerCase()] = handlers;
};
});
```
### Discovery Endpoint
**Location:** [backend/modules/caldav/protocol/discovery.js](../../backend/modules/caldav/protocol/discovery.js)
**RFC 6764 .well-known Discovery:**
```javascript
router.get('/.well-known/caldav', (req, res) => {
res.redirect(301, '/caldav/');
});
```
### PROPFIND Handler
**Location:** [backend/modules/caldav/webdav/propfind.js](../../backend/modules/caldav/webdav/propfind.js)
**Purpose:** List calendar resources and properties
**Request Example:**
```xml
```
**Response Structure:**
```xml
/caldav/user@example.com/tasks/task-uid-123/
text/calendar; component=VTODO
"etag-value-here"
BEGIN:VCALENDAR...END:VCALENDAR
HTTP/1.1 200 OK
```
**Implementation Notes:**
- Depth: 0 returns collection properties only
- Depth: 1 returns collection + all resources
- ETags generated from task `updated_at` timestamp
- Recurring tasks expanded into virtual instances
### REPORT Handler
**Location:** [backend/modules/caldav/webdav/report.js](../../backend/modules/caldav/webdav/report.js)
**Purpose:** Query/filter calendar resources
**Supported Reports:**
- `calendar-query` - Query with filters
- `calendar-multiget` - Fetch specific resources by URL
- `sync-collection` - Incremental sync (RFC 6578)
**calendar-query Example:**
```xml
```
**Supported Filters:**
- `comp-filter` - Component type (VCALENDAR, VTODO)
- `time-range` - Date/time range filtering
- `prop-filter` - Property value filtering (limited support)
### Task Operations (GET/PUT/DELETE)
**Location:** [backend/modules/caldav/webdav/task-handlers.js](../../backend/modules/caldav/webdav/task-handlers.js)
#### GET - Fetch Task
```http
GET /caldav/{username}/tasks/{uid}/ HTTP/1.1
Authorization: Basic base64(email:password)
```
**Response:**
```http
HTTP/1.1 200 OK
Content-Type: text/calendar; charset=utf-8
ETag: "etag-value"
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Tududi//CalDAV Server//EN
BEGIN:VTODO
UID:task-uid-123
SUMMARY:Task Title
...
END:VTODO
END:VCALENDAR
```
#### PUT - Create/Update Task
```http
PUT /caldav/{username}/tasks/{uid}/ HTTP/1.1
Authorization: Basic base64(email:password)
Content-Type: text/calendar; charset=utf-8
BEGIN:VCALENDAR
...
END:VCALENDAR
```
**Response:**
- 201 Created (new task)
- 204 No Content (updated task)
**ETag Handling:**
- If-Match header supported for optimistic locking
- If-None-Match: * to prevent overwrite
#### DELETE - Remove Task
```http
DELETE /caldav/{username}/tasks/{uid}/ HTTP/1.1
Authorization: Basic base64(email:password)
```
**Response:**
- 204 No Content (success)
- 404 Not Found (task doesn't exist)
---
## iCalendar Transformation
### Task → VTODO Serialization
**Location:** [backend/modules/caldav/icalendar/vtodo-serializer.js](../../backend/modules/caldav/icalendar/vtodo-serializer.js)
**Field Mappings:**
```javascript
const fieldMappings = {
uid: (task) => task.uid,
summary: (task) => task.name,
description: (task) => task.note || '',
due: (task) => task.due_date ? formatDateTimeUTC(task.due_date) : null,
dtstart: (task) => task.defer_until ? formatDateTimeUTC(task.defer_until) : null,
completed: (task) => task.completed_at ? formatDateTimeUTC(task.completed_at) : null,
status: (task) => mapStatus(task.status),
priority: (task) => mapPriority(task.priority),
created: (task) => formatDateTimeUTC(task.created_at),
dtstamp: () => formatDateTimeUTC(new Date()),
'last-modified': (task) => formatDateTimeUTC(task.updated_at)
};
```
**Status Mapping:**
```javascript
const STATUS_MAP = {
0: 'NEEDS-ACTION', // NOT_STARTED
1: 'IN-PROCESS', // IN_PROGRESS
2: 'COMPLETED', // DONE
3: 'COMPLETED', // ARCHIVED
4: 'NEEDS-ACTION', // WAITING
5: 'CANCELLED', // CANCELLED
6: 'NEEDS-ACTION' // PLANNED
};
```
**Priority Mapping (Inverse Scale):**
```javascript
function mapPriority(tududiPriority) {
// Tududi: 0=Low, 1=Medium, 2=High
// iCalendar: 1=Highest, 5=Medium, 9=Lowest
const priorityMap = { 0: 7, 1: 5, 2: 3 };
return priorityMap[tududiPriority] || 5;
}
```
**Recurrence (RRULE) Generation:**
```javascript
function generateRRule(task) {
const rruleMap = {
daily: () => `FREQ=DAILY;INTERVAL=${task.recurrence_interval || 1}`,
weekly: () => {
const days = task.recurrence_weekdays || [1]; // Default Monday
const byDay = days.map(d => ['SU','MO','TU','WE','TH','FR','SA'][d]).join(',');
return `FREQ=WEEKLY;BYDAY=${byDay}`;
},
monthly: () => {
if (task.recurrence_month_day) {
return `FREQ=MONTHLY;BYMONTHDAY=${task.recurrence_month_day}`;
}
// Monthly by weekday (e.g., 2nd Thursday)
const week = task.recurrence_week || 1;
const day = ['SU','MO','TU','WE','TH','FR','SA'][task.recurrence_weekday || 1];
return `FREQ=MONTHLY;BYDAY=${week}${day}`;
},
yearly: () => `FREQ=YEARLY;BYMONTH=${task.recurrence_month || 1}`
};
let rrule = rruleMap[task.recurrence_pattern]?.();
if (!rrule) return null;
// Add count or until
if (task.recurrence_count) {
rrule += `;COUNT=${task.recurrence_count}`;
} else if (task.recurrence_end_date) {
rrule += `;UNTIL=${formatDateTimeUTC(task.recurrence_end_date)}`;
}
return rrule;
}
```
### VTODO → Task Parsing
**Location:** [backend/modules/caldav/icalendar/vtodo-parser.js](../../backend/modules/caldav/icalendar/vtodo-parser.js)
**Parsing Workflow:**
```javascript
function vtodoToTask(vtodoString) {
// 1. Parse iCalendar string
const jcalData = ICAL.parse(vtodoString);
const comp = new ICAL.Component(jcalData);
const vtodoComp = comp.getFirstSubcomponent('vtodo');
// 2. Extract basic properties
const task = {
uid: vtodoComp.getFirstPropertyValue('uid'),
name: vtodoComp.getFirstPropertyValue('summary'),
note: vtodoComp.getFirstPropertyValue('description'),
};
// 3. Parse dates (convert to UTC)
const due = vtodoComp.getFirstPropertyValue('due');
if (due) task.due_date = due.toJSDate().toISOString();
const dtstart = vtodoComp.getFirstPropertyValue('dtstart');
if (dtstart) task.defer_until = dtstart.toJSDate().toISOString();
// 4. Parse status and priority
const status = vtodoComp.getFirstPropertyValue('status');
task.status = reverseMapStatus(status);
const priority = vtodoComp.getFirstPropertyValue('priority');
task.priority = reverseMapPriority(priority);
// 5. Parse recurrence
const rrule = vtodoComp.getFirstPropertyValue('rrule');
if (rrule) {
Object.assign(task, parseRRule(rrule));
}
// 6. Parse custom properties
const customProps = vtodoComp.getAllProperties().filter(p =>
p.name.startsWith('x-tududi-')
);
customProps.forEach(prop => {
// Handle X-TUDUDI-* properties
});
return task;
}
```
**Reverse Priority Mapping:**
```javascript
function reverseMapPriority(icalPriority) {
// iCalendar 1-9 → Tududi 0-2
if (icalPriority >= 1 && icalPriority <= 3) return 2; // High
if (icalPriority >= 4 && icalPriority <= 6) return 1; // Medium
if (icalPriority >= 7 && icalPriority <= 9) return 0; // Low
return 1; // Default to medium
}
```
---
## Synchronization Engine
### Architecture
**Location:** [backend/modules/caldav/sync/sync-engine.js](../../backend/modules/caldav/sync/sync-engine.js)
**Three-Phase Process:**
```
┌─────────────┐
│ PULL PHASE │ Fetch changes from remote CalDAV server
└──────┬──────┘
│
▼
┌─────────────┐
│ MERGE PHASE │ Detect conflicts, apply resolution strategy
└──────┬──────┘
│
▼
┌─────────────┐
│ PUSH PHASE │ Send local changes to remote server
└─────────────┘
```
### Pull Phase
**Location:** [backend/modules/caldav/sync/pull-phase.js](../../backend/modules/caldav/sync/pull-phase.js)
```javascript
async function pullChanges(remoteCalendar) {
// 1. Send PROPFIND or REPORT to remote server
const response = await fetch(`${remoteCalendar.server_url}${remoteCalendar.calendar_path}`, {
method: 'PROPFIND',
headers: {
'Authorization': `Basic ${getAuthHeader(remoteCalendar)}`,
'Depth': '1',
'Content-Type': 'application/xml'
},
body: buildPropfindRequest(['getetag', 'calendar-data'])
});
// 2. Parse multistatus response
const remoteResources = parseMultistatusResponse(await response.text());
// 3. Convert VTODOs to tasks
const remoteTasks = remoteResources.map(resource => ({
task: vtodoParser.vtodoToTask(resource.calendarData),
etag: resource.etag,
href: resource.href
}));
return remoteTasks;
}
```
### Merge Phase
**Location:** [backend/modules/caldav/sync/merge-phase.js](../../backend/modules/caldav/sync/merge-phase.js)
```javascript
async function mergeChanges(localTasks, remoteTasks, conflictStrategy) {
const conflicts = [];
const toUpdate = [];
const toCreate = [];
for (const remoteTask of remoteTasks) {
const localTask = localTasks.find(t => t.uid === remoteTask.task.uid);
if (!localTask) {
// New remote task, create locally
toCreate.push(remoteTask);
continue;
}
const syncState = await SyncStateRepository.findByTaskAndCalendar(
localTask.id,
calendar.id
);
// Check for conflicts
if (syncState.etag !== remoteTask.etag &&
localTask.updated_at > syncState.last_synced_at) {
// Both local and remote changed
conflicts.push({
task: localTask,
localVersion: localTask,
remoteVersion: remoteTask.task,
remoteEtag: remoteTask.etag
});
continue;
}
// Remote changed, local didn't
if (syncState.etag !== remoteTask.etag) {
toUpdate.push(remoteTask);
}
}
// Apply conflict resolution
const resolved = await resolveConflicts(conflicts, conflictStrategy);
return { toCreate, toUpdate, resolved };
}
```
### Push Phase
**Location:** [backend/modules/caldav/sync/push-phase.js](../../backend/modules/caldav/sync/push-phase.js)
```javascript
async function pushChanges(localChanges, remoteCalendar) {
for (const task of localChanges) {
const vtodo = vtodoSerializer.taskToVTodo(task);
const href = `${remoteCalendar.server_url}${remoteCalendar.calendar_path}${task.uid}/`;
const response = await fetch(href, {
method: 'PUT',
headers: {
'Authorization': `Basic ${getAuthHeader(remoteCalendar)}`,
'Content-Type': 'text/calendar; charset=utf-8',
'If-Match': task.syncState?.etag || '*'
},
body: vtodo
});
if (response.ok) {
const newEtag = response.headers.get('etag');
await SyncStateRepository.updateEtag(task.id, remoteCalendar.id, newEtag);
}
}
}
```
### Conflict Resolution Strategies
**Location:** [backend/modules/caldav/sync/conflict-resolver.js](../../backend/modules/caldav/sync/conflict-resolver.js)
```javascript
const strategies = {
last_write_wins: (local, remote) => {
return new Date(local.updated_at) > new Date(remote.updated_at)
? local
: remote;
},
local_wins: (local, remote) => local,
remote_wins: (local, remote) => remote,
manual: (local, remote) => {
// Store both versions for manual resolution
return {
requiresManualResolution: true,
local,
remote
};
}
};
```
---
## Authentication
### HTTP Basic Auth
**Location:** [backend/modules/caldav/middleware/caldav-auth.js](../../backend/modules/caldav/middleware/caldav-auth.js)
```javascript
async function caldavAuth(req, res, next) {
// 1. Check for existing session (web UI)
if (req.session?.userId) {
req.currentUser = await User.findByPk(req.session.userId);
return next();
}
// 2. Check for Bearer token (API)
if (req.headers.authorization?.startsWith('Bearer ')) {
// Validate API token
return next();
}
// 3. Parse HTTP Basic Auth
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Basic ')) {
return res.status(401)
.set('WWW-Authenticate', 'Basic realm="Tududi CalDAV"')
.json({ error: 'Authentication required' });
}
const credentials = Buffer.from(authHeader.split(' ')[1], 'base64').toString('utf8');
const [username, password] = credentials.split(':');
// 4. Validate credentials
const user = await User.findOne({ where: { email: username } });
if (!user || !await bcrypt.compare(password, user.password_digest)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
req.currentUser = user;
next();
}
```
---
## Security
### Password Encryption
**Location:** [backend/modules/caldav/services/encryption-service.js](../../backend/modules/caldav/services/encryption-service.js)
**AES-256-GCM Encryption:**
```javascript
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY || process.env.SECRET_KEY, 'utf-8').slice(0, 32);
function encrypt(plaintext) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return JSON.stringify({
iv: iv.toString('hex'),
encrypted,
authTag: authTag.toString('hex')
});
}
function decrypt(ciphertext) {
const { iv, encrypted, authTag } = JSON.parse(ciphertext);
const decipher = crypto.createDecipheriv(
ALGORITHM,
KEY,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
module.exports = { encrypt, decrypt };
```
### XML Injection Prevention
**Location:** [backend/modules/caldav/middleware/xml-parser.js](../../backend/modules/caldav/middleware/xml-parser.js)
```javascript
const xml2js = require('xml2js');
const parser = new xml2js.Parser({
explicitArray: false,
ignoreAttrs: false,
trim: true,
normalize: true,
// Security: Disable external entities
xmlns: false,
explicitRoot: true,
// Prevent billion laughs attack
strict: true
});
```
---
## Performance Optimizations
### Database Indexes
**Migration:** [20260420000005-add-caldav-indexes.js](../../backend/migrations/20260420000005-add-caldav-indexes.js)
Critical indexes for performance:
- `tasks.uid` - Fast task lookup by UID
- `tasks.updated_at` - Identify changed tasks
- `caldav_sync_state.task_id` - Sync state lookup
- `caldav_sync_state.calendar_id` - Calendar-based queries
- `caldav_sync_state.last_modified` - Incremental sync
### Caching Strategy
**ETag Caching:**
- ETags generated from task `updated_at` timestamp
- Clients cache VTODO data, re-fetch only if ETag changed
- Reduces bandwidth and serialization overhead
**CTag (Collection Tag):**
- Single tag for entire calendar collection
- Changes when any task in calendar changes
- Enables quick "has anything changed?" check
### Recurring Task Expansion
**Lazy Expansion:**
- Parent task stored once in database
- Instances generated on-demand during serialization
- Configurable limit (default: 365 instances)
- Prevents database bloat from thousands of future instances
**Implementation:**
```javascript
function expandRecurringTask(parentTask, maxInstances = 365) {
if (!parentTask.recurrence_pattern) return [parentTask];
const instances = [];
const rrule = parseRRule(parentTask);
const dates = rrule.between(new Date(), addDays(new Date(), 365), true, maxInstances);
dates.forEach((date, index) => {
const instance = {
...parentTask,
uid: `${parentTask.uid}`, // Same UID
recurrenceId: date.toISOString(), // RECURRENCE-ID property
due_date: date.toISOString()
};
instances.push(instance);
});
return instances;
}
```
---
## Testing
### Unit Tests
**Location:** [backend/tests/unit/caldav/](../../backend/tests/unit/caldav/)
Test coverage:
- iCalendar serialization/parsing
- RRULE generation/parsing
- Field mappings (status, priority)
- Encryption/decryption
- ETag/CTag generation
### Integration Tests
**Location:** [backend/tests/integration/caldav.test.js](../../backend/tests/integration/caldav.test.js)
Test scenarios:
- WebDAV method handlers (PROPFIND, REPORT, etc.)
- Authentication (Basic Auth, sessions)
- Task CRUD operations
- Recurring task expansion
- Conflict detection and resolution
- Sync engine phases
### E2E Tests
**Location:** [e2e/tests/caldav-client.spec.ts](../../e2e/tests/caldav-client.spec.ts)
Test real-world client interactions:
- CalDAV discovery
- PROPFIND with Depth: 0 and 1
- REPORT with calendar-query
- GET/PUT/DELETE task operations
- Recurring task synchronization
- Performance (large calendars)
---
## Debugging
### Enable Debug Logging
```bash
CALDAV_LOG_LEVEL=debug
CALDAV_LOG_REQUESTS=true
```
### Common Issues
**Issue:** PROPFIND returns empty multistatus
**Debug:**
```javascript
console.log('Tasks found:', tasks.length);
console.log('Expanded instances:', expandedTasks.length);
console.log('XML response:', xmlResponse);
```
**Issue:** RRULE parsing fails
**Debug:**
```javascript
const rrule = vtodoComp.getFirstPropertyValue('rrule');
console.log('RRULE object:', rrule);
console.log('Frequency:', rrule.freq);
console.log('Interval:', rrule.interval);
```
**Issue:** Sync conflicts not detected
**Debug:**
```javascript
console.log('Local updated_at:', localTask.updated_at);
console.log('Sync state updated_at:', syncState.last_synced_at);
console.log('Remote etag:', remoteTask.etag);
console.log('Sync state etag:', syncState.etag);
```
---
## References
### RFCs
- [RFC 4791 - CalDAV](https://datatracker.ietf.org/doc/html/rfc4791) - CalDAV protocol
- [RFC 5545 - iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) - iCalendar format
- [RFC 6578 - Sync-Collection](https://datatracker.ietf.org/doc/html/rfc6578) - Incremental sync
- [RFC 4918 - WebDAV](https://datatracker.ietf.org/doc/html/rfc4918) - WebDAV protocol
### Libraries
- [ical.js](https://github.com/kewisch/ical.js) - iCalendar parsing/generation
- [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) - XML parsing
- [node-cron](https://github.com/node-cron/node-cron) - Background scheduling
### External Resources
- [CalDAV Client Implementation Guide](https://sabredav.org/dav/building-a-caldav-client/)
- [Apple CalDAV Server](https://github.com/apple/ccs-calendarserver)
- [Radicale CalDAV Server](https://radicale.org/)
---
## Contributing
When working on CalDAV features:
1. **Follow existing patterns** - Use repository pattern for data access
2. **Write tests** - Unit tests for logic, integration tests for HTTP handlers
3. **Update docs** - Keep user and developer docs in sync
4. **Test with clients** - Verify with tasks.org, Thunderbird, or Apple Reminders
5. **Performance matters** - Profile with 1000+ tasks, optimize queries
6. **Security first** - Validate all inputs, use prepared statements, encrypt passwords
### Adding New VTODO Properties
1. Update field mappings in [field-mappings.js](../../backend/modules/caldav/icalendar/field-mappings.js)
2. Add serialization in [vtodo-serializer.js](../../backend/modules/caldav/icalendar/vtodo-serializer.js)
3. Add parsing in [vtodo-parser.js](../../backend/modules/caldav/icalendar/vtodo-parser.js)
4. Write round-trip tests in [backend/tests/unit/caldav/](../../backend/tests/unit/caldav/)
5. Update user documentation in [docs/11-caldav-sync.md](../11-caldav-sync.md)
---
**Document Version:** 1.0.0
**Last Updated:** 2026-04-20
**Maintainer:** Update when CalDAV implementation changes