UNPKG

tsdav

Version:

WebDAV, CALDAV, and CARDDAV client for Nodejs and the Browser

1,150 lines (1,137 loc) 78.2 kB
import { fetch } from 'cross-fetch'; import getLogger from 'debug'; import convert from 'xml-js'; import { encode } from 'base-64'; var DAVNamespace; (function (DAVNamespace) { DAVNamespace["CALENDAR_SERVER"] = "http://calendarserver.org/ns/"; DAVNamespace["CALDAV_APPLE"] = "http://apple.com/ns/ical/"; DAVNamespace["CALDAV"] = "urn:ietf:params:xml:ns:caldav"; DAVNamespace["CARDDAV"] = "urn:ietf:params:xml:ns:carddav"; DAVNamespace["DAV"] = "DAV:"; })(DAVNamespace || (DAVNamespace = {})); const DAVAttributeMap = { [DAVNamespace.CALDAV]: 'xmlns:c', [DAVNamespace.CARDDAV]: 'xmlns:card', [DAVNamespace.CALENDAR_SERVER]: 'xmlns:cs', [DAVNamespace.CALDAV_APPLE]: 'xmlns:ca', [DAVNamespace.DAV]: 'xmlns:d', }; var DAVNamespaceShort; (function (DAVNamespaceShort) { DAVNamespaceShort["CALDAV"] = "c"; DAVNamespaceShort["CARDDAV"] = "card"; DAVNamespaceShort["CALENDAR_SERVER"] = "cs"; DAVNamespaceShort["CALDAV_APPLE"] = "ca"; DAVNamespaceShort["DAV"] = "d"; })(DAVNamespaceShort || (DAVNamespaceShort = {})); var ICALObjects; (function (ICALObjects) { ICALObjects["VEVENT"] = "VEVENT"; ICALObjects["VTODO"] = "VTODO"; ICALObjects["VJOURNAL"] = "VJOURNAL"; ICALObjects["VFREEBUSY"] = "VFREEBUSY"; ICALObjects["VTIMEZONE"] = "VTIMEZONE"; ICALObjects["VALARM"] = "VALARM"; })(ICALObjects || (ICALObjects = {})); const camelCase = (str) => str.replace(/([-_]\w)/g, (g) => g[1].toUpperCase()); const nativeType = (value) => { const nValue = Number(value); if (!Number.isNaN(nValue)) { return nValue; } const bValue = value.toLowerCase(); if (bValue === 'true') { return true; } if (bValue === 'false') { return false; } return value; }; const urlEquals = (urlA, urlB) => { if (!urlA && !urlB) { return true; } if (!urlA || !urlB) { return false; } const trimmedUrlA = urlA.trim(); const trimmedUrlB = urlB.trim(); if (Math.abs(trimmedUrlA.length - trimmedUrlB.length) > 1) { return false; } const strippedUrlA = trimmedUrlA.slice(-1) === '/' ? trimmedUrlA.slice(0, -1) : trimmedUrlA; const strippedUrlB = trimmedUrlB.slice(-1) === '/' ? trimmedUrlB.slice(0, -1) : trimmedUrlB; return urlA.includes(strippedUrlB) || urlB.includes(strippedUrlA); }; const urlContains = (urlA, urlB) => { if (!urlA && !urlB) { return true; } if (!urlA || !urlB) { return false; } const trimmedUrlA = urlA.trim(); const trimmedUrlB = urlB.trim(); const strippedUrlA = trimmedUrlA.slice(-1) === '/' ? trimmedUrlA.slice(0, -1) : trimmedUrlA; const strippedUrlB = trimmedUrlB.slice(-1) === '/' ? trimmedUrlB.slice(0, -1) : trimmedUrlB; return urlA.includes(strippedUrlB) || urlB.includes(strippedUrlA); }; const getDAVAttribute = (nsArr) => nsArr.reduce((prev, curr) => ({ ...prev, [DAVAttributeMap[curr]]: curr }), {}); const cleanupFalsy = (obj) => Object.entries(obj).reduce((prev, [key, value]) => { if (value) return { ...prev, [key]: value }; return prev; }, {}); const conditionalParam = (key, param) => { if (param) { return { [key]: param, }; } return {}; }; const excludeHeaders = (headers, headersToExclude) => { if (!headers) { return {}; } if (!headersToExclude || headersToExclude.length === 0) { return headers; } return Object.fromEntries(Object.entries(headers).filter(([key]) => !headersToExclude.includes(key))); }; var requestHelpers = /*#__PURE__*/Object.freeze({ __proto__: null, cleanupFalsy: cleanupFalsy, conditionalParam: conditionalParam, excludeHeaders: excludeHeaders, getDAVAttribute: getDAVAttribute, urlContains: urlContains, urlEquals: urlEquals }); const debug$5 = getLogger('tsdav:request'); const davRequest = async (params) => { var _a; const { url, init, convertIncoming = true, parseOutgoing = true, fetchOptions = {} } = params; const { headers = {}, body, namespace, method, attributes } = init; const xmlBody = convertIncoming ? convert.js2xml({ _declaration: { _attributes: { version: '1.0', encoding: 'utf-8' } }, ...body, _attributes: attributes, }, { compact: true, spaces: 2, elementNameFn: (name) => { // add namespace to all keys without namespace if (namespace && !/^.+:.+/.test(name)) { return `${namespace}:${name}`; } return name; }, }) : body; // debug('outgoing xml:'); // debug(`${method} ${url}`); // debug( // `headers: ${JSON.stringify( // { // 'Content-Type': 'text/xml;charset=UTF-8', // ...cleanupFalsy(headers), // }, // null, // 2 // )}` // ); // debug(xmlBody); const fetchOptionsWithoutHeaders = { ...fetchOptions }; delete fetchOptionsWithoutHeaders.headers; const davResponse = await fetch(url, { headers: { 'Content-Type': 'text/xml;charset=UTF-8', ...cleanupFalsy(headers), ...(fetchOptions.headers || {}) }, body: xmlBody, method, ...fetchOptionsWithoutHeaders, }); const resText = await davResponse.text(); // filter out invalid responses // debug('response xml:'); // debug(resText); // debug(davResponse); if (!davResponse.ok || !((_a = davResponse.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('xml')) || !parseOutgoing) { return [ { href: davResponse.url, ok: davResponse.ok, status: davResponse.status, statusText: davResponse.statusText, raw: resText, }, ]; } const result = convert.xml2js(resText, { compact: true, trim: true, textFn: (value, parentElement) => { try { // This is needed for xml-js design reasons // eslint-disable-next-line no-underscore-dangle const parentOfParent = parentElement._parent; const pOpKeys = Object.keys(parentOfParent); const keyNo = pOpKeys.length; const keyName = pOpKeys[keyNo - 1]; const arrOfKey = parentOfParent[keyName]; const arrOfKeyLen = arrOfKey.length; if (arrOfKeyLen > 0) { const arr = arrOfKey; const arrIndex = arrOfKey.length - 1; arr[arrIndex] = nativeType(value); } else { parentOfParent[keyName] = nativeType(value); } } catch (e) { debug$5(e.stack); } }, // remove namespace & camelCase elementNameFn: (attributeName) => camelCase(attributeName.replace(/^.+:/, '')), attributesFn: (value) => { const newVal = { ...value }; delete newVal.xmlns; return newVal; }, ignoreDeclaration: true, }); const responseBodies = Array.isArray(result.multistatus.response) ? result.multistatus.response : [result.multistatus.response]; return responseBodies.map((responseBody) => { var _a, _b; const statusRegex = /^\S+\s(?<status>\d+)\s(?<statusText>.+)$/; if (!responseBody) { return { status: davResponse.status, statusText: davResponse.statusText, ok: davResponse.ok, }; } const matchArr = statusRegex.exec(responseBody.status); return { raw: result, href: responseBody.href, status: (matchArr === null || matchArr === void 0 ? void 0 : matchArr.groups) ? Number.parseInt(matchArr === null || matchArr === void 0 ? void 0 : matchArr.groups.status, 10) : davResponse.status, statusText: (_b = (_a = matchArr === null || matchArr === void 0 ? void 0 : matchArr.groups) === null || _a === void 0 ? void 0 : _a.statusText) !== null && _b !== void 0 ? _b : davResponse.statusText, ok: !responseBody.error, error: responseBody.error, responsedescription: responseBody.responsedescription, props: (Array.isArray(responseBody.propstat) ? responseBody.propstat : [responseBody.propstat]).reduce((prev, curr) => { return { ...prev, ...curr === null || curr === void 0 ? void 0 : curr.prop, }; }, {}), }; }); }; const propfind = async (params) => { const { url, props, depth, headers, headersToExclude, fetchOptions = {} } = params; return davRequest({ url, init: { method: 'PROPFIND', headers: excludeHeaders(cleanupFalsy({ depth, ...headers }), headersToExclude), namespace: DAVNamespaceShort.DAV, body: { propfind: { _attributes: getDAVAttribute([ DAVNamespace.CALDAV, DAVNamespace.CALDAV_APPLE, DAVNamespace.CALENDAR_SERVER, DAVNamespace.CARDDAV, DAVNamespace.DAV, ]), prop: props, }, }, }, fetchOptions, }); }; const createObject = async (params) => { const { url, data, headers, headersToExclude, fetchOptions = {} } = params; return fetch(url, { method: 'PUT', body: data, headers: excludeHeaders(headers, headersToExclude), ...fetchOptions, }); }; const updateObject = async (params) => { const { url, data, etag, headers, headersToExclude, fetchOptions = {} } = params; return fetch(url, { method: 'PUT', body: data, headers: excludeHeaders(cleanupFalsy({ 'If-Match': etag, ...headers }), headersToExclude), ...fetchOptions, }); }; const deleteObject = async (params) => { const { url, headers, etag, headersToExclude, fetchOptions = {} } = params; return fetch(url, { method: 'DELETE', headers: excludeHeaders(cleanupFalsy({ 'If-Match': etag, ...headers }), headersToExclude), ...fetchOptions, }); }; var request = /*#__PURE__*/Object.freeze({ __proto__: null, createObject: createObject, davRequest: davRequest, deleteObject: deleteObject, propfind: propfind, updateObject: updateObject }); function hasFields(obj, fields) { const inObj = (object) => fields.every((f) => object[f]); if (Array.isArray(obj)) { return obj.every((o) => inObj(o)); } return inObj(obj); } const findMissingFieldNames = (obj, fields) => fields.reduce((prev, curr) => (obj[curr] ? prev : `${prev.length ? `${prev},` : ''}${curr.toString()}`), ''); /* eslint-disable no-underscore-dangle */ const debug$4 = getLogger('tsdav:collection'); const collectionQuery = async (params) => { const { url, body, depth, defaultNamespace = DAVNamespaceShort.DAV, headers, headersToExclude, fetchOptions = {} } = params; const queryResults = await davRequest({ url, init: { method: 'REPORT', headers: excludeHeaders(cleanupFalsy({ depth, ...headers }), headersToExclude), namespace: defaultNamespace, body, }, fetchOptions, }); // empty query result if (queryResults.length === 1 && !queryResults[0].raw) { return []; } return queryResults; }; const makeCollection = async (params) => { const { url, props, depth, headers, headersToExclude, fetchOptions = {} } = params; return davRequest({ url, init: { method: 'MKCOL', headers: excludeHeaders(cleanupFalsy({ depth, ...headers }), headersToExclude), namespace: DAVNamespaceShort.DAV, body: props ? { mkcol: { set: { prop: props, }, }, } : undefined, }, fetchOptions }); }; const supportedReportSet = async (params) => { var _a, _b, _c, _d, _e; const { collection, headers, headersToExclude, fetchOptions = {} } = params; const res = await propfind({ url: collection.url, props: { [`${DAVNamespaceShort.DAV}:supported-report-set`]: {}, }, depth: '0', headers: excludeHeaders(headers, headersToExclude), fetchOptions }); return ((_e = (_d = (_c = (_b = (_a = res[0]) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.supportedReportSet) === null || _c === void 0 ? void 0 : _c.supportedReport) === null || _d === void 0 ? void 0 : _d.map((sr) => Object.keys(sr.report)[0])) !== null && _e !== void 0 ? _e : []); }; const isCollectionDirty = async (params) => { var _a, _b, _c; const { collection, headers, headersToExclude, fetchOptions = {} } = params; const responses = await propfind({ url: collection.url, props: { [`${DAVNamespaceShort.CALENDAR_SERVER}:getctag`]: {}, }, depth: '0', headers: excludeHeaders(headers, headersToExclude), fetchOptions }); const res = responses.filter((r) => urlContains(collection.url, r.href))[0]; if (!res) { throw new Error('Collection does not exist on server'); } return { isDirty: `${collection.ctag}` !== `${(_a = res.props) === null || _a === void 0 ? void 0 : _a.getctag}`, newCtag: (_c = (_b = res.props) === null || _b === void 0 ? void 0 : _b.getctag) === null || _c === void 0 ? void 0 : _c.toString(), }; }; /** * This is for webdav sync-collection only */ const syncCollection = (params) => { const { url, props, headers, syncLevel, syncToken, headersToExclude, fetchOptions } = params; return davRequest({ url, init: { method: 'REPORT', namespace: DAVNamespaceShort.DAV, headers: excludeHeaders({ ...headers }, headersToExclude), body: { 'sync-collection': { _attributes: getDAVAttribute([ DAVNamespace.CALDAV, DAVNamespace.CARDDAV, DAVNamespace.DAV, ]), 'sync-level': syncLevel, 'sync-token': syncToken, [`${DAVNamespaceShort.DAV}:prop`]: props, }, }, }, fetchOptions }); }; /** remote collection to local */ const smartCollectionSync = async (params) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; const { collection, method, headers, headersToExclude, account, detailedResult, fetchOptions = {} } = params; const requiredFields = ['accountType', 'homeUrl']; if (!account || !hasFields(account, requiredFields)) { if (!account) { throw new Error('no account for smartCollectionSync'); } throw new Error(`account must have ${findMissingFieldNames(account, requiredFields)} before smartCollectionSync`); } const syncMethod = method !== null && method !== void 0 ? method : (((_a = collection.reports) === null || _a === void 0 ? void 0 : _a.includes('syncCollection')) ? 'webdav' : 'basic'); debug$4(`smart collection sync with type ${account.accountType} and method ${syncMethod}`); if (syncMethod === 'webdav') { const result = await syncCollection({ url: collection.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${account.accountType === 'caldav' ? DAVNamespaceShort.CALDAV : DAVNamespaceShort.CARDDAV}:${account.accountType === 'caldav' ? 'calendar-data' : 'address-data'}`]: {}, [`${DAVNamespaceShort.DAV}:displayname`]: {}, }, syncLevel: 1, syncToken: collection.syncToken, headers: excludeHeaders(headers, headersToExclude), fetchOptions }); const objectResponses = result.filter((r) => { var _a; const extName = account.accountType === 'caldav' ? '.ics' : '.vcf'; return ((_a = r.href) === null || _a === void 0 ? void 0 : _a.slice(-4)) === extName; }); const changedObjectUrls = objectResponses.filter((o) => o.status !== 404).map((r) => r.href); const deletedObjectUrls = objectResponses.filter((o) => o.status === 404).map((r) => r.href); const multiGetObjectResponse = changedObjectUrls.length ? ((_c = (await ((_b = collection === null || collection === void 0 ? void 0 : collection.objectMultiGet) === null || _b === void 0 ? void 0 : _b.call(collection, { url: collection.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${account.accountType === 'caldav' ? DAVNamespaceShort.CALDAV : DAVNamespaceShort.CARDDAV}:${account.accountType === 'caldav' ? 'calendar-data' : 'address-data'}`]: {}, }, objectUrls: changedObjectUrls, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions })))) !== null && _c !== void 0 ? _c : []) : []; const remoteObjects = multiGetObjectResponse.map((res) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; return { url: (_a = res.href) !== null && _a !== void 0 ? _a : '', etag: (_b = res.props) === null || _b === void 0 ? void 0 : _b.getetag, data: (account === null || account === void 0 ? void 0 : account.accountType) === 'caldav' ? ((_e = (_d = (_c = res.props) === null || _c === void 0 ? void 0 : _c.calendarData) === null || _d === void 0 ? void 0 : _d._cdata) !== null && _e !== void 0 ? _e : (_f = res.props) === null || _f === void 0 ? void 0 : _f.calendarData) : ((_j = (_h = (_g = res.props) === null || _g === void 0 ? void 0 : _g.addressData) === null || _h === void 0 ? void 0 : _h._cdata) !== null && _j !== void 0 ? _j : (_k = res.props) === null || _k === void 0 ? void 0 : _k.addressData), }; }); const localObjects = (_d = collection.objects) !== null && _d !== void 0 ? _d : []; // no existing url const created = remoteObjects.filter((o) => localObjects.every((lo) => !urlContains(lo.url, o.url))); // debug(`created objects: ${created.map((o) => o.url).join('\n')}`); // have same url, but etag different const updated = localObjects.reduce((prev, curr) => { const found = remoteObjects.find((ro) => urlContains(ro.url, curr.url)); if (found && found.etag && found.etag !== curr.etag) { return [...prev, found]; } return prev; }, []); // debug(`updated objects: ${updated.map((o) => o.url).join('\n')}`); const deleted = deletedObjectUrls.map((o) => ({ url: o, etag: '', })); // debug(`deleted objects: ${deleted.map((o) => o.url).join('\n')}`); const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlContains(lo.url, ro.url) && ro.etag === lo.etag)); return { ...collection, objects: detailedResult ? { created, updated, deleted } : [...unchanged, ...created, ...updated], // all syncToken in the results are the same so we use the first one here syncToken: (_h = (_g = (_f = (_e = result[0]) === null || _e === void 0 ? void 0 : _e.raw) === null || _f === void 0 ? void 0 : _f.multistatus) === null || _g === void 0 ? void 0 : _g.syncToken) !== null && _h !== void 0 ? _h : collection.syncToken, }; } if (syncMethod === 'basic') { const { isDirty, newCtag } = await isCollectionDirty({ collection, headers: excludeHeaders(headers, headersToExclude), fetchOptions }); const localObjects = (_j = collection.objects) !== null && _j !== void 0 ? _j : []; const remoteObjects = (_l = (await ((_k = collection.fetchObjects) === null || _k === void 0 ? void 0 : _k.call(collection, { collection, headers: excludeHeaders(headers, headersToExclude), fetchOptions })))) !== null && _l !== void 0 ? _l : []; // no existing url const created = remoteObjects.filter((ro) => localObjects.every((lo) => !urlContains(lo.url, ro.url))); // debug(`created objects: ${created.map((o) => o.url).join('\n')}`); // have same url, but etag different const updated = localObjects.reduce((prev, curr) => { const found = remoteObjects.find((ro) => urlContains(ro.url, curr.url)); if (found && found.etag && found.etag !== curr.etag) { return [...prev, found]; } return prev; }, []); // debug(`updated objects: ${updated.map((o) => o.url).join('\n')}`); // does not present in remote const deleted = localObjects.filter((cal) => remoteObjects.every((ro) => !urlContains(ro.url, cal.url))); // debug(`deleted objects: ${deleted.map((o) => o.url).join('\n')}`); const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlContains(lo.url, ro.url) && ro.etag === lo.etag)); if (isDirty) { return { ...collection, objects: detailedResult ? { created, updated, deleted } : [...unchanged, ...created, ...updated], ctag: newCtag, }; } } return detailedResult ? { ...collection, objects: { created: [], updated: [], deleted: [], }, } : collection; }; var collection = /*#__PURE__*/Object.freeze({ __proto__: null, collectionQuery: collectionQuery, isCollectionDirty: isCollectionDirty, makeCollection: makeCollection, smartCollectionSync: smartCollectionSync, supportedReportSet: supportedReportSet, syncCollection: syncCollection }); /* eslint-disable no-underscore-dangle */ const debug$3 = getLogger('tsdav:addressBook'); const addressBookQuery = async (params) => { const { url, props, filters, depth, headers, headersToExclude, fetchOptions = {} } = params; return collectionQuery({ url, body: { 'addressbook-query': cleanupFalsy({ _attributes: getDAVAttribute([DAVNamespace.CARDDAV, DAVNamespace.DAV]), [`${DAVNamespaceShort.DAV}:prop`]: props, filter: filters !== null && filters !== void 0 ? filters : { 'prop-filter': { _attributes: { name: 'FN', }, }, }, }), }, defaultNamespace: DAVNamespaceShort.CARDDAV, depth, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); }; const addressBookMultiGet = async (params) => { const { url, props, objectUrls, depth, headers, headersToExclude, fetchOptions = {} } = params; return collectionQuery({ url, body: { 'addressbook-multiget': cleanupFalsy({ _attributes: getDAVAttribute([DAVNamespace.DAV, DAVNamespace.CARDDAV]), [`${DAVNamespaceShort.DAV}:prop`]: props, [`${DAVNamespaceShort.DAV}:href`]: objectUrls, }), }, defaultNamespace: DAVNamespaceShort.CARDDAV, depth, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); }; const fetchAddressBooks = async (params) => { const { account, headers, props: customProps, headersToExclude, fetchOptions = {}, } = params !== null && params !== void 0 ? params : {}; const requiredFields = ['homeUrl', 'rootUrl']; if (!account || !hasFields(account, requiredFields)) { if (!account) { throw new Error('no account for fetchAddressBooks'); } throw new Error(`account must have ${findMissingFieldNames(account, requiredFields)} before fetchAddressBooks`); } const res = await propfind({ url: account.homeUrl, props: customProps !== null && customProps !== void 0 ? customProps : { [`${DAVNamespaceShort.DAV}:displayname`]: {}, [`${DAVNamespaceShort.CALENDAR_SERVER}:getctag`]: {}, [`${DAVNamespaceShort.DAV}:resourcetype`]: {}, [`${DAVNamespaceShort.DAV}:sync-token`]: {}, }, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); return Promise.all(res .filter((r) => { var _a, _b; return Object.keys((_b = (_a = r.props) === null || _a === void 0 ? void 0 : _a.resourcetype) !== null && _b !== void 0 ? _b : {}).includes('addressbook'); }) .map((rs) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const displayName = (_c = (_b = (_a = rs.props) === null || _a === void 0 ? void 0 : _a.displayname) === null || _b === void 0 ? void 0 : _b._cdata) !== null && _c !== void 0 ? _c : (_d = rs.props) === null || _d === void 0 ? void 0 : _d.displayname; debug$3(`Found address book named ${typeof displayName === 'string' ? displayName : ''}, props: ${JSON.stringify(rs.props)}`); return { url: new URL((_e = rs.href) !== null && _e !== void 0 ? _e : '', (_f = account.rootUrl) !== null && _f !== void 0 ? _f : '').href, ctag: (_g = rs.props) === null || _g === void 0 ? void 0 : _g.getctag, displayName: typeof displayName === 'string' ? displayName : '', resourcetype: Object.keys((_h = rs.props) === null || _h === void 0 ? void 0 : _h.resourcetype), syncToken: (_j = rs.props) === null || _j === void 0 ? void 0 : _j.syncToken, }; }) .map(async (addr) => ({ ...addr, reports: await supportedReportSet({ collection: addr, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }), }))); }; const fetchVCards = async (params) => { const { addressBook, headers, objectUrls, headersToExclude, urlFilter = (url) => url, useMultiGet = true, fetchOptions = {}, } = params; debug$3(`Fetching vcards from ${addressBook === null || addressBook === void 0 ? void 0 : addressBook.url}`); const requiredFields = ['url']; if (!addressBook || !hasFields(addressBook, requiredFields)) { if (!addressBook) { throw new Error('cannot fetchVCards for undefined addressBook'); } throw new Error(`addressBook must have ${findMissingFieldNames(addressBook, requiredFields)} before fetchVCards`); } const vcardUrls = (objectUrls !== null && objectUrls !== void 0 ? objectUrls : // fetch all objects of the calendar (await addressBookQuery({ url: addressBook.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {} }, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, })).map((res) => { var _a; return (res.ok ? ((_a = res.href) !== null && _a !== void 0 ? _a : '') : ''); })) .map((url) => (url.startsWith('http') || !url ? url : new URL(url, addressBook.url).href)) .filter(urlFilter) .map((url) => new URL(url).pathname); let vCardResults = []; if (vcardUrls.length > 0) { if (useMultiGet) { vCardResults = await addressBookMultiGet({ url: addressBook.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CARDDAV}:address-data`]: {}, }, objectUrls: vcardUrls, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); } else { vCardResults = await addressBookQuery({ url: addressBook.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CARDDAV}:address-data`]: {}, }, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); } } return vCardResults.map((res) => { var _a, _b, _c, _d, _e, _f; return ({ url: new URL((_a = res.href) !== null && _a !== void 0 ? _a : '', addressBook.url).href, etag: (_b = res.props) === null || _b === void 0 ? void 0 : _b.getetag, data: (_e = (_d = (_c = res.props) === null || _c === void 0 ? void 0 : _c.addressData) === null || _d === void 0 ? void 0 : _d._cdata) !== null && _e !== void 0 ? _e : (_f = res.props) === null || _f === void 0 ? void 0 : _f.addressData, }); }); }; const createVCard = async (params) => { const { addressBook, vCardString, filename, headers, headersToExclude, fetchOptions = {}, } = params; return createObject({ url: new URL(filename, addressBook.url).href, data: vCardString, headers: excludeHeaders({ 'content-type': 'text/vcard; charset=utf-8', 'If-None-Match': '*', ...headers, }, headersToExclude), fetchOptions, }); }; const updateVCard = async (params) => { const { vCard, headers, headersToExclude, fetchOptions = {} } = params; return updateObject({ url: vCard.url, data: vCard.data, etag: vCard.etag, headers: excludeHeaders({ 'content-type': 'text/vcard; charset=utf-8', ...headers, }, headersToExclude), fetchOptions, }); }; const deleteVCard = async (params) => { const { vCard, headers, headersToExclude, fetchOptions = {} } = params; return deleteObject({ url: vCard.url, etag: vCard.etag, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); }; var addressBook = /*#__PURE__*/Object.freeze({ __proto__: null, addressBookMultiGet: addressBookMultiGet, addressBookQuery: addressBookQuery, createVCard: createVCard, deleteVCard: deleteVCard, fetchAddressBooks: fetchAddressBooks, fetchVCards: fetchVCards, updateVCard: updateVCard }); /* eslint-disable no-underscore-dangle */ const debug$2 = getLogger('tsdav:calendar'); const fetchCalendarUserAddresses = async (params) => { var _a, _b, _c; const { account, headers, headersToExclude, fetchOptions = {} } = params; const requiredFields = ['principalUrl', 'rootUrl']; if (!hasFields(account, requiredFields)) { throw new Error(`account must have ${findMissingFieldNames(account, requiredFields)} before fetchUserAddresses`); } debug$2(`Fetch user addresses from ${account.principalUrl}`); const responses = await propfind({ url: account.principalUrl, props: { [`${DAVNamespaceShort.CALDAV}:calendar-user-address-set`]: {} }, depth: '0', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); const matched = responses.find((r) => urlContains(account.principalUrl, r.href)); if (!matched || !matched.ok) { throw new Error('cannot find calendarUserAddresses'); } const addresses = ((_c = (_b = (_a = matched === null || matched === void 0 ? void 0 : matched.props) === null || _a === void 0 ? void 0 : _a.calendarUserAddressSet) === null || _b === void 0 ? void 0 : _b.href) === null || _c === void 0 ? void 0 : _c.filter(Boolean)) || []; debug$2(`Fetched calendar user addresses ${addresses}`); return addresses; }; const calendarQuery = async (params) => { const { url, props, filters, timezone, depth, headers, headersToExclude, fetchOptions = {}, } = params; return collectionQuery({ url, body: { 'calendar-query': cleanupFalsy({ _attributes: getDAVAttribute([ DAVNamespace.CALDAV, DAVNamespace.CALENDAR_SERVER, DAVNamespace.CALDAV_APPLE, DAVNamespace.DAV, ]), [`${DAVNamespaceShort.DAV}:prop`]: props, filter: filters, timezone, }), }, defaultNamespace: DAVNamespaceShort.CALDAV, depth, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); }; const calendarMultiGet = async (params) => { const { url, props, objectUrls, filters, timezone, depth, headers, headersToExclude, fetchOptions = {}, } = params; return collectionQuery({ url, body: { 'calendar-multiget': cleanupFalsy({ _attributes: getDAVAttribute([DAVNamespace.DAV, DAVNamespace.CALDAV]), [`${DAVNamespaceShort.DAV}:prop`]: props, [`${DAVNamespaceShort.DAV}:href`]: objectUrls, filter: filters, timezone, }), }, defaultNamespace: DAVNamespaceShort.CALDAV, depth, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); }; const makeCalendar = async (params) => { const { url, props, depth, headers, headersToExclude, fetchOptions = {} } = params; return davRequest({ url, init: { method: 'MKCALENDAR', headers: excludeHeaders(cleanupFalsy({ depth, ...headers }), headersToExclude), namespace: DAVNamespaceShort.DAV, body: { [`${DAVNamespaceShort.CALDAV}:mkcalendar`]: { _attributes: getDAVAttribute([ DAVNamespace.DAV, DAVNamespace.CALDAV, DAVNamespace.CALDAV_APPLE, ]), set: { prop: props, }, }, }, }, fetchOptions, }); }; const fetchCalendars = async (params) => { const { headers, account, props: customProps, projectedProps, headersToExclude, fetchOptions = {}, } = params !== null && params !== void 0 ? params : {}; const requiredFields = ['homeUrl', 'rootUrl']; if (!account || !hasFields(account, requiredFields)) { if (!account) { throw new Error('no account for fetchCalendars'); } throw new Error(`account must have ${findMissingFieldNames(account, requiredFields)} before fetchCalendars`); } const res = await propfind({ url: account.homeUrl, props: customProps !== null && customProps !== void 0 ? customProps : { [`${DAVNamespaceShort.CALDAV}:calendar-description`]: {}, [`${DAVNamespaceShort.CALDAV}:calendar-timezone`]: {}, [`${DAVNamespaceShort.DAV}:displayname`]: {}, [`${DAVNamespaceShort.CALDAV_APPLE}:calendar-color`]: {}, [`${DAVNamespaceShort.CALENDAR_SERVER}:getctag`]: {}, [`${DAVNamespaceShort.DAV}:resourcetype`]: {}, [`${DAVNamespaceShort.CALDAV}:supported-calendar-component-set`]: {}, [`${DAVNamespaceShort.DAV}:sync-token`]: {}, }, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); return Promise.all(res .filter((r) => { var _a, _b; return Object.keys((_b = (_a = r.props) === null || _a === void 0 ? void 0 : _a.resourcetype) !== null && _b !== void 0 ? _b : {}).includes('calendar'); }) .filter((rc) => { var _a, _b, _c, _d, _e, _f; // filter out none iCal format calendars. const components = Array.isArray((_b = (_a = rc.props) === null || _a === void 0 ? void 0 : _a.supportedCalendarComponentSet) === null || _b === void 0 ? void 0 : _b.comp) ? (_c = rc.props) === null || _c === void 0 ? void 0 : _c.supportedCalendarComponentSet.comp.map((sc) => sc._attributes.name) : [(_f = (_e = (_d = rc.props) === null || _d === void 0 ? void 0 : _d.supportedCalendarComponentSet) === null || _e === void 0 ? void 0 : _e.comp) === null || _f === void 0 ? void 0 : _f._attributes.name]; return components.some((c) => Object.values(ICALObjects).includes(c)); }) .map((rs) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r; // debug(`Found calendar ${rs.props?.displayname}`); const description = (_a = rs.props) === null || _a === void 0 ? void 0 : _a.calendarDescription; const timezone = (_b = rs.props) === null || _b === void 0 ? void 0 : _b.calendarTimezone; return { description: typeof description === 'string' ? description : '', timezone: typeof timezone === 'string' ? timezone : '', url: new URL((_c = rs.href) !== null && _c !== void 0 ? _c : '', (_d = account.rootUrl) !== null && _d !== void 0 ? _d : '').href, ctag: (_e = rs.props) === null || _e === void 0 ? void 0 : _e.getctag, calendarColor: (_f = rs.props) === null || _f === void 0 ? void 0 : _f.calendarColor, displayName: (_h = (_g = rs.props) === null || _g === void 0 ? void 0 : _g.displayname._cdata) !== null && _h !== void 0 ? _h : (_j = rs.props) === null || _j === void 0 ? void 0 : _j.displayname, components: Array.isArray((_k = rs.props) === null || _k === void 0 ? void 0 : _k.supportedCalendarComponentSet.comp) ? (_l = rs.props) === null || _l === void 0 ? void 0 : _l.supportedCalendarComponentSet.comp.map((sc) => sc._attributes.name) : [(_o = (_m = rs.props) === null || _m === void 0 ? void 0 : _m.supportedCalendarComponentSet.comp) === null || _o === void 0 ? void 0 : _o._attributes.name], resourcetype: Object.keys((_p = rs.props) === null || _p === void 0 ? void 0 : _p.resourcetype), syncToken: (_q = rs.props) === null || _q === void 0 ? void 0 : _q.syncToken, ...conditionalParam('projectedProps', Object.fromEntries(Object.entries((_r = rs.props) !== null && _r !== void 0 ? _r : {}).filter(([key]) => projectedProps === null || projectedProps === void 0 ? void 0 : projectedProps[key]))), }; }) .map(async (cal) => ({ ...cal, reports: await supportedReportSet({ collection: cal, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }), }))); }; const fetchCalendarObjects = async (params) => { const { calendar, objectUrls, filters: customFilters, timeRange, headers, expand, urlFilter = (url) => Boolean(url === null || url === void 0 ? void 0 : url.includes('.ics')), useMultiGet = true, headersToExclude, fetchOptions = {}, } = params; if (timeRange) { // validate timeRange const ISO_8601 = /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i; const ISO_8601_FULL = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i; if ((!ISO_8601.test(timeRange.start) || !ISO_8601.test(timeRange.end)) && (!ISO_8601_FULL.test(timeRange.start) || !ISO_8601_FULL.test(timeRange.end))) { throw new Error('invalid timeRange format, not in ISO8601'); } } debug$2(`Fetching calendar objects from ${calendar === null || calendar === void 0 ? void 0 : calendar.url}`); const requiredFields = ['url']; if (!calendar || !hasFields(calendar, requiredFields)) { if (!calendar) { throw new Error('cannot fetchCalendarObjects for undefined calendar'); } throw new Error(`calendar must have ${findMissingFieldNames(calendar, requiredFields)} before fetchCalendarObjects`); } // default to fetch all const filters = customFilters !== null && customFilters !== void 0 ? customFilters : [ { 'comp-filter': { _attributes: { name: 'VCALENDAR', }, 'comp-filter': { _attributes: { name: 'VEVENT', }, ...(timeRange ? { 'time-range': { _attributes: { start: `${new Date(timeRange.start) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, end: `${new Date(timeRange.end) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, }, }, } : {}), }, }, }, ]; const calendarObjectUrls = (objectUrls !== null && objectUrls !== void 0 ? objectUrls : // fetch all objects of the calendar (await calendarQuery({ url: calendar.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: { ...(expand && timeRange ? { [`${DAVNamespaceShort.CALDAV}:expand`]: { _attributes: { start: `${new Date(timeRange.start) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, end: `${new Date(timeRange.end) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, }, }, } : {}), }, }, filters, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, })).map((res) => { var _a; return (_a = res.href) !== null && _a !== void 0 ? _a : ''; })) .map((url) => (url.startsWith('http') || !url ? url : new URL(url, calendar.url).href)) // patch up to full url if url is not full .filter(urlFilter) // custom filter function on calendar objects .map((url) => new URL(url).pathname); // obtain pathname of the url let calendarObjectResults = []; if (calendarObjectUrls.length > 0) { if (!useMultiGet || expand) { calendarObjectResults = await calendarQuery({ url: calendar.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CALDAV}:calendar-data`]: { ...(expand && timeRange ? { [`${DAVNamespaceShort.CALDAV}:expand`]: { _attributes: { start: `${new Date(timeRange.start) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, end: `${new Date(timeRange.end) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, }, }, } : {}), }, }, filters, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); } else { calendarObjectResults = await calendarMultiGet({ url: calendar.url, props: { [`${DAVNamespaceShort.DAV}:getetag`]: {}, [`${DAVNamespaceShort.CALDAV}:calendar-data`]: { ...(expand && timeRange ? { [`${DAVNamespaceShort.CALDAV}:expand`]: { _attributes: { start: `${new Date(timeRange.start) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, end: `${new Date(timeRange.end) .toISOString() .slice(0, 19) .replace(/[-:.]/g, '')}Z`, }, }, } : {}), }, }, objectUrls: calendarObjectUrls, depth: '1', headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); } } return calendarObjectResults.map((res) => { var _a, _b, _c, _d, _e, _f; return ({ url: new URL((_a = res.href) !== null && _a !== void 0 ? _a : '', calendar.url).href, etag: `${(_b = res.props) === null || _b === void 0 ? void 0 : _b.getetag}`, data: (_e = (_d = (_c = res.props) === null || _c === void 0 ? void 0 : _c.calendarData) === null || _d === void 0 ? void 0 : _d._cdata) !== null && _e !== void 0 ? _e : (_f = res.props) === null || _f === void 0 ? void 0 : _f.calendarData, }); }); }; const createCalendarObject = async (params) => { const { calendar, iCalString, filename, headers, headersToExclude, fetchOptions = {} } = params; return createObject({ url: new URL(filename, calendar.url).href, data: iCalString, headers: excludeHeaders({ 'content-type': 'text/calendar; charset=utf-8', 'If-None-Match': '*', ...headers, }, headersToExclude), fetchOptions, }); }; const updateCalendarObject = async (params) => { const { calendarObject, headers, headersToExclude, fetchOptions = {} } = params; return updateObject({ url: calendarObject.url, data: calendarObject.data, etag: calendarObject.etag, headers: excludeHeaders({ 'content-type': 'text/calendar; charset=utf-8', ...headers, }, headersToExclude), fetchOptions, }); }; const deleteCalendarObject = async (params) => { const { calendarObject, headers, headersToExclude, fetchOptions = {} } = params; return deleteObject({ url: calendarObject.url, etag: calendarObject.etag, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); }; /** * Sync remote calendars to local */ const syncCalendars = async (params) => { var _a; const { oldCalendars, account, detailedResult, headers, headersToExclude, fetchOptions = {}, } = params; if (!account) { throw new Error('Must have account before syncCalendars'); } const localCalendars = (_a = oldCalendars !== null && oldCalendars !== void 0 ? oldCalendars : account.calendars) !== null && _a !== void 0 ? _a : []; const remoteCalendars = await fetchCalendars({ account, headers: excludeHeaders(headers, headersToExclude), fetchOptions, }); // no existing url const created = remoteCalendars.filter((rc) => localCalendars.every((lc) => !urlContains(lc.url, rc.url))); debug$2(`new calendars: ${created.map((cc) => cc.displayName)}`); // have same url, but syncToken/ctag different const updated = localCalendars.reduce((prev, curr) => { const found = remoteCalendars.find((rc) => urlContains(rc.url, curr.url)); if (found && ((found.syncToken && `${found.syncToken}` !== `${curr.syncToken}`) || (found.ctag && `${found.ctag}` !== `${curr.ctag}`))) { return [...prev, found]; } return prev; }, []); debug$2(`updated calendars: ${updated.map((cc) => cc.displayName)}`); const updatedWithObjects = await Promise.all(updated.map(async (u) => { const result = await smartCollectionSync({ collection: { ...u, objectMultiGet: calendarMultiGet }, method: 'webdav', headers: excludeHeaders(headers, headersToExclude), account, fetchOptions, }); return result; })); // does not present in remote const deleted = localCalendars.filter((cal) => remoteCalendars.every((rc) => !urlContains(rc.url, cal.url))); debug$2(`deleted calendars: ${