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.