caldav.rb
A CalDAV client library for Ruby, built on webdav and implementing RFC 4791. This release ships Layer 1 (Protocol) 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. This release ships Layer 1 only; the higher layers are future releases with reserved require paths.
- Layer 1 — Protocol (this release). 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. Most users will want Layer 2 once it ships; until then, the protocol layer is usable directly by reading
resource.calendar_dataand parsing it with theicalendargem. - Layer 2 — Objects (future,
require 'caldav.rb/objects').CalDAV::Event,CalDAV::Calendar, andCalDAV::Principalvalue objects with iCalendar parsing and convenience predicates. - Layer 3 — Queryable (future,
require 'caldav.rb/queryable'). A Namo-backedCalDAV::Query::Calendarexposing events as queryable rows with derived columns.
Usage
require 'caldav'
caldav = CalDAV.new('https://caldav.example.com/dav/', username: 'user', password: 'pass')
Discovery
principal = caldav.current_user_principal
home = caldav.calendar_home_set(principal)
caldav.calendars(home).each{|href| puts href}
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
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 an array of calendar collection URL strings.
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 URLcalendar_data— the iCalendar string from<c:calendar-data>calendar_description—<c:calendar-description>supported_calendar_component_set—<c:supported-calendar-component-set>calendar_timezone— the VTIMEZONE string from<c:calendar-timezone>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
This is a Layer 1 protocol release. Known boundaries:
- Read-only. Conditional writes need
If-Match, which webdav'sputdoes not yet expose; writes arrive with Layer 2. - No iCalendar parsing. Use the
icalendargem onresource.calendar_dataif you need parsed events now. - Basic auth only. No OAuth.
- Tested against one real CalDAV server. Other servers should work but are unverified.
- No sync-collection. Incremental sync 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