CalDAVEx
Elixir CalDAV client library for calendar and event management.
CalDAVEx provides a clean, idiomatic Elixir interface to CalDAV servers with robust XML parsing, iCalendar support, and comprehensive event filtering.
Features
- 🔍 Discovery - Automatic principal and calendar-home-set discovery
- 📅 Calendar Management - List calendars with metadata (display name, description, ctag)
- 📆 Event Retrieval - List and fetch events with time-range filtering
- 🎯 Robust Parsing - Saxy-based XML parsing for reliable CalDAV responses
-
📝 iCalendar Support - Full iCalendar parsing via the
icallibrary -
⏰ Time Zones - Proper timezone handling with
tz - ✅ Well Tested - Comprehensive test suite with Bypass-backed HTTP mocking
Installation
Add caldav_ex to your list of dependencies in mix.exs:
def deps do
[
{:caldav_ex, "~> 0.1.0"}
]
endQuick Start
# 1. Create a client
config = CalDAVEx.new_config(
"https://caldav.example.com",
CalDAVEx.basic_auth("username", "password")
)
client = CalDAVEx.new_client(config)
# 2. Discover calendar endpoints
{:ok, discovery_info} = CalDAVEx.discover(client)
# 3. List calendars
{:ok, calendars} = CalDAVEx.list_calendars(client, discovery_info)
# 4. Get events from a calendar
calendar = List.first(calendars)
{:ok, events} = CalDAVEx.list_events(client, calendar.url)
# 5. Filter events by time range
{:ok, events} = CalDAVEx.list_events(client, calendar.url,
from: ~U[2025-05-01 00:00:00Z],
to: ~U[2025-05-31 23:59:59Z]
)
# 6. Get a single event
event = List.first(events)
{:ok, full_event} = CalDAVEx.get_event(client, event.href)Usage Examples
Authentication
# Basic authentication
config = CalDAVEx.new_config(
"https://caldav.example.com",
CalDAVEx.basic_auth("user", "pass")
)
# No authentication (for testing)
config = CalDAVEx.new_config(
"http://localhost:8080",
CalDAVEx.no_auth()
)Working with Events
# List all events
{:ok, events} = CalDAVEx.list_events(client, calendar_url)
# Filter by date range
{:ok, events} = CalDAVEx.list_events(client, calendar_url,
from: DateTime.utc_now(),
to: DateTime.add(DateTime.utc_now(), 7, :day)
)
# Access event properties
Enum.each(events, fn event ->
IO.puts("#{event.summary}")
IO.puts(" Start: #{event.dtstart}")
IO.puts(" End: #{event.dtend}")
IO.puts(" ETag: #{event.etag}")
end)
# Calculate event duration
events
|> Enum.filter(fn e -> match?(%DateTime{}, e.dtstart) end)
|> Enum.map(fn e ->
%{
summary: e.summary,
duration_minutes: DateTime.diff(e.dtend, e.dtstart, :minute)
}
end)Working with Calendars
# List all calendars
{:ok, calendars} = CalDAVEx.list_calendars(client, discovery_info)
# Find a specific calendar
calendar = Enum.find(calendars, fn c ->
c.display_name == "Work"
end)
# Access calendar properties
IO.inspect(calendar.display_name)
IO.inspect(calendar.description)
IO.inspect(calendar.ctag)
IO.inspect(calendar.url)Handling All-Day Events
CalDAVEx correctly distinguishes between timed events and all-day events:
events
|> Enum.map(fn e ->
case e.dtstart do
%DateTime{} ->
IO.puts("Timed event: #{e.summary} at #{e.dtstart}")
%Date{} ->
IO.puts("All-day event: #{e.summary} on #{e.dtstart}")
end
end)CalDAV Server Compatibility
CalDAVEx has been tested with:
- ✅ iCloud Calendar
- ✅ Google Calendar (via CalDAV)
- ✅ Nextcloud
- ✅ Radicale
Data Structures
Event
%CalDAVEx.Types.Event{
href: "https://caldav.example.com/calendars/user/cal/event.ics",
etag: "\"abc123\"",
calendar_data: "BEGIN:VCALENDAR\n...",
summary: "Team Meeting",
dtstart: ~U[2025-05-15 14:00:00Z],
dtend: ~U[2025-05-15 15:00:00Z],
content_type: "text/calendar"
}Calendar
%CalDAVEx.Types.Calendar{
url: "https://caldav.example.com/calendars/user/work/",
display_name: "Work",
description: "Work calendar",
ctag: "abc123"
}DiscoveryInfo
%CalDAVEx.Types.DiscoveryInfo{
principal_url: "https://caldav.example.com/principals/user/",
calendar_home_set_url: "https://caldav.example.com/calendars/user/"
}Error Handling
All functions return {:ok, result} or {:error, error} tuples:
case CalDAVEx.list_events(client, calendar_url) do
{:ok, events} ->
IO.puts("Found #{length(events)} events")
{:error, %CalDAVEx.Error{type: :http, message: message}} ->
IO.puts("HTTP error: #{message}")
{:error, %CalDAVEx.Error{type: :xml, message: message}} ->
IO.puts("XML parsing error: #{message}")
{:error, %CalDAVEx.Error{type: :protocol, message: message}} ->
IO.puts("CalDAV protocol error: #{message}")
endDevelopment
# Get dependencies
mix deps.get
# Run tests
mix test
# Generate documentation
mix docs
# Format code
mix formatRoadmap
- Calendar resource type filtering
- Extended event properties (UID, description, location, recurrence)
- Recurring event expansion
- Event creation/modification/deletion
- Calendar creation/deletion
- Sync token support for efficient updates
- Free/busy queries
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE for details.