README.md

# Elixir CalDAV Client

[![Hex.pm](https://img.shields.io/hexpm/v/caldav_client.svg)](https://hex.pm/packages/caldav_client)
[![API Docs](https://img.shields.io/badge/api-docs-brightgreen.svg)](https://hexdocs.pm/caldav_client/readme.html)


This library allows for managing calendars and events on a remote calendar server according to CalDAV specification ([RFC 4791](https://tools.ietf.org/html/rfc4791)). Supports time zones, recurrence expansion and ETags. Internally uses [Tesla](https://github.com/teamon/tesla) HTTP client.

Please note that conversion between native Elixir structures and iCalendar format ([RFC 5545](https://tools.ietf.org/html/rfc5545)) is beyond the scope of this library. The following packages are recommended:

* [ICalendar](https://github.com/lpil/icalendar)
* [Calibex](https://github.com/kbrw/calibex)

## Installation

CalDAV Client is published on [Hex](https://hex.pm/packages/caldav_client). Add it to your list of dependencies in `mix.exs`:

```elixir
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:

```elixir
# 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](https://hexdocs.pm/caldav_client).

## Examples

### Client

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

```elixir
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:

```elixir
%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.

```elixir
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

```elixir
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)).

You may get a single event by its URL address:

```elixir
{: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:

```elixir
{: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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)).

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

```elixir
: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.

```elixir
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.

```elixir
{: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:

```elixir
{: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:
```elixir
{: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](https://github.com/sabre-io/Baikal) (Docker image available [here](https://hub.docker.com/r/ckulka/baikal)).
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.

```elixir
# 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:

```sh
mix test --include integration
```

## Copyright and License

Copyright 2022, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=elixir-caldav-client)

[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=elixir-caldav-client-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=elixir-caldav-client)

The code located in this repository is licensed under the [Apache License, Version 2.0](LICENSE).