Another day of CalDAV

caldavcarddavneodavclaudeclaude-codeself-hosteddav-servergolangdavx5imip

Continuing my efforts on a DAV-server, coded by Claude. Another attempt at journaling my experiences with this project. I managed to get a web UI running and added features there to help users migrate to the service.

Optimized sessions, the hashing of passwords (to verify logins) is a bit slow. It probably doesn’t help that Rasberry Pi’s aren’t so powerful. So I optimized it by introducing a caching layer. This stores a key in memory based on the basic authentication, which gets invalidated when the user password changes.

Implemented iMIP, Internet Message-based Interoperability Protocol (RFC 6047). It’s the standard for sending calendar invitations via email, built on top of iTIP (RFC 5546) which defines the scheduling semantics (REQUEST, REPLY, CANCEL, etc.).

The frontend was generated with a-h/templ, htmx, and pico.css. It allows for management of calendars, address books, account settings, and app passwords. Claude confabulated (hallucinated) some features regarding the account/profile name which I asked it to remove.

Worked on a bug where the organizer also got an invitation email for the event they created. Still had to beg for tests here.

I noticed that linting wasn’t being done. I set up golangci-lint to help Claude keep the code more strict. This exposed some dead code which Claude removed, but it also added a ton of //nolint:errcheck comments as a quick fix. After pointing out that silencing linter warnings wasn’t ideal, it reworked those into _ = assignments, justified with ’the error is guaranteed to be nil'.

After a while I personally checked the structure of the project and noticed that some packages were placed outside internal directory. It looks like the directory structure looks a lot like the golang-standards/project-layout definition. This is what the directories looked like:

├── admin
├── api
├── caldav
├── carddav
├── cmd
│   └── neodav
├── docs
├── internal
│   ├── auth
│   ├── config
│   ├── etag
│   ├── imappoller
│   ├── itip
│   ├── logging
│   ├── mailer
│   ├── middleware
│   ├── props
│   ├── scheduler
│   ├── storage
│   ├── synctoken
│   └── user
└── web
    ├── session
    ├── static
    └── templates

Notably, admin, api, caldav, carddav seem to be misplaced, so I let Claude clean that up by moving them into internal, like Claude described:

cmd/neodav/
internal/
  auth/      config/    storage/   middleware/
  user/      etag/      synctoken/ props/
  logging/   mailer/    scheduler/ imappoller/ itip/
  caldav/    ← move
  carddav/   ← move
  admin/     ← move
  api/       ← move
web/           ← stays

After that cleanup, I wanted an import and export feature, so I could actually migrate to (and from) this service. Claude went on its way to plan and eventually implement the feature until it thought it was done. It forgot to add tests again, so I reminded it. After deploying the new release I tried import my contacts and my calendar. Noticing that the styling was a bit off, e.g., buttons had the wrong size.

I imported the contacts without issue. Then I headed into DAVx5 and tried to sync the contacts, this however didn’t work. Contacts were not synced to the device. Claude was adamant the issue was because DAVx5 needed more time to sync, so I waited an hour. This yielded no result, but Claude was very confident it was DAVx5. So I kept adjusting settings there until I had enough. Eventually I convinced Claude, however confident it was, that it could be the code it generated.

Claude checked the PROPFIND code for the CardDAV endpoint and discovered that there was a bug. There were duplicate propstat blocks with both 404 and 200 status codes for the same properties, which violates the WebDAV spec and confuses clients like DAVx5. I asked Claude to fix it and write a test for it. This fixed something, but the result in DAVx5 was the same. Even a trailing slash issue hit the application at some point. There was stuff incorrect with a sync-token/collection. A few more cycles of fixing problems, taking several frustrating hours, DAVx5 was eventually able to load some contacts from the service.

Eventually I checked out the DAVx5 repo and had Claude investigate that, check the contacts I exported from Google. This prompts it that there are missing FN fields, card images are stored remotely. It added a repair and download feature into the import. Eventually, I got it up and running with DAVx5.

At this stage the project seems pretty functional for daily use. The DAVx5 saga was a good reminder that protocol compliance issues are subtle. A server can feel like it’s working until a strict client proves otherwise. I’ll run it as my daily driver for a while and see what surfaces next.