CalDAV & Radicale Management
Interact with CalDAV servers (calendars, events, todos) and manage Radicale server configurations.
Overview
CalDAV is a protocol for accessing and managing calendaring information (RFC 4791). Radicale is a lightweight CalDAV/CardDAV server. This skill enables:
- Calendar CRUD operations (create, list, update, delete)
- Event management (create, update, delete, query)
- Todo/task management
- Radicale server configuration and administration
Prerequisites
Python Library
Install the caldav library:
pip install caldav
For async support:
pip install caldav[async]
Environment Variables (Recommended)
Store credentials securely in environment or config:
export CALDAV_URL="http://localhost:5232"
export CALDAV_USER="your_username"
export CALDAV_PASSWORD="your_password"
Or use a config file at ~/.config/caldav/config.json:
{
"url": "http://localhost:5232",
"username": "your_username",
"password": "your_password"
}
Scripts
Calendar Operations
# List all calendars
python3 {baseDir}/scripts/calendars.py list
# Create a new calendar
python3 {baseDir}/scripts/calendars.py create --name "Work Calendar" --id work
# Delete a calendar
python3 {baseDir}/scripts/calendars.py delete --id work
# Get calendar info
python3 {baseDir}/scripts/calendars.py info --id work
Event Operations
# List events (all calendars, next 30 days)
python3 {baseDir}/scripts/events.py list
# List events from specific calendar
python3 {baseDir}/scripts/events.py list --calendar work
# List events in date range
python3 {baseDir}/scripts/events.py list --start 2024-01-01 --end 2024-01-31
# Create an event
python3 {baseDir}/scripts/events.py create \
--calendar work \
--summary "Team Meeting" \
--start "2024-01-15 14:00" \
--end "2024-01-15 15:00" \
--description "Weekly sync"
# Create all-day event
python3 {baseDir}/scripts/events.py create \
--calendar personal \
--summary "Birthday" \
--start 2024-02-14 \
--allday
# Update an event
python3 {baseDir}/scripts/events.py update \
--uid "event-uid-here" \
--summary "Updated Title"
# Delete an event
python3 {baseDir}/scripts/events.py delete --uid "event-uid-here"
# Search events by text
python3 {baseDir}/scripts/events.py search --query "meeting"
Todo Operations
# List todos
python3 {baseDir}/scripts/todos.py list [--calendar name]
# Create a todo
python3 {baseDir}/scripts/todos.py create \
--calendar work \
--summary "Complete report" \
--due "2024-01-20"
# Complete a todo
python3 {baseDir}/scripts/todos.py complete --uid "todo-uid-here"
# Delete a todo
python3 {baseDir}/scripts/todos.py delete --uid "todo-uid-here"
Radicale Server Management
# Check Radicale status
python3 {baseDir}/scripts/radicale.py status
# List users (from htpasswd file)
python3 {baseDir}/scripts/radicale.py users list
# Add user
python3 {baseDir}/scripts/radicale.py users add --username newuser
# View Radicale config
python3 {baseDir}/scripts/radicale.py config show
# Validate Radicale config
python3 {baseDir}/scripts/radicale.py config validate
# Check storage integrity
python3 {baseDir}/scripts/radicale.py storage verify
Direct HTTP/DAV Operations
For low-level operations, use curl with CalDAV:
# Discover principal URL
curl -u user:pass -X PROPFIND \
-H "Depth: 0" \
-H "Content-Type: application/xml" \
-d '<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/></d:prop></d:propfind>' \
http://localhost:5232/
# List calendars
curl -u user:pass -X PROPFIND \
-H "Depth: 1" \
-H "Content-Type: application/xml" \
-d '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"><d:prop><d:displayname/><c:calendar-timezone/><d:resourcetype/></d:prop></d:propfind>' \
http://localhost:5232/user/
# Query events by time range
curl -u user:pass -X REPORT \
-H "Depth: 1" \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0" encoding="utf-8"?>
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop><d:getetag/><c:calendar-data/></d:prop>
<c:filter>
<c:comp-filter name="VCALENDAR">
<c:comp-filter name="VEVENT">
<c:time-range start="20240101T000000Z" end="20240131T235959Z"/>
</c:comp-filter>
</c:comp-filter>
</c:filter>
</c:calendar-query>' \
http://localhost:5232/user/calendar/
# Create calendar (MKCALENDAR)
curl -u user:pass -X MKCALENDAR \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0" encoding="utf-8"?>
<d:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:set><d:prop>
<d:displayname>New Calendar</d:displayname>
</d:prop></d:set>
</d:mkcalendar>' \
http://localhost:5232/user/new-calendar/
Radicale Configuration
Config File Location
Radicale looks for config in:
/etc/radicale/config(system-wide)~/.config/radicale/config(user)- Custom path via
--configorRADICALE_CONFIGenv
Key Configuration Sections
[server]
hosts = localhost:5232
max_connections = 20
max_content_length = 100000000
timeout = 30
ssl = False
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = autodetect
[storage]
filesystem_folder = /var/lib/radicale/collections
[rights]
type = owner_only
Authentication Types
| Type | Description |
|---|---|
none | No authentication (development only!) |
denyall | Deny all (default since 3.5.0) |
htpasswd | Apache htpasswd file |
remote_user | WSGI-provided username |
http_x_remote_user | Reverse proxy header |
ldap | LDAP/AD authentication |
dovecot | Dovecot auth socket |
imap | IMAP server authentication |
oauth2 | OAuth2 authentication |
pam | PAM authentication |
Creating htpasswd Users
# Create new file with SHA-512
htpasswd -5 -c /etc/radicale/users user1
# Add another user
htpasswd -5 /etc/radicale/users user2
Running as Service (systemd)
# Enable and start
sudo systemctl enable radicale
sudo systemctl start radicale
# Check status
sudo systemctl status radicale
# View logs
journalctl -u radicale -f
iCalendar Format Quick Reference
Events use iCalendar (RFC 5545) format:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp//CalDAV Client//EN
BEGIN:VEVENT
UID:unique-id@example.com
DTSTAMP:20240115T120000Z
DTSTART:20240115T140000Z
DTEND:20240115T150000Z
SUMMARY:Team Meeting
DESCRIPTION:Weekly sync
LOCATION:Conference Room
END:VEVENT
END:VCALENDAR
Common Properties
| Property | Description |
|---|---|
UID | Unique identifier |
DTSTART | Start time |
DTEND | End time |
DTSTAMP | Creation/modification time |
SUMMARY | Event title |
DESCRIPTION | Event description |
LOCATION | Location |
RRULE | Recurrence rule |
EXDATE | Excluded dates |
ATTENDEE | Participant |
ORGANIZER | Event organizer |
STATUS | CONFIRMED/TENTATIVE/CANCELLED |
Date Formats
# DateTime with timezone
DTSTART:20240115T140000Z ; UTC (Z suffix)
DTSTART;TZID=America/New_York:20240115T090000
# All-day event
DTSTART;VALUE=DATE:20240214
# DateTime local (floating)
DTSTART:20240115T140000
Troubleshooting
Connection Issues
# Test basic connectivity
curl -v http://localhost:5232/
# Check with authentication
curl -v -u user:pass http://localhost:5232/
# Verify CalDAV support
curl -X OPTIONS http://localhost:5232/ -I | grep -i dav
Common Errors
| Error | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Wrong credentials | Check htpasswd file |
| 403 Forbidden | Rights restriction | Check [rights] config |
| 404 Not Found | Wrong URL path | Check principal/calendar path |
| 409 Conflict | Resource exists | Use different UID |
| 415 Unsupported Media Type | Wrong Content-Type | Use text/calendar |
Debug Mode
Run Radicale with debug logging:
python3 -m radicale --debug
Python API Quick Reference
from caldav import DAVClient
# Connect
client = DAVClient(
url="http://localhost:5232",
username="user",
password="pass"
)
# Get principal
principal = client.principal()
# List calendars
for cal in principal.calendars():
print(f"Calendar: {cal.name} ({cal.url})")
# Create calendar
cal = principal.make_calendar(name="My Calendar", cal_id="my-cal")
# Create event
cal.save_event(
dtstart="2024-01-15 14:00",
dtend="2024-01-15 15:00",
summary="Meeting"
)
# Query events by date range
events = cal.date_search(
start="2024-01-01",
end="2024-01-31"
)
for event in events:
print(event.vobject_instance.vevent.summary.value)
# Get event by UID
event = cal.event_by_uid("event-uid")
# Delete event
event.delete()
# Create todo
todo = cal.save_todo(
summary="Task",
due="2024-01-20"
)
Workflow Examples
Create Recurring Event
python3 {baseDir}/scripts/events.py create \
--calendar work \
--summary "Weekly Standup" \
--start "2024-01-15 09:00" \
--end "2024-01-15 09:30" \
--rrule "FREQ=WEEKLY;BYDAY=MO,WE,FR"
Export Calendar to ICS
# Via curl
curl -u user:pass http://localhost:5232/user/calendar/ > calendar.ics
# Via script
python3 {baseDir}/scripts/calendars.py export --id work --output work.ics
Sync with Git (Radicale)
Configure Radicale to version changes:
[storage]
hook = git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"")
Initialize git in storage folder:
cd /var/lib/radicale/collections
git init
git config user.email "radicale@localhost"