Skip to content

ciroque/caldav_ex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CalDAVEx

Hex.pm Documentation CI License

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.

Acknowledgments

This project was inspired by the caldav_cleam project, and modeled after the python-caldav library.

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 ical library
  • 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"}
  ]
end

Quick 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}")
end

Development

# Get dependencies
mix deps.get

# Run tests
mix test

# Generate documentation
mix docs

# Format code
mix format

Roadmap

  • 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.

Acknowledgments

  • Built with Req for HTTP
  • XML parsing via Saxy
  • iCalendar support from ical
  • Timezone handling with tz

About

Elixir CalDAV client library for calendar and event management

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages