2023-35: Export calendar from Feishu to Fastmail

Original link: https://xuanwo.io/reports/2023-35/

At Databend Labs, we mainly use Feishu for daily communication and task coordination. For meetings that require voice communication, we use the calendar function to arrange the time uniformly. Personally, I prefer to use Fastmail and use its service to manage emails and schedules. Therefore, I began to consider whether it is possible to synchronize Feishu’s calendar to Fastmail so that all schedules can be managed on one platform.

In theory, it’s not complicated:

  • Feishu provides CalDAV to support calendar synchronization
  • Fastmail allows subscription to CalDAV service

It appears that just configuring CalDAV appropriately on Fastmail will do the trick. However, the reality is that every time you try to configure Feishu’s CalDAV to Fastmail, you will encounter an error. I didn’t think much of it before, but this weekend, I decided to give it a go.

TL;DR

The CalDAV implementation exposed by Feishu is not standard and depends on the Auto Discovery behavior of the client. The solution is to manually request the Feishu CalDAV service, obtain the real address and then configure it.

First send PROPFIND to /<username> to get the random calendar ID generated by Feishu

 curl -v -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/

In the response, there will be output in the form <D:response><D:href>/calendars/<uuid>/</D:href></D:response> , where /calendars/<uuid>/ is Feishu Random paths generated for personal calendars.

Then configure the CalDAV client as follows:

  • Username: <username>
  • Password: <password>
  • Server URL: https://caldav.feishu.cn/calendars/<uuid>/

background introduction

In the area of ​​calendar synchronization we mainly encounter the following standards:

CalDAV

CalDAV is an extension based on WebDAV , part of the HTTP extension, that allows users or applications to read and write calendar data stored on remote servers. The main use case for CalDAV is to synchronize calendar information, including events, reminders, and other related data, between multiple devices or multiple applications. Common services like Google Calendar and Apple iCloud support CalDAV.

iCal

iCal is a file format standard, formally known as iCalendar (.ics file), for storing calendar events, reminders, and other time-related information. This file format is widely accepted and supported and can be used to import, export, and share calendar data. In the calendar service, our most common way to subscribe to the calendar is through iCal .

lengthy debugging

According to the documentation provided by Feishu , how individual users can set up one-way synchronization from the local system calendar to the Feishu calendar , and based on some basic trust, we can assume that the implementation of this function of Feishu is correct, and it does not say in the documentation Nonsense, that is, the CalDAV function of Feishu is normal on macOS and Windows 10, and the abnormality of Fastmail can only be due to some differences in the specific implementation of CalDAV Client.

By querying the document and ChatGPT, we can know that the easiest way to use CalDAV is to query the properties of the specified item through PROPFIND :

The actual output of Feishu is compressed and formatted here

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/ <?xml version = "1.0" encoding = "UTF-8" ?> <D:multistatus xmlns:D = "DAV:" xmlns:C = "urn:ietf:params:xml:ns:caldav" xmlns:CS = "http://calendarserver.org/ns/" xmlns:ICAL = "http://apple.com/ns/ical/" xmlns:ME = "http://me.com/_namespace/" > <D:response> <D:href>/</D:href> </D:response> </D:multistatus>

We expected that the output here should be the attribute of the root path, but there is only one href here. Although we don’t know how to continue, we can already know that the request is responded normally, but the content is not as expected. According to the existing output, I guess Feishu used calendarserver to make some magic changes. In addition, CalDAV of Feishu should be multi-tenant, so I asked ChatGPT, if there are multiple users, how should I request?

 > calendarserver 如果有多个用户的话,他们的用户路径是什么样的?在多用户环境中使用CalendarServer,一种常用的开源日历服务器,通常涉及为每个用户设置单独的访问路径和账户。这通常通过一种叫做路径路由的机制来实现,该机制允许每个用户通过特定的URL路径来访问其各自的日历数据。假设CalendarServer的基本URL是`https://calendar.example.com/`,多个用户(比如`Alice` 和`Bob`)可能会有如下的用户路径: - Alice: `https://calendar.example.com/calendars/users/alice/` - Bob: `https://calendar.example.com/calendars/users/bob/`在这些路径下,各用户通常会有多个子路径,用于表示个人日历、工作日历、假期日历等。例如,Alice 的个人日历可能位于`https://calendar.example.com/calendars/users/alice/personal/`。这样的路径结构有助于实现多用户环境下的访问控制、数据隔离和个性化设置。这通常通过与后端数据库或者身份验证服务(如LDAP或OAuth)的集成来进一步强化。综上所述,多用户的CalendarServer通常会为每个用户提供一个专有的URL路径,用于访问他们各自的日历数据和设置。这样的设计有助于在多用户环境中实现安全和数据管理。

