Skip to content

software-mansion-labs/elixir-caldav-client

Repository files navigation

Elixir CalDAV Client

Hex.pm API Docs

This library allows for managing calendars and events on a remote calendar server according to CalDAV specification (RFC 4791). Supports time zones, recurrence expansion and ETags. Internally uses Tesla HTTP client.

Please note that conversion between native Elixir structures and iCalendar format (RFC 5545) is beyond the scope of this library. The following packages are recommended:

Installation

CalDAV Client is published on Hex. Add it to your list of dependencies in mix.exs:

def deps do
  [
    {:caldav_client, "~> 2.0"},

    # time zone database
    {:tzdata, "~> 1.1"},

    # recommended Tesla adapter
    {:hackney, "~> 1.18"},
  ]
end

Then run mix deps.get to install the package and its dependencies.

It is also required to configure the time zone database and the default Tesla adapter in the config/config.exs of your project:

# config/config.exs

import Config

config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase

config :tesla, adapter: Tesla.Adapter.Hackney

The default Tesla adapter is Erlang's built-in httpc, but currently it does not support custom HTTP methods such as MKCALENDAR or REPORT.

Documentation

Available at HexDocs.

Examples

Client

The %CalDAVClient.Client{} struct aggregates the connection details such as the server address and user credentials.

client = %CalDAVClient.Client{
  server_url: "http://127.0.0.1:8800/cal.php",
  auth: %CalDAVClient.Auth.Basic{
    username: "username",
    password: "password"
  }
}

The library supports Basic, Digest and Bearer authentication:

%CalDAVClient.Auth.Basic{
  username: "username",
  password: "password"
}

%CalDAVClient.Auth.Digest{
  username: "username",
  password: "password"
}

%CalDAVClient.Auth.Bearer{
  token: "token"
}

Calendar

Each calendar user (or principal, according to CalDAV terminology) can have multiple calendars, which are identified by URLs.

calendar_url = CalDAVClient.URL.Builder.build_calendar_url("username", "example")
# "/calendars/username/example"

:ok =
  client
  |> CalDAVClient.Calendar.create(calendar_url,
    name: "Example calendar",
    description: "This is an example calendar."
  )

:ok = client |> CalDAVClient.Calendar.update(calendar_url, name: "Lorem ipsum")

:ok = client |> CalDAVClient.Calendar.delete(calendar_url)

In case of any failure, {:error, reason} tuple will be returned.

Event

event_url = CalDAVClient.URL.Builder.build_event_url(calendar_url, "event.ics")
# "/calendars/username/example/event.ics"

event_icalendar = """
BEGIN:VCALENDAR
PRODID:-//Elixir//CalDAV//EN
VERSION:2.0
BEGIN:VEVENT
UID:totally-random-uid
DTSTAMP:20210101T120000Z
DTSTART:20210101T140000Z
END:VEVENT
END:VCALENDAR
"""

{:ok, etag} = client |> CalDAVClient.Event.create(event_url, event_icalendar)

CalDAVClient.Event.create/3 returns {:error, :unsupported_media_type} on malformed payload or {:error, :already_exists} when the specified URL is already taken (see If-None-Match).

You may get a single event by its URL address:

{:ok, icalendar, etag} = client |> CalDAVClient.Event.get(event_url)

It is also possible to find the event with a specific UID property within the calendar:

{:ok, %CalDAVClient.Event{url: url, icalendar: icalendar, etag: etag}} =
  client |> CalDAVClient.Event.find_by_uid(calendar_url, event_uid)

Both CalDAVClient.Event.get/2 and CalDAVClient.Event.find_by_uid/3 return {:error, :not_found} when the event does not exist.

When modifying an event, you may optionally include the etag option in order to prevent simultaneous updates and ensure that the appropriate version of the event will be overwritten (see ETag).

{:ok, etag} = client |> CalDAVClient.Event.update(event_url, event_icalendar, etag: etag)
:ok = client |> CalDAVClient.Event.delete(event_url, etag: etag)

When ETag does not match, both CalDAVClient.Event.update/4 and CalDAVClient.Event.delete/3 return {:error, :bad_etag}.

Events

CalDAV specification defines a way to retrieve all events that meet certain criteria, which can be used to list all events within a specified time range.

from = DateTime.from_naive!(~N[2021-01-01 00:00:00], "Europe/Warsaw")
to = DateTime.from_naive!(~N[2021-02-01 00:00:00], "Europe/Warsaw")

{:ok, events} = client |> CalDAVClient.Event.get_events(calendar_url, from, to)

You may also pass expand: true option to enable recurrence expansion, which will force the calendar server to convert all events having the RRULE property into a series of occurrences within the specified time range with the RECURRENCE-ID property set.

{:ok, events} = client |> CalDAVClient.Event.get_events(calendar_url, from, to, expand: true)

It is also possible to retrieve only the events with an alarm (VALARM) within a specified time range:

{:ok, events} = client |> CalDAVClient.Event.get_events_by_alarm(calendar_url, from, to)

For custom event reports, pass the XML request body to CalDAVClient.Event.get_events_by_xml/3 function:

{:ok, events} = client |> CalDAVClient.Event.get_events_by_xml(calendar_url, request_xml)

In all cases above, events is a list of %CalDAVClient.Event{} structs with url, icalendar and etag fields.

Testing

By default, mix test will execute only the unit tests which check XML building and parsing as well as URL generation and iCalendar date-time serialization.

The full test suite requires a connection to a calendar server, e.g. Baïkal (Docker image available here). When installed and configured, create a test user account and provide credentials along with the server details in config/test.exs in this library.

Please note that the test suite operates directly on the calendar server and will automatically create and delete the test calendar during execution.

# config/test.exs

config :caldav_client, :test_server,
  server_url: "http://127.0.0.1:8800/cal.php",
  username: "username",
  password: "password"

When configured, the test suite including integration tests can be executed by running:

mix test --include integration

Copyright and License

Copyright 2022, Software Mansion

Software Mansion

The code located in this repository is licensed under the Apache License, Version 2.0.