From fcc45a3f44eea677708da328763e29ce95a43877 Mon Sep 17 00:00:00 2001
From: DarkSky <25152247+darkskygit@users.noreply.github.com>
Date: Sat, 9 May 2026 02:40:47 +0800
Subject: [PATCH] fix(server): caldav compatibility (#14930)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
fix #14411
fix #14909
Some CalDAV servers do not implement standard responses; add
compatibility for these servers.
#### PR Dependency Tree
* **PR #14930** 👈
This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
## Summary by CodeRabbit
* **Bug Fixes**
* Improved CalDAV discovery error handling to gracefully fall back when
the server returns certain error statuses.
* **New Features**
* CalDAV account linking now returns the number of discovered calendars
associated with the account.
---
.../plugins/calendar/__tests__/caldav.spec.ts | 25 ++++++++++++++++++-
.../src/plugins/calendar/providers/caldav.ts | 2 +-
.../server/src/plugins/calendar/service.ts | 5 +++-
3 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/packages/backend/server/src/plugins/calendar/__tests__/caldav.spec.ts b/packages/backend/server/src/plugins/calendar/__tests__/caldav.spec.ts
index 8e2e636143..ed9d5a2614 100644
--- a/packages/backend/server/src/plugins/calendar/__tests__/caldav.spec.ts
+++ b/packages/backend/server/src/plugins/calendar/__tests__/caldav.spec.ts
@@ -147,6 +147,7 @@ const calendarQueryResponse = `
`;
const createCalDAVServer = async (options?: {
+ discoveryGetStatus?: number;
syncCollectionStatus?: number;
}) => {
const requests: Array<{ method: string; url: string; body: string }> = [];
@@ -169,6 +170,16 @@ const createCalDAVServer = async (options?: {
}
if (req.url === '/.well-known/caldav') {
+ if (options?.discoveryGetStatus) {
+ res.writeHead(options.discoveryGetStatus, {
+ 'Content-Type': 'application/xml',
+ });
+ res.end(`
+
+ Sabre\\DAV\\Exception\\NotImplemented
+`);
+ return;
+ }
res.writeHead(302, { Location: '/caldav/' });
res.end();
return;
@@ -275,7 +286,7 @@ const createCalendarModule = async (caldavConfig: Record) => {
};
test('linkCalDAVAccount discovers calendars and parses events', async t => {
- const server = await createCalDAVServer();
+ const server = await createCalDAVServer({ discoveryGetStatus: 501 });
t.teardown(() => server.server.close());
const module = await createCalendarModule({
@@ -311,6 +322,18 @@ test('linkCalDAVAccount discovers calendars and parses events', async t => {
account.id
);
t.is(subscriptions.length, 1);
+ t.is(account.calendarsCount, 1);
+ t.true(
+ server.requests.some(
+ request =>
+ request.method === 'GET' && request.url === '/.well-known/caldav'
+ )
+ );
+ t.true(
+ server.requests.some(
+ request => request.method === 'PROPFIND' && request.url === '/caldav/'
+ )
+ );
const events = await models.calendarEvent.listBySubscriptionsInRange(
[subscriptions[0].id],
diff --git a/packages/backend/server/src/plugins/calendar/providers/caldav.ts b/packages/backend/server/src/plugins/calendar/providers/caldav.ts
index a3e9c27ee0..1a31a1b447 100644
--- a/packages/backend/server/src/plugins/calendar/providers/caldav.ts
+++ b/packages/backend/server/src/plugins/calendar/providers/caldav.ts
@@ -1014,7 +1014,7 @@ export class CalDAVProvider extends CalendarProvider {
if (response.ok) {
return response.url;
}
- if ([400, 404, 405].includes(response.status)) {
+ if ([400, 404, 405, 501].includes(response.status)) {
return serverUrl;
}
const text = await response.text();
diff --git a/packages/backend/server/src/plugins/calendar/service.ts b/packages/backend/server/src/plugins/calendar/service.ts
index dc988d8b8a..8367ff0dca 100644
--- a/packages/backend/server/src/plugins/calendar/service.ts
+++ b/packages/backend/server/src/plugins/calendar/service.ts
@@ -272,7 +272,10 @@ export class CalendarService {
throw error;
}
- return account;
+ const calendars = await this.models.calendarSubscription.listByAccount(
+ account.id
+ );
+ return { ...account, calendarsCount: calendars.length };
}
async syncAccountCalendars(accountId: string) {