History of Radicale Cribsheet
<h2>Radicale CalDAV/CardDAV Server - Key Points</h2>
<p>{toc}<a href="/storage/attachments/172/172/12inYareDragon.jpg"><img alt="10in Yare Dragon" title="10in Yare Dragon" src="/storage/attachments/172/172/thumbs/small.jpg" style="" /></a></p>
<h3>Installing</h3>
<p>{code}zypper install radicale # Creates radicale:radicale system user automatically {/code}</p>
<h3>Configuration `/etc/radicale/config`</h3>
<p>Key active settings:</p>
<p>{code}[server] hosts = localhost:5232 [auth] type = htpasswd htpasswd_filename = /etc/radicale/users htpasswd_encryption = autodetect [storage] filesystem_folder = /srv/radicale/collections {/code}</p>
<h3>Critical systemd override</h3>
<p>Default service restricts writes to `/var/lib/radicale/` — must add `/srv/radicale/` to allowed paths. Copy service file to webstack and add:</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;ReadWritePaths=/var/lib/radicale/collections /srv/radicale/
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Storage setup</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;mkdir -p /srv/radicale/collections
chown -R radicale:radicale /srv/radicale
chmod 755 /srv/radicale
chmod 755 /srv/radicale/collections
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Critical — directories need 755 not 644!**</p>
<h3>Collection structure</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;# Create directory structure
mkdir -p /srv/radicale/collections/collection-root/lsces/calendar
mkdir -p /srv/radicale/collections/collection-root/lsces/contacts
# Props files &amp;amp;amp;amp;amp;mdash; collection-root needs one too!
echo &amp;amp;amp;amp;amp;#39;{}&amp;amp;amp;amp;amp;#39; &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/.Radicale.props
cat &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/lsces/.Radicale.props &amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;lt; &amp;amp;amp;amp;amp;#39;EOF&amp;amp;amp;amp;amp;#39;
{&amp;amp;amp;amp;amp;quot;D:displayname&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;lsces&amp;amp;amp;amp;amp;quot;}
EOF
cat &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/lsces/calendar/.Radicale.props &amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;lt; &amp;amp;amp;amp;amp;#39;EOF&amp;amp;amp;amp;amp;#39;
{&amp;amp;amp;amp;amp;quot;C:supported-calendar-component-set&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;VEVENT,VJOURNAL,VTODO&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;D:displayname&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;Personal Calendar&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;tag&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;VCALENDAR&amp;amp;amp;amp;amp;quot;}
EOF
cat &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/lsces/contacts/.Radicale.props &amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;lt; &amp;amp;amp;amp;amp;#39;EOF&amp;amp;amp;amp;amp;#39;
{&amp;amp;amp;amp;amp;quot;D:displayname&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;Personal Contacts&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;tag&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;VADDRESSBOOK&amp;amp;amp;amp;amp;quot;}
EOF
chown -R radicale:radicale /srv/radicale/collections
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Create user</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;htpasswd -B -c /etc/radicale/users lsces
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Enable and start</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;systemctl daemon-reload
systemctl enable radicale
systemctl start radicale
systemctl status radicale
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>nginx reverse proxy</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;location /dav/ {
proxy_pass http://localhost:5232/;
proxy_set_header X-Script-Name /dav;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass_header Authorization;
proxy_set_header Authorization $http_authorization;
}
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Both Authorization headers needed — nginx strips it without explicit passthrough!**</p>
<h3>Migrating from Nextcloud</h3>
<p>**Calendar export** — Nextcloud UI: Calendar → Edit → Export → saves `.ics` Radicale requires **one event per file** — split the export:</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;import re, os
with open(&amp;amp;amp;amp;amp;#39;personal.ics&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;r&amp;amp;amp;amp;amp;#39;) as f:
content = f.read()
header = content[:content.index(&amp;amp;amp;amp;amp;#39;BEGIN:VEVENT&amp;amp;amp;amp;amp;#39;)]
footer = &amp;amp;amp;amp;amp;#39;END:VCALENDARn&amp;amp;amp;amp;amp;#39;
events = re.findall(&amp;amp;amp;amp;amp;#39;BEGIN:VEVENT.*?END:VEVENT&amp;amp;amp;amp;amp;#39;, content, re.DOTALL)
os.makedirs(&amp;amp;amp;amp;amp;#39;/srv/radicale/split&amp;amp;amp;amp;amp;#39;, exist_ok=True)
for event in events:
uid = re.search(&amp;amp;amp;amp;amp;#39;UID:(.*?)n&amp;amp;amp;amp;amp;#39;, event).group(1).strip()
with open(f&amp;amp;amp;amp;amp;#39;/srv/radicale/split/{uid}.ics&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;w&amp;amp;amp;amp;amp;#39;) as f:
f.write(header)
f.write(event + &amp;amp;amp;amp;amp;#39;n&amp;amp;amp;amp;amp;#39;)
f.write(footer)
print(f&amp;amp;amp;amp;amp;#39;Split {len(events)} events&amp;amp;amp;amp;amp;#39;)
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Contacts export** — Nextcloud UI: Contacts → Settings → Export → saves `.vcf` Same split needed for contacts:</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;import re, os
with open(&amp;amp;amp;amp;amp;#39;contacts.vcf&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;r&amp;amp;amp;amp;amp;#39;) as f:
content = f.read()
contacts = re.findall(&amp;amp;amp;amp;amp;#39;BEGIN:VCARD.*?END:VCARD&amp;amp;amp;amp;amp;#39;, content, re.DOTALL)
os.makedirs(&amp;amp;amp;amp;amp;#39;/srv/radicale/split_contacts&amp;amp;amp;amp;amp;#39;, exist_ok=True)
for contact in contacts:
uid = re.search(&amp;amp;amp;amp;amp;#39;UID:(.*?)n&amp;amp;amp;amp;amp;#39;, contact)
uid = uid.group(1).strip() if uid else f&amp;amp;amp;amp;amp;#39;contact_{contacts.index(contact)}&amp;amp;amp;amp;amp;#39;
with open(f&amp;amp;amp;amp;amp;#39;/srv/radicale/split_contacts/{uid}.vcf&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;w&amp;amp;amp;amp;amp;#39;) as f:
f.write(contact + &amp;amp;amp;amp;amp;#39;n&amp;amp;amp;amp;amp;#39;)
print(f&amp;amp;amp;amp;amp;#39;Split {len(contacts)} contacts&amp;amp;amp;amp;amp;#39;)
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Import via curl:**</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;# Calendar events
for f in /srv/radicale/split/*.ics; do
uid=$(basename &amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot; .ics)
curl -s -u lsces:password
-X PUT
-H &amp;amp;amp;amp;amp;quot;Content-Type: text/calendar&amp;amp;amp;amp;amp;quot;
--data-binary @&amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot;
&amp;amp;amp;amp;amp;quot;https://rdm1.uk/dav/lsces/calendar/${uid}.ics&amp;amp;amp;amp;amp;quot;
echo &amp;amp;amp;amp;amp;quot;$uid&amp;amp;amp;amp;amp;quot;
done
# Contacts
for f in /srv/radicale/split_contacts/*.vcf; do
uid=$(basename &amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot; .vcf)
curl -s -u lsces:password
-X PUT
-H &amp;amp;amp;amp;amp;quot;Content-Type: text/vcard&amp;amp;amp;amp;amp;quot;
--data-binary @&amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot;
&amp;amp;amp;amp;amp;quot;https://rdm1.uk/dav/lsces/contacts/${uid}.vcf&amp;amp;amp;amp;amp;quot;
echo &amp;amp;amp;amp;amp;quot;$uid&amp;amp;amp;amp;amp;quot;
done
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Client configuration</h3>
<p>**Thunderbird** — Add new calendar/contacts: - Network → CalDAV/CardDAV - URL: `https://yourdomain/dav/username/calendar/` **DAVx⁵ (Android)** — Add account: - Base URL: `https://yourdomain/dav/username/` - Auto-discovers both calendar and contacts</p>
<p>{toc}<a href="/storage/attachments/172/172/12inYareDragon.jpg"><img alt="10in Yare Dragon" title="10in Yare Dragon" src="/storage/attachments/172/172/thumbs/small.jpg" style="" /></a></p>
<h3>Installing</h3>
<p>{code}zypper install radicale # Creates radicale:radicale system user automatically {/code}</p>
<h3>Configuration `/etc/radicale/config`</h3>
<p>Key active settings:</p>
<p>{code}[server] hosts = localhost:5232 [auth] type = htpasswd htpasswd_filename = /etc/radicale/users htpasswd_encryption = autodetect [storage] filesystem_folder = /srv/radicale/collections {/code}</p>
<h3>Critical systemd override</h3>
<p>Default service restricts writes to `/var/lib/radicale/` — must add `/srv/radicale/` to allowed paths. Copy service file to webstack and add:</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;ReadWritePaths=/var/lib/radicale/collections /srv/radicale/
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Storage setup</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;mkdir -p /srv/radicale/collections
chown -R radicale:radicale /srv/radicale
chmod 755 /srv/radicale
chmod 755 /srv/radicale/collections
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Critical — directories need 755 not 644!**</p>
<h3>Collection structure</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;# Create directory structure
mkdir -p /srv/radicale/collections/collection-root/lsces/calendar
mkdir -p /srv/radicale/collections/collection-root/lsces/contacts
# Props files &amp;amp;amp;amp;amp;mdash; collection-root needs one too!
echo &amp;amp;amp;amp;amp;#39;{}&amp;amp;amp;amp;amp;#39; &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/.Radicale.props
cat &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/lsces/.Radicale.props &amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;lt; &amp;amp;amp;amp;amp;#39;EOF&amp;amp;amp;amp;amp;#39;
{&amp;amp;amp;amp;amp;quot;D:displayname&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;lsces&amp;amp;amp;amp;amp;quot;}
EOF
cat &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/lsces/calendar/.Radicale.props &amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;lt; &amp;amp;amp;amp;amp;#39;EOF&amp;amp;amp;amp;amp;#39;
{&amp;amp;amp;amp;amp;quot;C:supported-calendar-component-set&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;VEVENT,VJOURNAL,VTODO&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;D:displayname&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;Personal Calendar&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;tag&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;VCALENDAR&amp;amp;amp;amp;amp;quot;}
EOF
cat &amp;amp;amp;amp;amp;gt; /srv/radicale/collections/collection-root/lsces/contacts/.Radicale.props &amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;lt; &amp;amp;amp;amp;amp;#39;EOF&amp;amp;amp;amp;amp;#39;
{&amp;amp;amp;amp;amp;quot;D:displayname&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;Personal Contacts&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;tag&amp;amp;amp;amp;amp;quot;: &amp;amp;amp;amp;amp;quot;VADDRESSBOOK&amp;amp;amp;amp;amp;quot;}
EOF
chown -R radicale:radicale /srv/radicale/collections
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Create user</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;htpasswd -B -c /etc/radicale/users lsces
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Enable and start</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;systemctl daemon-reload
systemctl enable radicale
systemctl start radicale
systemctl status radicale
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>nginx reverse proxy</h3>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;location /dav/ {
proxy_pass http://localhost:5232/;
proxy_set_header X-Script-Name /dav;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass_header Authorization;
proxy_set_header Authorization $http_authorization;
}
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Both Authorization headers needed — nginx strips it without explicit passthrough!**</p>
<h3>Migrating from Nextcloud</h3>
<p>**Calendar export** — Nextcloud UI: Calendar → Edit → Export → saves `.ics` Radicale requires **one event per file** — split the export:</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;import re, os
with open(&amp;amp;amp;amp;amp;#39;personal.ics&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;r&amp;amp;amp;amp;amp;#39;) as f:
content = f.read()
header = content[:content.index(&amp;amp;amp;amp;amp;#39;BEGIN:VEVENT&amp;amp;amp;amp;amp;#39;)]
footer = &amp;amp;amp;amp;amp;#39;END:VCALENDARn&amp;amp;amp;amp;amp;#39;
events = re.findall(&amp;amp;amp;amp;amp;#39;BEGIN:VEVENT.*?END:VEVENT&amp;amp;amp;amp;amp;#39;, content, re.DOTALL)
os.makedirs(&amp;amp;amp;amp;amp;#39;/srv/radicale/split&amp;amp;amp;amp;amp;#39;, exist_ok=True)
for event in events:
uid = re.search(&amp;amp;amp;amp;amp;#39;UID:(.*?)n&amp;amp;amp;amp;amp;#39;, event).group(1).strip()
with open(f&amp;amp;amp;amp;amp;#39;/srv/radicale/split/{uid}.ics&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;w&amp;amp;amp;amp;amp;#39;) as f:
f.write(header)
f.write(event + &amp;amp;amp;amp;amp;#39;n&amp;amp;amp;amp;amp;#39;)
f.write(footer)
print(f&amp;amp;amp;amp;amp;#39;Split {len(events)} events&amp;amp;amp;amp;amp;#39;)
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Contacts export** — Nextcloud UI: Contacts → Settings → Export → saves `.vcf` Same split needed for contacts:</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;import re, os
with open(&amp;amp;amp;amp;amp;#39;contacts.vcf&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;r&amp;amp;amp;amp;amp;#39;) as f:
content = f.read()
contacts = re.findall(&amp;amp;amp;amp;amp;#39;BEGIN:VCARD.*?END:VCARD&amp;amp;amp;amp;amp;#39;, content, re.DOTALL)
os.makedirs(&amp;amp;amp;amp;amp;#39;/srv/radicale/split_contacts&amp;amp;amp;amp;amp;#39;, exist_ok=True)
for contact in contacts:
uid = re.search(&amp;amp;amp;amp;amp;#39;UID:(.*?)n&amp;amp;amp;amp;amp;#39;, contact)
uid = uid.group(1).strip() if uid else f&amp;amp;amp;amp;amp;#39;contact_{contacts.index(contact)}&amp;amp;amp;amp;amp;#39;
with open(f&amp;amp;amp;amp;amp;#39;/srv/radicale/split_contacts/{uid}.vcf&amp;amp;amp;amp;amp;#39;, &amp;amp;amp;amp;amp;#39;w&amp;amp;amp;amp;amp;#39;) as f:
f.write(contact + &amp;amp;amp;amp;amp;#39;n&amp;amp;amp;amp;amp;#39;)
print(f&amp;amp;amp;amp;amp;#39;Split {len(contacts)} contacts&amp;amp;amp;amp;amp;#39;)
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<p>**Import via curl:**</p>
<pre>
&amp;amp;amp;amp;lt;code&amp;amp;amp;amp;gt;# Calendar events
for f in /srv/radicale/split/*.ics; do
uid=$(basename &amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot; .ics)
curl -s -u lsces:password
-X PUT
-H &amp;amp;amp;amp;amp;quot;Content-Type: text/calendar&amp;amp;amp;amp;amp;quot;
--data-binary @&amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot;
&amp;amp;amp;amp;amp;quot;https://rdm1.uk/dav/lsces/calendar/${uid}.ics&amp;amp;amp;amp;amp;quot;
echo &amp;amp;amp;amp;amp;quot;$uid&amp;amp;amp;amp;amp;quot;
done
# Contacts
for f in /srv/radicale/split_contacts/*.vcf; do
uid=$(basename &amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot; .vcf)
curl -s -u lsces:password
-X PUT
-H &amp;amp;amp;amp;amp;quot;Content-Type: text/vcard&amp;amp;amp;amp;amp;quot;
--data-binary @&amp;amp;amp;amp;amp;quot;$f&amp;amp;amp;amp;amp;quot;
&amp;amp;amp;amp;amp;quot;https://rdm1.uk/dav/lsces/contacts/${uid}.vcf&amp;amp;amp;amp;amp;quot;
echo &amp;amp;amp;amp;amp;quot;$uid&amp;amp;amp;amp;amp;quot;
done
&amp;amp;amp;amp;lt;/code&amp;amp;amp;amp;gt;</pre>
<h3>Client configuration</h3>
<p>**Thunderbird** — Add new calendar/contacts: - Network → CalDAV/CardDAV - URL: `https://yourdomain/dav/username/calendar/` **DAVx⁵ (Android)** — Add account: - Base URL: `https://yourdomain/dav/username/` - Auto-discovers both calendar and contacts</p>
