caldav.rb
A CalDAV client library for Ruby, built on webdav and implementing RFC 4791. It ships Layer 1 (Protocol) and the beginning of Layer 2 (Objects) — Calendar objects — of a planned three-layer ecosystem.
Installation
gem install caldav.rb
Or in your Gemfile:
gem 'caldav.rb'
Concepts
caldav.rb is designed in three layers, each a strict superset of the one below. Layer 1 is complete; Layer 2 has begun with Calendar objects; its Event and Principal objects, and all of Layer 3, are future releases.
- Layer 1 — Protocol. The CalDAV verbs, multistatus responses, and namespace-aware navigation accessors. The equivalent of a raw protocol library: it returns CalDAV-typed responses but does not parse iCalendar payloads or construct domain objects. For events, read
resource.calendar_dataand parse it with theicalendargem until Layer 2'sCalDAV::Eventlands. - Layer 2 — Objects (
require 'CalDAV/Objects'). Value objects over the protocol layer.CalDAV::Calendarand object-returning discovery (CalDAV::Calendar.all) ship now, with vendor properties (getctag, calendar-color) available and separable (require 'CalDAV/Objects/Core'to omit them).CalDAV::Event(with iCalendar parsing) andCalDAV::Principalare still to come. - Layer 3 — Queryable (future,
require 'CalDAV/Queryable'). A Namo-backedCalDAV::Query::Calendarexposing events as queryable rows with derived columns.
Usage
require 'caldav.rb'
caldav = CalDAV.new('https://caldav.example.com/dav/', username: 'user', password: 'pass')
The object layer (require 'CalDAV/Objects') is the high-level API; today it covers calendar discovery. Everything else below is the protocol layer it is built on — which you can always use directly for raw access.
Discovery
With the object layer, listing calendars returns CalDAV::Calendar objects — Calendar.all walks principal → home-set → calendars for you:
require 'CalDAV/Objects'
CalDAV::Calendar.all(client: caldav).each{|calendar| puts calendar.display_name}
Each Calendar exposes its collection's properties:
calendar = CalDAV::Calendar.all(client: caldav).first
calendar.path # "/dav/calendars/user/work/"
calendar.description # the calendar-description string
calendar.timezone # the raw VTIMEZONE string, if the server sets one
calendar.supported_components # the raw supported-component-set (unparsed markup)
calendar.ctag # change tag for cheap sync-detection (vendor extension)
calendar.color # the calendar colour (vendor extension)
calendar.to_s # display name, or path when unnamed
ctag and color come from the vendor extensions, loaded by default. Require the core objects instead to omit them — the readers stay, but return nil:
require 'CalDAV/Objects/Core' # strictly RFC 4791 — no vendor extensions
CalDAV::Calendar.all(client: caldav).first.ctag # => nil
Drop to the protocol layer when you want the raw resources, or aren't loading the object layer — the same walk, step by step:
principal = caldav.current_user_principal
home = caldav.calendar_home_set(principal)
caldav.calendars(home).each{|resource| puts resource.href} # CalDAV::Resource objects
Each discovery helper defaults its argument to the result of the previous step, so caldav.calendars alone walks principal → home-set → calendars.
Discovery begins by PROPFINDing the path of the base URL. Many servers do not expose the current-user-principal at /, so point the base URL at the server's CalDAV context — e.g. https://caldav.example.com/dav/ rather than https://caldav.example.com/. (RFC 6764 .well-known/caldav redirects are not auto-followed for PROPFIND in this release.) You can also override the starting point per call: caldav.current_user_principal('/dav/').
Querying a calendar
result = caldav.calendar_query('/calendars/user/work/', body: query_xml)
result.resources.each do |resource|
puts resource.href
puts resource.calendar_data # the raw iCalendar string
end
Fetching specific events
result = caldav.calendar_multiget('/calendars/user/work/', body: multiget_xml)
Free/busy
response = caldav.freebusy_query('/calendars/user/work/', body: freebusy_xml)
puts response.body # VFREEBUSY iCalendar data — not a multistatus
Creating a calendar
caldav.mkcalendar('/calendars/user/new/', body: mkcalendar_xml)
Methods
Layer 1 — Protocol (require 'caldav.rb')
Raw CalDAV-typed responses and navigation resources; no domain objects.
Protocol verbs (RFC 4791)
mkcalendar(path, body:)— §5.3.1. Create a calendar collection. Returns aWebDAV::Response.calendar_query(path, body:, depth:)— §7.8. REPORT with a<c:calendar-query>body. Returns aCalDAV::MultiStatus.calendar_multiget(path, body:, depth:)— §7.9. REPORT with a<c:calendar-multiget>body. Returns aCalDAV::MultiStatus.freebusy_query(path, body:, depth:)— §7.10. REPORT with a<c:free-busy-query>body. Returns a rawWebDAV::Responsecarrying VFREEBUSY iCalendar data — not a multistatus. This asymmetry is inherent to the CalDAV spec.
Discovery
current_user_principal(path = base URL path)— returns the principal URL string. PROPFINDs the base URL's path by default; pass a path to start discovery elsewhere.calendar_home_set(principal = current_user_principal)— returns the calendar-home-set URL string.calendars(home = calendar_home_set)— returns the calendar-collectionCalDAV::Resources.
Layer 2 — Objects (require 'CalDAV/Objects')
Value objects built over Layer 1. The default require also loads the vendor extensions (getctag, calendar-color); require 'CalDAV/Objects/Core' omits them.
CalDAV::Calendar.all(client:, home: nil)— discovers a home's calendars and returnsCalDAV::Calendarobjects.homedefaults to the client's discovered calendar-home-set.CalDAV::Calendar.from_resource(resource)— maps a singleCalDAV::Resourceto aCalDAV::Calendar.CalDAV::Calendarreaders —path,display_name,description,timezone,supported_components,ctag,color;ctagandcolorarenilunless the vendor extensions are loaded.to_sgives the display name or path.
Responses
The REPORT verbs return CalDAV::MultiStatus, a type-preserving subclass of WebDAV::MultiStatus whose resources are CalDAV::Resource objects. Each CalDAV::Resource adds CalDAV-namespace navigation accessors over the underlying webdav resource:
href— the resource URLdisplay_name— the human-readable name from<d:displayname>calendar_data— the iCalendar string from<c:calendar-data>calendar_description—<c:calendar-description>supported_calendar_component_set—<c:supported-calendar-component-set>supported_calendar_data—<c:supported-calendar-data>calendar_timezone— the VTIMEZONE string from<c:calendar-timezone>max_resource_size—<c:max-resource-size>min_date_time—<c:min-date-time>max_date_time—<c:max-date-time>max_instances—<c:max-instances>max_attendees_per_instance—<c:max-attendees-per-instance>is_calendar?— true when<d:resourcetype>includes<c:calendar/>
These are strictly navigation: they return raw strings and values, never parsed iCalendar objects. Parsing is Layer 2's job.
Limitations
The client reads but does not write, and the object layer is only beginning. Known boundaries:
- Read-only. No creating, updating or deleting yet — conditional writes need
If-Match, which webdav'sputdoes not yet expose. Writes are a later increment, beyond Layer 2's read-only objects, not part of them. - No iCalendar parsing. Use the
icalendargem onresource.calendar_dataif you need parsed events now, until Layer 2'sCalDAV::Eventlands. - Basic auth only. No OAuth.
- Tested against one real CalDAV server. Other servers should work but are unverified.
- No sync-collection.
getctaggives coarse per-collection change-detection now; fine-grained sync-collection (RFC 6578) is deferred.
Dependencies
Testing
rake
Unit tests stub at the request boundary and need no network. A separate set of
integration tests (test/integration_test.rb) run against a real CalDAV server
via VCR: they record real interactions into
host- and credential-scrubbed cassettes under test/cassettes/ on first run, then
replay offline. Without a cassette and without credentials they skip, so the
default suite stays green.
To record against a live account, supply the server and credentials through the
environment and run rake:
CALDAV_URL='https://your-caldav-host/' \
CALDAV_USERNAME='you@example.com' \
CALDAV_PASSWORD='app-password' \
rake
See test/cassettes/README.md for details.
Contributing
- Fork it https://github.com/thoran/caldav.rb/fork
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new pull request
Licence
MIT