CalDAV
A comprehensive Dart client library for CalDAV servers (RFC 4791). Provides high-level APIs for calendar and event management.
Features
- Server Discovery (RFC 6764) - Automatic endpoint detection via
.well-known/caldav - Calendar Management - List, create, update, delete calendars with unique identifiers
- Event Management - Full CRUD operations with iCalendar (RFC 5545) support
- Event Search - Find events by UID across all calendars with server-side filtering
- Multiple Authentication - Basic Auth and Bearer Token (OAuth)
- Conflict Detection - ETag-based optimistic locking
Installation
Add to your pubspec.yaml:
dependencies:
caldav: ^1.4.0
Quick Start
import 'package:caldav/caldav.dart';
void main() async {
// Connect with auto-discovery
final client = await CalDavClient.connect(
baseUrl: 'https://caldav.example.com',
username: 'user@example.com',
password: 'password',
);
try {
// Get all calendars
final calendars = await client.getCalendars();
for (final cal in calendars) {
print('${cal.displayName} (uid: ${cal.uid})');
}
// Query events (use UTC DateTime)
final start = DateTime.utc(2024, 1, 1);
final end = DateTime.utc(2024, 2, 1);
for (final calendar in calendars) {
final events = await client.getEvents(calendar, start: start, end: end);
for (final event in events) {
print('${event.summary} at ${event.start}');
}
}
// Find event by UID across all calendars
final event = await client.getEventByUid('unique-event-id');
if (event != null) {
print('Found: ${event.summary} in calendar ${event.calendarId}');
}
} finally {
client.close();
}
}
Security
By default, only HTTPS connections are allowed to protect credentials. For local development or testing, you can disable this check:
// For development only - NOT recommended for production!
final client = await CalDavClient.connect(
baseUrl: 'http://localhost:8080',
username: 'test',
password: 'test',
allowInsecure: true, // Allows HTTP connections
);
⚠️ Warning: Using HTTP transmits credentials in plain text and is vulnerable to man-in-the-middle attacks. Only use allowInsecure: true for local development.
Authentication Methods
Basic Authentication
final client = await CalDavClient.connect(
baseUrl: 'https://caldav.example.com',
username: 'user@example.com',
password: 'password',
);
Bearer Token (OAuth)
final client = CalDavClient.withToken(
baseUrl: 'https://caldav.example.com',
token: 'your_oauth_access_token',
);
await client.discover();
Calendar Operations
List Calendars
final calendars = await client.getCalendars();
for (final cal in calendars) {
print('${cal.displayName} (uid: ${cal.uid})');
print(' URL: ${cal.href}');
print(' Color: ${cal.color}');
print(' Timezone: ${cal.timezone}');
}
Create Calendar
final calendar = await client.createCalendar(
'Work Calendar',
description: 'Work-related events',
color: '#3366CC',
timezone: 'Asia/Seoul',
);
Update Calendar
await client.updateCalendar(
calendar,
displayName: 'Updated Name',
color: '#FF5733',
);
Delete Calendar
await client.deleteCalendar(calendar);
Check Read-Only Status
final calendars = await client.getCalendars();
for (final cal in calendars) {
if (cal.isReadOnly) {
print('${cal.displayName} is read-only (shared or subscribed)');
} else {
print('${cal.displayName} is writable');
}
}
Event Operations
Query Events
// Use UTC DateTime for time range filtering
final start = DateTime.utc(2024, 1, 1);
final end = DateTime.utc(2024, 12, 31);
final events = await client.getEvents(
calendar,
start: start,
end: end,
);
Get All Events (Parallel Fetch)
// Efficiently fetch all events from all calendars in parallel
final allEvents = await client.getAllEvents(
start: DateTime.utc(2024, 1, 1),
end: DateTime.utc(2024, 12, 31),
);
Get Events from Multiple Calendars
// Fetch events from specific calendars in parallel
final eventsMap = await client.getEventsFromCalendars(
[calendar1, calendar2, calendar3],
start: start,
end: end,
);
// eventsMap is Map<String, List<CalendarEvent>> keyed by calendar UID
for (final entry in eventsMap.entries) {
print('Calendar ${entry.key}: ${entry.value.length} events');
}
Find Event by UID
// Efficiently search across all calendars using server-side filtering
final event = await client.getEventByUid('unique-event-id');
if (event != null) {
print('Found in calendar: ${event.calendarId}');
}
Create Event
final event = CalendarEvent(
uid: 'unique-event-id-${DateTime.now().millisecondsSinceEpoch}',
start: DateTime.utc(2024, 6, 15, 14, 0),
end: DateTime.utc(2024, 6, 15, 15, 0),
summary: 'Team Meeting',
description: 'Weekly sync',
location: 'Conference Room A',
);
final created = await client.createEvent(calendar, event);
Create All-Day Event
final event = CalendarEvent(
uid: 'all-day-event-id',
start: DateTime.utc(2024, 6, 15),
end: DateTime.utc(2024, 6, 16),
summary: 'Company Holiday',
isAllDay: true,
);
Update Event
try {
final updated = await client.updateEvent(
event.copyWith(summary: 'Updated Meeting Title'),
);
} on ConflictException {
// Event was modified by another client
print('Conflict detected, please refresh and retry');
}
Delete Event
await client.deleteEvent(event);
Recurring Events
The library parses recurring event fields from iCalendar data:
final events = await client.getEvents(calendar, start: start, end: end);
for (final event in events) {
if (event.rrule != null) {
print('Recurring event: ${event.summary}');
print(' Rule: ${event.rrule}'); // e.g., "FREQ=DAILY;COUNT=10"
if (event.exdate != null) {
print(' Excluded dates: ${event.exdate}');
}
}
if (event.recurrenceId != null) {
print('Modified instance of recurring event');
print(' Original date: ${event.recurrenceId}');
}
}
Note: The library provides raw RRULE strings. For recurrence expansion, use a dedicated library like rrule.
Data Models
Calendar
| Property | Type | Description |
|---|---|---|
uid |
String? |
Unique identifier (DAV:geteuid) |
href |
Uri |
Calendar resource URL |
displayName |
String |
Display name |
description |
String? |
Calendar description |
color |
String? |
Color (#RRGGBB or #RRGGBBAA) |
timezone |
String? |
Default timezone (IANA format) |
ctag |
String? |
Collection tag for sync |
supportedComponents |
List<String> |
Supported components (VEVENT, VTODO, etc.) |
isReadOnly |
bool |
Read-only status based on ACL privileges |
CalendarEvent
| Property | Type | Description |
|---|---|---|
uid |
String |
Unique identifier (iCalendar UID) |
calendarId |
String? |
Parent calendar's UID |
href |
Uri? |
Event resource URL |
etag |
String? |
Entity tag for concurrency |
start |
DateTime |
Start time (UTC) |
end |
DateTime? |
End time (UTC) |
summary |
String |
Event title |
description |
String? |
Event description |
location |
String? |
Event location |
isAllDay |
bool |
All-day event flag |
rawIcalendar |
String? |
Raw iCalendar data |
isReadOnly |
bool |
Read-only status (inherited from calendar) |
rrule |
String? |
RFC 5545 recurrence rule (e.g., "FREQ=DAILY;COUNT=10") |
recurrenceId |
String? |
RECURRENCE-ID for modified instances |
exdate |
List<String>? |
Exception dates excluded from recurrence |
Error Handling
try {
final calendars = await client.getCalendars();
} on NotFoundException catch (e) {
print('Resource not found: ${e.message}');
} on ConflictException catch (e) {
print('Concurrent modification: ${e.message}');
} on CalDavException catch (e) {
print('CalDAV error: ${e.statusCode} - ${e.message}');
}
Exception Types
| Exception | Status Code | Description |
|---|---|---|
CalDavException |
Various | Base exception for CalDAV errors |
NotFoundException |
404 | Resource not found |
ConflictException |
409, 412 | Concurrent modification conflict |
Protocol Support
| RFC | Standard | Coverage |
|---|---|---|
| 4791 | CalDAV | Full core support |
| 4918 | WebDAV | PROPFIND, PROPPATCH, MKCALENDAR, REPORT |
| 6764 | CalDAV Discovery | Full implementation |
| 5545 | iCalendar | Parsing and generation |
Libraries
- caldav
- A Dart client library for interacting with CalDAV servers.