Well, now we boldly guess that Feishu follows the same design:

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/users/<username> <?xml version = "1.0" encoding = "UTF-8" ?> <D:multistatus xmlns:D = "DAV:" xmlns:C = "urn:ietf:params:xml:ns:caldav" xmlns:CS = "http://calendarserver.org/ns/" xmlns:ICAL = "http://apple.com/ns/ical/" xmlns:ME = "http://me.com/_namespace/" > </D:multistatus>

Unfortunately, I didn’t guess right. Accessing /calendars/users/<username> is a completely empty result, what about /calendars/users/ ?

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/users/ < HTTP/1.1 400 Bad Request < Server: TLB < Content-Length: 0 < Connection: keep-alive

Interesting, I reported an error directly, then go further, what will be output when visiting /calendars/ ?

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/ <?xml version = "1.0" encoding = "UTF-8" ?> <D:multistatus xmlns:D = "DAV:" xmlns:C = "urn:ietf:params:xml:ns:caldav" xmlns:CS = "http://calendarserver.org/ns/" xmlns:ICAL = "http://apple.com/ns/ical/" xmlns:ME = "http://me.com/_namespace/" > <D:response> <D:href>/calendars/</D:href> </D:response> <D:response> <D:href>/calendars/<uuid>/</D:href> </D:response> </D:multistatus>

There is a change! We got a new href pointing to a uuid, we continue this idea and continue to request:

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/<uuid>/ <?xml version = "1.0" encoding = "UTF-8" ?> <D:multistatus xmlns:D = "DAV:" xmlns:C = "urn:ietf:params:xml:ns:caldav" xmlns:CS = "http://calendarserver.org/ns/" xmlns:ICAL = "http://apple.com/ns/ical/" xmlns:ME = "http://me.com/_namespace/" > <D:response> <D:href>/calendars/<uuid>/d0f47ac4-a047-4589-b7c1-9af6e3cc471b.ics</D:href> </D:response> <D:response> <D:href>/calendars/<uuid>/bb010e10-cd76-4213-9d47-28a9bf48417e.ics</D:href> </D:response> <D:response> <D:href>/calendars/<uuid>/c0e28572-ed2e-447c-88ed-251dd437eb72.ics</D:href> </D:response> <D:response> <D:href>/calendars/<uuid>/2fb352a3-37d3-4487-aea2-dbc1b18ec371.ics</D:href> </D:response> <D:response> <D:href>/calendars/<uuid>/c987311c-3fee-4be5-881c-7d8e5113deea.ics</D:href> </D:response> </D:multistatus>

Ok, we now have a series of paths to ics , and it looks like each ics points to a specific event. I used https://caldav.feishu.cn/calendars/<uuid>/ as the Server URL to try to connect, and found that Fastmail successfully connected to Feishu’s CalDAV and got the events correctly!

Everything is Grass Team

Ok, Feishu has successfully connected, but I am still very curious, why is it just /calendars/<uuid> ? Looking at the results returned by PROPFIND, I tried accessing / :

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn// <?xml version = "1.0" encoding = "UTF-8" ?> <D:multistatus xmlns:D = "DAV:" xmlns:C = "urn:ietf:params:xml:ns:caldav" xmlns:CS = "http://calendarserver.org/ns/" xmlns:ICAL = "http://apple.com/ns/ical/" xmlns:ME = "http://me.com/_namespace/" > <D:response> <D:href>//</D:href> </D:response> <D:response> <D:href>//<uuid>/</D:href> </D:response> </D:multistatus>

Clam? I couldn’t believe my eyes, then I tried some other possibilities:

 > curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/feishu_is_really_cool/ <?xml version = "1.0" encoding = "UTF-8" ?> <D:multistatus xmlns:D = "DAV:" xmlns:C = "urn:ietf:params:xml:ns:caldav" xmlns:CS = "http://calendarserver.org/ns/" xmlns:ICAL = "http://apple.com/ns/ical/" xmlns:ME = "http://me.com/_namespace/" > <D:response> <D:href>/feishu_is_really_cool/</D:href> </D:response> <D:response> <D:href>/feishu_is_really_cool/61AEE45F-1E58-401C-61AE-E45F1E58401C/</D:href> </D:response> </D:multistatus>

I get it: Feishu’s CalDAV implementation is so sloppy that it only works incorrectly under the root path.

Summarize

This article shares the whole process of debugging Feishu CalDAV. I would like to thank ChatGPT for its strong support and the Feishu team for the surprise~

This article is transferred from: https://xuanwo.io/reports/2023-35/
This site is only for collection, and the copyright belongs to the original author.