# tsdav Documentation > TypeScript WebDAV client library wrapping CalDAV and CardDAV workflows for browsers and Node.js. This file contains all documentation content in a single document following the llmstxt.org standard. ## Intro [WEBDAV](https://tools.ietf.org/html/rfc4918), `Web Distributed Authoring and Versioning`, is an extension of the HTTP to allow handling distributed authoring, versioning of various resources. It's very common to be used for cloud storage(limited support), as well as calendar, contacts information syncing. ### Cloud provider support status | Provider name | WEBDAV | CALDAV | CARDDAV | | ------------- | ------ | ------ | ------- | | Apple | ✅ | ✅ | ✅ | | Google | ✅ | ✅ | ✅ | | Fastmail | ✅ | ✅ | ✅ | | Nextcloud | ✅ | ✅ | ✅ | | Baikal | ✅ | ✅ | ✅ | | ZOHO | ✅ | ✅ | ✅ | | DAViCal | ✅ | ✅ | ⛔️ | | Forward Email | ⛔️ | ✅ | ✅ | For more information on cloud providers, go to [cloud providers](./cloud%20providers.md) for more information. ### Install ```bash yarn add tsdav ``` or ```bash npm install tsdav ``` ### Browser usage Use the ESM bundle in modern browsers: ```html ``` Browser requests to CalDAV/CardDAV endpoints are often blocked by CORS. Prefer running tsdav in a server environment, proxying requests through your backend, or using a [custom transport](#custom-transport-electroncors). ### Cloudflare Workers `tsdav` is compatible with Cloudflare Workers. It automatically detects and uses the native `fetch` provided by the Workers runtime. Example (using standard Worker syntax): ```ts export default { async fetch(request, env) { const client = await createDAVClient({ serverUrl: 'https://caldav.icloud.com', credentials: { username: env.APPLE_ID, password: env.APPLE_PASSWORD, }, authMethod: 'Basic', defaultAccountType: 'caldav', }); const calendars = await client.fetchCalendars(); return new Response(JSON.stringify(calendars), { headers: { 'Content-Type': 'application/json' }, }); }, }; ``` ### Custom Transport (Electron/CORS) If you are using `tsdav` in an environment like an Electron renderer process where `fetch` is restricted by CORS (especially for providers like iCloud or Gmail), you can provide a custom `fetch` implementation to route requests through a proxy or Electron's main process. ```ts const client = await createDAVClient({ serverUrl: 'https://caldav.icloud.com', credentials: { ... }, authMethod: 'Basic', // Custom fetch override fetch: async (url, options) => { // Implement your own transport here, e.g., IPC to main process const response = await window.electronAPI.makeRequest(url, options); return response; }, }); ``` The custom `fetch` should follow the standard Fetch API interface. This is also supported in the `DAVClient` constructor and most high-level functions. ### Basic usage #### Import the dependency ```ts ``` or ```ts ``` #### Create Client By creating a client, you can now use all tsdav methods without supplying authentication header or accounts. However, you can always pass in custom header or account to override the default for each request. For Google ```ts const client = await createDAVClient({ serverUrl: 'https://apidata.googleusercontent.com/caldav/v2/', credentials: { tokenUrl: 'https://accounts.google.com/o/oauth2/token', username: 'YOUR_EMAIL_ADDRESS', refreshToken: 'YOUR_REFRESH_TOKEN_WITH_CALDAV_PERMISSION', clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', }, authMethod: 'Oauth', defaultAccountType: 'caldav', }); ``` or ```ts const client = await createDAVClient({ serverUrl: 'https://apidata.googleusercontent.com/caldav/v2/', credentials: { authorizationCode: 'AUTH_CODE_OBTAINED_FROM_OAUTH_CALLBACK', tokenUrl: 'https://accounts.google.com/o/oauth2/token', clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', }, authMethod: 'Oauth', defaultAccountType: 'caldav', }); ``` For Apple ```ts const client = await createDAVClient({ serverUrl: 'https://caldav.icloud.com', credentials: { username: 'YOUR_APPLE_ID', password: 'YOUR_APP_SPECIFIC_PASSWORD', }, authMethod: 'Basic', defaultAccountType: 'caldav', }); ``` Need help generating app-specific passwords? See the [Apple app-specific password guide](https://support.apple.com/en-us/HT204397). After `v1.1.0`, you have a new way of creating clients. :::info You need to call `client.login()` with this method before using the functions ::: For Google ```ts const client = new DAVClient({ serverUrl: 'https://apidata.googleusercontent.com/caldav/v2/', credentials: { tokenUrl: 'https://accounts.google.com/o/oauth2/token', username: 'YOUR_EMAIL_ADDRESS', refreshToken: 'YOUR_REFRESH_TOKEN_WITH_CALDAV_PERMISSION', clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', }, authMethod: 'Oauth', defaultAccountType: 'caldav', }); ``` For Apple ```ts const client = new DAVClient({ serverUrl: 'https://caldav.icloud.com', credentials: { username: 'YOUR_APPLE_ID', password: 'YOUR_APP_SPECIFIC_PASSWORD', }, authMethod: 'Basic', defaultAccountType: 'caldav', }); ``` #### Get calendars If you are using the class-based `DAVClient`, call `await client.login()` once before fetching calendars or other resources. ```ts const calendars = await client.fetchCalendars(); ``` #### Get calendar objects on calendars ```ts const calendarObjects = await client.fetchCalendarObjects({ calendar: myCalendar, }); ``` #### Get specific calendar objects on calendar using urls ```ts const calendarObjects = await client.fetchCalendarObjects({ calendar: myCalendar, calendarObjectUrls: urlArray, }); ``` ##### Get calendars changes from remote ```ts const { created, updated, deleted } = await client.syncCalendars({ calendars: myCalendars, detailedResult: true, }); ``` #### Get calendar object changes on a calendar from remote ```ts const { created, updated, deleted } = ( await client.smartCollectionSync({ collection: { url: localCalendar.url, ctag: localCalendar.ctag, syncToken: localCalendar.syncToken, objects: localCalendarObjects, objectMultiGet: client.calendarMultiGet, }, method: 'webdav', detailedResult: true, }) ).objects; ``` --- ## CreateAccount ## `createAccount` construct webdav account information need to requests ```ts const account = await createAccount({ account: { serverUrl: 'https://caldav.icloud.com/', accountType: 'caldav', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` **required**, account with `serverUrl` and `accountType` - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation - `loadCollections` defaults to false, whether to load all collections of the account - `loadObjects` defaults to false, whether to load all objects of collections as well, must be used with `loadCollections` set to `true` ### Return Value created [DAVAccount](../../types/DAVAccount.md) ### Behavior perform [serviceDiscovery](serviceDiscovery.md),[fetchHomeUrl](fetchHomeUrl.md), [fetchPrincipalUrl](fetchPrincipalUrl.md) for account information gathering. make fetch collections & fetch objects requests depend on options. --- ## FetchHomeUrl ## `fetchHomeUrl` fetch resource home set url ```ts const url = await fetchHomeUrl({ account: { principalUrl, serverUrl: 'https://caldav.icloud.com/', rootUrl: 'https://caldav.icloud.com/', accountType: 'caldav', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` **required**, account with `principalUrl` and `accountType` - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation ### Return Value resource home set url ### Behavior send calendar-home-set or addressbook-home-set (based on accountType) PROPFIND request and extract resource home set url from xml response --- ## FetchPrincipalUrl ## `fetchPrincipalUrl` fetch resource principal collection url ```ts const url = await fetchPrincipalUrl({ account: { serverUrl: 'https://caldav.icloud.com/', rootUrl: 'https://caldav.icloud.com/', accountType: 'caldav', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` **required**, account with `rootUrl` and `accountType` - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation ### Return Value principal collection url ### Behavior send current-user-principal PROPFIND request and extract principal collection url from xml response --- ## ServiceDiscovery ## `serviceDiscovery` automatically discover service root url ```ts const url = await serviceDiscovery({ account: { serverUrl: 'https://caldav.icloud.com/', accountType: 'caldav' }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` **required**, account with `serverUrl` and `accountType` - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation ### Return Value root url ### Behavior use `/.well-known/` request to follow redirects to find redirected url --- ## CollectionQuery ## `collectionQuery` query on [DAVCollection](../../types/DAVCollection.md) ```ts const result = await collectionQuery({ url: 'https://contacts.icloud.com/123456/carddavhome/card/', body: { 'addressbook-query': { _attributes: getDAVAttribute([DAVNamespace.CARDDAV, DAVNamespace.DAV]), [`${DAVNamespaceShort.DAV}:prop`]: props, filter: { 'prop-filter': { _attributes: { name: 'FN', }, }, }, }, }, defaultNamespace: DAVNamespaceShort.CARDDAV, depth: '1', headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, collection url - `body` **required**, query request body - `depth` [DAVDepth](../../types/DAVDepth.md) - `defaultNamespace` defaults to `DAVNamespaceShort.DAV`, default namespace for the the request body - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation ### Return Value array of [DAVResponse](../../types/DAVResponse.md) ### Behavior Sends a REPORT request to the target collection and parses the response XML into an array of [DAVResponse](../../types/DAVResponse.md). ### Error Handling `collectionQuery` will reject with an error if: - The server returns a non-OK response (e.g., 504 Gateway Timeout, 401 Unauthorized). - Any individual response within a Multi-Status payload has a status code >= 400. - The server response is not valid XML when a Multi-Status response is expected. --- ## IsCollectionDirty ## `isCollectionDirty` detect if the collection have changed ```ts const { isDirty, newCtag } = await isCollectionDirty({ collection: calendars[0], headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `collection` **required**, [DAVCollection](../../types/DAVCollection.md) to detect - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value - `isDirty` a boolean indicate if the collection is dirty - `newCtag` if collection is dirty, new ctag of the collection ### Behavior use PROPFIND to fetch new ctag of the collection and compare it with current ctag, if the ctag changed, it means collection changed. --- ## MakeCollection ## `makeCollection` [create a new collection](https://datatracker.ietf.org/doc/html/rfc5689#section-3) ```ts const response = await makeCollection({ url: 'https://caldav.icloud.com/12345676/calendars/c623f6be-a2d4-4c60-932a-043e67025dde/', props: { displayname: 'my-calendar', [`${DAVNamespaceShort.CALDAV}:calendar-description`]: 'calendar description', }, headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, url of the collection to create - `props` [ElementCompact](../../types/ElementCompact.md) prop for the collection - `depth` [DAVDepth](../../types/DAVDepth.md) - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVResponse](../../types/DAVResponse.md) ### Behavior send MKCOL request and parse response xml into array of [DAVResponse](../../types/DAVResponse.md) --- ## SmartCollectionSync ## `smartCollectionSync` smart version of collection sync that combines ctag based sync with webdav sync. ```ts const { created, updated, deleted } = ( await smartCollectionSync({ collection: { url: 'https://caldav.icloud.com/12345676/calendars/c623f6be-a2d4-4c60-932a-043e67025dde/', ctag: 'eWd9Vz8OwS0DE==', syncToken: 'eWdLSfo8439Vz8OwS0DE==', objects: [ { etag: '"63758758580"', id: '0003ffbe-cb71-49f5-bc7b-9fafdd756784', data: 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//ZContent.net//Zap Calendar 1.0//EN\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nBEGIN:VEVENT\nSUMMARY:Abraham Lincoln\nUID:c7614cff-3549-4a00-9152-d25cc1fe077d\nSEQUENCE:0\nSTATUS:CONFIRMED\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12\nDTSTART:20080212\nDTEND:20080213\nDTSTAMP:20150421T141403\nCATEGORIES:U.S. Presidents,Civil War People\nLOCATION:Hodgenville, Kentucky\nGEO:37.5739497;-85.7399606\nDESCRIPTION:Born February 12, 1809\nSixteenth President (1861-1865)\n\n\n\n \nhttp://AmericanHistoryCalendar.com\nURL:http://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincol\n n\nEND:VEVENT\nEND:VCALENDAR', url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', }, ], objectMultiGet: calendarMultiGet, }, method: 'webdav', detailedResult: true, account: { accountType: 'caldav', homeUrl: 'https://caldav.icloud.com/123456/calendars/', }, headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }) ).objects; ``` ### Arguments - `collection` **required**, the target collection to sync - `method` defaults to auto detect, one of `basic` and `webdav` - `account` [DAVAccount](../../types/DAVAccount.md) to sync - `detailedResult` boolean indicate whether the return value should be detailed or not - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation :::info `objects` inside `collection` are not needed when `detailedResult` is `true`. ::: ### Return Value depend on `detailedResult` option if `detailedResult` is falsy, array of latest [DAVObject](../../types/DAVObject.md) if `detailedResult` is `true`, an object of - `objects` - `created` array of [DAVObject](../../types/DAVObject.md) - `updated` array of [DAVObject](../../types/DAVObject.md) - `deleted` array of [DAVObject](../../types/DAVObject.md) ### Behavior detect if collection support sync-collection REPORT if they supports, use [rfc6578 webdav sync](https://datatracker.ietf.org/doc/html/rfc6578) to detect if collection changed, else use ctag to detect if collection changed. if collection changed, fetch the latest list of [DAVObject](../../types/DAVObject.md) from remote, compare the provided list and the latest list to find out `created`, `updated`, and `deleted` objects. if `detailedResult` is falsy, fetch the latest list of [DAVObject](../../types/DAVObject.md) from changed collection using [rfc6578 webdav sync](https://datatracker.ietf.org/doc/html/rfc6578) and `objectMultiGet` if `detailedResult` is `true`, return three list of separate objects for `created`, `updated`, and `deleted` --- ## SupportedReportSet ## `supportedReportSet` identifies the [reports that are supported by the resource](https://datatracker.ietf.org/doc/html/rfc3253#section-3.1.5) ```ts const reports = await supportedReportSet({ collection, headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `collection` **required**, [DAVCollection](../../types/DAVCollection.md) to query on - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of supported REPORT name in camelCase ### Behavior send supported-report-set PROPFIND request and parse response xml to extract names and convert them to camel case. --- ## SyncCollection ## `syncCollection` [One way to synchronize data between two entities is to use some form of synchronization token](https://datatracker.ietf.org/doc/html/rfc6578#section-3.2) ```ts const result = await syncCollection({ url: 'https://caldav.icloud.com/12345676/calendars/c623f6be-a2d4-4c60-932a-043e67025dde/', props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CALDAV}:calendar-data`]: {}, [`${DAVNamespaceShort.DAV}:displayname`]: {}, }, syncLevel: 1, syncToken: 'bb399205ff6ff07', headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, target collection url - `props` **required**, [ElementCompact](../../types/ElementCompact.md) - `syncLevel` [Indicates the "scope" of the synchronization report request](https://datatracker.ietf.org/doc/html/rfc6578#section-6.3) - `syncToken` [The synchronization token provided by the server and returned by the client](https://datatracker.ietf.org/doc/html/rfc6578#section-6.2) - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVResponse](../../types/DAVResponse.md) ### Behavior send sync-collection REPORT to target collection url, parse response xml into array of [DAVResponse](../../types/DAVResponse.md) --- ## CreateObject ## `createObject` create an object ```ts const response = await createObject({ url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', data: 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//ZContent.net//Zap Calendar 1.0//EN\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nBEGIN:VEVENT\nSUMMARY:Abraham Lincoln\nUID:c7614cff-3549-4a00-9152-d25cc1fe077d\nSEQUENCE:0\nSTATUS:CONFIRMED\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12\nDTSTART:20080212\nDTEND:20080213\nDTSTAMP:20150421T141403\nCATEGORIES:U.S. Presidents,Civil War People\nLOCATION:Hodgenville, Kentucky\nGEO:37.5739497;-85.7399606\nDESCRIPTION:Born February 12, 1809\nSixteenth President (1861-1865)\n\n\n\n \nhttp://AmericanHistoryCalendar.com\nURL:http://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincol\n n\nEND:VEVENT\nEND:VCALENDAR', headers: { 'content-type': 'text/calendar; charset=utf-8', authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, object url - `data` **required**, object data - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior send PUT request to target url with body of data, with `If-None-Match` header `*` to avoid accidental overwrite. --- ## DavRequest ## `davRequest` core request function of the library, based on `cross-fetch`, so the api should work across browser and Node.js using `xml-js` so that js objects can be passed as request. ```ts const [result] = await davRequest({ url: 'https://caldav.icloud.com/', init: { method: 'PROPFIND', namespace: 'd', body: { propfind: { _attributes: { 'xmlns:d': 'DAV:', }, prop: { 'd:current-user-principal': {} }, }, }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }, }); ``` ### Arguments - `url` **required**, request url - `init` **required**, [DAVRequest](davRequest.md) Object - `convertIncoming` defaults to `true`, whether to convert the passed in init object request body, if `false`, davRequest would expect `init->body` is `xml` string, and would send it directly to target `url` without processing. - `parseOutgoing` defaults to `true`, whether to parse the return value in response body, if `false`, the response `raw` would be raw `xml` string returned from server. - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation to override the default `cross-fetch` ### Return Value array of [DAVResponse](../types/DAVResponse.md) - response-> raw will be `string` if `parseOutgoing` is `false` or request failed. ### Behavior depend on options, use `xml-js` to convert passed in json object into valid xml request, also use `xml-js` to convert received xml response into json object. if request failed, response-> raw will be raw response text returned from server. --- ## DeleteObject ## `deleteObject` delete an object ```ts const response = await deleteObject({ url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', etag: '"63758758580"', headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, url of object to delete - `etag` the version string of content, if [etag](https://tools.ietf.org/id/draft-reschke-http-etag-on-write-08.html) changed, `data` must have been changed. - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior send DELETE request with etag header object will not be deleted if etag do not match --- ## Propfind ## `propfind` The [PROPFIND](https://datatracker.ietf.org/doc/html/rfc4918#section-9.1) method retrieves properties defined on the resource identified by the Request-URI ```ts const [result] = await propfind({ url: 'https://caldav.icloud.com/', props: [{ name: 'current-user-principal', namespace: DAVNamespace.DAV }], headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, request url - `props` **required**, [ElementCompact](../types/ElementCompact.md) props to find - `depth` [DAVDepth](../types/DAVDepth.md) - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVResponse](../types/DAVResponse.md) ### Behavior send a prop find request, parse the response xml to an array of [DAVResponse](../types/DAVResponse.md). --- ## UpdateObject ## `updateObject` update an object ```ts const response = await updateObject({ url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', data: 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//ZContent.net//Zap Calendar 1.0//EN\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nBEGIN:VEVENT\nSUMMARY:Abraham Lincoln\nUID:c7614cff-3549-4a00-9152-d25cc1fe077d\nSEQUENCE:0\nSTATUS:CONFIRMED\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12\nDTSTART:20080212\nDTEND:20080213\nDTSTAMP:20150421T141403\nCATEGORIES:U.S. Presidents,Civil War People\nLOCATION:Hodgenville, Kentucky\nGEO:37.5739497;-85.7399606\nDESCRIPTION:Born February 12, 1809\nSixteenth President (1861-1865)\n\n\n\n \nhttp://AmericanHistoryCalendar.com\nURL:http://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincol\n n\nEND:VEVENT\nEND:VCALENDAR', etag: '"63758758580"', headers: { 'content-type': 'text/calendar; charset=utf-8', authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, url of object to update - `data` **required**, new object content - `etag` the version string of content, if [etag](https://tools.ietf.org/id/draft-reschke-http-etag-on-write-08.html) changed, `data` must have been changed. - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior send PUT request to with data body and etag header object will not be updated if etag do not match --- ## CalendarMultiGet ## `calendarMultiGet` calendarMultiGet is used to retrieve specific calendar object resources from within a collection. If the Request-URI is a calendar object resource. This method is similar to the [calendarQuery](./calendarQuery.md), except that it takes a list of calendar object urls, instead of a filter, to determine which calendar objects to return. ```ts // fetch 2 specific objects from one calendar const calendarObjects = await calendarMultiGet({ url: 'https://caldav.icloud.com/1234567/calendars/personal/', props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CALDAV}:calendar-data`]: {}, }, objectUrls: [ 'https://caldav.icloud.com/1234567/calendars/personal/1.ics', 'https://caldav.icloud.com/1234567/calendars/personal/2.ics', ], depth: '1', headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, url of CALDAV server - `objectUrls` **required**, urls of calendar object to get - `depth` **required**, [DAVDepth](../types/DAVDepth.md) of the request - `props` [CALDAV prop element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.4) in [ElementCompact](../types/ElementCompact.md) form - `filters` [CALDAV filter element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.7) in [ElementCompact](../types/ElementCompact.md) form - `timezone` timezone of the calendar - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVCalendarObject](../types/DAVCalendarObject.md) ### Behavior send caldav:calendar-multiget REPORT request and parse the response xml to extract an array of [DAVCalendarObject](../types/DAVCalendarObject.md) data. --- ## CalendarQuery ## `calendarQuery` calendarQuery performs a search for all calendar object resources that match a specified filter. The response of this report will contain all the WebDAV properties and calendar object resource data specified in the request. In the case of the `calendarData` element, one can explicitly specify the calendar components and properties that should be returned in the calendar object resource data that matches the filter. ```ts // fetch all objects of the calendar const results = await calendarQuery({ url: 'https://caldav.icloud.com/1234567/calendars/personal/', props: [{ name: 'getetag', namespace: DAVNamespace.DAV }], filters: [ { 'comp-filter': { _attributes: { name: 'VCALENDAR', }, }, }, ], depth: '1', headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, request target url - `props` **required**, [CALDAV prop element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.4) in [ElementCompact](../types/ElementCompact.md) form - `filters` [CALDAV filter element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.7) in [ElementCompact](../types/ElementCompact.md) form - `depth` [DAVDepth](../types/DAVDepth.md) - `timezone` iana timezone name, like `America/Los_Angeles` - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVResponse](../types/DAVResponse.md) ### Behavior send a calendar-query REPORT request, after server applies the filters and parse the response xml to an array of [DAVResponse](../types/DAVResponse.md). --- ## CreateCalendarObject ## `createCalendarObject` create one calendar object on the target calendar ```ts const result = await createCalendarObject({ calendar: calendars[0], filename: 'test.ics', iCalString: 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//ZContent.net//Zap Calendar 1.0//EN\nBEGIN:VEVENT\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nSUMMARY:Abraham Lincoln\nUID:c7614cff-3549-4a00-9152-d25cc1fe077d\nSEQUENCE:0\nSTATUS:CONFIRMED\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12\nDTSTART:20080212T182145Z\nDTEND:20080213T182145Z\nDTSTAMP:20150421T182145Z\nCATEGORIES:U.S. Presidents,Civil War People\nLOCATION:Hodgenville, Kentucky\nGEO:37.5739497;-85.7399606\nDESCRIPTION:Born February 12, 1809 Sixteenth President (1861-1865) http\://AmericanHistoryCalendar.com\nURL:http\://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincoln\nEND:VEVENT\nEND:VCALENDAR', headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `calendar` **required**, the [DAVCalendar](../types/DAVCalendar.md) which the client wish to create object on. - `filename` **required**, file name of the new calendar object, should end in `.ics` - `iCalString` **required**, calendar file data - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior use PUT request to create a new calendar object ### See Also - [Importing iCal Feeds (Airbnb, Booking.com, etc.)](./import-ical-feed.md) --- ## DeleteCalendarObject ## `deleteCalendarObject` delete one calendar object on the target calendar ```ts const result = await deleteCalendarObject({ calendarObject: { url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', etag: '"63758758580"', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `calendarObject` **required**, [DAVCalendarObject](../types/DAVCalendarObject.md) to delete - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior use DELETE request to delete a new calendar object --- ## FetchCalendarObjects ## `fetchCalendarObjects` get all/specified calendarObjects of the passed in calendar ```ts const objects = await fetchCalendarObjects({ calendar: calendars[0], headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `calendar` **required**, [DAVCalendar](../types/DAVCalendar.md) to fetch calendar objects from - `objectUrls` calendar object urls to fetch - `filters` [CALDAV filter element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.7) in [ElementCompact](../types/ElementCompact.md) form - `timeRange` time range in iso format - `start` start time in [ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601), format that's not in ISO 8601 will cause an error be thrown. - `end` end time in [ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601), format that's not in ISO 8601 will cause an error be thrown. - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function :::info some calendar providers may return their objects with different suffix than .ics such as `http://api.xx/97ec5f81-5ecc-4505-9621-08806f6796a3` or `http://api.xx/calobj1.abc` in this case, you need to pass in your own calendar object name filter so that you can have results you need. ::: - `urlFilter` **default: function which only keep .ics objects** predicate function to filter urls from the calendar objects before fetching - `expand` whether to [expand](https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5) the calendar objects, forcing the server to expand recurring components into individual calendar objects. :::info some calendar providers may not support calendarMultiGet, then it's necessary to use calendarQuery to fetch calendar object data. ::: - `useMultiGet` **default: true** whether to use [calendarMultiGet](./calendarMultiGet.md) as underlying function to fetch calendar objects, if set to false, it will use [calendarQuery](./calendarQuery.md) to fetch instead. ### Return Value array of [DAVCalendarObject](../types/DAVCalendarObject.md) ### Behavior a mix of [calendarMultiGet](calendarMultiGet.md) and [calendarQuery](calendarQuery.md), you can specify both filters and objectUrls here. --- ## FetchCalendarUserAddresses ## `fetchCalendarUserAddresses` Fetch all calendar user addresses of the passed in CALDAV account ```ts const addresses = await fetchCalendarUserAddresses({ account: { principalUrl, serverUrl: 'https://caldav.icloud.com/', rootUrl: 'https://caldav.icloud.com/', accountType: 'caldav', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` **required**, account with `principalUrl` - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value Array of user addresses Example of calendar user address: - `mailto:john.doe@example.com` - `/12345679/principal` - `/aMjIxa0AwMRbhws5kZV25Wnb3-ZH0vD9R89O32XQwIxJV2zMDAwMTcwMjIxMD28Aa/principal/` - `urn:uuid:12345679` ### Behavior send calendar-user-address-set PROPFIND request and extract user addresses set from xml response --- ## FetchCalendars ## `fetchCalendars` get all calendars of the passed in CALDAV account ```ts const calendars = await fetchCalendars({ account, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` [DAVAccount](../types/DAVAccount.md) - `header` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation - `props` [CALDAV prop element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.4) in [ElementCompact](../types/ElementCompact.md) form, overriding default props to fetch - `projectedProps` custom props projection object, used as a map to map fetched custom props to values :::caution when overriding props, supported-calendar-component-set and resourcetype are required ::: ### Return Value array of [DAVCalendar](../types/DAVCalendar.md) of the account ### Behavior use `PROPFIND` to get all the basic info about calendars on certain account --- ## FreeBusyQuery ## `freeBusyQuery` query free busy data on calendar :::caution a lot of caldav providers do not support this method like google, apple. use with caution. ::: ```ts const freeBusyQuery = await freeBusyQuery({ url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/', timeRange: { start: '2022-01-28T16:25:33.125Z', end: '2022-02-28T16:25:33.125Z', }, depth: '0', headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, collection url - `timeRange` **required** time range in iso format - `start` start time in [ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601), format that's not in ISO 8601 will cause an error be thrown. - `end` end time in [ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601), format that's not in ISO 8601 will cause an error be thrown. - `depth` [DAVDepth](../types/DAVDepth.md) - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [DAVResponse](../types/DAVResponse.md) ### Behavior send free-busy-query REPORT request to caldav server. --- ## Importing iCal Feeds (Airbnb, Booking.com, etc.) `tsdav` is a CalDAV and CardDAV client, which means it is designed to interact with DAV servers. It does not natively support ingesting and syncing iCal subscription feeds (like those provided by Airbnb or Booking.com) directly. However, you can easily implement this functionality by combining `tsdav` with an iCal parser. ## Recommended Approach 1. **Fetch the iCal feed**: Use a standard HTTP client (like `fetch` or `axios`) to download the `.ics` file from the source URL. 2. **Parse the feed**: Use a library like [ical.js](https://github.com/mozilla-comm/ical.js) or [node-ical](https://github.com/peterbraden/node-ical) to parse the iCal data. 3. **Push to CalDAV**: Iterate through the events in the parsed feed and use `createCalendarObject` to upload them to your target CalDAV calendar. ## Example Integration Here is a conceptual example of how you might import events from an external iCal URL into a CalDAV calendar using `node-ical` and `tsdav`. ```ts async function importExternalFeed( feedUrl: string, targetCalendar: DAVCalendar, authHeader: string, ) { // 1. Fetch and parse the feed const events = await ical.async.fromURL(feedUrl); for (const event of Object.values(events)) { if (event.type !== 'VEVENT') continue; // 2. Convert the event back to an iCal string (if needed) or use the raw source // Note: You should wrap each event in its own VCALENDAR/VEVENT structure for CalDAV const iCalString = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//tsdav//Import Helper//EN BEGIN:VEVENT UID:${event.uid} SUMMARY:${event.summary} DTSTART:${event.start.toISOString().replace(/[-:]/g, '').split('.')[0]}Z DTEND:${event.end.toISOString().replace(/[-:]/g, '').split('.')[0]}Z DESCRIPTION:${event.description || ''} END:VEVENT END:VCALENDAR`; // 3. Upload to CalDAV try { await createCalendarObject({ calendar: targetCalendar, filename: `${event.uid}.ics`, iCalString, headers: { authorization: authHeader, }, }); console.log(`Imported event: ${event.summary}`); } catch (error) { // In a real application, you'd handle 412 Precondition Failed (If-None-Match) // if the event already exists, or use updateCalendarObject if you want to sync. console.error(`Failed to import event ${event.uid}:`, error); } } } ``` ## Considerations - **Syncing**: To avoid duplicates, you should track which events have already been imported (e.g., by keeping a map of UIDs). - **Updates**: If an event changes in the source feed, you should use `updateCalendarObject` instead of `createCalendarObject`. - **Feed Polling**: You will need to set up your own periodic job (e.g., a cron job or a background worker) to poll the iCal feed and trigger the import process. --- ## MakeCalendar ## `makeCalendar` create a new calendar on target account ```ts const result = await makeCalendar({ url: 'https://caldav.icloud.com/12345676/calendars/c623f6be-a2d4-4c60-932a-043e67025dde/', props: { displayname: 'personal calendar', [`${DAVNamespaceShort.CALDAV}:calendar-description`]: 'calendar description', }, headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, the target url - `props` **required**, [CALDAV prop element](https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.4) in [ElementCompact](../types/ElementCompact.md) form - `depth` [DAVDepth](../types/DAVDepth.md) - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVResponse](../types/DAVResponse.md) ### Behavior send a MKCALENDAR request and calendar data that creates a new calendar --- ## SyncCalendars ## `syncCalendars` sync local version of calendars with remote. ```ts const { created, updated, deleted } = await syncCalendars({ oldCalendars: [ { displayName: 'personal calendar', syncToken: 'HwoQEgwAAAAAAAAAAAAAAAAYARgAIhsI4pnF4erDm4CsARDdl6K9rqa9/pYBKAA=', ctag: '63758742166', url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/', objects: [ { etag: '"63758758580"', id: '0003ffbe-cb71-49f5-bc7b-9fafdd756784', data: 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//ZContent.net//Zap Calendar 1.0//EN\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nBEGIN:VEVENT\nSUMMARY:Abraham Lincoln\nUID:c7614cff-3549-4a00-9152-d25cc1fe077d\nSEQUENCE:0\nSTATUS:CONFIRMED\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12\nDTSTART:20080212\nDTEND:20080213\nDTSTAMP:20150421T141403\nCATEGORIES:U.S. Presidents,Civil War People\nLOCATION:Hodgenville, Kentucky\nGEO:37.5739497;-85.7399606\nDESCRIPTION:Born February 12, 1809\nSixteenth President (1861-1865)\n\n\n\n \nhttp://AmericanHistoryCalendar.com\nURL:http://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincol\n n\nEND:VEVENT\nEND:VCALENDAR', url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', }, ], }, ], detailedResult: true, headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `oldCalendars` **required**, locally version of calendars of this account, should contain [calendar objects](../types/DAVCalendarObject.md) as well if `detailedResult` is `false` - `account` the account which calendars belong to, - `detailedResult` if falsy, the result would be latest version of the calendars of this account, otherwise they would be separated into three groups of `created`, `updated`, and `deleted`. - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function :::info `objects` inside `oldCalendars` are not needed when `detailedResult` is `true`. ::: ### Return Value depend on `detailedResult` option if `detailedResult` is falsy, array of [DAVCalendar](../types/DAVCalendar.md) with calendar objects. if `detailedResult` is `true`, an object of - `created` array of [DAVCalendar](../types/DAVCalendar.md) without calendar objects. - `updated` array of [DAVCalendar](../types/DAVCalendar.md) without calendar objects. - `deleted` array of [DAVCalendar](../types/DAVCalendar.md) without calendar objects. ### Behavior fetch the latest list of [DAVCalendar](../types/DAVCalendar.md) from remote, compare the provided list and the latest list to find out `created`, `updated`, and `deleted` calendars. if `detailedResult` is falsy, fetch the latest list of [DAVCalendarObject](../types/DAVCalendarObject.md) from updated calendars using [rfc6578 webdav sync](https://datatracker.ietf.org/doc/html/rfc6578) and [calendarMultiGet](calendarMultiGet.md) return latest list of calendars with latest list of objects for `updated` calendars. if `detailedResult` is `true`, return three list of separate calendars without objects for `created`, `updated`, and `deleted`. --- ## UpdateCalendarObject ## `updateCalendarObject` create one calendar object ```ts const result = updateCalendarObject({ calendarObject: { url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/test.ics', data: 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//ZContent.net//Zap Calendar 1.0//EN\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nBEGIN:VEVENT\nSUMMARY:Abraham Lincoln\nUID:c7614cff-3549-4a00-9152-d25cc1fe077d\nSEQUENCE:0\nSTATUS:CONFIRMED\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=12\nDTSTART:20080212\nDTEND:20080213\nDTSTAMP:20150421T141403\nCATEGORIES:U.S. Presidents,Civil War People\nLOCATION:Hodgenville, Kentucky\nGEO:37.5739497;-85.7399606\nDESCRIPTION:Born February 12, 1809\nSixteenth President (1861-1865)\n\n\n\n \nhttp://AmericanHistoryCalendar.com\nURL:http://americanhistorycalendar.com/peoplecalendar/1,328-abraham-lincol\n n\nEND:VEVENT\nEND:VCALENDAR', etag: '"63758758580"', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `calendarObject` **required**, [DAVCalendarObject](../types/DAVCalendarObject.md) to update - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior use PUT request to update a new calendar object --- ## AddressBookMultiGet ## `addressBookMultiGet` addressBookMultiGet is used to retrieve specific address object resources from within a collection. If the Request-URI is an address object resource. This report is similar to the [addressBookQuery](addressBookQuery.md) except that it takes a list of vcard urls, instead of a filter, to determine which vcards to return. ```ts // fetch 2 specific vcards from one addressBook const vcards = await addressBookMultiGet({ url: 'https://contacts.icloud.com/1234567/carddavhome/card/', props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CARDDAV}:address-data`]: {}, }, objectUrls: [ 'https://contacts.icloud.com/1234567/carddavhome/card/1.vcf', 'https://contacts.icloud.com/1234567/carddavhome/card/2.vcf', ], depth: '1', headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, url of CARDDAV server - `objectUrls` **required**, urls of vcards to get - `props` **required**, [CARDDAV prop element](https://datatracker.ietf.org/doc/html/rfc6352#section-10.4.2) in [ElementCompact](../types/ElementCompact.md) form - `filters` [CARDDAV filter element](https://datatracker.ietf.org/doc/html/rfc6352#section-10.5) in [ElementCompact](../types/ElementCompact.md) form, overriding default filters - `depth` **required**, [DAVDepth](../types/DAVDepth.md) of the request - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVVCard](../types/DAVVCard.md) ### Behavior send carddav:addressbook-multiget REPORT request and parse the response xml to extract an array of [DAVVCard](../types/DAVVCard.md) data. --- ## AddressBookQuery ## `addressBookQuery` performs a search for all address object resources that match a specified filter The response of this report will contain all the WebDAV properties and address object resource data specified in the request. In the case of the `addressData` element, one can explicitly specify the vCard properties that should be returned in the address object resource data that matches the filter. ```ts const addressbooks = await addressBookQuery({ url: 'https://contacts.icloud.com/123456/carddavhome/card/', props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, }, depth: '1', headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `url` **required**, request target url - `props` **required**, [CARDDAV prop element](https://datatracker.ietf.org/doc/html/rfc6352#section-10.4.2) in [ElementCompact](../types/ElementCompact.md) form - `filters` [CARDDAV filter element](https://datatracker.ietf.org/doc/html/rfc6352#section-10.5) in [ElementCompact](../types/ElementCompact.md) form - `depth` [DAVDepth](../types/DAVDepth.md) - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value array of [DAVResponse](../types/DAVResponse.md) ### Behavior send a addressbook-query REPORT request, after server applies the filters and parse the response xml to an array of [DAVResponse](../types/DAVResponse.md). --- ## CreateVCard ## `createVCard` create one vcard on the target addressBook ```ts const result = await createVCard({ addressBook: addressBooks[0], filename: 'test.vcf' vCardString: 'BEGIN:VCARD\nVERSION:3.0\nN:;Test BBB;;;\nFN:Test BBB\nUID:0976cf06-a0e8-44bd-9217-327f6907242c\nPRODID:-//Apple Inc.//iCloud Web Address Book 2109B35//EN\nREV:2021-06-16T01:28:23Z\nEND:VCARD', headers: { authorization: 'Basic x0C9ueWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `addressBook` **required**, [DAVAddressBook](../types/DAVAddressBook.md) - `filename` **required**, file name of the new vcard, should end in `.vcf` - `vCardString` **required**, vcard file data - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior use PUT request to create a new vcard --- ## DeleteVCard ## `deleteVCard` delete one vcard on the target addressBook ```ts const result = await deleteCalendarObject({ vCard: { url: 'https://contacts.icloud.com/123456/carddavhome/card/test.vcf', etag: '"63758758580"', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `vCard` **required**, [DAVVCard](../types/DAVVCard.md) to delete - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior use DELETE request to delete a new vcard --- ## FetchAddressBooks ## `fetchAddressBooks` get all addressBooks of the passed in CARDDAV account ```ts const addressBooks = await fetchAddressBooks({ account, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `account` [DAVAccount](../types/DAVAccount.md) - `props` [CARDDAV prop element](https://datatracker.ietf.org/doc/html/rfc6352#section-10.4.2) in [ElementCompact](../types/ElementCompact.md) form, overriding default props to fetch. - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation :::caution when overriding props, resourcetype is required ::: ### Return Value array of [DAVAddressBook](../types/DAVAddressBook.md) ### Behavior use `PROPFIND` and to get all the basic info about addressBook on certain account --- ## FetchVCards ## `fetchVCards` get all/specified vcards of the passed in addressBook ```ts const vcards = await fetchVCards({ addressBook: addressBooks[0], headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `addressBook` **required**, [DAVAddressBook](../types/DAVAddressBook.md) to fetch vcards from - `objectUrls` vcard urls to fetch - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function :::info some providers may return their objects with different suffixes such as `http://api.xx/97ec5f81-5ecc-4505-9621-08806f6796a3` or `http://api.xx/calobj1.abc` in this case, you can pass in your own object name filter ::: - `urlFilter` **default: no filter**predicate function to filter urls from the address book before fetching :::info some providers may not support addressBookMultiGet, then it's necessary to use addressBookQuery to fetch vcards. ::: - `useMultiGet` **default: true** whether to use [addressBookMultiGet](./addressBookMultiGet.md) as underlying function to fetch vcards, if set to false, it will use [addressBookQuery](./addressBookQuery.md) instead ### Return Value array of [DAVVCard](../types/DAVVCard.md) ### Behavior a mix of [addressBookMultiGet](addressBookMultiGet.md) and [addressBookQuery](addressBookQuery.md), you can specify objectUrls here. --- ## UpdateVCard ## `updateVCard` create one vcard ```ts const result = updateVCard({ vCard: { url: 'https://contacts.icloud.com/123456/carddavhome/card/test.vcf', data: 'BEGIN:VCARD\nVERSION:3.0\nN:;Test BBB;;;\nFN:Test BBB\nUID:0976cf06-a0e8-44bd-9217-327f6907242c\nPRODID:-//Apple Inc.//iCloud Web Address Book 2109B35//EN\nREV:2021-06-16T01:28:23Z\nEND:VCARD', etag: '"63758758580"', }, headers: { authorization: 'Basic x0C9uFWd9Vz8OwS0DEAtkAlj', }, }); ``` ### Arguments - `vCard` **required**, [DAVVCard](../types/DAVVCard.md) to update - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function ### Return Value [fetch api response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ### Behavior use PUT request to update a new vcard --- ## DAVAccount ```ts export type DAVAccount = { accountType: 'caldav' | 'carddav'; serverUrl: string; credentials?: DAVCredentials; rootUrl?: string; principalUrl?: string; homeUrl?: string; calendars?: DAVCalendar[]; addressBooks?: DAVAddressBook[]; }; ``` - `accountType` can be `caldav` or `carddav` - `serverUrl` server url of the account - `credentials` [DAVCredentials](DAVCredentials.md) - `rootUrl` root url of the account - `principalUrl` principal resource url - `homeUrl` resource home set url - `calendars` calendars of the account, will only be populated by [createAccount](../webdav/account/createAccount.md) - `addressBooks` addressBooks of the account, will only be populated by [createAccount](../webdav/account/createAccount.md) --- ## DAVAddressBook ```ts export type DAVAddressBook = DAVCollection; ``` alias of [DAVCollection](DAVCollection.md) --- ## DAVCalendar ```ts export type DAVCalendar = { components?: string[]; timezone?: string; projectedProps?: Record; } & DAVCollection; ``` alias of [DAVCollection](DAVCollection.md) with - `timezone` iana timezone name of calendar - `components` array of calendar components defined in [rfc5455](https://datatracker.ietf.org/doc/html/rfc5545#section-3.6) - `projectedProps` object of fetched additional props by passing custom props to [fetchCalendars](../caldav/fetchCalendars.md) function --- ## DAVCalendarObject ```ts export type DAVCalendarObject = DAVObject; ``` alias of [DAVObject](DAVObject.md) --- ## DAVCollection ```ts export type DAVCollection = { objects?: DAVObject[]; ctag?: string; description?: string; displayName?: string | Record; reports?: any; resourcetype?: any; syncToken?: string; url: string; }; ``` - `objects` objects of the - `ctag` [Collection Entity Tag](https://github.com/apple/ccs-calendarserver/blob/master/doc/Extensions/caldav-ctag.txt) - `description` description of the collection - `displayName` [display name of the collection](https://datatracker.ietf.org/doc/html/rfc2518#section-13.2) - `reports` list of [reports that are supported by the resource](https://datatracker.ietf.org/doc/html/rfc3253#section-3.1.5). (in camel case), usually a string array - `resourcetype` [type of the resource](https://datatracker.ietf.org/doc/html/rfc2518#section-13.9), usually a string array - `syncToken` [the value of the synchronization token](https://datatracker.ietf.org/doc/html/rfc6578#section-4) - `url` url of the resource --- ## DAVCredentials ```ts export type DAVCredentials = { username?: string; password?: string; clientId?: string; clientSecret?: string; authorizationCode?: string; redirectUrl?: string; tokenUrl?: string; accessToken?: string; refreshToken?: string; expiration?: number; digestString?: string; customData?: Record; }; ``` refer to [this page](https://developers.google.com/identity/protocols/oauth2) for more on what these fields mean - `username` basic auth username - `password` basic auth password - `clientId` oauth client id - `clientSecret` oauth client secret - `authorizationCode` oauth callback auth code - `redirectUrl` oauth callback redirect url - `tokenUrl` oauth api token url - `accessToken` oauth access token - `refreshToken` oauth refresh token - `expiration` oauth access token expiration time - `digestString` string used for digest auth - `customData` custom data used for custom auth, can be anything --- ## DAVDepth ```ts export type DAVDepth = '0' | '1' | 'infinity'; ``` [depth header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.2) of webdav requests | Depth | Action apply to | | -------- | ------------------------------------------ | | 0 | only the resource | | 1 | the resource and its internal members only | | infinity | the resource and all its members | --- ## DAVObject ```ts export type DAVObject = { data?: any; etag?: string; url: string; }; ``` - `data` the raw content of an WEBDAV object. - `etag` the version string of content, if [etag](https://tools.ietf.org/id/draft-reschke-http-etag-on-write-08.html) changed, `data` must have been changed. - `url` url of the WEBDAV object. --- ## DAVRequest(Types) ```ts export type DAVRequest = { headers?: Record; method: DAVMethods | HTTPMethods; body: any; namespace?: string; attributes?: Record; }; ``` [davRequest](../webdav/davRequest.md) init body - `headers` request headers - `headersToExclude` array of keys of the headers you want to exclude - `fetchOptions` options to pass to underlying fetch function - `method` request method - `body` request body - `namespace` default namespace for all xml nodes - `attributes` root node xml attributes --- ## DAVResponse ```ts export type DAVResponse = { raw?: any; href?: string; status: number; statusText: string; ok: boolean; error?: { [key: string]: any }; responsedescription?: string; props?: { [key: string]: { status: number; statusText: string; ok: boolean; value: any } | any }; }; ``` sample DAVResponse ```json { "raw": { "multistatus": { "response": { "href": "/", "propstat": { "prop": { "currentUserPrincipal": { "href": "/123456/principal/" } }, "status": "HTTP/1.1 200 OK" } } } }, "href": "/", "status": 207, "statusText": "Multi-Status", "ok": true, "props": { "currentUserPrincipal": { "href": "/123456/principal/" } } } ``` response type of [davRequest](../webdav/davRequest.md) - `raw` the entire [response](https://datatracker.ietf.org/doc/html/rfc4918#section-14.24) object, useful when need something that is not a prop or href - `href` [content element URI](https://datatracker.ietf.org/doc/html/rfc2518#section-12.3) - `status` fetch response status - `statusText` fetch response statusText - `ok` fetch response ok - `error` error object from error response - `responsedescription` [information about a status response within a Multi-Status](https://datatracker.ietf.org/doc/html/rfc4918#section-14.25) - `props` response [propstat](https://datatracker.ietf.org/doc/html/rfc4918#section-14.22) props with camel case names. --- ## DAVTokens ```ts export type DAVTokens = { access_token?: string; refresh_token?: string; expires_in?: number; id_token?: string; token_type?: string; scope?: string; }; ``` oauth token response - `access_token` oauth access token - `refresh_token` oauth refresh token - `expires_in` token expires time in ms - `id_token` oauth id token - `token_type` oauth token type, usually `Bearer` - `scope` oauth token scope --- ## DAVVCard ```ts export type DAVVCard = DAVObject; ``` alias of [DAVObject](DAVObject.md) --- ## ElementCompact ```ts export interface ElementCompact { [key: string]: any; _declaration?: { _attributes?: DeclarationAttributes; }; _instruction?: { [key: string]: string; }; _attributes?: Attributes; _cdata?: string; _doctype?: string; _comment?: string; _text?: string | number; } ``` [xml-js defined](https://github.com/nashwaan/xml-js/blob/f0376f265c4f299100fb4766828ebf066a0edeec/types/index.d.ts#L11) compact js object representation of xml elements. you can use this [helper](../helper.mdx) to help converting xml between js objects. --- ## AuthHelpers ### getBasicAuthHeaders convert the `username:password` into base64 auth header string: ```ts const result = getBasicAuthHeaders({ username: 'test', password: '12345', }); ``` #### Return Value ```ts { authorization: 'Basic dGVzdDoxMjM0NQ=='; } ``` ### fetchOauthTokens fetch oauth token using code obtained from oauth2 authorization code grant ```ts const tokens = await fetchOauthTokens({ authorizationCode: '123', clientId: 'clientId', clientSecret: 'clientSecret', tokenUrl: 'https://oauth.example.com/tokens', redirectUrl: 'https://yourdomain.com/oauth-callback', }); ``` #### Return Value ```ts { access_token: 'kTKGQ2TBEqn03KJMM9AqIA'; refresh_token: 'iHwWwqytfW3AfOjNbM1HLg'; expires_in: 12800; id_token: 'TKfsafGQ2JMM9AqIA'; token_type: 'bearer'; scope: 'openid email'; } ``` ### refreshAccessToken using refresh token to fetch access token from given token endpoint ```ts const result = await refreshAccessToken({ clientId: 'clientId', clientSecret: 'clientSecret', tokenUrl: 'https://oauth.example.com/tokens', refreshToken: 'iHwWwqytfW3AfOjNbM1HLg', }); ``` #### Return Value ```ts { access_token: 'eeMCxYgdCF3xfLxgd1NE8A'; expires_in: 12800; } ``` ### getOauthHeaders the combination of `fetchOauthTokens` and `refreshAccessToken`, it will return the authorization header needed for authorizing the requests as well as automatically renewing the access token using refresh token obtained from server when it expires. ```ts const result = await getOauthHeaders({ authorizationCode: '123', clientId: 'clientId', clientSecret: 'clientSecret', tokenUrl: 'https://oauth.example.com/tokens', redirectUrl: 'https://yourdomain.com/oauth-callback', }); ``` #### Return Value ```ts { tokens: { access_token: 'kTKGQ2TBEqn03KJMM9AqIA'; refresh_token: 'iHwWwqytfW3AfOjNbM1HLg'; expires_in: 12800; id_token: 'TKfsafGQ2JMM9AqIA'; token_type: 'bearer'; scope: 'openid email'; }, headers: { authorization: `Bearer q-2OCH2g3RctZOJOG9T2Q`, }, } ``` ### defaultParam :::caution Internal function, not intended to be used outside. ::: Provide default parameter for passed in function and allows default parameters be overridden when the function was actually passed with same parameters. would only work on functions that have only one object style parameter. ```ts const fn1 = (params: { a?: number; b?: number }) => { const { a = 0, b = 0 } = params; return a + b; }; const fn2 = defaultParam(fn1, { b: 10 }); ``` ### digest auth and custom auth for digest auth, you need to handle the auth process yourself, pass the final digest string like ``` username="Mufasa", realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41 ``` as `digestString` param in DAVCredentials for custom auth, you can pass additional data via `customData` prop to DAVCredentials, you can pass in your custom auth function as `authFunction` param and will have DAVCredentials available to it. ### getBearerAuthHeaders Generate Bearer authorization headers from an access token (useful for OIDC providers such as Nextcloud when supported). ```ts const result = getBearerAuthHeaders({ accessToken: 'YOUR_OIDC_ACCESS_TOKEN', }); ``` #### Return Value ```ts { authorization: 'Bearer YOUR_OIDC_ACCESS_TOKEN', } ``` --- ## Constants ### DAVNamespace xml namespace enum for convenience | Name | Value | Description | | --------------- | ------------------------------ | ---------------------------- | | CALENDAR_SERVER | http://calendarserver.org/ns/ | calendarserver.org namespace | | CALDAV_APPLE | http://apple.com/ns/ical/ | Apple CALDAV namespace | | CALDAV | urn:ietf:params:xml:ns:caldav | CALDAV namespace | | CARDDAV | urn:ietf:params:xml:ns:carddav | CARDDAV namespace | | DAV | DAV: | WEBDAV namespace | ### DAVNamespaceShort shortened xml namespace enum | Name | Value | | --------------- | ----- | | CALENDAR_SERVER | cs | | CALDAV_APPLE | ca | | CALDAV | c | | CARDDAV | card | | DAV | d | ### DAVAttributeMap map WEBDAV namespace to attributes to allow better readability when dealing with raw xml data | Name | Value | | --------------- | ---------- | | CALDAV | xmlns:c | | CARDDAV | xmlns:card | | CALENDAR_SERVER | xmlns:cs | | CALDAV_APPLE | xmlns:ca | | DAV | xmlns:d | --- ## RequestHelpers :::caution All functions below are intended to only be used internally and may subject to change without notice. ::: ### urlEquals Check if two urls are mostly equal Will first trim out white spaces, and can omit the last `/` For example, `https://www.example.com/` and ` https://www.example.com` are considered equal ### urlContains Basically `urlEqual`, but without length constraint. For example, `https://www.example.com/` and ` www.example.com` are considered contains. ### getDAVAttribute Convert `DAVNamespace` to intended format to be consumed by `xml-js` to be used as `xml` attributes. ### cleanupFalsy Clean up `falsy` values within an object, this is useful when sending headers, Where undefined object property will cause an error. ### excludeHeaders Remove header value by its key value useful when sending headers --- ## Helper Helper to convert xml to expected js object to be consumed by tsdav ### xml -> js `} /> ### js -> xml --- ## Smart calendar sync To actually achieve two way syncing of calendar events between cloud provider and your service. ## preparation You need to #### create database structures to store the calendar info for databases, you need following structures: ##### App calendar your app's calendar object type like: ```ts type AppCalendar = { id: string; userId: string; timezone?: string; name?: string; description?: string; email?: string; createdAt: string; updatedAt: string; } ``` this table is used for your app's display, daily use, etc, optional if you do not alredy have a table like this or you do not want one-to-many relations with your app calendar, you can skip creating this table. ##### Caldav calendar you need to store caldav calendar information obtained from `fetchCalendars` ```typescript type CaldavCalendar = { id: string; userId: string; timezone: string; name: string; source: string; // your caldav provider name ctag: string; // obtained from remote syncToken: string; // obtained from remote url: string; credentialId: createAt: string; }; ``` ##### credentials save caldav calendar credentials in another table, encryption is recommended: ```ts type CalendarCredential = { account: string; refreshToken?: string; password?: string; valid: boolean; source: // your caldav provider name } ``` ##### caldav calendar objects you also need to store caldav calendar objects obtained from `fetchCalendarObjects` ```ts export type CalendarObject = { id: string; calendarId: string; // foreign key reference the CaldavCalendar if needed url: string; etag: string; start: string; // recommend to have this field for easy filtering/sorting end: string; // recommend to have this field for easy filtering/sorting data: string; // actual ics data }; ``` to parse and obtain information from ics data, it's recommended to use a combination of https://github.com/natelindev/pretty-jcal and https://github.com/kewisch/ical.js for generating new ics data, it's recommended to use https://github.com/nwcell/ics.js/ ## Actual syncing First you need to have user go through authorization process and obtain valid `CalendarCredential`, the method differs for each caldav provider. You need to find and setup it yourself. after having obtained the credentials, you can begin the actual sync First you need to get all stored calendars for the user ```ts const localCalendars = await this.db.getCalendarByUserIdAndSource( userId, source ); ``` then you need to create caldav client using credentials ```ts const client = new DAVClient({ serverUrl: 'https://caldav.icloud.com', credentials: { username: 'YOUR_APPLE_ID', password: 'YOUR_APP_SPECIFIC_PASSWORD', }, authMethod: 'Basic', defaultAccountType: 'caldav', }); ``` ##### Remote to local you can use `syncCalendars` function from the lib with `detailedResult` set as `true` ```ts const { created, updated, deleted } = await client.syncCalendars({ oldCalendars: localCalendars.map((lc) => ({ displayName: lc.name, syncToken: lc.syncToken, ctag: lc.ctag, url: lc.url, }) ), detailedResult: true, }); ``` make sure you send the `syncToken` and `ctag`, this way the remote will know your last sync and identify the calendar changes. now you have all calendar changes on remote. make actual changes to your database like: ```ts await this.db.transaction(async (tx) => { await Promise.all(created.map(async (c) => { // created const calendarObjects = await client.fetchCalendarObjects({ calendar: c }) if (calendarObjects.length > 0) { await Promise.all( calendarObjects.map((co) => { // parse start end time if needed // const parsedObject = parse(co); // const { start, end } = parsedObject; return this.db.createCalendarObject(tx, { ...co, // start, // end, calendarId: c.id }) }) ) } })) // deleted if (deleted.length > 0) { await this.db.deleteByUrls( tx, filteredDeleted.map((d) => d.url) ); } // updated const localCalendarsToBeUpdated = await this.db.getByUrls( tx, updated.map((u) => u.url) ); // find out and apply the change on calendar await Promise.all( localCalendarsToBeUpdated.map(async (lc) => { const localObjects = await this.db.getCalendarById( tx, lc.id ); const { created: createdObjects, updated: updatedObjects, deleted: deletedObjects, } = ( await client.smartCollectionSync({ collection: { url: lc.url, ctag: lc.ctag, syncToken: lc.syncToken, objects: localObjects, objectMultiGet: client.calendarMultiGet, }, method: 'webdav', detailedResult: true, }) ).objects; // apply changes to local calendar objects // created objects if (createdObjects.length > 0) { await Promise.all( createdObjects .filter((co) => co.url.includes('.ics')) .map((co) => { // parse start end time if needed // const parsedObject = parse(co); // const { start, end } = parsedObject; return this.db.createCalendarObject(tx, { ...co, // start, // end, url: URL.resolve(lc.url, co.url), calendarId: lc.id, }); }) ); } // deleted objects if (deletedObjects.length > 0) { await this.db.deleteCalendarObjectByUrls( tx, deletedObjects.map((d) => URL.resolve(lc.url, d.url)) ); } // updated objects if (updatedObjects.length > 0) { await Promise.all( updatedObjects.map((uo) => { // parse start end time if needed // const parsedObject = parse(co); // const { start, end } = parsedObject; return this.db.updateCalendarObjectByUrl( tx, URL.resolve(lc.url, uo.url), { etag: uo.etag, data: uo.data, start, end, } ); }) ); } }) ); // update the syncToken & ctag for the calendars to be updated await Promise.all( filteredUpdated.map((u) => { const lcu = localCalendarToBeUpdated.find((uo) => uo.url === u.url); if (!lcu) { throw new ValidationError(`local calendar with url ${u.url} not found `); } return this.db.updateCalendarById(tx, lcu.id, { syncToken: u.syncToken, ctag: u.ctag, }); }) ); }) ``` ##### Local to remote when going local to remote, it's rather easy just generate the ics data and then use `createCalendarObject` , `updateCalendarObject` and `deleteCalendarObject` directly on remote caldav calendars. Update your locally stored calendar objects in your database after remote operation success. --- ## Cloud providers ### Prepare credentials ##### Apple For apple you want to go to [this page](https://support.apple.com/en-us/HT204397) and after following the guide, you will have Apple ID and app-specific password. ##### Google For google you want to go to Google Cloud Platform/Credentials page, then create a credential that suite your use case. You want `clientId` ,`client secret` and after this. Also you need to enable Google CALDAV/CARDDAV for your project. Also you need to setup oauth screen, add needed oauth2 scopes (`https://www.googleapis.com/auth/calendar` for CalDAV), use proper oauth2 grant flow and you might need to get your application verified by google in order to be able to use CALDAV/CARDDAV api. Refer to [this page](https://developers.google.com/identity/protocols/oauth2) for more details. After the oauth2 offline grant you should be able to obtain oauth2 refresh token. ##### Fastmail Generate an app specific password just like apple, follow [this guide](https://www.fastmail.help/hc/en-us/articles/360058752834) for more information. ##### Nextcloud Nextcloud supports CalDAV/CardDAV with Basic auth (username + app password) and Bearer auth (OIDC tokens). - **Basic Auth:** Recommended for most use cases. Use your username and an app-specific password. - **Bearer Auth:** Supported if Nextcloud is configured with OIDC (e.g., via `user_oidc` app). Use `authMethod: 'Bearer'` and provide your access token in `credentials.accessToken`. For browser clients, you may need to use a server-side proxy or trusted domain configuration to avoid CORS issues. A common server URL is: `https:///remote.php/dav` :::info Other cloud providers not listed are not currently tested, in theory any cloud with basic auth and oauth2 should work, stay tuned for updates. ::: ##### ZOHO Please follow [official guide](https://help.zoho.com/portal/en/kb/calendar/syncing-other-calendars/articles/setting-up-caldav-sync-in-zoho-calendar#Configuring_CalDAV_sync_between_Zoho_Calendar_and_your_device) ##### Forward Email Please follow [offical guide](https://forwardemail.net/en/faq#do-you-support-calendars-caldav) ### Caveats Some cloud providers' APIs are not very standard, they might includes several quirks. ##### Google 1. CALDAV will always use UID inside `ics` file as filename, for example if you created a new event with name `1.ics` but have `abc` as its UID inside, you can only access it using `abc.ics`. 2. CARDDAV will always generate a new UID for your `.vcf` files. For example a file created with name `1.vcf` with UID `abc`, it will have a UID like `1bcde2e` inside the actual created data. 3. Both CALDAV and CARDDAV will add custom data entries inside your `ics` and `vcf` files like new `PRODID`, new `TIMETAMP` and some custom `X-` fields. 4. For CARDDAV, you cannot create a `vcf` file with the same name again even if it's already deleted. ##### ZOHO 1. CALDAV will always use UID inside `ics` file as filename, for example if you created a new event with name `1.ics` but have `abc` as its UID inside, you can only access it using `abc.ics`. 2. Both CALDAV and CARDDAV will add custom data entries inside your `ics` and `vcf` files like new `PRODID`, new `TIMETAMP` and some custom `X-` fields. 3. For CALDAV, you cannot create a `ics` file with the same UUID again even if it's already deleted. ##### NextCloud 1. Object deletion will result in original object be renamed rather than actual deletion. This can cause problems at times. ### Platform Specifics #### KaiOS (mozSystem) KaiOS allows making cross-origin requests from privileged apps using `XMLHttpRequest` with `mozSystem: true`. You can use `tsdav` in KaiOS by providing a custom `fetch` implementation that wraps `XMLHttpRequest`. ```typescript const kaiOSFetch = (url, init) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest({ mozSystem: true }); xhr.open(init.method || 'GET', url); if (init.headers) { Object.entries(init.headers).forEach(([key, value]) => { xhr.setRequestHeader(key, value); }); } xhr.onload = () => { resolve({ ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, statusText: xhr.statusText, text: () => Promise.resolve(xhr.responseText), headers: { get: (name) => xhr.getResponseHeader(name), }, }); }; xhr.onerror = () => reject(new Error('Network error')); xhr.send(init.body); }); }; const client = new DAVClient({ serverUrl: '...', credentials: { ... }, fetch: kaiOSFetch as any, }); ``` --- ## Migration ### 1.1.x -> 2.x.x Auto prop/filter converter that can help you transition from 1.1.x to 1.2.x. --- ## Contributing First you need to clone the repo and ### Build ```bash npm run build ``` or ```bash yarn build ``` #### Test to run tests locally, you need to setup environnement variables using `.env` file from `.env.example` ```bash mv .env.example .env ``` and fill in the missing values If you didn't add any new api, you should set MOCK_FETCH="true". If you added new api and need to test against cloud providers, you should set MOCK_FETCH="false" and RECORD_NETWORK_REQUESTS="true" to updated network request mock data. ### WEBDAV quick guide WEBDAV uses xml for all its data when communicating, the basic element is `object`, multiple `object`s can form a `collection`, webdav server have `account`s and an `account` have a `principal` resource (i.e the default, main resource) and under that principal resource we have `home set` of the said resource where your actual resources are. `syncToken` and `ctag` are basically like hash of the object/collection, if anything in it changes, this token will change. For caldav, the calendar data in caldav are in `rfc5545` ical format, there's `iCal2Js` and `js2iCal` function with my other project [pretty-jcal](https://github.com/natelindev/pretty-jcal) to help your convert them from/to js objects. Here's cheat sheet on webdav operations compared with rest: Operation Webdav REST Create collection predefined id: `MKCOL /entities/$predefined_id` no predefined id: not possible can set attributes right away with extended-mkcol extension Status: 201 Created `POST /entities` with JSON body with attributes, response contains new id Status: 200 with new id and optionally the whole object Create entity predefined id: `PUT /entities/$predefined_id` with body, empty response no predefined id: `POST /entities`, receive id as part of Content-Location header can't set attributes right away, need subsequent PROPPATCH Status: 201 Created Update entity body `PUT /entities/$predefined_id` with new body (no attributes) Status: 204 No Content Full: `PUT /entities/$id` with full JSON body with attributes Status 200, receive full object back Partial: `PATCH /entities/$id` with partial JSON containing only attributes to update. Status 200, full/partial object returned Update entity attributes `PROPPATCH /entities/$id` with XML body of attributes to change Status: 207, XML body with accepted attributes Delete entity `DELETE /entities/$id` Sattus: 204 no content List entities `PROPFIND /entities` with XML body of attributes to fetch Status 207 multi-status XML response with multiple entities and their respective attributes `GET /entities` Status: 200 OK, receive JSON response array with JSON body of entity attributes Get entity `GET /entities/$id` Status: 200 OK with entitiy body `GET /entities/$id` Status 200 OK, receive JSON body of entity attributes Get entity attributes `PROPFIND /entities/$id` with XML body of attributes to fetch Status 207 multi-status XML response with entity attributes Notes cannot always set attributes right away at creation time, need subsequent `PROPPATCH` no concept of body vs attributes entity can be either collection or model (for collection `/entities/$collectionId/$itemId`) --- ## LLM Integration This documentation site provides special routes designed for Large Language Models (LLMs) to better understand and interact with the tsdav documentation. Whether you're using ChatGPT, Claude, Cursor, Copilot, or any other AI assistant, these routes make it easy to feed tsdav docs directly into your workflow. ## Available Routes ### Full Documentation #### [`/llms-full.txt`](https://tsdav.vercel.app/llms-full.txt) A plain-text representation of the **entire** documentation, optimized for LLM consumption. This file concatenates all documentation content into a single, easily parseable text file that includes: - Page titles and URLs - Page descriptions - Full content of each documentation page Use this when you want an AI assistant to have complete knowledge of the tsdav library. ### Documentation Index #### [`/llms.txt`](https://tsdav.vercel.app/llms.txt) A lightweight index of all documentation pages with titles, descriptions, and links. Use this to give an LLM an overview of available documentation and let it decide which pages to read in detail. ### Individual Pages as Markdown #### `/docs/[path].md` You can access the raw Markdown source of any individual documentation page by appending `.md` to the docs URL. For example: - [`/docs/intro.md`](https://tsdav.vercel.app/docs/intro.md) — Getting started guide - [`/docs/caldav/fetchCalendars.md`](https://tsdav.vercel.app/docs/caldav/fetchCalendars.md) — CalDAV calendar fetching - [`/docs/carddav/fetchVCards.md`](https://tsdav.vercel.app/docs/carddav/fetchVCards.md) — CardDAV vCard fetching This is useful when you want an AI assistant to focus on a specific topic without loading the entire documentation. ## How to Use with AI Assistants ### ChatGPT / Claude / Other Chat UIs Paste the full documentation URL into your prompt: ``` Please read https://tsdav.vercel.app/llms-full.txt and help me set up CalDAV sync with Google Calendar. ``` ### Cursor / Copilot / AI-Enabled Editors Add the documentation URL as context in your AI-enabled editor: - **Cursor**: Use `@docs` or paste the URL in chat to provide tsdav context - **Copilot**: Reference the docs URL when asking questions about tsdav ### Custom LLM Applications If you're building an application that uses LLMs, you can programmatically fetch the documentation: ```typescript // Fetch full documentation for RAG or context injection const response = await fetch('https://tsdav.vercel.app/llms-full.txt'); const docs = await response.text(); // Or fetch a specific page const calendarDocs = await fetch('https://tsdav.vercel.app/docs/caldav/fetchCalendars.md'); const calendarContent = await calendarDocs.text(); ``` ## What's Included The LLM-friendly output covers all tsdav documentation: | Section | Description | |---------|-------------| | **Introduction** | Library overview, installation, and quick start | | **WebDAV** | Core WebDAV operations — `davRequest`, `propfind`, account discovery | | **CalDAV** | Calendar operations — fetch, create, update, delete calendar objects | | **CardDAV** | Contact operations — fetch, create, update, delete vCards | | **Types** | TypeScript type definitions and models | | **Helpers** | Authentication helpers, request utilities, and constants | | **Smart Calendar Sync** | End-to-end sync workflow with database schema guidance | | **Cloud Providers** | Provider-specific setup for Apple, Google, Fastmail, and more | ## Why LLM-Friendly Docs? - **Faster onboarding**: AI assistants can instantly learn the full API surface - **Better code generation**: With complete type information and examples, AI produces more accurate code - **Reduced hallucination**: Grounding AI responses in actual documentation prevents incorrect API usage - **Always up-to-date**: The routes serve the latest built documentation automatically