caldav-adapter 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- const ical = require('node-ical');
2
1
  const raw = require('raw-body');
3
2
  const { DOMParser } = require('@xmldom/xmldom');
4
3
 
@@ -10,7 +9,5 @@ module.exports = async function (ctx) {
10
9
 
11
10
  if (ctx.request.type.includes('xml')) {
12
11
  ctx.request.xml = new DOMParser().parseFromString(ctx.request.body);
13
- } else if (ctx.request.type === 'text/calendar') {
14
- ctx.request.ical = await ical.async.parseICS(ctx.request.body);
15
12
  }
16
13
  };
package/common/tags.js CHANGED
@@ -83,10 +83,10 @@ module.exports = function (options) {
83
83
  },
84
84
  getetag: {
85
85
  doc: 'https://tools.ietf.org/html/rfc4791#section-5.3.4',
86
- async resp({ resource, event }) {
86
+ async resp({ resource, ctx, event }) {
87
87
  if (resource === 'event') {
88
88
  return {
89
- [buildTag(dav, 'getetag')]: options.data.getETag(event)
89
+ [buildTag(dav, 'getetag')]: options.data.getETag(ctx, event)
90
90
  };
91
91
  }
92
92
  }
@@ -199,8 +199,8 @@ module.exports = function (options) {
199
199
  [cal]: {
200
200
  'calendar-data': {
201
201
  doc: 'https://tools.ietf.org/html/rfc4791#section-9.6',
202
- async resp({ event, calendar }) {
203
- const ics = await options.data.buildICS(event, calendar);
202
+ async resp({ event, ctx, calendar }) {
203
+ const ics = await options.data.buildICS(ctx, event, calendar);
204
204
  return {
205
205
  [buildTag(cal, 'calendar-data')]: ics
206
206
  };
package/index.js CHANGED
@@ -85,7 +85,7 @@ module.exports = function (options) {
85
85
  return false;
86
86
  }
87
87
 
88
- ctx.state.user = await options.authenticate({
88
+ ctx.state.user = await options.authenticate(ctx, {
89
89
  username: creds.name,
90
90
  password: creds.pass,
91
91
  principalId: ctx.state.params.principalId
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "caldav-adapter",
3
3
  "description": "CalDAV server for Node.js and Koa. Modernized and maintained for Forward Email.",
4
- "version": "5.0.0",
4
+ "version": "6.0.0",
5
5
  "author": "Sanders DeNardi and Forward Email LLC",
6
6
  "contributors": [
7
7
  "Sanders DeNardi <sedenardi@gmail.com> (http://www.sandersdenardi.com/)",
@@ -12,7 +12,6 @@
12
12
  "basic-auth": "^2.0.1",
13
13
  "lodash": "^4.17.21",
14
14
  "moment": "^2.30.1",
15
- "node-ical": "^0.17.1",
16
15
  "path-to-regexp": "^6.2.1",
17
16
  "raw-body": "^2.5.2",
18
17
  "winston": "^3.11.0",
@@ -19,7 +19,7 @@ module.exports = function (options) {
19
19
 
20
20
  const hrefParts = href.split('/');
21
21
  const eventId = hrefParts.at(-1).slice(0, -4);
22
- const event = await options.data.getEvent({
22
+ const event = await options.data.getEvent(ctx, {
23
23
  eventId,
24
24
  principalId: ctx.state.params.principalId,
25
25
  calendarId: ctx.state.params.calendarId,
@@ -31,10 +31,10 @@ module.exports = function (options) {
31
31
  return response(href, status[404]);
32
32
  }
33
33
 
34
- const ics = await options.data.buildICS(event, calendar);
34
+ const ics = await options.data.buildICS(ctx, event, calendar);
35
35
  return response(href, status[200], [
36
36
  {
37
- 'D:getetag': options.data.getETag(event)
37
+ 'D:getetag': options.data.getETag(ctx, event)
38
38
  },
39
39
  {
40
40
  'CAL:calendar-data': ics
@@ -21,9 +21,9 @@ module.exports = function (options) {
21
21
  });
22
22
 
23
23
  if (!filters?.[0]) {
24
- const events = await options.data.getEventsForCalendar({
24
+ const events = await options.data.getEventsForCalendar(ctx, {
25
25
  principalId: ctx.state.params.principalId,
26
- calendarId: options.data.getCalendarId(calendar),
26
+ calendarId: options.data.getCalendarId(ctx, calendar),
27
27
  user: ctx.state.user,
28
28
  fullData
29
29
  });
@@ -67,9 +67,9 @@ module.exports = function (options) {
67
67
  if (endAttr && endAttr.nodeValue && moment(endAttr.nodeValue).isValid())
68
68
  end = moment(endAttr.nodeValue).toDate();
69
69
 
70
- const events = await options.data.getEventsByDate({
70
+ const events = await options.data.getEventsByDate(ctx, {
71
71
  principalId: ctx.state.params.principalId,
72
- calendarId: options.data.getCalendarId(calendar),
72
+ calendarId: options.data.getCalendarId(ctx, calendar),
73
73
  start,
74
74
  end,
75
75
  user: ctx.state.user,
@@ -17,7 +17,7 @@ module.exports = function (options) {
17
17
  return;
18
18
  }
19
19
 
20
- const existing = await options.data.getEvent({
20
+ const existing = await options.data.getEvent(ctx, {
21
21
  eventId: ctx.state.params.eventId,
22
22
  principalId: ctx.state.params.principalId,
23
23
  calendarId: ctx.state.params.calendarId,
@@ -26,7 +26,7 @@ module.exports = function (options) {
26
26
  });
27
27
  log.debug(`existing event${existing ? '' : ' not'} found`);
28
28
 
29
- await options.data.deleteEvent({
29
+ await options.data.deleteEvent(ctx, {
30
30
  eventId: ctx.state.params.eventId,
31
31
  principalId: ctx.state.params.principalId,
32
32
  calendarId: ctx.state.params.calendarId,
@@ -5,7 +5,7 @@ module.exports = function (options) {
5
5
  const log = winston({ ...options, label: 'calendar/get' });
6
6
 
7
7
  const exec = async function (ctx, calendar) {
8
- const event = await options.data.getEvent({
8
+ const event = await options.data.getEvent(ctx, {
9
9
  eventId: ctx.state.params.eventId,
10
10
  principalId: ctx.state.params.principalId,
11
11
  calendarId: ctx.state.params.calendarId,
@@ -18,7 +18,7 @@ module.exports = function (options) {
18
18
  return;
19
19
  }
20
20
 
21
- return options.data.buildICS(event, calendar);
21
+ return options.data.buildICS(ctx, event, calendar);
22
22
  };
23
23
 
24
24
  return {
@@ -31,7 +31,7 @@ module.exports = function (options) {
31
31
 
32
32
  const calendarUrl = path.join(
33
33
  ctx.state.calendarHomeUrl,
34
- options.data.getCalendarId(calendar),
34
+ options.data.getCalendarId(ctx, calendar),
35
35
  '/'
36
36
  );
37
37
  const props = _.compact(res);
@@ -53,9 +53,9 @@ module.exports = function (options) {
53
53
  const fullData = _.some(children, (child) => {
54
54
  return child.localName === 'calendar-data';
55
55
  });
56
- const events = await options.data.getEventsForCalendar({
56
+ const events = await options.data.getEventsForCalendar(ctx, {
57
57
  principalId: ctx.state.params.principalId,
58
- calendarId: options.data.getCalendarId(calendar),
58
+ calendarId: options.data.getCalendarId(ctx, calendar),
59
59
  user: ctx.state.user,
60
60
  fullData
61
61
  });
@@ -1,4 +1,3 @@
1
- const _ = require('lodash');
2
1
  const { notFound, preconditionFail } = require('../../../common/x-build');
3
2
  const { setMissingMethod } = require('../../../common/response');
4
3
  const winston = require('../../../common/winston');
@@ -19,14 +18,16 @@ module.exports = function (options) {
19
18
  return;
20
19
  }
21
20
 
22
- const incoming = _.find(ctx.request.ical, { type: 'VEVENT' });
23
- if (!incoming) {
21
+ if (
22
+ ctx.request.type !== 'text/calendar' ||
23
+ typeof ctx.request.body !== 'string'
24
+ ) {
24
25
  log.warn('incoming VEVENT not present');
25
26
  ctx.body = notFound(ctx.url); // Make more meaningful
26
27
  return;
27
28
  }
28
29
 
29
- const existing = await options.data.getEvent({
30
+ const existing = await options.data.getEvent(ctx, {
30
31
  eventId: ctx.state.params.eventId,
31
32
  principalId: ctx.state.params.principalId,
32
33
  calendarId: ctx.state.params.calendarId,
@@ -43,30 +44,28 @@ module.exports = function (options) {
43
44
  return;
44
45
  }
45
46
 
46
- const updateObject = await options.data.updateEvent({
47
+ const updateObject = await options.data.updateEvent(ctx, {
47
48
  eventId: ctx.state.params.eventId,
48
49
  principalId: ctx.state.params.principalId,
49
50
  calendarId: ctx.state.params.calendarId,
50
- event: incoming,
51
51
  user: ctx.state.user
52
52
  });
53
53
  log.debug('event updated');
54
54
 
55
55
  /* https://tools.ietf.org/html/rfc4791#section-5.3.2 */
56
56
  ctx.status = 201;
57
- ctx.set('ETag', options.data.getETag(updateObject));
57
+ ctx.set('ETag', options.data.getETag(ctx, updateObject));
58
58
  } else {
59
- const newObject = await options.data.createEvent({
59
+ const newObject = await options.data.createEvent(ctx, {
60
60
  eventId: ctx.state.params.eventId,
61
61
  principalId: ctx.state.params.principalId,
62
62
  calendarId: ctx.state.params.calendarId,
63
- event: incoming,
64
63
  user: ctx.state.user
65
64
  });
66
65
  log.debug('new event created');
67
66
  /* https://tools.ietf.org/html/rfc4791#section-5.3.2 */
68
67
  ctx.status = 201;
69
- ctx.set('ETag', options.data.getETag(newObject));
68
+ ctx.set('ETag', options.data.getETag(ctx, newObject));
70
69
  }
71
70
  };
72
71
 
@@ -19,9 +19,9 @@ module.exports = function (options) {
19
19
  const fullData = _.some(children, (child) => {
20
20
  return child.localName === 'calendar-data';
21
21
  });
22
- const events = await options.data.getEventsForCalendar({
22
+ const events = await options.data.getEventsForCalendar(ctx, {
23
23
  principalId: ctx.state.params.principalId,
24
- calendarId: options.data.getCalendarId(calendar),
24
+ calendarId: options.data.getCalendarId(ctx, calendar),
25
25
  user: ctx.state.user,
26
26
  fullData
27
27
  });
@@ -32,7 +32,7 @@ module.exports = function (options) {
32
32
 
33
33
  if (calendarId) {
34
34
  // Check calendar exists & user has access
35
- const calendar = await options.data.getCalendar({
35
+ const calendar = await options.data.getCalendar(ctx, {
36
36
  principalId: ctx.state.params.principalId,
37
37
  calendarId,
38
38
  user: ctx.state.user
@@ -36,7 +36,7 @@ module.exports = function (options) {
36
36
  response(ctx.url, props.length > 0 ? status[200] : status[404], props)
37
37
  ];
38
38
 
39
- const calendars = await options.data.getCalendarsForPrincipal({
39
+ const calendars = await options.data.getCalendarsForPrincipal(ctx, {
40
40
  principalId: ctx.state.params.principalId,
41
41
  user: ctx.state.user
42
42
  });
@@ -0,0 +1,69 @@
1
+ const _ = require('lodash');
2
+ const xml = require('../../common/xml');
3
+
4
+ // TODO: need to implement tests for MKCALENDAR
5
+ // <https://github.com/sabre-io/dav/blob/da8c1f226f1c053849540a189262274ef6809d1c/tests/Sabre/CalDAV/PluginTest.php#L142-L428>
6
+ module.exports = function (options) {
7
+ return async function (ctx) {
8
+ const { children } = xml.getWithChildren(
9
+ '/CAL:mkcalendar/D:set/D:prop',
10
+ ctx.request.xml
11
+ );
12
+
13
+ const calendar = {};
14
+ for (const child of children) {
15
+ if (!child.localName || !child.textContent) continue;
16
+ switch (child.localName) {
17
+ case 'displayname': {
18
+ calendar.name = child.textContent;
19
+
20
+ break;
21
+ }
22
+
23
+ case 'calendar-description': {
24
+ calendar.description = child.textContent;
25
+
26
+ break;
27
+ }
28
+
29
+ case 'calendar-timezone': {
30
+ calendar.timezone = child.textContent;
31
+
32
+ break;
33
+ }
34
+ // No default
35
+ }
36
+ }
37
+
38
+ // TODO: better error handling
39
+ if (_.isEmpty(calendar)) {
40
+ const err = new TypeError('Calendar update was empty');
41
+ err.xml = ctx.request.body;
42
+ throw err;
43
+ }
44
+
45
+ // TODO: we may need to implement this similar workaround
46
+ // <https://github.com/sabre-io/dav/blob/da8c1f226f1c053849540a189262274ef6809d1c/lib/CalDAV/Plugin.php#L294-L304>
47
+
48
+ // > Clients SHOULD NOT set the DAV: displayname property to be the same as any other calendar collection at the same URI "level".
49
+ // > If a request body is included, it MUST be a CALDAV:mkcalendar XML element.
50
+
51
+ // DAV:displayname
52
+ // CALDAV:calendar-description,
53
+ // CALDAV:supported-calendar-component-set
54
+ // CALDAV:calendar-timezone
55
+
56
+ // <c:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:ca="http://apple.com/ns/ical/">
57
+ // <d:set>
58
+ // <d:prop>
59
+ // <d:displayname>personal calendar</d:displayname>
60
+ // <c:calendar-description>some calendar description</c:calendar-description>
61
+ // </d:prop>
62
+ // </d:set>
63
+ // </c:mkcalendar>
64
+
65
+ const calendarObject = await options.data.createCalendar(ctx, calendar);
66
+ ctx.status = 201;
67
+ ctx.set('ETag', options.data.getETag(ctx, calendarObject));
68
+ };
69
+ };
@@ -2,13 +2,15 @@ const { notFound } = require('../../common/x-build');
2
2
  const { setMultistatusResponse, setOptions } = require('../../common/response');
3
3
  const winston = require('../../common/winston');
4
4
  const routePropfind = require('./propfind');
5
+ const routeMkCalendar = require('./mkcalendar');
5
6
  // const routeReport = require('./report');
6
7
 
7
8
  module.exports = function (options) {
8
9
  const log = winston({ ...options, label: 'principal' });
9
10
  const methods = {
10
- propfind: routePropfind(options)
11
+ propfind: routePropfind(options),
11
12
  // report: reportReport(opts)
13
+ mkcalendar: routeMkCalendar(options)
12
14
  };
13
15
 
14
16
  return async function (ctx) {