diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts
index af74cd0fcd..bc982d7aa4 100644
--- a/packages/editor-ui/src/main.ts
+++ b/packages/editor-ui/src/main.ts
@@ -93,6 +93,7 @@ import {
faTrash,
faUndo,
faUsers,
+ faClock,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@@ -174,6 +175,7 @@ library.add(faTimes);
library.add(faTrash);
library.add(faUndo);
library.add(faUsers);
+library.add(faClock);
Vue.component('font-awesome-icon', FontAwesomeIcon);
diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts
index 8279d654b4..d0aeb44731 100644
--- a/packages/nodes-base/nodes/DateTime.node.ts
+++ b/packages/nodes-base/nodes/DateTime.node.ts
@@ -23,7 +23,7 @@ export class DateTime implements INodeType {
description: INodeTypeDescription = {
displayName: 'Date & Time',
name: 'dateTime',
- icon: 'fa:calendar',
+ icon: 'fa:clock',
group: ['transform'],
version: 1,
description: 'Allows you to manipulate date and time values',
diff --git a/packages/nodes-base/nodes/ICalendar.node.ts b/packages/nodes-base/nodes/ICalendar.node.ts
new file mode 100644
index 0000000000..97e66fd67b
--- /dev/null
+++ b/packages/nodes-base/nodes/ICalendar.node.ts
@@ -0,0 +1,358 @@
+import {
+ IExecuteFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeDescription,
+} from 'n8n-workflow';
+
+import {
+ promisify,
+} from 'util';
+
+import * as moment from 'moment-timezone';
+
+import * as ics from 'ics';
+
+const createEvent = promisify(ics.createEvent);
+
+export class ICalendar implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'iCalendar',
+ name: 'iCal',
+ icon: 'fa:calendar',
+ group: ['input'],
+ version: 1,
+ subtitle: '={{$parameter["operation"]}}',
+ description: 'Create iCalendar file',
+ defaults: {
+ name: 'iCalendar',
+ color: '#408000',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [],
+ properties: [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ options: [
+ {
+ name: 'Create Event File',
+ value: 'createEventFile',
+ },
+ ],
+ default: 'createEventFile',
+ },
+ {
+ displayName: 'Event Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ },
+ {
+ displayName: 'Start',
+ name: 'start',
+ type: 'dateTime',
+ default: '',
+ required: true,
+ description: 'Date and time at which the event begins. (For all-day events, the time will be ignored.)',
+ },
+ {
+ displayName: 'End',
+ name: 'end',
+ type: 'dateTime',
+ default: '',
+ required: true,
+ description: 'Date and time at which the event ends. (For all-day events, the time will be ignored.)',
+ },
+ {
+ displayName: 'All Day',
+ name: 'allDay',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the event lasts all day or not.',
+ },
+ {
+ displayName: 'Binary Property',
+ name: 'binaryPropertyName',
+ type: 'string',
+ default: 'data',
+ required: true,
+ description: 'The field that your iCalendar file will be
available under in the output.',
+ },
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ type: 'collection',
+ placeholder: 'Add Field',
+ default: {},
+ displayOptions: {
+ show: {
+ operation: [
+ 'createEventFile',
+ ],
+ },
+ },
+ options: [
+ {
+ displayName: 'Attendees',
+ name: 'attendeesUi',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ placeholder: 'Add Attendee',
+ default: {},
+ options: [
+ {
+ displayName: 'Attendees',
+ name: 'attendeeValues',
+ values: [
+ {
+ displayName: 'Name',
+ name: 'name',
+ type: 'string',
+ required: true,
+ default: '',
+ },
+ {
+ displayName: 'Email',
+ name: 'email',
+ type: 'string',
+ required: true,
+ default: '',
+ },
+ {
+ displayName: 'RSVP',
+ name: 'rsvp',
+ type: 'boolean',
+ default: false,
+ description: `Whether the attendee has to confirm attendance or not.`,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Busy Status',
+ name: 'busyStatus',
+ type: 'options',
+ options: [
+ {
+ name: 'Busy',
+ value: 'BUSY',
+ },
+ {
+ name: 'Tentative',
+ value: 'TENTATIVE',
+ },
+ ],
+ default: '',
+ description: 'Used to specify busy status for Microsoft applications, like Outlook.',
+ },
+ {
+ displayName: 'Calendar Name',
+ name: 'calName',
+ type: 'string',
+ default: '',
+ description: 'Specifies the calendar (not event) name. Used by Apple iCal and Microsoft Outlook (spec).',
+ },
+ {
+ displayName: 'Description',
+ name: 'description',
+ type: 'string',
+ default: '',
+ },
+ {
+ displayName: 'File Name',
+ name: 'fileName',
+ type: 'string',
+ default: '',
+ description: 'The name of the file to be generated. Default value is event.ics.',
+ },
+ {
+ displayName: 'Geolocation',
+ name: 'geolocationUi',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: false,
+ },
+ placeholder: 'Add Geolocation',
+ default: {},
+ options: [
+ {
+ displayName: 'Geolocation',
+ name: 'geolocationValues',
+ values: [
+ {
+ displayName: 'Latitude',
+ name: 'lat',
+ type: 'string',
+ default: '',
+ },
+ {
+ displayName: 'Longitude',
+ name: 'lon',
+ type: 'string',
+ default: '',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Location',
+ name: 'location',
+ type: 'string',
+ default: '',
+ description: 'The intended venue.',
+ },
+ {
+ displayName: 'Recurrence Rule',
+ name: 'recurrenceRule',
+ type: 'string',
+ default: '',
+ description: `A rule to define the repeat pattern of the event (RRULE). (Rule generator)`,
+ },
+ {
+ displayName: 'Organizer',
+ name: 'organizerUi',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: false,
+ },
+ placeholder: 'Add Organizer',
+ default: {},
+ options: [
+ {
+ displayName: 'Organizer',
+ name: 'organizerValues',
+ values: [
+ {
+ displayName: 'Name',
+ name: 'name',
+ type: 'string',
+ default: '',
+ required: true,
+ },
+ {
+ displayName: 'Email',
+ name: 'email',
+ type: 'string',
+ default: '',
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Sequence',
+ name: 'sequence',
+ type: 'number',
+ default: 0,
+ description: 'When sending an update for an event (with the same uid), defines the revision sequence number.',
+ },
+ {
+ displayName: 'Status',
+ name: 'status',
+ type: 'options',
+ options: [
+ {
+ name: 'Confirmed',
+ value: 'CONFIRMED',
+ },
+ {
+ name: 'Cancelled',
+ value: 'CANCELLED',
+ },
+ {
+ name: 'Tentative',
+ value: 'TENTATIVE',
+ },
+ ],
+ default: 'CONFIRMED',
+ },
+ {
+ displayName: 'UID',
+ name: 'uid',
+ type: 'string',
+ default: '',
+ description: `Universally unique id for the event (will be auto-generated if not specified here). Should be globally unique.`,
+ },
+ {
+ displayName: 'URL',
+ name: 'url',
+ type: 'string',
+ default: '',
+ description: 'URL associated with event.',
+ },
+ ],
+ },
+ ],
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const length = (items.length as unknown) as number;
+ const returnData: INodeExecutionData[] = [];
+ const operation = this.getNodeParameter('operation', 0) as string;
+ if (operation === 'createEventFile') {
+ for (let i = 0; i < length; i++) {
+ const title = this.getNodeParameter('title', i) as string;
+ const allDay = this.getNodeParameter('allDay', i) as boolean;
+ const start = this.getNodeParameter('start', i) as string;
+ let end = this.getNodeParameter('end', i) as string;
+ end = (allDay) ? moment(end).utc().add(1, 'day').format() as string : end;
+ const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
+ const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
+ let fileName = 'event.ics';
+
+ if (additionalFields.fileName) {
+ fileName = additionalFields.fileName as string;
+ }
+
+ const data: ics.EventAttributes = {
+ title,
+ start: (moment(start).toArray().splice(0, (allDay) ? 3 : 6) as ics.DateArray),
+ end: (moment(end).toArray().splice(0, (allDay) ? 3 : 6) as ics.DateArray),
+ startInputType: 'utc',
+ endInputType: 'utc',
+ };
+
+ if (additionalFields.geolocationUi) {
+ data.geo = (additionalFields.geolocationUi as IDataObject).geolocationValues as ics.GeoCoordinates;
+ delete additionalFields.geolocationUi;
+ }
+
+ if (additionalFields.organizerUi) {
+ data.organizer = (additionalFields.organizerUi as IDataObject).organizerValues as ics.Person;
+ delete additionalFields.organizerUi;
+ }
+
+ if (additionalFields.attendeesUi) {
+ data.attendees = (additionalFields.attendeesUi as IDataObject).attendeeValues as ics.Attendee[];
+ delete additionalFields.attendeesUi;
+ }
+
+ Object.assign(data, additionalFields);
+ const buffer = Buffer.from(await createEvent(data) as string);
+ const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'text/calendar');
+ returnData.push(
+ {
+ json: {},
+ binary: {
+ [binaryPropertyName]: binaryData,
+ },
+ },
+ );
+ }
+ }
+ return [returnData];
+ }
+}
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index fac858637f..9fd9a948ce 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -394,6 +394,7 @@
"dist/nodes/Hubspot/HubspotTrigger.node.js",
"dist/nodes/HumanticAI/HumanticAi.node.js",
"dist/nodes/Hunter/Hunter.node.js",
+ "dist/nodes/ICalendar.node.js",
"dist/nodes/If.node.js",
"dist/nodes/Iterable/Iterable.node.js",
"dist/nodes/Intercom/Intercom.node.js",
@@ -608,6 +609,7 @@
"glob-promise": "^3.4.0",
"gm": "^1.23.1",
"iconv-lite": "^0.6.2",
+ "ics": "^2.27.0",
"imap-simple": "^4.3.0",
"iso-639-1": "^2.1.3",
"jsonwebtoken": "^8.5.1",