From 556a6132bafc3eeb574fbd753a438a5e0f2c466d Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:52:18 +0300 Subject: [PATCH] feat(Microsoft Outlook Node): Node overhaul (#4449) [N8N-4995](https://linear.app/n8n/issue/N8N-4995) --------- Co-authored-by: Giulio Andreini --- .../MicrosoftOutlookOAuth2Api.credentials.ts | 18 +- .../Outlook/MicrosoftOutlook.node.ts | 1224 +---------------- .../test/v2/node/calendar/create.test.ts | 78 ++ .../v2/node/calendar/create.workflow.json | 92 ++ .../test/v2/node/calendar/delete.test.ts | 57 + .../v2/node/calendar/delete.workflow.json | 74 + .../Outlook/test/v2/node/calendar/get.test.ts | 79 ++ .../test/v2/node/calendar/get.workflow.json | 93 ++ .../test/v2/node/calendar/getAll.test.ts | 98 ++ .../v2/node/calendar/getAll.workflow.json | 112 ++ .../test/v2/node/calendar/update.test.ts | 78 ++ .../v2/node/calendar/update.workflow.json | 98 ++ .../test/v2/node/contact/create.test.ts | 107 ++ .../test/v2/node/contact/create.workflow.json | 121 ++ .../test/v2/node/contact/update.test.ts | 118 ++ .../test/v2/node/contact/update.workflow.json | 138 ++ .../Outlook/test/v2/node/draft/create.test.ts | 147 ++ .../test/v2/node/draft/create.workflow.json | 154 +++ .../Outlook/test/v2/node/draft/send.test.ts | 62 + .../test/v2/node/draft/send.workflow.json | 76 + .../Outlook/test/v2/node/event/create.test.ts | 155 +++ .../test/v2/node/event/create.workflow.json | 170 +++ .../test/v2/node/folder/create.test.ts | 70 + .../test/v2/node/folder/create.workflow.json | 85 ++ .../test/v2/node/folderMessage/getAll.test.ts | 65 + .../node/folderMessage/getAll.workflow.json | 84 ++ .../Outlook/test/v2/node/message/move.test.ts | 61 + .../test/v2/node/message/move.workflow.json | 81 ++ .../test/v2/node/message/reply.test.ts | 128 ++ .../test/v2/node/message/reply.workflow.json | 134 ++ .../Outlook/test/v2/node/message/send.test.ts | 67 + .../test/v2/node/message/send.workflow.json | 73 + .../Outlook/test/v2/utils/utils.test.ts | 231 ++++ .../Outlook/{ => v1}/DraftDescription.ts | 0 .../{ => v1}/DraftMessageSharedDescription.ts | 0 .../Outlook/{ => v1}/FolderDescription.ts | 0 .../{ => v1}/FolderMessageDecription.ts | 0 .../Outlook/{ => v1}/GenericFunctions.ts | 0 .../{ => v1}/MessageAttachmentDescription.ts | 0 .../Outlook/{ => v1}/MessageDescription.ts | 0 .../Outlook/v1/MicrosoftOutlookV1.node.ts | 1138 +++++++++++++++ .../Outlook/v2/MicrosoftOutlookV2.node.ts | 29 + .../v2/actions/calendar/create.operation.ts | 116 ++ .../v2/actions/calendar/delete.operation.ts | 30 + .../v2/actions/calendar/get.operation.ts | 38 + .../v2/actions/calendar/getAll.operation.ts | 78 ++ .../Outlook/v2/actions/calendar/index.ts | 61 + .../v2/actions/calendar/update.operation.ts | 108 ++ .../v2/actions/contact/create.operation.ts | 62 + .../v2/actions/contact/delete.operation.ts | 29 + .../v2/actions/contact/get.operation.ts | 85 ++ .../v2/actions/contact/getAll.operation.ts | 137 ++ .../Outlook/v2/actions/contact/index.ts | 60 + .../v2/actions/contact/update.operation.ts | 49 + .../v2/actions/draft/create.operation.ts | 259 ++++ .../v2/actions/draft/delete.operation.ts | 29 + .../Outlook/v2/actions/draft/get.operation.ts | 127 ++ .../Outlook/v2/actions/draft/index.ts | 61 + .../v2/actions/draft/send.operation.ts | 52 + .../v2/actions/draft/update.operation.ts | 206 +++ .../v2/actions/event/create.operation.ts | 291 ++++ .../v2/actions/event/delete.operation.ts | 33 + .../Outlook/v2/actions/event/get.operation.ts | 84 ++ .../v2/actions/event/getAll.operation.ts | 162 +++ .../Outlook/v2/actions/event/index.ts | 61 + .../v2/actions/event/update.operation.ts | 283 ++++ .../v2/actions/folder/create.operation.ts | 65 + .../v2/actions/folder/delete.operation.ts | 33 + .../v2/actions/folder/get.operation.ts | 69 + .../v2/actions/folder/getAll.operation.ts | 111 ++ .../Outlook/v2/actions/folder/index.ts | 60 + .../v2/actions/folder/update.operation.ts | 46 + .../actions/folderMessage/getAll.operation.ts | 302 ++++ .../Outlook/v2/actions/folderMessage/index.ts | 29 + .../v2/actions/message/delete.operation.ts | 29 + .../v2/actions/message/get.operation.ts | 181 +++ .../v2/actions/message/getAll.operation.ts | 311 +++++ .../Outlook/v2/actions/message/index.ts | 77 ++ .../v2/actions/message/move.operation.ts | 41 + .../v2/actions/message/reply.operation.ts | 306 +++++ .../v2/actions/message/send.operation.ts | 272 ++++ .../v2/actions/message/update.operation.ts | 224 +++ .../messageAttachment/add.operation.ts | 157 +++ .../messageAttachment/download.operation.ts | 86 ++ .../messageAttachment/get.operation.ts | 94 ++ .../messageAttachment/getAll.operation.ts | 100 ++ .../v2/actions/messageAttachment/index.ts | 52 + .../Outlook/v2/actions/node.description.ts | 82 ++ .../Microsoft/Outlook/v2/actions/node.type.ts | 14 + .../Microsoft/Outlook/v2/actions/router.ts | 84 ++ .../v2/descriptions/common.descriptions.ts | 394 ++++++ .../Outlook/v2/descriptions/index.ts | 2 + .../v2/descriptions/rlc.description.ts | 229 +++ .../Microsoft/Outlook/v2/helpers/utils.ts | 302 ++++ .../Microsoft/Outlook/v2/methods/index.ts | 2 + .../Outlook/v2/methods/listSearch.ts | 289 ++++ .../Outlook/v2/methods/loadOptions.ts | 54 + .../Microsoft/Outlook/v2/transport/index.ts | 224 +++ 98 files changed, 11215 insertions(+), 1202 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/test/v2/utils/utils.test.ts rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/DraftDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/DraftMessageSharedDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/FolderDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/FolderMessageDecription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/GenericFunctions.ts (100%) rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/MessageAttachmentDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Outlook/{ => v1}/MessageDescription.ts (100%) create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v1/MicrosoftOutlookV1.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/MicrosoftOutlookV2.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/send.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/move.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/reply.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/send.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/add.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/download.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.type.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/common.descriptions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/rlc.description.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/listSearch.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/loadOptions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts diff --git a/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts index 74b179020b..fb8d979e73 100644 --- a/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts @@ -1,5 +1,20 @@ import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +const scopes = [ + 'openid', + 'offline_access', + 'Contacts.Read', + 'Contacts.ReadWrite', + 'Calendars.Read', + 'Calendars.Read.Shared', + 'Calendars.ReadWrite', + 'Mail.ReadWrite', + 'Mail.ReadWrite.Shared', + 'Mail.Send', + 'Mail.Send.Shared', + 'MailboxSettings.Read', +]; + export class MicrosoftOutlookOAuth2Api implements ICredentialType { name = 'microsoftOutlookOAuth2Api'; @@ -15,8 +30,7 @@ export class MicrosoftOutlookOAuth2Api implements ICredentialType { displayName: 'Scope', name: 'scope', type: 'hidden', - default: - 'openid offline_access Mail.ReadWrite Mail.ReadWrite.Shared Mail.Send Mail.Send.Shared MailboxSettings.Read', + default: scopes.join(' '), }, { displayName: 'Use Shared Mailbox', diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts index bc81e07926..ead49195f8 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts @@ -1,1202 +1,26 @@ -import type { - IDataObject, - IExecuteFunctions, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, - JsonObject, -} from 'n8n-workflow'; -import { NodeApiError, NodeOperationError } from 'n8n-workflow'; - -import { - binaryToAttachments, - createMessage, - downloadAttachments, - makeRecipient, - microsoftApiRequest, - microsoftApiRequestAllItems, -} from './GenericFunctions'; - -import { draftFields, draftOperations } from './DraftDescription'; - -import { draftMessageSharedFields } from './DraftMessageSharedDescription'; - -import { messageFields, messageOperations } from './MessageDescription'; - -import { - messageAttachmentFields, - messageAttachmentOperations, -} from './MessageAttachmentDescription'; - -import { folderFields, folderOperations } from './FolderDescription'; - -import { folderMessageFields, folderMessageOperations } from './FolderMessageDecription'; - -export class MicrosoftOutlook implements INodeType { - description: INodeTypeDescription = { - displayName: 'Microsoft Outlook', - name: 'microsoftOutlook', - group: ['transform'], - icon: 'file:outlook.svg', - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Microsoft Outlook API', - defaults: { - name: 'Microsoft Outlook', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'microsoftOutlookOAuth2Api', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - default: 'message', - options: [ - { - name: 'Draft', - value: 'draft', - }, - { - name: 'Folder', - value: 'folder', - }, - { - name: 'Folder Message', - value: 'folderMessage', - }, - { - name: 'Message', - value: 'message', - }, - { - name: 'Message Attachment', - value: 'messageAttachment', - }, - ], - }, - // Draft - ...draftOperations, - ...draftFields, - // Message - ...messageOperations, - ...messageFields, - // Message Attachment - ...messageAttachmentOperations, - ...messageAttachmentFields, - // Folder - ...folderOperations, - ...folderFields, - // Folder Message - ...folderMessageOperations, - ...folderMessageFields, - - // Draft & Message - ...draftMessageSharedFields, - ], - }; - - methods = { - loadOptions: { - // Get all the categories to display them to user so that they can - // select them easily - async getCategories(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const categories = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - '/outlook/masterCategories', - ); - for (const category of categories) { - returnData.push({ - name: category.displayName as string, - value: category.displayName as string, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const length = items.length; - const qs: IDataObject = {}; - let responseData; - - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - - if (['draft', 'message'].includes(resource)) { - if (operation === 'delete') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - responseData = await microsoftApiRequest.call(this, 'DELETE', `/messages/${messageId}`); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ success: true }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'get') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); - - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/messages/${messageId}`, - undefined, - qs, - ); - - if (additionalFields.dataPropertyAttachmentsPrefixName) { - const prefix = additionalFields.dataPropertyAttachmentsPrefixName as string; - const data = await downloadAttachments.call( - this, - responseData as IDataObject, - prefix, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(data), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } - - if (additionalFields.dataPropertyAttachmentsPrefixName) { - return [returnData]; - } - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'update') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - - const updateFields = this.getNodeParameter('updateFields', i); - - // Create message from optional fields - const body: IDataObject = createMessage(updateFields); - - responseData = await microsoftApiRequest.call( - this, - 'PATCH', - `/messages/${messageId}`, - body, - {}, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - } - - if (resource === 'draft') { - if (operation === 'create') { - for (let i = 0; i < length; i++) { - try { - const additionalFields = this.getNodeParameter('additionalFields', i); - - const subject = this.getNodeParameter('subject', i) as string; - - const bodyContent = this.getNodeParameter('bodyContent', i, '') as string; - - additionalFields.subject = subject; - - additionalFields.bodyContent = bodyContent || ' '; - - // Create message object from optional fields - const body: IDataObject = createMessage(additionalFields); - - if (additionalFields.attachments) { - const attachments = (additionalFields.attachments as IDataObject) - .attachments as IDataObject[]; - - // Handle attachments - body.attachments = await binaryToAttachments.call(this, attachments, items, i); - } - - responseData = await microsoftApiRequest.call(this, 'POST', '/messages', body, {}); - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'send') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i); - const additionalFields = this.getNodeParameter('additionalFields', i, {}); - - if (additionalFields?.recipients) { - const recipients = (additionalFields.recipients as string) - .split(',') - .filter((email) => !!email); - if (recipients.length !== 0) { - await microsoftApiRequest.call(this, 'PATCH', `/messages/${messageId}`, { - toRecipients: recipients.map((recipient: string) => makeRecipient(recipient)), - }); - } - } - - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/messages/${messageId}/send`, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ success: true }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - } - - if (resource === 'message') { - if (operation === 'reply') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const replyType = this.getNodeParameter('replyType', i) as string; - const comment = this.getNodeParameter('comment', i) as string; - const send = this.getNodeParameter('send', i, false) as boolean; - const additionalFields = this.getNodeParameter('additionalFields', i, {}); - - const body: IDataObject = {}; - - let action = 'createReply'; - if (replyType === 'replyAll') { - body.comment = comment; - action = 'createReplyAll'; - } else { - body.comment = comment; - body.message = {}; - Object.assign(body.message, createMessage(additionalFields)); - //@ts-ignore - delete body.message.attachments; - } - - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/messages/${messageId}/${action}`, - body, - ); - - if (additionalFields.attachments) { - const attachments = (additionalFields.attachments as IDataObject) - .attachments as IDataObject[]; - // Handle attachments - const data = await binaryToAttachments.call(this, attachments, items, i); - - for (const attachment of data) { - await microsoftApiRequest.call( - this, - 'POST', - `/messages/${responseData.id}/attachments`, - attachment, - {}, - ); - } - } - - if (send) { - await microsoftApiRequest.call(this, 'POST', `/messages/${responseData.id}/send`); - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'getMime') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i); - const response = await microsoftApiRequest.call( - this, - 'GET', - `/messages/${messageId}/$value`, - undefined, - {}, - undefined, - {}, - { encoding: null, resolveWithFullResponse: true }, - ); - - let mimeType: string | undefined; - if (response.headers['content-type']) { - mimeType = response.headers['content-type']; - } - - const newItem: INodeExecutionData = { - json: items[i].json, - binary: {}, - }; - - if (items[i].binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - Object.assign(newItem.binary!, items[i].binary); - } - - items[i] = newItem; - - const fileName = `${messageId}.eml`; - const data = Buffer.from(response.body as string, 'utf8'); - items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData( - data as unknown as Buffer, - fileName, - mimeType, - ); - } catch (error) { - if (this.continueOnFail()) { - items[i].json = { error: error.message }; - continue; - } - throw error; - } - } - } - - if (operation === 'getAll') { - let additionalFields: IDataObject = {}; - for (let i = 0; i < length; i++) { - try { - const returnAll = this.getNodeParameter('returnAll', i); - additionalFields = this.getNodeParameter('additionalFields', i); - - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - - const endpoint = '/messages'; - - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - endpoint, - undefined, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); - responseData = responseData.value; - } - - if (additionalFields.dataPropertyAttachmentsPrefixName) { - const prefix = additionalFields.dataPropertyAttachmentsPrefixName as string; - const data = await downloadAttachments.call( - this, - responseData as IDataObject, - prefix, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(data), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - - if (additionalFields.dataPropertyAttachmentsPrefixName) { - return [returnData]; - } - } - - if (operation === 'move') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const destinationId = this.getNodeParameter('folderId', i) as string; - const body: IDataObject = { - destinationId, - }; - - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/messages/${messageId}/move`, - body, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ success: true }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'send') { - for (let i = 0; i < length; i++) { - try { - const additionalFields = this.getNodeParameter('additionalFields', i); - - const toRecipients = this.getNodeParameter('toRecipients', i) as string; - - const subject = this.getNodeParameter('subject', i) as string; - - const bodyContent = this.getNodeParameter('bodyContent', i, '') as string; - - additionalFields.subject = subject; - - additionalFields.bodyContent = bodyContent || ' '; - - additionalFields.toRecipients = toRecipients; - - const saveToSentItems = - additionalFields.saveToSentItems === undefined - ? true - : additionalFields.saveToSentItems; - delete additionalFields.saveToSentItems; - - // Create message object from optional fields - const message: IDataObject = createMessage(additionalFields); - - if (additionalFields.attachments) { - const attachments = (additionalFields.attachments as IDataObject) - .attachments as IDataObject[]; - - // Handle attachments - message.attachments = await binaryToAttachments.call(this, attachments, items, i); - } - - const body: IDataObject = { - message, - saveToSentItems, - }; - - responseData = await microsoftApiRequest.call(this, 'POST', '/sendMail', body, {}); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ success: true }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - } - - if (resource === 'messageAttachment') { - if (operation === 'add') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0); - const additionalFields = this.getNodeParameter('additionalFields', i); - - const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); - const dataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); - - const fileName = - additionalFields.fileName === undefined - ? binaryData.fileName - : additionalFields.fileName; - - if (!fileName) { - throw new NodeOperationError( - this.getNode(), - 'File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!', - { itemIndex: i }, - ); - } - - // Check if the file is over 3MB big - if (dataBuffer.length > 3e6) { - // Maximum chunk size is 4MB - const chunkSize = 4e6; - const body: IDataObject = { - AttachmentItem: { - attachmentType: 'file', - name: fileName, - size: dataBuffer.length, - }, - }; - - // Create upload session - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/messages/${messageId}/attachments/createUploadSession`, - body, - ); - const uploadUrl = responseData.uploadUrl; - - if (uploadUrl === undefined) { - throw new NodeApiError(this.getNode(), responseData as JsonObject, { - message: 'Failed to get upload session', - }); - } - - for ( - let bytesUploaded = 0; - bytesUploaded < dataBuffer.length; - bytesUploaded += chunkSize - ) { - // Upload the file chunk by chunk - const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length); - const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`; - - const data = dataBuffer.subarray(bytesUploaded, nextChunk); - - responseData = await this.helpers.request(uploadUrl, { - method: 'PUT', - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Length': data.length, - 'Content-Range': contentRange, - }, - body: data, - }); - } - } else { - const body: IDataObject = { - '@odata.type': '#microsoft.graph.fileAttachment', - name: fileName, - contentBytes: dataBuffer.toString('base64'), - }; - - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/messages/${messageId}/attachments`, - body, - {}, - ); - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ success: true }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'download') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const attachmentId = this.getNodeParameter('attachmentId', i) as string; - const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i); - - // Get attachment details first - const attachmentDetails = await microsoftApiRequest.call( - this, - 'GET', - `/messages/${messageId}/attachments/${attachmentId}`, - undefined, - { $select: 'id,name,contentType' }, - ); - - let mimeType: string | undefined; - if (attachmentDetails.contentType) { - mimeType = attachmentDetails.contentType; - } - const fileName = attachmentDetails.name; - - const response = await microsoftApiRequest.call( - this, - 'GET', - `/messages/${messageId}/attachments/${attachmentId}/$value`, - undefined, - {}, - undefined, - {}, - { encoding: null, resolveWithFullResponse: true }, - ); - - const newItem: INodeExecutionData = { - json: items[i].json, - binary: {}, - }; - - if (items[i].binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - Object.assign(newItem.binary!, items[i].binary); - } - - items[i] = newItem; - const data = Buffer.from(response.body as string, 'utf8'); - items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData( - data as unknown as Buffer, - fileName as string, - mimeType, - ); - } catch (error) { - if (this.continueOnFail()) { - items[i].json = { error: error.message }; - continue; - } - throw error; - } - } - } - - if (operation === 'get') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const attachmentId = this.getNodeParameter('attachmentId', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); - - // Have sane defaults so we don't fetch attachment data in this operation - qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline'; - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/messages/${messageId}/attachments/${attachmentId}`, - undefined, - qs, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'getAll') { - for (let i = 0; i < length; i++) { - try { - const messageId = this.getNodeParameter('messageId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const additionalFields = this.getNodeParameter('additionalFields', i); - - // Have sane defaults so we don't fetch attachment data in this operation - qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline'; - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - - const endpoint = `/messages/${messageId}/attachments`; - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - endpoint, - undefined, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); - responseData = responseData.value; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - } - - if (resource === 'folder') { - if (operation === 'create') { - for (let i = 0; i < length; i++) { - try { - const displayName = this.getNodeParameter('displayName', i) as string; - const folderType = this.getNodeParameter('folderType', i) as string; - const body: IDataObject = { - displayName, - }; - - let endpoint = '/mailFolders'; - - if (folderType === 'searchFolder') { - endpoint = '/mailFolders/searchfolders/childFolders'; - const includeNestedFolders = this.getNodeParameter('includeNestedFolders', i); - const sourceFolderIds = this.getNodeParameter('sourceFolderIds', i); - const filterQuery = this.getNodeParameter('filterQuery', i); - Object.assign(body, { - '@odata.type': 'microsoft.graph.mailSearchFolder', - includeNestedFolders, - sourceFolderIds, - filterQuery, - }); - } - - responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'delete') { - for (let i = 0; i < length; i++) { - try { - const folderId = this.getNodeParameter('folderId', i) as string; - responseData = await microsoftApiRequest.call( - this, - 'DELETE', - `/mailFolders/${folderId}`, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ success: true }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'get') { - for (let i = 0; i < length; i++) { - try { - const folderId = this.getNodeParameter('folderId', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); - - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/mailFolders/${folderId}`, - {}, - qs, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'getAll') { - for (let i = 0; i < length; i++) { - try { - const returnAll = this.getNodeParameter('returnAll', i); - const additionalFields = this.getNodeParameter('additionalFields', i); - - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - '/mailFolders', - {}, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call(this, 'GET', '/mailFolders', {}, qs); - responseData = responseData.value; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'getChildren') { - for (let i = 0; i < length; i++) { - try { - const folderId = this.getNodeParameter('folderId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const additionalFields = this.getNodeParameter('additionalFields', i); - - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/mailFolders/${folderId}/childFolders`, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/mailFolders/${folderId}/childFolders`, - undefined, - qs, - ); - responseData = responseData.value; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if (operation === 'update') { - for (let i = 0; i < length; i++) { - try { - const folderId = this.getNodeParameter('folderId', i) as string; - const updateFields = this.getNodeParameter('updateFields', i); - - const body: IDataObject = { - ...updateFields, - }; - - responseData = await microsoftApiRequest.call( - this, - 'PATCH', - `/mailFolders/${folderId}`, - body, - ); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - } - - if (resource === 'folderMessage') { - for (let i = 0; i < length; i++) { - try { - if (operation === 'getAll') { - const folderId = this.getNodeParameter('folderId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const additionalFields = this.getNodeParameter('additionalFields', i); - - if (additionalFields.fields) { - qs.$select = additionalFields.fields; - } - - if (additionalFields.filter) { - qs.$filter = additionalFields.filter; - } - - const endpoint = `/mailFolders/${folderId}/messages`; - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - endpoint, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); - responseData = responseData.value; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } - } catch (error) { - if (this.continueOnFail()) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - continue; - } - throw error; - } - } - } - - if ( - (resource === 'message' && operation === 'getMime') || - (resource === 'messageAttachment' && operation === 'download') - ) { - return [items]; - } else { - return [returnData]; - } +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { MicrosoftOutlookV1 } from './v1/MicrosoftOutlookV1.node'; +import { MicrosoftOutlookV2 } from './v2/MicrosoftOutlookV2.node'; + +export class MicrosoftOutlook extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Microsoft Outlook', + name: 'microsoftOutlook', + group: ['transform'], + icon: 'file:outlook.svg', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Outlook API', + defaultVersion: 2, + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new MicrosoftOutlookV1(baseDescription), + 2: new MicrosoftOutlookV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.test.ts new file mode 100644 index 0000000000..e5ffebe495 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.test.ts @@ -0,0 +1,78 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendarGroups('AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRu_AAA%3D')/calendars/$entity", + id: 'AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAFXBBZ_AAA=', + name: 'New Calendar', + color: 'lightOrange', + hexColor: '#fcab73', + isDefaultCalendar: false, + changeKey: 'WX+A3vy5K0qqTyPHso1JgAABVtwWTA==', + canShare: true, + canViewPrivateItems: true, + canEdit: true, + allowedOnlineMeetingProviders: ['teamsForBusiness'], + defaultOnlineMeetingProvider: 'teamsForBusiness', + isTallyingResponses: false, + isRemovable: true, + owner: { + name: 'User Name', + address: 'test@mail.com', + }, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, calendar => create', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/calendar/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/calendarGroups/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRu_AAA=/calendars', + { color: 'lightOrange', name: 'New Calendar' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.workflow.json new file mode 100644 index 0000000000..705155d787 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/create.workflow.json @@ -0,0 +1,92 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "calendar", + "operation": "create", + "name": "New Calendar", + "additionalFields": { + "calendarGroup": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRu_AAA=", + "color": "lightOrange" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendarGroups('AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRu_AAA%3D')/calendars/$entity", + "id": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAFXBBZ_AAA=", + "name": "New Calendar", + "color": "lightOrange", + "hexColor": "#fcab73", + "isDefaultCalendar": false, + "changeKey": "WX+A3vy5K0qqTyPHso1JgAABVtwWTA==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": [ + "teamsForBusiness" + ], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "User Name", + "address": "test@mail.com" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "074cdbef-4193-45b8-970a-9f55b8a0999b", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.test.ts new file mode 100644 index 0000000000..ff79c798a8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.test.ts @@ -0,0 +1,57 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return {}; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, calendar => delete', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/calendar/delete.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/calendars/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvIAAA=', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.workflow.json new file mode 100644 index 0000000000..739d7e5a4c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/delete.workflow.json @@ -0,0 +1,74 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "calendar", + "operation": "delete", + "calendarId": { + "__rl": true, + "value": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvIAAA=", + "mode": "list", + "cachedResultName": "Foo" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "888e5578-c726-4a84-9658-321ba04fd0c7", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.test.ts new file mode 100644 index 0000000000..35d3b7f173 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.test.ts @@ -0,0 +1,79 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendars/$entity", + id: 'AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=', + name: 'Foo Calendar', + color: 'lightGreen', + hexColor: '#87d28e', + isDefaultCalendar: false, + changeKey: 'WX+A3vy5K0qqTyPHso1JgAAAi67hiw==', + canShare: true, + canViewPrivateItems: true, + canEdit: true, + allowedOnlineMeetingProviders: ['teamsForBusiness'], + defaultOnlineMeetingProvider: 'teamsForBusiness', + isTallyingResponses: false, + isRemovable: true, + owner: { + name: 'User Name', + address: 'test@mail.com', + }, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, calendar => get', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/calendar/get.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + '/calendars/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=', + undefined, + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.workflow.json new file mode 100644 index 0000000000..0657fca3af --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/get.workflow.json @@ -0,0 +1,93 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "calendar", + "operation": "get", + "calendarId": { + "__rl": true, + "value": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=", + "mode": "list", + "cachedResultName": "Foo Calendar" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendars/$entity", + "id": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=", + "name": "Foo Calendar", + "color": "lightGreen", + "hexColor": "#87d28e", + "isDefaultCalendar": false, + "changeKey": "WX+A3vy5K0qqTyPHso1JgAAAi67hiw==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": [ + "teamsForBusiness" + ], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "User Name", + "address": "test@mail.com" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "33e1fc57-ac36-453b-b01e-d79784b4a4bb", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.test.ts new file mode 100644 index 0000000000..114d27baa8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.test.ts @@ -0,0 +1,98 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + id: 'AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAAJ9-JDAAA=', + name: 'Calendar', + color: 'auto', + hexColor: '', + isDefaultCalendar: true, + changeKey: 'WX+A3vy5K0qqTyPHso1JgAAACfdHfw==', + canShare: true, + canViewPrivateItems: true, + canEdit: true, + allowedOnlineMeetingProviders: ['teamsForBusiness'], + defaultOnlineMeetingProvider: 'teamsForBusiness', + isTallyingResponses: true, + isRemovable: false, + owner: { + name: 'User Name', + address: 'test@mail.com', + }, + }, + { + id: 'AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvBAAA=', + name: 'Third calendar', + color: 'lightYellow', + hexColor: '#fde300', + isDefaultCalendar: false, + changeKey: 'WX+A3vy5K0qqTyPHso1JgAAAi67hIw==', + canShare: true, + canViewPrivateItems: true, + canEdit: true, + allowedOnlineMeetingProviders: ['teamsForBusiness'], + defaultOnlineMeetingProvider: 'teamsForBusiness', + isTallyingResponses: false, + isRemovable: true, + owner: { + name: 'User Name', + address: 'test@mail.com', + }, + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, calendar => getAll', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith('GET', '/calendars', undefined, { + $filter: 'canEdit eq true', + $top: 2, + }); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.workflow.json new file mode 100644 index 0000000000..0b189aefc1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/getAll.workflow.json @@ -0,0 +1,112 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "calendar", + "limit": 2, + "filters": { + "custom": "canEdit eq true" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "id": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAAJ9-JDAAA=", + "name": "Calendar", + "color": "auto", + "hexColor": "", + "isDefaultCalendar": true, + "changeKey": "WX+A3vy5K0qqTyPHso1JgAAACfdHfw==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": [ + "teamsForBusiness" + ], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": true, + "isRemovable": false, + "owner": { + "name": "User Name", + "address": "test@mail.com" + } + } + }, + { + "json": { + "id": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvBAAA=", + "name": "Third calendar", + "color": "lightYellow", + "hexColor": "#fde300", + "isDefaultCalendar": false, + "changeKey": "WX+A3vy5K0qqTyPHso1JgAAAi67hIw==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": [ + "teamsForBusiness" + ], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "User Name", + "address": "test@mail.com" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "8f4e7775-0476-4506-a183-1365265b446a", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.test.ts new file mode 100644 index 0000000000..60d68db8ee --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.test.ts @@ -0,0 +1,78 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'PATCH') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendars/$entity", + id: 'AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=', + name: 'Foo', + color: 'lightOrange', + hexColor: '#fcab73', + isDefaultCalendar: false, + changeKey: 'WX+A3vy5K0qqTyPHso1JgAABVtwYKA==', + canShare: true, + canViewPrivateItems: true, + canEdit: true, + allowedOnlineMeetingProviders: ['teamsForBusiness'], + defaultOnlineMeetingProvider: 'teamsForBusiness', + isTallyingResponses: false, + isRemovable: true, + owner: { + name: 'User Name', + address: 'test@mail.com', + }, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, calendar => update', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/calendar/update.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'PATCH', + '/calendars/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=', + { color: 'lightOrange', isDefaultCalendar: false, name: 'Foo' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.workflow.json new file mode 100644 index 0000000000..6ab4347c87 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/calendar/update.workflow.json @@ -0,0 +1,98 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "calendar", + "operation": "update", + "calendarId": { + "__rl": true, + "value": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=", + "mode": "list", + "cachedResultName": "Foo Calendar" + }, + "updateFields": { + "color": "lightOrange", + "isDefaultCalendar": false, + "name": "Foo" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendars/$entity", + "id": "AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=", + "name": "Foo", + "color": "lightOrange", + "hexColor": "#fcab73", + "isDefaultCalendar": false, + "changeKey": "WX+A3vy5K0qqTyPHso1JgAABVtwYKA==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": [ + "teamsForBusiness" + ], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "User Name", + "address": "test@mail.com" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "f8e68e5f-8c26-4c1f-b11b-8b4e4aeaa61f", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.test.ts new file mode 100644 index 0000000000..a0d09bd87a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.test.ts @@ -0,0 +1,107 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/contacts/$entity", + '@odata.etag': 'W/"EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bob"', + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=', + createdDateTime: '2023-09-04T08:48:39Z', + lastModifiedDateTime: '2023-09-04T08:48:39Z', + changeKey: 'EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bob', + categories: ['blue', 'green'], + parentFolderId: + 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAAA=', + birthday: '1991-09-19T11:59:00Z', + fileAs: '', + displayName: 'User Name', + givenName: 'User', + initials: null, + middleName: null, + nickName: null, + surname: 'Name', + title: 'Title', + yomiGivenName: null, + yomiSurname: null, + yomiCompanyName: null, + generation: null, + imAddresses: [], + jobTitle: null, + companyName: 'Company', + department: 'IT', + officeLocation: null, + profession: null, + businessHomePage: null, + assistantName: 'Assistant', + manager: null, + homePhones: [], + mobilePhone: null, + businessPhones: [], + spouseName: null, + personalNotes: '', + children: [], + emailAddresses: [], + homeAddress: {}, + businessAddress: {}, + otherAddress: {}, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, contact => create', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/contact/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith('POST', '/contacts', { + assistantName: 'Assistant', + birthday: '1991-09-19T21:00:00.000Z', + categories: ['blue', 'green'], + companyName: 'Company', + department: 'IT', + displayName: 'User Name', + givenName: 'User', + surname: 'Name', + title: 'Title', + }); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.workflow.json new file mode 100644 index 0000000000..cc5893cb51 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/create.workflow.json @@ -0,0 +1,121 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "contact", + "operation": "create", + "givenName": "User", + "surname": "Name", + "additionalFields": { + "assistantName": "Assistant", + "birthday": "1991-09-19T21:00:00.000Z", + "categories": "blue, green", + "companyName": "Company", + "department": "IT", + "displayName": "User Name", + "title": "Title" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/contacts/$entity", + "@odata.etag": "W/\"EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bob\"", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=", + "createdDateTime": "2023-09-04T08:48:39Z", + "lastModifiedDateTime": "2023-09-04T08:48:39Z", + "changeKey": "EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bob", + "categories": [ + "blue", + "green" + ], + "parentFolderId": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAAA=", + "birthday": "1991-09-19T11:59:00Z", + "fileAs": "", + "displayName": "User Name", + "givenName": "User", + "initials": null, + "middleName": null, + "nickName": null, + "surname": "Name", + "title": "Title", + "yomiGivenName": null, + "yomiSurname": null, + "yomiCompanyName": null, + "generation": null, + "imAddresses": [], + "jobTitle": null, + "companyName": "Company", + "department": "IT", + "officeLocation": null, + "profession": null, + "businessHomePage": null, + "assistantName": "Assistant", + "manager": null, + "homePhones": [], + "mobilePhone": null, + "businessPhones": [], + "spouseName": null, + "personalNotes": "", + "children": [], + "emailAddresses": [], + "homeAddress": {}, + "businessAddress": {}, + "otherAddress": {} + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "51df88ff-b679-43df-9129-d1b26ea6b82d", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.test.ts new file mode 100644 index 0000000000..84f367691f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.test.ts @@ -0,0 +1,118 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'PATCH') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/contacts/$entity", + '@odata.etag': 'W/"EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bou"', + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=', + createdDateTime: '2023-09-04T08:48:39Z', + lastModifiedDateTime: '2023-09-04T09:06:21Z', + changeKey: 'EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bou', + categories: ['blue', 'green'], + parentFolderId: + 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAAA=', + birthday: '1991-09-19T11:59:00Z', + fileAs: '', + displayName: 'Username', + givenName: 'User', + initials: null, + middleName: null, + nickName: null, + surname: 'Name', + title: 'Title', + yomiGivenName: null, + yomiSurname: null, + yomiCompanyName: null, + generation: null, + imAddresses: [], + jobTitle: null, + companyName: 'Company', + department: 'IT', + officeLocation: null, + profession: null, + businessHomePage: null, + assistantName: 'Assistant', + manager: 'Manager', + homePhones: [], + mobilePhone: '', + businessPhones: ['999000555777'], + spouseName: '', + personalNotes: '', + children: [], + emailAddresses: [], + homeAddress: {}, + businessAddress: { + street: 'Street', + city: 'City', + state: 'State', + countryOrRegion: 'Country', + postalCode: '777777', + }, + otherAddress: {}, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, contact => update', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/contact/update.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'PATCH', + '/contacts/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=', + { + businessAddress: { + city: 'City', + countryOrRegion: 'Country', + postalCode: '777777', + state: 'State', + street: 'Street', + }, + businessPhones: ['999000555777'], + displayName: 'Username', + manager: 'Manager', + }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.workflow.json new file mode 100644 index 0000000000..82f66ff1cd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/contact/update.workflow.json @@ -0,0 +1,138 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "contact", + "operation": "update", + "contactId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=", + "mode": "list", + "cachedResultName": "User Name" + }, + "additionalFields": { + "businessAddress": { + "values": { + "city": "City", + "countryOrRegion": "Country", + "postalCode": "777777", + "state": "State", + "street": "Street" + } + }, + "businessPhones": "999000555777", + "displayName": "Username", + "manager": "Manager" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/contacts/$entity", + "@odata.etag": "W/\"EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bou\"", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=", + "createdDateTime": "2023-09-04T08:48:39Z", + "lastModifiedDateTime": "2023-09-04T09:06:21Z", + "changeKey": "EQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bou", + "categories": [ + "blue", + "green" + ], + "parentFolderId": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAAA=", + "birthday": "1991-09-19T11:59:00Z", + "fileAs": "", + "displayName": "Username", + "givenName": "User", + "initials": null, + "middleName": null, + "nickName": null, + "surname": "Name", + "title": "Title", + "yomiGivenName": null, + "yomiSurname": null, + "yomiCompanyName": null, + "generation": null, + "imAddresses": [], + "jobTitle": null, + "companyName": "Company", + "department": "IT", + "officeLocation": null, + "profession": null, + "businessHomePage": null, + "assistantName": "Assistant", + "manager": "Manager", + "homePhones": [], + "mobilePhone": "", + "businessPhones": [ + "999000555777" + ], + "spouseName": "", + "personalNotes": "", + "children": [], + "emailAddresses": [], + "homeAddress": {}, + "businessAddress": { + "street": "Street", + "city": "City", + "state": "State", + "countryOrRegion": "Country", + "postalCode": "777777" + }, + "otherAddress": {} + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "842587ce-f153-49ac-8818-d173d4d6aac6", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.test.ts new file mode 100644 index 0000000000..8a344f411e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.test.ts @@ -0,0 +1,147 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/messages/$entity", + '@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bo2"', + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=', + createdDateTime: '2023-09-04T09:18:35Z', + lastModifiedDateTime: '2023-09-04T09:18:35Z', + changeKey: 'CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bo2', + categories: [ + 'd10cd8f9-14ac-460e-a6ec-c40dd1876ea2', + '6844a34e-4d23-4805-9fec-38b7f6e1a780', + 'fbf44fcd-7689-43a0-99c8-2c9faf6d825a', + ], + receivedDateTime: '2023-09-04T09:18:35Z', + sentDateTime: '2023-09-04T09:18:35Z', + hasAttachments: false, + internetMessageId: + '', + subject: 'New Draft', + bodyPreview: 'draft message', + importance: 'normal', + parentFolderId: + 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAAA=', + conversationId: + 'AAQkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAQAKELKTNBg5JJuTnBGaTDyl0=', + conversationIndex: 'AQHZ3xDIoQspM0GDkkm5OcEZpMPKXQ==', + isDeliveryReceiptRequested: false, + isReadReceiptRequested: true, + isRead: true, + isDraft: true, + webLink: + 'https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEPAABZf4De%2FLkrSqpPI8eyjUmAAAFXBDupAAA%3D&exvsurl=1&viewmodel=ReadMessageItem', + inferenceClassification: 'focused', + body: { + contentType: 'text', + content: 'draft message', + }, + toRecipients: [ + { + emailAddress: { + name: 'some@mail.com', + address: 'some@mail.com', + }, + }, + ], + ccRecipients: [], + bccRecipients: [ + { + emailAddress: { + name: 'name1@mail.com', + address: 'name1@mail.com', + }, + }, + { + emailAddress: { + name: 'name2@mail.com', + address: 'name2@mail.com', + }, + }, + ], + replyTo: [ + { + emailAddress: { + name: 'reply@mail.com', + address: 'reply@mail.com', + }, + }, + ], + flag: { + flagStatus: 'notFlagged', + }, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, draft => create', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/draft/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/messages', + { + bccRecipients: [ + { emailAddress: { address: 'name1@mail.com' } }, + { emailAddress: { address: 'name2@mail.com' } }, + ], + body: { content: 'draft message', contentType: 'Text' }, + categories: [ + 'd10cd8f9-14ac-460e-a6ec-c40dd1876ea2', + '6844a34e-4d23-4805-9fec-38b7f6e1a780', + 'fbf44fcd-7689-43a0-99c8-2c9faf6d825a', + ], + importance: 'Normal', + internetMessageHeaders: [{ name: 'x-my-header', value: 'header value' }], + isReadReceiptRequested: true, + replyTo: [{ emailAddress: { address: 'reply@mail.com' } }], + subject: 'New Draft', + toRecipients: [{ emailAddress: { address: 'some@mail.com' } }], + }, + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.workflow.json new file mode 100644 index 0000000000..d4691956ba --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/create.workflow.json @@ -0,0 +1,154 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "draft", + "subject": "New Draft", + "bodyContent": "draft message", + "additionalFields": { + "bccRecipients": "name1@mail.com, name2@mail.com", + "categories": [ + "d10cd8f9-14ac-460e-a6ec-c40dd1876ea2", + "6844a34e-4d23-4805-9fec-38b7f6e1a780", + "fbf44fcd-7689-43a0-99c8-2c9faf6d825a" + ], + "internetMessageHeaders": { + "headers": [ + { + "name": "x-my-header", + "value": "header value" + } + ] + }, + "importance": "Normal", + "bodyContentType": "Text", + "isReadReceiptRequested": true, + "replyTo": "reply@mail.com", + "toRecipients": "some@mail.com" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/messages/$entity", + "@odata.etag": "W/\"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bo2\"", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=", + "createdDateTime": "2023-09-04T09:18:35Z", + "lastModifiedDateTime": "2023-09-04T09:18:35Z", + "changeKey": "CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3Bo2", + "categories": [ + "d10cd8f9-14ac-460e-a6ec-c40dd1876ea2", + "6844a34e-4d23-4805-9fec-38b7f6e1a780", + "fbf44fcd-7689-43a0-99c8-2c9faf6d825a" + ], + "receivedDateTime": "2023-09-04T09:18:35Z", + "sentDateTime": "2023-09-04T09:18:35Z", + "hasAttachments": false, + "internetMessageId": "", + "subject": "New Draft", + "bodyPreview": "draft message", + "importance": "normal", + "parentFolderId": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAAA=", + "conversationId": "AAQkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAQAKELKTNBg5JJuTnBGaTDyl0=", + "conversationIndex": "AQHZ3xDIoQspM0GDkkm5OcEZpMPKXQ==", + "isDeliveryReceiptRequested": false, + "isReadReceiptRequested": true, + "isRead": true, + "isDraft": true, + "webLink": "https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEPAABZf4De%2FLkrSqpPI8eyjUmAAAFXBDupAAA%3D&exvsurl=1&viewmodel=ReadMessageItem", + "inferenceClassification": "focused", + "body": { + "contentType": "text", + "content": "draft message" + }, + "toRecipients": [ + { + "emailAddress": { + "name": "some@mail.com", + "address": "some@mail.com" + } + } + ], + "ccRecipients": [], + "bccRecipients": [ + { + "emailAddress": { + "name": "name1@mail.com", + "address": "name1@mail.com" + } + }, + { + "emailAddress": { + "name": "name2@mail.com", + "address": "name2@mail.com" + } + } + ], + "replyTo": [ + { + "emailAddress": { + "name": "reply@mail.com", + "address": "reply@mail.com" + } + } + ], + "flag": { + "flagStatus": "notFlagged" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "7c801e19-1108-4bae-85f1-902820df8c5e", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.test.ts new file mode 100644 index 0000000000..210dabed68 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.test.ts @@ -0,0 +1,62 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return {}; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, draft => send', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/draft/send.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(2); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'PATCH', + '/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=', + { toRecipients: [{ emailAddress: { address: 'michael.k@radency.com' } }] }, + ); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=/send', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.workflow.json new file mode 100644 index 0000000000..9d626d7648 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/draft/send.workflow.json @@ -0,0 +1,76 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "draft", + "operation": "send", + "draftId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=", + "mode": "list", + "cachedResultName": "New Draft", + "cachedResultUrl": "https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEPAABZf4De%2FLkrSqpPI8eyjUmAAAFXBDupAAA%3D&exvsurl=1&viewmodel=ReadMessageItem" + }, + "to": "michael.k@radency.com" + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "4bbe3403-3da7-4e6e-8341-7da5e8433330", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.test.ts new file mode 100644 index 0000000000..38605a67ec --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.test.ts @@ -0,0 +1,155 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendars('AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAAJ9-JDAAA%3D')/events/$entity", + '@odata.etag': 'W/"WX+A3vy5K0qqTyPHso1JgAABVtwgEQ=="', + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAENAABZf4De-LkrSqpPI8eyjUmAAAFXBFUSAAA=', + createdDateTime: '2023-09-04T10:12:47.1985121Z', + lastModifiedDateTime: '2023-09-04T10:12:48.2173253Z', + changeKey: 'WX+A3vy5K0qqTyPHso1JgAABVtwgEQ==', + categories: ['Yellow category', 'Orange category'], + transactionId: null, + originalStartTimeZone: 'UTC', + originalEndTimeZone: 'UTC', + iCalUId: + '040000008200E00074C5B7101A82E0080000000062DD545A18DFD90100000000000000001000000004C50947C7B42140B29018ABAB42C965', + reminderMinutesBeforeStart: 15, + isReminderOn: true, + hasAttachments: false, + subject: 'New Event', + bodyPreview: + 'event description\r\n________________________________________________________________________________\r\nMicrosoft Teams meeting\r\nJoin on your computer, mobile app or room device\r\nClick here to join the meeting\r\nMeeting ID: 355 132 640 047\r\nPasscode: xgUo7v', + importance: 'normal', + sensitivity: 'personal', + isAllDay: false, + isCancelled: false, + isOrganizer: true, + responseRequested: true, + seriesMasterId: null, + showAs: 'busy', + type: 'singleInstance', + webLink: + 'https://outlook.office365.com/owa/?itemid=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAENAABZf4De%2FLkrSqpPI8eyjUmAAAFXBFUSAAA%3D&exvsurl=1&path=/calendar/item', + onlineMeetingUrl: null, + isOnlineMeeting: true, + onlineMeetingProvider: 'teamsForBusiness', + allowNewTimeProposals: true, + occurrenceId: null, + isDraft: false, + hideAttendees: true, + responseStatus: { + response: 'organizer', + time: '0001-01-01T00:00:00Z', + }, + body: { + contentType: 'html', + content: + '\r\n\r\n\r\n\r\n\r\nevent description
\r\n
________________________________________________________________________________\r\n
\r\n
\r\n
Microsoft Teams meeting\r\n
\r\n
\r\n
Join on your computer, mobile app or room device\r\n
\r\nClick\r\n here to join the meeting
\r\n
\r\n
Meeting ID:\r\n355 132 640 047
\r\nPasscode: xgUo7v\r\n\r\n\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
________________________________________________________________________________\r\n
\r\n\r\n\r\n', + }, + start: { + dateTime: '2023-09-05T07:26:47.0000000', + timeZone: 'UTC', + }, + end: { + dateTime: '2023-09-06T07:56:47.0000000', + timeZone: 'UTC', + }, + location: { + displayName: 'Microsoft Teams Meeting', + locationType: 'default', + uniqueId: 'Microsoft Teams Meeting', + uniqueIdType: 'private', + }, + locations: [ + { + displayName: 'Microsoft Teams Meeting', + locationType: 'default', + uniqueId: 'Microsoft Teams Meeting', + uniqueIdType: 'private', + }, + ], + recurrence: null, + attendees: [], + organizer: { + emailAddress: { + name: 'Michael Kret', + address: 'MichaelDevSandbox@5w1hb7.onmicrosoft.com', + }, + }, + onlineMeeting: { + joinUrl: + 'https://teams.microsoft.com/l/meetup-join/19%3ameeting_MDZmMzZmYzYtMDc4Yi00NTA2LWE3MTMtZDc5ZDI1M2JmY2M3%40thread.v2/0?context=%7b%22Tid%22%3a%2223786ca6-7ff2-4672-87d0-5c649ee0a337%22%2c%22Oid%22%3a%22b834447b-6848-4af9-8390-d2259ce46b74%22%7d', + }, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, contact => event', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/event/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/calendars/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAAJ9-JDAAA=/events', + { + body: { content: 'event description', contentType: 'html' }, + bodyPreview: 'preview', + categories: ['Yellow category', 'Orange category'], + end: { dateTime: '2023-09-06T07:56:47.000Z', timeZone: 'UTC' }, + hideAttendees: true, + importance: 'normal', + isAllDay: false, + isCancelled: false, + isDraft: false, + isOnlineMeeting: true, + sensitivity: 'personal', + showAs: 'busy', + start: { dateTime: '2023-09-05T07:26:47.000Z', timeZone: 'UTC' }, + subject: 'New Event', + type: 'occurrence', + }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.workflow.json new file mode 100644 index 0000000000..78ac7a7781 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/event/create.workflow.json @@ -0,0 +1,170 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "event", + "operation": "create", + "calendarId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAAJ9-JDAAA=", + "mode": "list", + "cachedResultName": "Calendar" + }, + "subject": "New Event", + "startDateTime": "2023-09-05T07:26:47.000Z", + "endDateTime": "2023-09-06T07:56:47.000Z", + "additionalFields": { + "categories": [ + "Yellow category", + "Orange category" + ], + "body": "event description", + "bodyPreview": "preview", + "hideAttendees": true, + "importance": "normal", + "isAllDay": false, + "isCancelled": false, + "isDraft": false, + "isOnlineMeeting": true, + "sensitivity": "personal", + "showAs": "busy", + "type": "occurrence" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/calendars('AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAAAJ9-JDAAA%3D')/events/$entity", + "@odata.etag": "W/\"WX+A3vy5K0qqTyPHso1JgAABVtwgEQ==\"", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAENAABZf4De-LkrSqpPI8eyjUmAAAFXBFUSAAA=", + "createdDateTime": "2023-09-04T10:12:47.1985121Z", + "lastModifiedDateTime": "2023-09-04T10:12:48.2173253Z", + "changeKey": "WX+A3vy5K0qqTyPHso1JgAABVtwgEQ==", + "categories": [ + "Yellow category", + "Orange category" + ], + "transactionId": null, + "originalStartTimeZone": "UTC", + "originalEndTimeZone": "UTC", + "iCalUId": "040000008200E00074C5B7101A82E0080000000062DD545A18DFD90100000000000000001000000004C50947C7B42140B29018ABAB42C965", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "New Event", + "bodyPreview": "event description\r\n________________________________________________________________________________\r\nMicrosoft Teams meeting\r\nJoin on your computer, mobile app or room device\r\nClick here to join the meeting\r\nMeeting ID: 355 132 640 047\r\nPasscode: xgUo7v", + "importance": "normal", + "sensitivity": "personal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": true, + "responseRequested": true, + "seriesMasterId": null, + "showAs": "busy", + "type": "singleInstance", + "webLink": "https://outlook.office365.com/owa/?itemid=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAENAABZf4De%2FLkrSqpPI8eyjUmAAAFXBFUSAAA%3D&exvsurl=1&path=/calendar/item", + "onlineMeetingUrl": null, + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": null, + "isDraft": false, + "hideAttendees": true, + "responseStatus": { + "response": "organizer", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "\r\n\r\n\r\n\r\n\r\nevent description
\r\n
________________________________________________________________________________\r\n
\r\n
\r\n
Microsoft Teams meeting\r\n
\r\n
\r\n
Join on your computer, mobile app or room device\r\n
\r\nClick\r\n here to join the meeting
\r\n
\r\n
Meeting ID:\r\n355 132 640 047
\r\nPasscode: xgUo7v\r\n\r\n\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
________________________________________________________________________________\r\n
\r\n\r\n\r\n" + }, + "start": { + "dateTime": "2023-09-05T07:26:47.0000000", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2023-09-06T07:56:47.0000000", + "timeZone": "UTC" + }, + "location": { + "displayName": "Microsoft Teams Meeting", + "locationType": "default", + "uniqueId": "Microsoft Teams Meeting", + "uniqueIdType": "private" + }, + "locations": [ + { + "displayName": "Microsoft Teams Meeting", + "locationType": "default", + "uniqueId": "Microsoft Teams Meeting", + "uniqueIdType": "private" + } + ], + "recurrence": null, + "attendees": [], + "organizer": { + "emailAddress": { + "name": "Michael Kret", + "address": "MichaelDevSandbox@5w1hb7.onmicrosoft.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MDZmMzZmYzYtMDc4Yi00NTA2LWE3MTMtZDc5ZDI1M2JmY2M3%40thread.v2/0?context=%7b%22Tid%22%3a%2223786ca6-7ff2-4672-87d0-5c649ee0a337%22%2c%22Oid%22%3a%22b834447b-6848-4af9-8390-d2259ce46b74%22%7d" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "dceb08aa-5897-44d1-afbc-748d1e8a2626", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.test.ts new file mode 100644 index 0000000000..ceec75919c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.test.ts @@ -0,0 +1,70 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/mailFolders('AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA%3D')/childFolders/$entity", + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=', + displayName: 'Folder 42', + parentFolderId: + 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA=', + childFolderCount: 0, + unreadItemCount: 0, + totalItemCount: 0, + sizeInBytes: 0, + isHidden: false, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, contact => folder', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/folder/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/mailFolders/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA=/childFolders', + { displayName: 'Folder 42' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.workflow.json new file mode 100644 index 0000000000..43a0032b71 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folder/create.workflow.json @@ -0,0 +1,85 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "folder", + "displayName": "Folder 42", + "options": { + "folderId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA=", + "mode": "list", + "cachedResultName": "New folder", + "cachedResultUrl": "https://outlook.office365.com/mail/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De%2FLkrSqpPI8eyjUmAAAFXBAEGAAA%3D" + } + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/mailFolders('AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA%3D')/childFolders/$entity", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=", + "displayName": "Folder 42", + "parentFolderId": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA=", + "childFolderCount": 0, + "unreadItemCount": 0, + "totalItemCount": 0, + "sizeInBytes": 0, + "isHidden": false + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "6f7a33a8-d48a-4ae2-ab1c-bbe0a83d376f", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.test.ts new file mode 100644 index 0000000000..081320390c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.test.ts @@ -0,0 +1,65 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequestAllItems: jest.fn(async function (method: string) { + return [ + { + '@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CAj"', + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAABZf4De-LkrSqpPI8eyjUmAAAFXBGDUAAA=', + subject: 'XXXX', + bodyPreview: 'test', + }, + ]; + }), + }; +}); + +describe('Test MicrosoftOutlookV2, folderMessage => getAll', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequestAllItems).toHaveBeenCalledWith( + 'value', + 'GET', + '/mailFolders/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=/messages', + undefined, + { $select: 'bodyPreview,subject' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.workflow.json new file mode 100644 index 0000000000..2c451ead0a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/folderMessage/getAll.workflow.json @@ -0,0 +1,84 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "resource": "folderMessage", + "folderId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=", + "mode": "list", + "cachedResultName": "Folder 42", + "cachedResultUrl": "https://outlook.office365.com/mail/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De%2FLkrSqpPI8eyjUmAAAFXBAEHAAA%3D" + }, + "returnAll": true, + "output": "fields", + "fields": [ + "bodyPreview", + "subject" + ], + "options": {} + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.etag": "W/\"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CAj\"", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAABZf4De-LkrSqpPI8eyjUmAAAFXBGDUAAA=", + "subject": "XXXX", + "bodyPreview": "test" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "63e4c95c-2998-4f3a-bcbc-1dfea015fecb", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.test.ts new file mode 100644 index 0000000000..3d0d88abc3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.test.ts @@ -0,0 +1,61 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return {}; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, message => move', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/message/move.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEJAABZf4De-LkrSqpPI8eyjUmAAAFXBEVwAAA=/move', + { + destinationId: + 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=', + }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.workflow.json new file mode 100644 index 0000000000..b171498cd5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/move.workflow.json @@ -0,0 +1,81 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "operation": "move", + "messageId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEJAABZf4De-LkrSqpPI8eyjUmAAAFXBEVwAAA=", + "mode": "list", + "cachedResultName": "Hello", + "cachedResultUrl": "https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEJAABZf4De%2FLkrSqpPI8eyjUmAAAFXBEVwAAA%3D&exvsurl=1&viewmodel=ReadMessageItem" + }, + "folderId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=", + "mode": "list", + "cachedResultName": "Folder 42", + "cachedResultUrl": "https://outlook.office365.com/mail/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De%2FLkrSqpPI8eyjUmAAAFXBAEHAAA%3D" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "1434b547-7141-4650-8dce-333dda26e092", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.test.ts new file mode 100644 index 0000000000..0fbc694ece --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.test.ts @@ -0,0 +1,128 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/messages/$entity", + '@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CX+"', + id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDurAAA=', + createdDateTime: '2023-09-04T12:29:59Z', + lastModifiedDateTime: '2023-09-04T12:29:59Z', + changeKey: 'CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CX+', + categories: [], + receivedDateTime: '2023-09-04T12:29:59Z', + sentDateTime: '2023-09-04T12:29:59Z', + hasAttachments: false, + internetMessageId: + '', + subject: 'Reply Subject', + bodyPreview: 'Reply message', + importance: 'high', + parentFolderId: + 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAAA=', + conversationId: + 'AAQkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAQAKwkQLinj69KtoFOxMG2lVY=', + conversationIndex: 'AQHZ3yq3rCRAuKePr0q2gU7EwbaVVrAKmLQ4', + isDeliveryReceiptRequested: false, + isReadReceiptRequested: false, + isRead: true, + isDraft: true, + webLink: + 'https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEPAABZf4De%2FLkrSqpPI8eyjUmAAAFXBDurAAA%3D&exvsurl=1&viewmodel=ReadMessageItem', + inferenceClassification: 'focused', + body: { + contentType: 'html', + content: + '\r\nReply message ', + }, + sender: { + emailAddress: { + name: 'Michael Kret', + address: 'MichaelDevSandbox@5w1hb7.onmicrosoft.com', + }, + }, + from: { + emailAddress: { + name: 'Michael Kret', + address: 'MichaelDevSandbox@5w1hb7.onmicrosoft.com', + }, + }, + toRecipients: [ + { + emailAddress: { + name: 'reply@mail.com', + address: 'reply@mail.com', + }, + }, + ], + ccRecipients: [], + bccRecipients: [], + replyTo: [], + flag: { + flagStatus: 'notFlagged', + }, + }; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, message => reply', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/message/reply.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(2); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEJAABZf4De-LkrSqpPI8eyjUmAAAFXBEVwAAA=/createReply', + { + message: { + body: { content: 'Reply message', contentType: 'html' }, + importance: 'High', + subject: 'Reply Subject', + }, + }, + ); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDurAAA=/send', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.workflow.json new file mode 100644 index 0000000000..9df40c729f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/reply.workflow.json @@ -0,0 +1,134 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "operation": "reply", + "messageId": { + "__rl": true, + "value": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEJAABZf4De-LkrSqpPI8eyjUmAAAFXBEVwAAA=", + "mode": "list", + "cachedResultName": "Hello", + "cachedResultUrl": "https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEJAABZf4De%2FLkrSqpPI8eyjUmAAAFXBEVwAAA%3D&exvsurl=1&viewmodel=ReadMessageItem" + }, + "replyToSenderOnly": true, + "message": "Reply message", + "additionalFields": { + "importance": "High", + "bodyContentType": "html", + "subject": "Reply Subject" + }, + "options": {} + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('b834447b-6848-4af9-8390-d2259ce46b74')/messages/$entity", + "@odata.etag": "W/\"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CX+\"", + "id": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDurAAA=", + "createdDateTime": "2023-09-04T12:29:59Z", + "lastModifiedDateTime": "2023-09-04T12:29:59Z", + "changeKey": "CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CX+", + "categories": [], + "receivedDateTime": "2023-09-04T12:29:59Z", + "sentDateTime": "2023-09-04T12:29:59Z", + "hasAttachments": false, + "internetMessageId": "", + "subject": "Reply Subject", + "bodyPreview": "Reply message", + "importance": "high", + "parentFolderId": "AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAAA=", + "conversationId": "AAQkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAQAKwkQLinj69KtoFOxMG2lVY=", + "conversationIndex": "AQHZ3yq3rCRAuKePr0q2gU7EwbaVVrAKmLQ4", + "isDeliveryReceiptRequested": false, + "isReadReceiptRequested": false, + "isRead": true, + "isDraft": true, + "webLink": "https://outlook.office365.com/owa/?ItemID=AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De%2FLkrSqpPI8eyjUmAAAAAAAEPAABZf4De%2FLkrSqpPI8eyjUmAAAFXBDurAAA%3D&exvsurl=1&viewmodel=ReadMessageItem", + "inferenceClassification": "focused", + "body": { + "contentType": "html", + "content": "\r\nReply message " + }, + "sender": { + "emailAddress": { + "name": "Michael Kret", + "address": "MichaelDevSandbox@5w1hb7.onmicrosoft.com" + } + }, + "from": { + "emailAddress": { + "name": "Michael Kret", + "address": "MichaelDevSandbox@5w1hb7.onmicrosoft.com" + } + }, + "toRecipients": [ + { + "emailAddress": { + "name": "reply@mail.com", + "address": "reply@mail.com" + } + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "flag": { + "flagStatus": "notFlagged" + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "edd34dd1-0321-4d5b-a935-0c47d4c746b8", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.test.ts new file mode 100644 index 0000000000..92334903d7 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.test.ts @@ -0,0 +1,67 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return {}; + } + }), + }; +}); + +describe('Test MicrosoftOutlookV2, message => send', () => { + const workflows = ['nodes/Microsoft/Outlook/test/v2/node/message/send.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/sendMail', + { + message: { + body: { content: 'message description', contentType: 'Text' }, + replyTo: [{ emailAddress: { address: 'reply@mail.com' } }], + subject: 'Hello', + toRecipients: [{ emailAddress: { address: 'to@mail.com' } }], + }, + saveToSentItems: true, + }, + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.workflow.json b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.workflow.json new file mode 100644 index 0000000000..62c3d999f8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/send.workflow.json @@ -0,0 +1,73 @@ +{ + "name": "My workflow 21", + "nodes": [ + { + "parameters": {}, + "id": "e524f588-b6a3-4849-8777-b32a8a755ae5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 820, + 360 + ] + }, + { + "parameters": { + "toRecipients": "to@mail.com", + "subject": "Hello", + "bodyContent": "message description", + "additionalFields": { + "bodyContentType": "Text", + "replyTo": "reply@mail.com" + } + }, + "id": "baff6798-0304-4255-bdb0-dd3f2659373b", + "name": "Microsoft Outlook", + "type": "n8n-nodes-base.microsoftOutlook", + "typeVersion": 2, + "position": [ + 1040, + 360 + ], + "credentials": { + "microsoftOutlookOAuth2Api": { + "id": "iXJCki7i5Vz0bdks", + "name": "Microsoft Outlook account 2" + } + } + } + ], + "pinData": { + "Microsoft Outlook": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Outlook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "fed8ec2e-8663-4dfc-8234-33eb83257260", + "id": "1CYHzBXQw1nfPGtB", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/utils/utils.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/utils/utils.test.ts new file mode 100644 index 0000000000..9d3da8937f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/utils/utils.test.ts @@ -0,0 +1,231 @@ +import { + createMessage, + makeRecipient, + prepareContactFields, + prepareFilterString, + simplifyOutputMessages, +} from '../../../v2/helpers/utils'; + +describe('Test MicrosoftOutlookV2, makeRecipient', () => { + it('should create recipient object', () => { + const replyTo = 'test1@mail.com, test2@mail.com'; + const result = replyTo.split(',').map((recipient: string) => { + return makeRecipient(recipient.trim()); + }); + + expect(result).toEqual([ + { + emailAddress: { + address: 'test1@mail.com', + }, + }, + { + emailAddress: { + address: 'test2@mail.com', + }, + }, + ]); + }); +}); + +describe('Test MicrosoftOutlookV2, prepareContactFields', () => { + it('should create contact object', () => { + const fields = { + assistantName: 'Assistant', + birthday: '2023-07-31T21:00:00.000Z', + businessAddress: { + values: { + city: 'City', + countryOrRegion: 'Country', + postalCode: '777777', + state: 'State', + street: 'Street', + }, + }, + businessHomePage: 'page.com', + categories: 'cat1,cat2', + companyName: 'Company', + }; + const result = { + assistantName: 'Assistant', + birthday: '2023-07-31T21:00:00.000Z', + businessAddress: { + city: 'City', + countryOrRegion: 'Country', + postalCode: '777777', + state: 'State', + street: 'Street', + }, + businessHomePage: 'page.com', + categories: ['cat1', 'cat2'], + companyName: 'Company', + }; + + const data = prepareContactFields(fields); + + expect(data).toEqual(result); + }); +}); + +describe('Test MicrosoftOutlookV2, prepareFilterString', () => { + it('should create filter string', () => { + const filters = { + filterBy: 'filters', + filters: { + custom: 'isRead eq false', + hasAttachments: true, + foldersToExclude: ['AAAxBBB...='], + foldersToInclude: ['DDDxCCC...='], + readStatus: 'unread', + receivedAfter: '2023-07-31T21:00:00.000Z', + receivedBefore: '2023-08-14T21:00:00.000Z', + sender: 'test@mail.com', + }, + }; + const result = + "parentFolderId eq 'DDDxCCC...=' and parentFolderId ne 'AAAxBBB...=' and (from/emailAddress/address eq 'test@mail.com' or from/emailAddress/name eq 'test@mail.com') and hasAttachments eq true and isRead eq false and receivedDateTime ge 2023-07-31T21:00:00.000Z and receivedDateTime le 2023-08-14T21:00:00.000Z and isRead eq false"; + + const data = prepareFilterString(filters); + + expect(data).toEqual(result); + }); +}); + +describe('Test MicrosoftOutlookV2, simplifyOutputMessages', () => { + it('should create recipient object', () => { + const responseData = { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#users('')/messages(id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments)/$entity", + '@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFSpKec"', + id: 'AAAxBBBxCCC...=', + categories: [], + hasAttachments: false, + subject: 'My draft', + bodyPreview: + 'test\r\n________________________________\r\nFrom: Me\r\nSent: Tuesday, August 29, 2023 7:33:28 AM\r\nTo: from@mail.com \r\nSubject: My draft\r\n\r\nthis is a draft', + conversationId: 'AAAQQQMMM..=', + from: { + emailAddress: { + name: 'Me', + address: 'test@mail.com', + }, + }, + toRecipients: [ + { + emailAddress: { + name: 'Me', + address: 'test@mail.com', + }, + }, + ], + }; + const result = [ + { + id: 'AAAxBBBxCCC...=', + conversationId: 'AAAQQQMMM..=', + subject: 'My draft', + bodyPreview: + 'test\r\n________________________________\r\nFrom: Me\r\nSent: Tuesday, August 29, 2023 7:33:28 AM\r\nTo: from@mail.com \r\nSubject: My draft\r\n\r\nthis is a draft', + from: 'test@mail.com', + to: ['test@mail.com'], + categories: [], + hasAttachments: false, + }, + ]; + + const data = simplifyOutputMessages([responseData]); + + expect(data).toEqual(result); + }); +}); + +describe('Test MicrosoftOutlookV2, createMessage', () => { + it('should create message object', () => { + const fields = { + bodyContent: 'Test message', + bodyContentType: 'Text', + bccRecipients: 'test1@mail.com, test2@mail.com', + categories: ['cat1', 'cat2', 'cat3'], + ccRecipients: 'test3@mail.com', + internetMessageHeaders: [ + { + name: 'customHeader', + value: 'customValue', + }, + { + name: 'customHeader2', + value: 'customValue2', + }, + ], + from: 'me@mail.com', + importance: 'Normal', + isReadReceiptRequested: true, + replyTo: 'test4@mail.com', + subject: 'Test Subject', + toRecipients: 'to@mail.com', + }; + + const result = { + body: { + content: 'Test message', + contentType: 'Text', + }, + bccRecipients: [ + { + emailAddress: { + address: 'test1@mail.com', + }, + }, + { + emailAddress: { + address: 'test2@mail.com', + }, + }, + ], + categories: ['cat1', 'cat2', 'cat3'], + ccRecipients: [ + { + emailAddress: { + address: 'test3@mail.com', + }, + }, + ], + internetMessageHeaders: [ + { + name: 'customHeader', + value: 'customValue', + }, + { + name: 'customHeader2', + value: 'customValue2', + }, + ], + from: { + emailAddress: { + address: 'me@mail.com', + }, + }, + importance: 'Normal', + isReadReceiptRequested: true, + replyTo: [ + { + emailAddress: { + address: 'test4@mail.com', + }, + }, + ], + subject: 'Test Subject', + toRecipients: [ + { + emailAddress: { + address: 'to@mail.com', + }, + }, + ], + }; + + const message = createMessage(fields); + + expect(message).toEqual(result); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/DraftDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/DraftDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/DraftDescription.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/DraftDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/DraftMessageSharedDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/DraftMessageSharedDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/FolderDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/FolderDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/FolderDescription.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/FolderDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/FolderMessageDecription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/FolderMessageDecription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/FolderMessageDecription.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/FolderMessageDecription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/GenericFunctions.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MessageAttachmentDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageAttachmentDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/MessageAttachmentDescription.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageAttachmentDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts rename to packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v1/MicrosoftOutlookV1.node.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MicrosoftOutlookV1.node.ts new file mode 100644 index 0000000000..ef8b39a18d --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MicrosoftOutlookV1.node.ts @@ -0,0 +1,1138 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ + +import type { + IBinaryKeyData, + IDataObject, + IExecuteFunctions, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; + +import { + createMessage, + downloadAttachments, + makeRecipient, + microsoftApiRequest, + microsoftApiRequestAllItems, +} from './GenericFunctions'; + +import { draftFields, draftOperations } from './DraftDescription'; + +import { draftMessageSharedFields } from './DraftMessageSharedDescription'; + +import { messageFields, messageOperations } from './MessageDescription'; + +import { + messageAttachmentFields, + messageAttachmentOperations, +} from './MessageAttachmentDescription'; + +import { folderFields, folderOperations } from './FolderDescription'; + +import { folderMessageFields, folderMessageOperations } from './FolderMessageDecription'; + +import { oldVersionNotice } from '@utils/descriptions'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Outlook', + name: 'microsoftOutlook', + group: ['transform'], + icon: 'file:outlook.svg', + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Outlook API', + defaults: { + name: 'Microsoft Outlook', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftOutlookOAuth2Api', + required: true, + }, + ], + properties: [ + oldVersionNotice, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + default: 'message', + options: [ + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Folder', + value: 'folder', + }, + { + name: 'Folder Message', + value: 'folderMessage', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Message Attachment', + value: 'messageAttachment', + }, + ], + }, + // Draft + ...draftOperations, + ...draftFields, + // Message + ...messageOperations, + ...messageFields, + // Message Attachment + ...messageAttachmentOperations, + ...messageAttachmentFields, + // Folder + ...folderOperations, + ...folderFields, + // Folder Message + ...folderMessageOperations, + ...folderMessageFields, + + // Draft & Message + ...draftMessageSharedFields, + ], +}; + +export class MicrosoftOutlookV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + // Get all the categories to display them to user so that he can + // select them easily + async getCategories(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const categories = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + '/outlook/masterCategories', + ); + for (const category of categories) { + returnData.push({ + name: category.displayName as string, + value: category.id as string, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length; + const qs: IDataObject = {}; + let responseData; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + if (['draft', 'message'].includes(resource)) { + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + responseData = await microsoftApiRequest.call(this, 'DELETE', `/messages/${messageId}`); + + returnData.push({ success: true }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}`, + undefined, + qs, + ); + + if (additionalFields.dataPropertyAttachmentsPrefixName) { + const prefix = additionalFields.dataPropertyAttachmentsPrefixName as string; + const data = await downloadAttachments.call( + this, + responseData as IDataObject[], + prefix, + ); + returnData.push.apply(returnData, data as unknown as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + + if (additionalFields.dataPropertyAttachmentsPrefixName) { + return [returnData as INodeExecutionData[]]; + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i); + + // Create message from optional fields + const body: IDataObject = createMessage(updateFields); + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/messages/${messageId}`, + body, + {}, + ); + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + } + + if (resource === 'draft') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + try { + const additionalFields = this.getNodeParameter('additionalFields', i); + + const subject = this.getNodeParameter('subject', i) as string; + + const bodyContent = this.getNodeParameter('bodyContent', i, '') as string; + + additionalFields.subject = subject; + + additionalFields.bodyContent = bodyContent || ' '; + + // Create message object from optional fields + const body: IDataObject = createMessage(additionalFields); + + if (additionalFields.attachments) { + const attachments = (additionalFields.attachments as IDataObject) + .attachments as IDataObject[]; + + // // Handle attachments + body.attachments = attachments.map((attachment) => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[i].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: i, + }); + } + + if ((items[i].binary as IDataObject)[binaryPropertyName] === undefined) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: i }, + ); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: binaryData.fileName, + contentBytes: binaryData.data, + }; + }); + } + + responseData = await microsoftApiRequest.call(this, 'POST', '/messages', body, {}); + + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'send') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i); + const additionalFields = this.getNodeParameter('additionalFields', i, {}); + + if (additionalFields?.recipients) { + const recipients = (additionalFields.recipients as string) + .split(',') + .filter((email) => !!email); + if (recipients.length !== 0) { + await microsoftApiRequest.call(this, 'PATCH', `/messages/${messageId}`, { + toRecipients: recipients.map((recipient: string) => makeRecipient(recipient)), + }); + } + } + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/send`, + ); + + returnData.push({ success: true }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + } + + if (resource === 'message') { + if (operation === 'reply') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const replyType = this.getNodeParameter('replyType', i) as string; + const comment = this.getNodeParameter('comment', i) as string; + const send = this.getNodeParameter('send', i, false) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i, {}); + + const body: IDataObject = {}; + + let action = 'createReply'; + if (replyType === 'replyAll') { + body.comment = comment; + action = 'createReplyAll'; + } else { + body.comment = comment; + body.message = {}; + Object.assign(body.message, createMessage(additionalFields)); + delete (body.message as IDataObject).attachments; + } + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/${action}`, + body, + ); + + if (additionalFields.attachments) { + const attachments = (additionalFields.attachments as IDataObject) + .attachments as IDataObject[]; + // // Handle attachments + const data = attachments.map((attachment) => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[i].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: i, + }); + } + + if ((items[i].binary as IDataObject)[binaryPropertyName] === undefined) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: i }, + ); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: binaryData.fileName, + contentBytes: binaryData.data, + }; + }); + + for (const attachment of data) { + await microsoftApiRequest.call( + this, + 'POST', + `/messages/${responseData.id}/attachments`, + attachment, + {}, + ); + } + } + + if (send) { + await microsoftApiRequest.call(this, 'POST', `/messages/${responseData.id}/send`); + } + + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'getMime') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i); + const response = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/$value`, + undefined, + {}, + undefined, + {}, + { encoding: null, resolveWithFullResponse: true }, + ); + + let mimeType: string | undefined; + if (response.headers['content-type']) { + mimeType = response.headers['content-type']; + } + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary!, items[i].binary); + } + + items[i] = newItem; + + const fileName = `${messageId}.eml`; + const data = Buffer.from(response.body as string, 'utf8'); + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData( + data as unknown as Buffer, + fileName, + mimeType, + ); + } catch (error) { + if (this.continueOnFail()) { + items[i].json = { error: error.message }; + continue; + } + throw error; + } + } + } + + if (operation === 'getAll') { + let additionalFields: IDataObject = {}; + for (let i = 0; i < length; i++) { + try { + const returnAll = this.getNodeParameter('returnAll', i); + additionalFields = this.getNodeParameter('additionalFields', i); + + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + + const endpoint = '/messages'; + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + + if (additionalFields.dataPropertyAttachmentsPrefixName) { + const prefix = additionalFields.dataPropertyAttachmentsPrefixName as string; + const data = await downloadAttachments.call( + this, + responseData as IDataObject[], + prefix, + ); + returnData.push.apply(returnData, data as unknown as IDataObject[]); + } else { + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + + if (additionalFields.dataPropertyAttachmentsPrefixName) { + return [returnData as INodeExecutionData[]]; + } + } + + if (operation === 'move') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const destinationId = this.getNodeParameter('folderId', i) as string; + const body: IDataObject = { + destinationId, + }; + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/move`, + body, + ); + returnData.push({ success: true }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'send') { + for (let i = 0; i < length; i++) { + try { + const additionalFields = this.getNodeParameter('additionalFields', i); + + const toRecipients = this.getNodeParameter('toRecipients', i) as string; + + const subject = this.getNodeParameter('subject', i) as string; + + const bodyContent = this.getNodeParameter('bodyContent', i, '') as string; + + additionalFields.subject = subject; + + additionalFields.bodyContent = bodyContent || ' '; + + additionalFields.toRecipients = toRecipients; + + const saveToSentItems = + additionalFields.saveToSentItems === undefined + ? true + : additionalFields.saveToSentItems; + delete additionalFields.saveToSentItems; + + // Create message object from optional fields + const message: IDataObject = createMessage(additionalFields); + + if (additionalFields.attachments) { + const attachments = (additionalFields.attachments as IDataObject) + .attachments as IDataObject[]; + + // // Handle attachments + message.attachments = attachments.map((attachment) => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[i].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: i, + }); + } + if ((items[i].binary as IDataObject)[binaryPropertyName] === undefined) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: i }, + ); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: binaryData.fileName, + contentBytes: binaryData.data, + }; + }); + } + + const body: IDataObject = { + message, + saveToSentItems, + }; + + responseData = await microsoftApiRequest.call(this, 'POST', '/sendMail', body, {}); + returnData.push({ success: true }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + } + + if (resource === 'messageAttachment') { + if (operation === 'add') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0); + const additionalFields = this.getNodeParameter('additionalFields', i); + + if (items[i].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!'); + } + + if ((items[i].binary as IDataObject)[binaryPropertyName] === undefined) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: i }, + ); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + const dataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); + + const fileName = + additionalFields.fileName === undefined + ? binaryData.fileName + : additionalFields.fileName; + + if (!fileName) { + throw new NodeOperationError( + this.getNode(), + 'File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!', + { itemIndex: i }, + ); + } + + // Check if the file is over 3MB big + if (dataBuffer.length > 3e6) { + // Maximum chunk size is 4MB + const chunkSize = 4e6; + const body: IDataObject = { + AttachmentItem: { + attachmentType: 'file', + name: fileName, + size: dataBuffer.length, + }, + }; + + // Create upload session + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/attachments/createUploadSession`, + body, + ); + const uploadUrl = responseData.uploadUrl; + + if (uploadUrl === undefined) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: 'Failed to get upload session', + }); + } + + for ( + let bytesUploaded = 0; + bytesUploaded < dataBuffer.length; + bytesUploaded += chunkSize + ) { + // Upload the file chunk by chunk + const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length); + const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`; + + const data = dataBuffer.subarray(bytesUploaded, nextChunk); + + responseData = await this.helpers.request(uploadUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': data.length, + 'Content-Range': contentRange, + }, + body: data, + }); + } + } else { + const body: IDataObject = { + '@odata.type': '#microsoft.graph.fileAttachment', + name: fileName, + contentBytes: binaryData.data, + }; + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/attachments`, + body, + {}, + ); + } + returnData.push({ success: true }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'download') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i); + + // Get attachment details first + const attachmentDetails = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments/${attachmentId}`, + undefined, + { $select: 'id,name,contentType' }, + ); + + let mimeType: string | undefined; + if (attachmentDetails.contentType) { + mimeType = attachmentDetails.contentType; + } + const fileName = attachmentDetails.name; + + const response = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments/${attachmentId}/$value`, + undefined, + {}, + undefined, + {}, + { encoding: null, resolveWithFullResponse: true }, + ); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary!, items[i].binary); + } + + items[i] = newItem; + const data = Buffer.from(response.body as string, 'utf8'); + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData( + data as unknown as Buffer, + fileName as string, + mimeType, + ); + } catch (error) { + if (this.continueOnFail()) { + items[i].json = { error: error.message }; + continue; + } + throw error; + } + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + + // Have sane defaults so we don't fetch attachment data in this operation + qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline'; + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments/${attachmentId}`, + undefined, + qs, + ); + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + try { + const messageId = this.getNodeParameter('messageId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const additionalFields = this.getNodeParameter('additionalFields', i); + + // Have sane defaults so we don't fetch attachment data in this operation + qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline'; + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + + const endpoint = `/messages/${messageId}/attachments`; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + } + + if (resource === 'folder') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + try { + const displayName = this.getNodeParameter('displayName', i) as string; + const folderType = this.getNodeParameter('folderType', i) as string; + const body: IDataObject = { + displayName, + }; + + let endpoint = '/mailFolders'; + + if (folderType === 'searchFolder') { + endpoint = '/mailFolders/searchfolders/childFolders'; + const includeNestedFolders = this.getNodeParameter('includeNestedFolders', i); + const sourceFolderIds = this.getNodeParameter('sourceFolderIds', i); + const filterQuery = this.getNodeParameter('filterQuery', i); + Object.assign(body, { + '@odata.type': 'microsoft.graph.mailSearchFolder', + includeNestedFolders, + sourceFolderIds, + filterQuery, + }); + } + + responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body); + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + try { + const folderId = this.getNodeParameter('folderId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'DELETE', + `/mailFolders/${folderId}`, + ); + returnData.push({ success: true }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + try { + const folderId = this.getNodeParameter('folderId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/mailFolders/${folderId}`, + {}, + qs, + ); + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + try { + const returnAll = this.getNodeParameter('returnAll', i); + const additionalFields = this.getNodeParameter('additionalFields', i); + + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + '/mailFolders', + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call(this, 'GET', '/mailFolders', {}, qs); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'getChildren') { + for (let i = 0; i < length; i++) { + try { + const folderId = this.getNodeParameter('folderId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const additionalFields = this.getNodeParameter('additionalFields', i); + + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/mailFolders/${folderId}/childFolders`, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/mailFolders/${folderId}/childFolders`, + undefined, + qs, + ); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + try { + const folderId = this.getNodeParameter('folderId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i); + + const body: IDataObject = { + ...updateFields, + }; + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/mailFolders/${folderId}`, + body, + ); + returnData.push(responseData as IDataObject); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + } + + if (resource === 'folderMessage') { + for (let i = 0; i < length; i++) { + try { + if (operation === 'getAll') { + const folderId = this.getNodeParameter('folderId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const additionalFields = this.getNodeParameter('additionalFields', i); + + if (additionalFields.fields) { + qs.$select = additionalFields.fields; + } + + if (additionalFields.filter) { + qs.$filter = additionalFields.filter; + } + + const endpoint = `/mailFolders/${folderId}/messages`; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + } + + if ( + (resource === 'message' && operation === 'getMime') || + (resource === 'messageAttachment' && operation === 'download') + ) { + return [items]; + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/MicrosoftOutlookV2.node.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/MicrosoftOutlookV2.node.ts new file mode 100644 index 0000000000..ac4cc3eb1b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/MicrosoftOutlookV2.node.ts @@ -0,0 +1,29 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ + +import type { + IExecuteFunctions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { loadOptions, listSearch } from './methods'; +import { description } from './actions/node.description'; +import { router } from './actions/router'; + +export class MicrosoftOutlookV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...description, + }; + } + + methods = { loadOptions, listSearch }; + + async execute(this: IExecuteFunctions) { + return router.call(this); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/create.operation.ts new file mode 100644 index 0000000000..7602109481 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/create.operation.ts @@ -0,0 +1,116 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + description: 'The name of the calendar to create', + placeholder: 'e.g. My Calendar', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Calendar Group', + name: 'calendarGroup', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendarGroups', + }, + default: [], + description: + 'If set, the calendar will be created in the specified calendar group. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + default: 'lightBlue', + options: [ + { + name: 'Light Blue', + value: 'lightBlue', + }, + { + name: 'Light Brown', + value: 'lightBrown', + }, + { + name: 'Light Gray', + value: 'lightGray', + }, + { + name: 'Light Green', + value: 'lightGreen', + }, + { + name: 'Light Orange', + value: 'lightOrange', + }, + { + name: 'Light Pink', + value: 'lightPink', + }, + { + name: 'Light Red', + value: 'lightRed', + }, + { + name: 'Light Teal', + value: 'lightTeal', + }, + { + name: 'Light Yellow', + value: 'lightYellow', + }, + ], + description: 'Specify the color to distinguish the calendar from the others', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['calendar'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const additionalFields = this.getNodeParameter('additionalFields', index); + const name = this.getNodeParameter('name', index) as string; + + let endpoint = '/calendars'; + + if (additionalFields.calendarGroup) { + endpoint = `/calendarGroups/${additionalFields.calendarGroup}/calendars`; + delete additionalFields.calendarGroup; + } + + const body: IDataObject = { + name, + ...additionalFields, + }; + + const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/delete.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/delete.operation.ts new file mode 100644 index 0000000000..087e1b9864 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/delete.operation.ts @@ -0,0 +1,30 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { calendarRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [calendarRLC]; + +const displayOptions = { + show: { + resource: ['calendar'], + operation: ['delete'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const calendarId = this.getNodeParameter('calendarId', index, undefined, { + extractValue: true, + }) as string; + + await microsoftApiRequest.call(this, 'DELETE', `/calendars/${calendarId}`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/get.operation.ts new file mode 100644 index 0000000000..a0963c9218 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/get.operation.ts @@ -0,0 +1,38 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { calendarRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [calendarRLC]; + +const displayOptions = { + show: { + resource: ['calendar'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const qs: IDataObject = {}; + + const calendarId = this.getNodeParameter('calendarId', index, undefined, { + extractValue: true, + }) as string; + + const responseData = await microsoftApiRequest.call( + this, + 'GET', + `/calendars/${calendarId}`, + undefined, + qs, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/getAll.operation.ts new file mode 100644 index 0000000000..9fe31434a3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/getAll.operation.ts @@ -0,0 +1,78 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + ...returnAllOrLimit, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Filter Query', + name: 'custom', + type: 'string', + default: '', + placeholder: 'e.g. canShare eq true', + hint: 'Search query to filter calendars. More info.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['calendar'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs = {} as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', index); + const filters = this.getNodeParameter('filters', index, {}); + + if (Object.keys(filters).length) { + const filterString: string[] = []; + + if (filters.custom) { + filterString.push(filters.custom as string); + } + + if (filterString.length) { + qs.$filter = filterString.join(' and '); + } + } + + const endpoint = '/calendars'; + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', index); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/index.ts new file mode 100644 index 0000000000..132fa72b4e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/index.ts @@ -0,0 +1,61 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as create from './create.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as del from './delete.operation'; +import * as update from './update.operation'; + +export { create, del as delete, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['calendar'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new calendar', + action: 'Create a calendar', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a calendar', + action: 'Delete a calendar', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a calendar', + action: 'Get a calendar', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'List and search calendars', + action: 'Get many calendars', + }, + { + name: 'Update', + value: 'update', + description: 'Update a calendar', + action: 'Update a calendar', + }, + ], + default: 'getAll', + }, + + ...create.description, + ...del.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/update.operation.ts new file mode 100644 index 0000000000..267bccc7d3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/calendar/update.operation.ts @@ -0,0 +1,108 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { calendarRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + calendarRLC, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Color', + name: 'color', + type: 'options', + default: 'lightBlue', + options: [ + { + name: 'Light Blue', + value: 'lightBlue', + }, + { + name: 'Light Brown', + value: 'lightBrown', + }, + { + name: 'Light Gray', + value: 'lightGray', + }, + { + name: 'Light Green', + value: 'lightGreen', + }, + { + name: 'Light Orange', + value: 'lightOrange', + }, + { + name: 'Light Pink', + value: 'lightPink', + }, + { + name: 'Light Red', + value: 'lightRed', + }, + { + name: 'Light Teal', + value: 'lightTeal', + }, + { + name: 'Light Yellow', + value: 'lightYellow', + }, + ], + description: 'Specify the color to distinguish the calendar from the others', + }, + { + displayName: 'Default Calendar', + name: 'isDefaultCalendar', + type: 'boolean', + default: false, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. My Calendar', + description: 'The name of the calendar', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['calendar'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const updateFields = this.getNodeParameter('updateFields', index); + + const calendarId = this.getNodeParameter('calendarId', index, undefined, { + extractValue: true, + }) as string; + + const endpoint = `/calendars/${calendarId}`; + + const body: IDataObject = { + ...updateFields, + }; + + const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/create.operation.ts new file mode 100644 index 0000000000..223dbfb378 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/create.operation.ts @@ -0,0 +1,62 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { prepareContactFields } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { contactFields } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + { + displayName: 'First Name', + name: 'givenName', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Last Name', + name: 'surname', + type: 'string', + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: contactFields, + }, +]; + +const displayOptions = { + show: { + resource: ['contact'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const additionalFields = this.getNodeParameter('additionalFields', index); + const givenName = this.getNodeParameter('givenName', index) as string; + const surname = this.getNodeParameter('surname', index) as string; + + const body: IDataObject = { + givenName, + ...prepareContactFields(additionalFields), + }; + + if (surname) { + body.surname = surname; + } + + const responseData = await microsoftApiRequest.call(this, 'POST', '/contacts', body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/delete.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/delete.operation.ts new file mode 100644 index 0000000000..5e2be1dcb0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/delete.operation.ts @@ -0,0 +1,29 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { contactRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [contactRLC]; + +const displayOptions = { + show: { + resource: ['contact'], + operation: ['delete'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const contactId = this.getNodeParameter('contactId', index, undefined, { + extractValue: true, + }) as string; + await microsoftApiRequest.call(this, 'DELETE', `/contacts/${contactId}`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/get.operation.ts new file mode 100644 index 0000000000..3bd192ad66 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/get.operation.ts @@ -0,0 +1,85 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { contactFields } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { contactRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + contactRLC, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: contactFields, + default: [], + }, +]; + +const displayOptions = { + show: { + resource: ['contact'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const qs: IDataObject = {}; + + const contactId = this.getNodeParameter('contactId', index, undefined, { + extractValue: true, + }) as string; + + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = 'id,displayName,emailAddresses,businessPhones,mobilePhone'; + } + + const responseData = await microsoftApiRequest.call( + this, + 'GET', + `/contacts/${contactId}`, + undefined, + qs, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/getAll.operation.ts new file mode 100644 index 0000000000..4217b82b4c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/getAll.operation.ts @@ -0,0 +1,137 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { contactFields } from '../../helpers/utils'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + ...returnAllOrLimit, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: contactFields, + default: [], + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Filter Query', + name: 'custom', + type: 'string', + default: '', + placeholder: "e.g. displayName eq 'John Doe'", + hint: 'Search query to filter contacts. More info.', + }, + { + displayName: 'Email Address', + name: 'emailAddress', + type: 'string', + default: '', + description: + 'If contacts that you want to retrieve have multiple email addresses, you can enter them separated by commas', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['contact'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs = {} as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', index); + const filters = this.getNodeParameter('filters', index, {}); + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = 'id,displayName,emailAddresses,businessPhones,mobilePhone'; + } + + if (Object.keys(filters).length) { + const filterString: string[] = []; + + if (filters.emailAddress) { + const emails = (filters.emailAddress as string) + .split(',') + .map((email) => `emailAddresses/any(a:a/address eq '${email.trim()}')`); + filterString.push(emails.join(' and ')); + } + + if (filters.custom) { + filterString.push(filters.custom as string); + } + + if (filterString.length) { + qs.$filter = filterString.join(' and '); + } + } + + const endpoint = '/contacts'; + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', index); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/index.ts new file mode 100644 index 0000000000..81d2bbcf4e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/index.ts @@ -0,0 +1,60 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as create from './create.operation'; +import * as del from './delete.operation'; +import * as getAll from './getAll.operation'; +import * as get from './get.operation'; +import * as update from './update.operation'; + +export { create, del as delete, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['contact'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new contact', + action: 'Create a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + action: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a contact', + action: 'Get a contact', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'List and search contacts', + action: 'Get many contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + action: 'Update a contact', + }, + ], + default: 'getAll', + }, + ...create.description, + ...del.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/update.operation.ts new file mode 100644 index 0000000000..07ffde1fda --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/contact/update.operation.ts @@ -0,0 +1,49 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { prepareContactFields } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { contactFields, contactRLC } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + contactRLC, + { + displayName: 'Update Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: contactFields, + }, +]; + +const displayOptions = { + show: { + resource: ['contact'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const additionalFields = this.getNodeParameter('additionalFields', index); + const contactId = this.getNodeParameter('contactId', index, undefined, { + extractValue: true, + }) as string; + + const body: IDataObject = prepareContactFields(additionalFields); + + const responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/contacts/${contactId}`, + body, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/create.operation.ts new file mode 100644 index 0000000000..f7f6bb3c48 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/create.operation.ts @@ -0,0 +1,259 @@ +import type { + IBinaryKeyData, + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { createMessage } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message', + type: 'string', + default: '', + }, + { + displayName: 'Message', + name: 'bodyContent', + description: 'Message body content', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'fixedCollection', + placeholder: 'Add Attachment', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachments', + displayName: 'Attachment', + values: [ + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + type: 'string', + default: '', + placeholder: 'e.g. data', + hint: 'The name of the input field containing the binary file data to be attached', + }, + ], + }, + ], + }, + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Comma-separated list of email addresses of BCC recipients', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + { + displayName: 'Category Names or IDs', + name: 'categories', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + typeOptions: { + loadOptionsMethod: 'getCategoriesNames', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: 'Comma-separated list of email addresses of CC recipients', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + { + displayName: 'Custom Headers', + name: 'internetMessageHeaders', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'headers', + displayName: 'Header', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the header', + }, + ], + }, + ], + }, + { + displayName: 'From', + name: 'from', + description: + 'The owner of the mailbox from which the message is sent. Must correspond to the actual mailbox used.', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + { + displayName: 'Importance', + name: 'importance', + description: 'The importance of the message', + type: 'options', + options: [ + { + name: 'Low', + value: 'Low', + }, + { + name: 'Normal', + value: 'Normal', + }, + { + name: 'High', + value: 'High', + }, + ], + default: 'Normal', + }, + { + displayName: 'Message Type', + name: 'bodyContentType', + description: 'Message body content type', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Whether a read receipt is requested for the message', + type: 'boolean', + default: false, + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email address to use when replying', + type: 'string', + placeholder: 'e.g. replyto@example.com', + default: '', + }, + { + displayName: 'To', + name: 'toRecipients', + description: 'Comma-separated list of email addresses of recipients', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['draft'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) { + const additionalFields = this.getNodeParameter('additionalFields', index); + const subject = this.getNodeParameter('subject', index) as string; + const bodyContent = this.getNodeParameter('bodyContent', index, '') as string; + + additionalFields.subject = subject; + + additionalFields.bodyContent = bodyContent || ' '; + + // Create message object from optional fields + const body: IDataObject = createMessage(additionalFields); + + if (additionalFields.attachments) { + const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[]; + + // // Handle attachments + body.attachments = attachments.map((attachment) => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[index].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: index, + }); + } + + if ( + items[index].binary && + (items[index].binary as IDataObject)[binaryPropertyName] === undefined + ) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: index }, + ); + } + + const binaryData = (items[index].binary as IBinaryKeyData)[binaryPropertyName]; + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: binaryData.fileName, + contentBytes: binaryData.data, + }; + }); + } + + const responseData = await microsoftApiRequest.call(this, 'POST', '/messages', body, {}); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/delete.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/delete.operation.ts new file mode 100644 index 0000000000..3bf4f95716 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/delete.operation.ts @@ -0,0 +1,29 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { draftRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [draftRLC]; + +const displayOptions = { + show: { + resource: ['draft'], + operation: ['delete'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const draftId = this.getNodeParameter('draftId', index, undefined, { + extractValue: true, + }) as string; + await microsoftApiRequest.call(this, 'DELETE', `/messages/${draftId}`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/get.operation.ts new file mode 100644 index 0000000000..be252ae435 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/get.operation.ts @@ -0,0 +1,127 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { messageFields, simplifyOutputMessages } from '../../helpers/utils'; +import { downloadAttachments, microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { draftRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + draftRLC, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: messageFields, + default: [], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Attachments Prefix', + name: 'attachmentsPrefix', + type: 'string', + default: 'attachment_', + description: + 'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + description: + "Whether the message's attachments will be downloaded and included in the output", + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['draft'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs: IDataObject = {}; + + const draftId = this.getNodeParameter('draftId', index, undefined, { + extractValue: true, + }) as string; + const options = this.getNodeParameter('options', index, {}); + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + + if (options.downloadAttachments) { + fields.push('hasAttachments'); + } + + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = + 'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments'; + } + + responseData = await microsoftApiRequest.call(this, 'GET', `/messages/${draftId}`, undefined, qs); + + if (output === 'simple') { + responseData = simplifyOutputMessages([responseData as IDataObject]); + } + + let executionData: INodeExecutionData[] = []; + + if (options.downloadAttachments) { + const prefix = (options.attachmentsPrefix as string) || 'attachment_'; + executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix); + } else { + executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + } + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/index.ts new file mode 100644 index 0000000000..9e8cc5cd11 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/index.ts @@ -0,0 +1,61 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as create from './create.operation'; +import * as del from './delete.operation'; +import * as get from './get.operation'; +import * as send from './send.operation'; +import * as update from './update.operation'; + +export { create, del as delete, get, send, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['draft'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new email draft', + action: 'Create a draft', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an email draft', + action: 'Delete a draft', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an email draft', + action: 'Get a draft', + }, + { + name: 'Send', + value: 'send', + description: 'Send an existing email draft', + action: 'Send a draft', + }, + { + name: 'Update', + value: 'update', + description: 'Update an email draft', + action: 'Update a draft', + }, + ], + default: 'create', + }, + + ...create.description, + ...del.description, + ...get.description, + ...send.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/send.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/send.operation.ts new file mode 100644 index 0000000000..4cbb353bb4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/send.operation.ts @@ -0,0 +1,52 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { makeRecipient } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { draftRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + draftRLC, + { + displayName: 'To', + name: 'to', + description: 'Comma-separated list of email addresses of recipients', + type: 'string', + default: '', + }, +]; + +const displayOptions = { + show: { + resource: ['draft'], + operation: ['send'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const draftId = this.getNodeParameter('draftId', index, undefined, { extractValue: true }); + const to = this.getNodeParameter('to', index) as string; + + if (to) { + const recipients = to + .split(',') + .map((s) => s.trim()) + .filter((email) => email); + + if (recipients.length !== 0) { + await microsoftApiRequest.call(this, 'PATCH', `/messages/${draftId}`, { + toRecipients: recipients.map((recipient: string) => makeRecipient(recipient)), + }); + } + } + + await microsoftApiRequest.call(this, 'POST', `/messages/${draftId}/send`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/update.operation.ts new file mode 100644 index 0000000000..70d0ca0306 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/draft/update.operation.ts @@ -0,0 +1,206 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { createMessage } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { draftRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + draftRLC, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Comma-separated list of email addresses of BCC recipients', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + { + displayName: 'Category Names or IDs', + name: 'categories', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + typeOptions: { + loadOptionsMethod: 'getCategoriesNames', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: 'Comma-separated list of email addresses of CC recipients', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + { + displayName: 'Custom Headers', + name: 'internetMessageHeaders', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'headers', + displayName: 'Header', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the header', + }, + ], + }, + ], + }, + { + displayName: 'From', + name: 'from', + description: + 'The owner of the mailbox from which the message is sent. Must correspond to the actual mailbox used.', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + { + displayName: 'Importance', + name: 'importance', + description: 'The importance of the message', + type: 'options', + options: [ + { + name: 'Low', + value: 'Low', + }, + { + name: 'Normal', + value: 'Normal', + }, + { + name: 'High', + value: 'High', + }, + ], + default: 'Normal', + }, + { + displayName: 'Is Read', + name: 'isRead', + description: 'Whether the message must be marked as read', + type: 'boolean', + default: false, + }, + { + displayName: 'Message', + name: 'bodyContent', + description: 'Message body content', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Message Type', + name: 'bodyContentType', + description: 'Message body content type', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Whether a read receipt is requested for the message', + type: 'boolean', + default: false, + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email address to use when replying', + type: 'string', + placeholder: 'e.g. replyto@example.com', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message', + type: 'string', + default: '', + }, + { + displayName: 'To', + name: 'toRecipients', + description: 'Comma-separated list of email addresses of recipients', + type: 'string', + placeholder: 'e.g. john@example.com', + default: '', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['draft'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const draftId = this.getNodeParameter('draftId', index, undefined, { + extractValue: true, + }) as string; + + const updateFields = this.getNodeParameter('updateFields', index); + + // Create message from optional fields + const body: IDataObject = createMessage(updateFields); + + const responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/messages/${draftId}`, + body, + {}, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/create.operation.ts new file mode 100644 index 0000000000..2b7fd8fef3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/create.operation.ts @@ -0,0 +1,291 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; + +import { DateTime } from 'luxon'; +import { calendarRLC } from '../../descriptions'; +import moment from 'moment-timezone'; + +export const properties: INodeProperties[] = [ + calendarRLC, + { + displayName: 'Title', + name: 'subject', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Start', + name: 'startDateTime', + type: 'dateTime', + default: DateTime.now().toISO(), + required: true, + }, + { + displayName: 'End', + name: 'endDateTime', + type: 'dateTime', + required: true, + default: DateTime.now().plus({ minutes: 30 }).toISO(), + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Categories', + name: 'categories', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + typeOptions: { + loadOptionsMethod: 'getCategoriesNames', + }, + default: [], + }, + { + displayName: 'Description', + name: 'body', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Description Preview', + name: 'bodyPreview', + type: 'string', + default: '', + }, + { + displayName: 'Hide Attendees', + name: 'hideAttendees', + type: 'boolean', + default: false, + description: + 'Whether to allow each attendee to only see themselves in the meeting request and meeting tracking list', + }, + { + displayName: 'Importance', + name: 'importance', + type: 'options', + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + ], + default: 'normal', + }, + { + displayName: 'Is All Day', + name: 'isAllDay', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Cancelled', + name: 'isCancelled', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Draft', + name: 'isDraft', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Online Meeting', + name: 'isOnlineMeeting', + type: 'boolean', + default: false, + }, + { + displayName: 'Sensitivity', + name: 'sensitivity', + type: 'options', + default: 'normal', + options: [ + { + name: 'Normal', + value: 'normal', + }, + { + name: 'Personal', + value: 'personal', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Confidential', + value: 'confidential', + }, + ], + }, + { + displayName: 'Show As', + name: 'showAs', + type: 'options', + default: 'free', + options: [ + { + name: 'Busy', + value: 'busy', + }, + { + name: 'Free', + value: 'free', + }, + { + name: 'Oof', + value: 'oof', + }, + { + name: 'Tentative', + value: 'tentative', + }, + { + name: 'Working Elsewhere', + value: 'workingElsewhere', + }, + ], + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + default: 'UTC', + options: moment.tz.names().map((name) => ({ + name, + value: name, + })), + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'singleInstance', + options: [ + { + name: 'Single Instance', + value: 'singleInstance', + }, + { + name: 'Occurrence', + value: 'occurrence', + }, + { + name: 'Exception', + value: 'exception', + }, + { + name: 'Series Master', + value: 'seriesMaster', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['event'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let additionalFields = this.getNodeParameter('additionalFields', index); + + additionalFields = Object.keys(additionalFields).reduce((acc: IDataObject, key: string) => { + if (additionalFields[key] !== '' || additionalFields[key] !== undefined) { + acc[key] = additionalFields[key]; + } + return acc; + }, {}); + + const calendarId = this.getNodeParameter('calendarId', index, '', { + extractValue: true, + }) as string; + + if (calendarId === '') { + throw new NodeOperationError(this.getNode(), 'Calendar ID is required'); + } + const subject = this.getNodeParameter('subject', index) as string; + + const endpoint = `/calendars/${calendarId}/events`; + + let timeZone = 'UTC'; + + if (additionalFields.timeZone) { + timeZone = additionalFields.timeZone as string; + delete additionalFields.timeZone; + } + + if (additionalFields.body) { + additionalFields.body = { + content: additionalFields.body, + contentType: 'html', + }; + } + + let startDateTime = this.getNodeParameter('startDateTime', index) as string; + let endDateTime = this.getNodeParameter('endDateTime', index) as string; + + if (additionalFields.isAllDay) { + startDateTime = DateTime.fromISO(startDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd'); + endDateTime = DateTime.fromISO(endDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd'); + + const minimalWholeDayDuration = 24; + const duration = DateTime.fromISO(startDateTime, { zone: timeZone }).diff( + DateTime.fromISO(endDateTime, { zone: timeZone }), + ).hours; + + if (duration < minimalWholeDayDuration) { + endDateTime = DateTime.fromISO(startDateTime, { zone: timeZone }).plus({ hours: 24 }).toISO(); + } + } + + const body: IDataObject = { + subject, + start: { + dateTime: startDateTime, + timeZone, + }, + end: { + dateTime: endDateTime, + timeZone, + }, + ...additionalFields, + }; + + const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/delete.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/delete.operation.ts new file mode 100644 index 0000000000..bcebd39e10 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/delete.operation.ts @@ -0,0 +1,33 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { calendarRLC, eventRLC } from '../../descriptions'; +import { decodeOutlookId } from '../../helpers/utils'; + +export const properties: INodeProperties[] = [calendarRLC, eventRLC]; + +const displayOptions = { + show: { + resource: ['event'], + operation: ['delete'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const eventId = decodeOutlookId( + this.getNodeParameter('eventId', index, undefined, { + extractValue: true, + }) as string, + ); + + await microsoftApiRequest.call(this, 'DELETE', `/calendar/events/${eventId}`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/get.operation.ts new file mode 100644 index 0000000000..e0b17cf767 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/get.operation.ts @@ -0,0 +1,84 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { decodeOutlookId, eventfields } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { calendarRLC, eventRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + calendarRLC, + eventRLC, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: eventfields, + default: [], + }, +]; + +const displayOptions = { + show: { + resource: ['event'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const qs = {} as IDataObject; + + const eventId = decodeOutlookId( + this.getNodeParameter('eventId', index, undefined, { + extractValue: true, + }) as string, + ); + + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = 'id,subject,bodyPreview,start,end,organizer,attendees,webLink'; + } + + const endpoint = `/calendar/events/${eventId}`; + + const responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/getAll.operation.ts new file mode 100644 index 0000000000..db234045bc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/getAll.operation.ts @@ -0,0 +1,162 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { eventfields } from '../../helpers/utils'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { calendarRLC, returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + { + displayName: 'From All Calendars', + name: 'fromAllCalendars', + type: 'boolean', + default: true, + }, + { + ...calendarRLC, + displayOptions: { + show: { + fromAllCalendars: [false], + }, + }, + }, + ...returnAllOrLimit, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: eventfields, + default: [], + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Filter Query', + name: 'custom', + type: 'string', + default: '', + placeholder: "e.g. contains(subject,'Hello')", + hint: 'Search query to filter events. More info.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['event'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const responseData: IDataObject[] = []; + const qs = {} as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', index); + const filters = this.getNodeParameter('filters', index, {}); + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = 'id,subject,bodyPreview,start,end,organizer,attendees,webLink'; + } + + if (Object.keys(filters).length) { + const filterString: string[] = []; + + if (filters.custom) { + filterString.push(filters.custom as string); + } + + if (filterString.length) { + qs.$filter = filterString.join(' and '); + } + } + + const calendars: string[] = []; + + const fromAllCalendars = this.getNodeParameter('fromAllCalendars', index) as boolean; + + if (fromAllCalendars) { + const response = await microsoftApiRequest.call(this, 'GET', '/calendars', undefined, { + $select: 'id', + }); + for (const calendar of response.value) { + calendars.push(calendar.id as string); + } + } else { + const calendarId = this.getNodeParameter('calendarId', index, undefined, { + extractValue: true, + }) as string; + + calendars.push(calendarId); + } + const limit = this.getNodeParameter('limit', index, 0); + + for (const calendarId of calendars) { + const endpoint = `/calendars/${calendarId}/events`; + + if (returnAll) { + const response = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + responseData.push(...response); + } else { + qs.$top = limit - responseData.length; + + if (qs.$top <= 0) break; + + const response = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData.push(...response.value); + } + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/index.ts new file mode 100644 index 0000000000..d2881a1858 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/index.ts @@ -0,0 +1,61 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as create from './create.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as del from './delete.operation'; +import * as update from './update.operation'; + +export { create, del as delete, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['event'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new event', + action: 'Create an event', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an event', + action: 'Delete an event', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an event', + action: 'Get an event', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'List and search events', + action: 'Get many events', + }, + { + name: 'Update', + value: 'update', + description: 'Update an event', + action: 'Update an event', + }, + ], + default: 'getAll', + }, + + ...create.description, + ...del.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/update.operation.ts new file mode 100644 index 0000000000..014475a3f9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/event/update.operation.ts @@ -0,0 +1,283 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; + +import { DateTime } from 'luxon'; +import { calendarRLC, eventRLC } from '../../descriptions'; +import { decodeOutlookId } from '../../helpers/utils'; + +export const properties: INodeProperties[] = [ + calendarRLC, + eventRLC, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Categories', + name: 'categories', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + typeOptions: { + loadOptionsMethod: 'getCategoriesNames', + }, + default: [], + }, + { + displayName: 'Description', + name: 'body', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Description Preview', + name: 'bodyPreview', + type: 'string', + default: '', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + }, + { + displayName: 'Hide Attendees', + name: 'hideAttendees', + type: 'boolean', + default: false, + description: + 'Whether to allow each attendee to only see themselves in the meeting request and meeting tracking list', + }, + { + displayName: 'Importance', + name: 'importance', + type: 'options', + default: 'low', + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + ], + }, + { + displayName: 'Is All Day', + name: 'isAllDay', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Cancelled', + name: 'isCancelled', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Draft', + name: 'isDraft', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Online Meeting', + name: 'isOnlineMeeting', + type: 'boolean', + default: true, + }, + { + displayName: 'Sensitivity', + name: 'sensitivity', + type: 'options', + default: 'normal', + options: [ + { + name: 'Normal', + value: 'normal', + }, + { + name: 'Personal', + value: 'personal', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Confidential', + value: 'confidential', + }, + ], + }, + { + displayName: 'Show As', + name: 'showAs', + type: 'options', + default: 'free', + options: [ + { + name: 'Busy', + value: 'busy', + }, + { + name: 'Free', + value: 'free', + }, + { + name: 'Oof', + value: 'oof', + }, + { + name: 'Tentative', + value: 'tentative', + }, + { + name: 'Working Elsewhere', + value: 'workingElsewhere', + }, + ], + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'subject', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'singleInstance', + options: [ + { + name: 'Single Instance', + value: 'singleInstance', + }, + { + name: 'Occurrence', + value: 'occurrence', + }, + { + name: 'Exception', + value: 'exception', + }, + { + name: 'Series Master', + value: 'seriesMaster', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['event'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const additionalFields = this.getNodeParameter('additionalFields', index); + + const eventId = decodeOutlookId( + this.getNodeParameter('eventId', index, undefined, { + extractValue: true, + }) as string, + ); + + let timeZone = 'UTC'; + + if (additionalFields.timeZone) { + timeZone = additionalFields.timeZone as string; + delete additionalFields.timeZone; + } + + if (additionalFields.body) { + additionalFields.body = { + content: additionalFields.body, + contentType: 'html', + }; + } + + let startDateTime = additionalFields.start as string; + let endDateTime = additionalFields.end as string; + + if (additionalFields.isAllDay) { + startDateTime = + DateTime.fromISO(startDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd') || + DateTime.utc().toFormat('yyyy-MM-dd'); + endDateTime = + DateTime.fromISO(endDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd') || + DateTime.utc().toFormat('yyyy-MM-dd'); + + const minimalWholeDayDuration = 24; + const duration = DateTime.fromISO(startDateTime, { zone: timeZone }).diff( + DateTime.fromISO(endDateTime, { zone: timeZone }), + ).hours; + + if (duration < minimalWholeDayDuration) { + endDateTime = DateTime.fromISO(startDateTime, { zone: timeZone }).plus({ hours: 24 }).toISO(); + } + } + + const body: IDataObject = { + ...additionalFields, + }; + + if (startDateTime) { + body.start = { + dateTime: startDateTime, + timeZone, + }; + } + + if (endDateTime) { + body.end = { + dateTime: endDateTime, + timeZone, + }; + } + + const endpoint = `/calendar/events/${eventId}`; + + const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/create.operation.ts new file mode 100644 index 0000000000..7ae12135a0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/create.operation.ts @@ -0,0 +1,65 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { folderRLC } from '../../descriptions'; +import { decodeOutlookId } from '../../helpers/utils'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Name', + name: 'displayName', + description: 'Name of the folder', + type: 'string', + required: true, + default: '', + placeholder: 'e.g. My Folder', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [{ ...folderRLC, displayName: 'Parent Folder', required: false }], + }, +]; + +const displayOptions = { + show: { + resource: ['folder'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const displayName = this.getNodeParameter('displayName', index) as string; + + const folderId = decodeOutlookId( + this.getNodeParameter('options.folderId', index, '', { + extractValue: true, + }) as string, + ); + + const body: IDataObject = { + displayName, + }; + + let endpoint; + + if (folderId) { + endpoint = `/mailFolders/${folderId}/childFolders`; + } else { + endpoint = '/mailFolders'; + } + + const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/delete.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/delete.operation.ts new file mode 100644 index 0000000000..7be79a9adb --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/delete.operation.ts @@ -0,0 +1,33 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { folderRLC } from '../../descriptions'; +import { decodeOutlookId } from '../../helpers/utils'; + +export const properties: INodeProperties[] = [folderRLC]; + +const displayOptions = { + show: { + resource: ['folder'], + operation: ['delete'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const folderId = decodeOutlookId( + this.getNodeParameter('folderId', index, undefined, { + extractValue: true, + }) as string, + ); + + await microsoftApiRequest.call(this, 'DELETE', `/mailFolders/${folderId}`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/get.operation.ts new file mode 100644 index 0000000000..abc38fd81a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/get.operation.ts @@ -0,0 +1,69 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { folderFields, folderRLC } from '../../descriptions'; +import { decodeOutlookId } from '../../helpers/utils'; + +export const properties: INodeProperties[] = [ + folderRLC, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + options: folderFields, + default: [], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['folder'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const qs: IDataObject = {}; + + const folderId = decodeOutlookId( + this.getNodeParameter('folderId', index, undefined, { + extractValue: true, + }) as string, + ); + + const options = this.getNodeParameter('options', index); + + if (options.fields) { + qs.$select = (options.fields as string[]).join(','); + } + + if (options.filter) { + qs.$filter = options.filter; + } + const responseData = await microsoftApiRequest.call( + this, + 'GET', + `/mailFolders/${folderId}`, + {}, + qs, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/getAll.operation.ts new file mode 100644 index 0000000000..9d9c7f3b64 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/getAll.operation.ts @@ -0,0 +1,111 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { getSubfolders, microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { folderFields, folderRLC, returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + ...returnAllOrLimit, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Filter Query', + name: 'filter', + type: 'string', + default: '', + placeholder: "e.g. displayName eq 'My Folder'", + hint: 'Search query to filter folders. More info.', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + options: folderFields, + default: [], + }, + { + displayName: 'Include Child Folders', + name: 'includeChildFolders', + type: 'boolean', + default: false, + description: 'Whether to include child folders in the response', + }, + { + ...folderRLC, + displayName: 'Parent Folder', + required: false, + description: 'The folder you want to search in', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['folder'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs: IDataObject = {}; + + const returnAll = this.getNodeParameter('returnAll', index); + const options = this.getNodeParameter('options', index); + const filter = this.getNodeParameter('filters.filter', index, '') as string; + + const parentFolderId = this.getNodeParameter('options.folderId', index, '', { + extractValue: true, + }) as string; + + if (options.fields) { + qs.$select = (options.fields as string[]).join(','); + } + + if (filter) { + qs.$filter = filter; + } + + let endpoint; + if (parentFolderId) { + endpoint = `/mailFolders/${parentFolderId}/childFolders`; + } else { + endpoint = '/mailFolders'; + } + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {}, qs); + } else { + qs.$top = this.getNodeParameter('limit', index); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, {}, qs); + responseData = responseData.value; + } + + if (options.includeChildFolders) { + responseData = await getSubfolders.call(this, responseData as IDataObject[]); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/index.ts new file mode 100644 index 0000000000..46fd4c038f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/index.ts @@ -0,0 +1,60 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as create from './create.operation'; +import * as del from './delete.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as update from './update.operation'; + +export { create, del as delete, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['folder'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: "Create a mail folder in the root folder of the user's mailbox", + action: 'Create a folder', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a folder', + action: 'Delete a folder', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a folder', + action: 'Get a folder', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many folders', + action: 'Get many folders', + }, + { + name: 'Update', + value: 'update', + description: 'Update a folder', + action: 'Update a folder', + }, + ], + default: 'create', + }, + ...create.description, + ...del.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/update.operation.ts new file mode 100644 index 0000000000..1a4e2e1bb4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folder/update.operation.ts @@ -0,0 +1,46 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { folderRLC } from '../../descriptions'; +import { decodeOutlookId } from '../../helpers/utils'; + +export const properties: INodeProperties[] = [ + folderRLC, + { + displayName: 'Name', + name: 'displayName', + description: 'Name of the folder', + type: 'string', + default: '', + required: true, + }, +]; + +const displayOptions = { + show: { + resource: ['folder'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const folderId = decodeOutlookId( + this.getNodeParameter('folderId', index, undefined, { + extractValue: true, + }) as string, + ); + const displayName = this.getNodeParameter('displayName', index, undefined) as string; + + const responseData = await microsoftApiRequest.call(this, 'PATCH', `/mailFolders/${folderId}`, { + displayName, + }); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/getAll.operation.ts new file mode 100644 index 0000000000..027f5122eb --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/getAll.operation.ts @@ -0,0 +1,302 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { + decodeOutlookId, + messageFields, + prepareFilterString, + simplifyOutputMessages, +} from '../../helpers/utils'; +import { + downloadAttachments, + microsoftApiRequest, + microsoftApiRequestAllItems, +} from '../../transport'; + +import { updateDisplayOptions } from '@utils/utilities'; +import { folderRLC, returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + folderRLC, + ...returnAllOrLimit, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: messageFields, + default: [], + }, + { + displayName: + 'Fetching a lot of messages may take a long time. Consider using filters to speed things up', + name: 'filtersNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + returnAll: [true], + }, + }, + }, + { + displayName: 'Filters', + name: 'filtersUI', + type: 'fixedCollection', + placeholder: 'Add Filters', + default: {}, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'Filter By', + name: 'filterBy', + type: 'options', + options: [ + { + name: 'Filters', + value: 'filters', + }, + { + name: 'Search', + value: 'search', + }, + ], + default: 'filters', + }, + { + displayName: 'Search', + name: 'search', + type: 'string', + default: '', + placeholder: 'e.g. automation', + description: + 'Only return messages that contains search term. Without specific message properties, the search is carried out on the default search properties of from, subject, and body. More info.', + displayOptions: { + show: { + filterBy: ['search'], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + filterBy: ['filters'], + }, + }, + options: [ + { + displayName: 'Filter Query', + name: 'custom', + type: 'string', + default: '', + placeholder: 'e.g. isRead eq false', + hint: 'Search query to filter messages. More info.', + }, + { + displayName: 'Has Attachments', + name: 'hasAttachments', + type: 'boolean', + default: false, + }, + { + displayName: 'Read Status', + name: 'readStatus', + type: 'options', + default: 'unread', + hint: 'Filter messages by whether they have been read or not', + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Unread and read messages', + value: 'both', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Unread messages only', + value: 'unread', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Read messages only', + value: 'read', + }, + ], + }, + { + displayName: 'Received After', + name: 'receivedAfter', + type: 'dateTime', + default: '', + description: + 'Get all messages received after the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + displayName: 'Received Before', + name: 'receivedBefore', + type: 'dateTime', + default: '', + description: + 'Get all messages received before the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + displayName: 'Sender', + name: 'sender', + type: 'string', + default: '', + description: 'Sender name or email to filter by', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Attachments Prefix', + name: 'attachmentsPrefix', + type: 'string', + default: 'attachment_', + description: + 'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + description: + "Whether the message's attachments will be downloaded and included in the output", + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['folderMessage'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs: IDataObject = {}; + + const folderId = decodeOutlookId( + this.getNodeParameter('folderId', index, undefined, { + extractValue: true, + }) as string, + ); + + const returnAll = this.getNodeParameter('returnAll', index); + const filters = this.getNodeParameter('filtersUI.values', index, {}) as IDataObject; + const options = this.getNodeParameter('options', index, {}); + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + + if (options.downloadAttachments) { + fields.push('hasAttachments'); + } + + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = + 'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments'; + } + + if (filters.filterBy === 'search' && filters.search !== '') { + qs.$search = `"${filters.search}"`; + } + + if (filters.filterBy === 'filters') { + const filterString = prepareFilterString(filters); + + if (filterString) { + qs.$filter = filterString; + } + } + + const endpoint = `/mailFolders/${folderId}/messages`; + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', index); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + + if (output === 'simple') { + responseData = simplifyOutputMessages(responseData as IDataObject[]); + } + + let executionData: INodeExecutionData[] = []; + + if (options.downloadAttachments) { + const prefix = (options.attachmentsPrefix as string) || 'attachment_'; + executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix); + } else { + executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: index } }, + ); + } + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/index.ts new file mode 100644 index 0000000000..6e20612378 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/folderMessage/index.ts @@ -0,0 +1,29 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as getAll from './getAll.operation'; + +export { getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['folderMessage'], + }, + }, + options: [ + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieves the messages in a folder', + action: 'Get many folder messages', + }, + ], + default: 'getAll', + }, + + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/delete.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/delete.operation.ts new file mode 100644 index 0000000000..8ee7511b41 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/delete.operation.ts @@ -0,0 +1,29 @@ +import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { messageRLC } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [messageRLC]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['delete'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + await microsoftApiRequest.call(this, 'DELETE', `/messages/${messageId}`); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/get.operation.ts new file mode 100644 index 0000000000..b253659e43 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/get.operation.ts @@ -0,0 +1,181 @@ +import type { + IBinaryKeyData, + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { messageFields, simplifyOutputMessages } from '../../helpers/utils'; +import { downloadAttachments, getMimeContent, microsoftApiRequest } from '../../transport'; +import { messageRLC } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + messageRLC, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: messageFields, + default: [], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Attachments Prefix', + name: 'attachmentsPrefix', + type: 'string', + default: 'attachment_', + description: + 'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + description: + "Whether the message's attachments will be downloaded and included in the output", + }, + { + displayName: 'Get MIME Content', + name: 'getMimeContent', + type: 'fixedCollection', + default: { values: { binaryPropertyName: 'data' } }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'Put Output in Field', + name: 'binaryPropertyName', + type: 'string', + default: '', + hint: 'The name of the output field to put the binary file data in', + }, + { + displayName: 'File Name', + name: 'outputFileName', + type: 'string', + placeholder: 'message', + default: '', + description: 'Optional name of the output file, if not set message ID is used', + }, + ], + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs: IDataObject = {}; + + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const options = this.getNodeParameter('options', index, {}); + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + + if (options.downloadAttachments) { + fields.push('hasAttachments'); + } + + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = + 'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments'; + } + + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}`, + undefined, + qs, + ); + + if (output === 'simple') { + responseData = simplifyOutputMessages([responseData as IDataObject]); + } + + let executionData: INodeExecutionData[] = []; + + if (options.downloadAttachments) { + const prefix = (options.attachmentsPrefix as string) || 'attachment_'; + executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix); + } else { + executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + } + + if (options.getMimeContent) { + const { binaryPropertyName, outputFileName } = (options.getMimeContent as IDataObject) + .values as IDataObject; + + const binary = await getMimeContent.call( + this, + messageId, + binaryPropertyName as string, + outputFileName as string, + ); + + executionData[0].binary = { + ...(executionData[0].binary || {}), + ...(binary as IBinaryKeyData), + }; + } + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/getAll.operation.ts new file mode 100644 index 0000000000..ad266f4313 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/getAll.operation.ts @@ -0,0 +1,311 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { messageFields, prepareFilterString, simplifyOutputMessages } from '../../helpers/utils'; +import { + downloadAttachments, + microsoftApiRequest, + microsoftApiRequestAllItems, +} from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + ...returnAllOrLimit, + { + displayName: 'Output', + name: 'output', + type: 'options', + default: 'simple', + options: [ + { + name: 'Simplified', + value: 'simple', + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Select Included Fields', + value: 'fields', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + displayOptions: { + show: { + output: ['fields'], + }, + }, + options: messageFields, + default: [], + }, + { + displayName: + 'Fetching a lot of messages may take a long time. Consider using filters to speed things up', + name: 'filtersNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + returnAll: [true], + }, + }, + }, + { + displayName: 'Filters', + name: 'filtersUI', + type: 'fixedCollection', + placeholder: 'Add Filters', + default: {}, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'Filter By', + name: 'filterBy', + type: 'options', + options: [ + { + name: 'Filters', + value: 'filters', + }, + { + name: 'Search', + value: 'search', + }, + ], + default: 'filters', + }, + { + displayName: 'Search', + name: 'search', + type: 'string', + default: '', + placeholder: 'e.g. automation', + description: + 'Only return messages that contains search term. Without specific message properties, the search is carried out on the default search properties of from, subject, and body. More info.', + displayOptions: { + show: { + filterBy: ['search'], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + filterBy: ['filters'], + }, + }, + options: [ + { + displayName: 'Filter Query', + name: 'custom', + type: 'string', + default: '', + placeholder: 'e.g. isRead eq false', + hint: 'Search query to filter messages. More info.', + }, + { + displayName: 'Has Attachments', + name: 'hasAttachments', + type: 'boolean', + default: false, + }, + { + displayName: 'Folders to Exclude', + name: 'foldersToExclude', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getFolders', + }, + default: [], + description: + 'Choose from the list, or specify IDs using an expression', + }, + { + displayName: 'Folders to Include', + name: 'foldersToInclude', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getFolders', + }, + default: [], + description: + 'Choose from the list, or specify IDs using an expression', + }, + { + displayName: 'Read Status', + name: 'readStatus', + type: 'options', + default: 'unread', + hint: 'Filter messages by whether they have been read or not', + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Unread and read messages', + value: 'both', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Unread messages only', + value: 'unread', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Read messages only', + value: 'read', + }, + ], + }, + { + displayName: 'Received After', + name: 'receivedAfter', + type: 'dateTime', + default: '', + description: + 'Get all messages received after the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + displayName: 'Received Before', + name: 'receivedBefore', + type: 'dateTime', + default: '', + description: + 'Get all messages received before the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + displayName: 'Sender', + name: 'sender', + type: 'string', + default: '', + description: 'Sender name or email to filter by', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Attachments Prefix', + name: 'attachmentsPrefix', + type: 'string', + default: 'attachment_', + description: + 'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + description: + "Whether the message's attachments will be downloaded and included in the output", + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs = {} as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', index); + const filters = this.getNodeParameter('filtersUI.values', index, {}) as IDataObject; + const options = this.getNodeParameter('options', index, {}); + const output = this.getNodeParameter('output', index) as string; + + if (output === 'fields') { + const fields = this.getNodeParameter('fields', index) as string[]; + + if (options.downloadAttachments) { + fields.push('hasAttachments'); + } + + qs.$select = fields.join(','); + } + + if (output === 'simple') { + qs.$select = + 'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments'; + } + + if (filters.filterBy === 'search' && filters.search !== '') { + qs.$search = `"${filters.search}"`; + } + + if (filters.filterBy === 'filters') { + const filterString = prepareFilterString(filters); + + if (filterString) { + qs.$filter = filterString; + } + } + + const endpoint = '/messages'; + + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', index); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + + if (output === 'simple') { + responseData = simplifyOutputMessages(responseData as IDataObject[]); + } + + let executionData: INodeExecutionData[] = []; + + if (options.downloadAttachments) { + const prefix = (options.attachmentsPrefix as string) || 'attachment_'; + executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix); + } else { + executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: index } }, + ); + } + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/index.ts new file mode 100644 index 0000000000..bc81ba47d1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/index.ts @@ -0,0 +1,77 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as del from './delete.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as move from './move.operation'; +import * as reply from './reply.operation'; +import * as send from './send.operation'; +import * as update from './update.operation'; + +export { del as delete, get, getAll, move, reply, send, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['message'], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a message', + action: 'Delete a message', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a single message', + action: 'Get a message', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'List and search messages', + action: 'Get many messages', + }, + { + name: 'Move', + value: 'move', + description: 'Move a message to a folder', + action: 'Move a message', + }, + { + name: 'Reply', + value: 'reply', + description: 'Create a reply to a message', + action: 'Reply to a message', + }, + { + name: 'Send', + value: 'send', + description: 'Send a message', + action: 'Send a message', + }, + { + name: 'Update', + value: 'update', + description: 'Update a message', + action: 'Update a message', + }, + ], + default: 'send', + }, + + ...del.description, + ...get.description, + ...getAll.description, + ...move.description, + ...reply.description, + ...send.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/move.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/move.operation.ts new file mode 100644 index 0000000000..55c0f634e4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/move.operation.ts @@ -0,0 +1,41 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { folderRLC, messageRLC } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + messageRLC, + { ...folderRLC, displayName: 'Parent Folder' }, +]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['move'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const destinationId = this.getNodeParameter('folderId', index, undefined, { + extractValue: true, + }) as string; + + const body: IDataObject = { + destinationId, + }; + + await microsoftApiRequest.call(this, 'POST', `/messages/${messageId}/move`, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/reply.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/reply.operation.ts new file mode 100644 index 0000000000..d9f102b0b5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/reply.operation.ts @@ -0,0 +1,306 @@ +import type { + IBinaryKeyData, + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { createMessage } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { messageRLC } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + messageRLC, + { + displayName: 'Reply to Sender Only', + name: 'replyToSenderOnly', + type: 'boolean', + default: false, + description: 'Whether to reply to the sender only or to the entire list of recipients', + }, + { + displayName: 'Message', + name: 'message', + // name: 'bodyContent', + description: 'Message body content', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + replyToSenderOnly: [true], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'fixedCollection', + placeholder: 'Add Attachment', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachments', + displayName: 'Attachment', + values: [ + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + type: 'string', + default: '', + placeholder: 'e.g. data', + hint: 'The name of the input field containing the binary file data to be attached', + }, + ], + }, + ], + }, + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Comma-separated list of email addresses of BCC recipients', + type: 'string', + default: '', + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: 'Comma-separated list of email addresses of CC recipients', + type: 'string', + default: '', + }, + { + displayName: 'Custom Headers', + name: 'internetMessageHeaders', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'headers', + displayName: 'Header', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the header', + }, + ], + }, + ], + }, + { + displayName: 'From', + name: 'from', + description: + 'The owner of the mailbox from which the message is sent. Must correspond to the actual mailbox used.', + type: 'string', + default: '', + }, + { + displayName: 'Importance', + name: 'importance', + description: 'The importance of the message', + type: 'options', + options: [ + { + name: 'Low', + value: 'Low', + }, + { + name: 'Normal', + value: 'Normal', + }, + { + name: 'High', + value: 'High', + }, + ], + default: 'Normal', + }, + { + displayName: 'Message Type', + name: 'bodyContentType', + description: 'Message body content type', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Whether a read receipt is requested for the message', + type: 'boolean', + default: false, + }, + { + displayName: 'To', + name: 'toRecipients', + description: 'Comma-separated list of email addresses of recipients', + type: 'string', + default: '', + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email address to use when replying', + type: 'string', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message', + type: 'string', + default: '', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Save as Draft', + name: 'saveAsDraft', + description: + 'Whether to save the message as a draft. If false, the message is sent immediately.', + type: 'boolean', + default: false, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['reply'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) { + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + const replyToSenderOnly = this.getNodeParameter('replyToSenderOnly', index, false) as string; + const message = this.getNodeParameter('message', index) as string; + const saveAsDraft = this.getNodeParameter('options.saveAsDraft', index, false) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', index, {}); + + const body: IDataObject = {}; + + let action = 'createReply'; + + if (!replyToSenderOnly) { + body.comment = message; + action = 'createReplyAll'; + } else { + // body.comment = comment; + body.message = {} as IDataObject; + additionalFields.bodyContent = message; + Object.assign(body.message, createMessage(additionalFields)); + + delete (body.message as IDataObject).attachments; + } + + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/${action}`, + body, + ); + + if (additionalFields.attachments) { + const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[]; + // // Handle attachments + const data = attachments.map((attachment) => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[index].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: index, + }); + } + + if ( + items[index].binary && + (items[index].binary as IDataObject)[binaryPropertyName] === undefined + ) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: index }, + ); + } + + const binaryData = (items[index].binary as IBinaryKeyData)[binaryPropertyName]; + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: binaryData.fileName, + contentBytes: binaryData.data, + }; + }); + + for (const attachment of data) { + await microsoftApiRequest.call( + this, + 'POST', + `/messages/${responseData.id}/attachments`, + attachment, + {}, + ); + } + } + + if (!saveAsDraft) { + await microsoftApiRequest.call(this, 'POST', `/messages/${responseData.id}/send`); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/send.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/send.operation.ts new file mode 100644 index 0000000000..f8bc763317 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/send.operation.ts @@ -0,0 +1,272 @@ +import type { + IBinaryKeyData, + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { createMessage } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + { + displayName: 'To', + name: 'toRecipients', + description: 'Comma-separated list of email addresses of recipients', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message', + type: 'string', + default: '', + }, + { + displayName: 'Message', + name: 'bodyContent', + description: 'Message body content', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'fixedCollection', + placeholder: 'Add Attachment', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachments', + displayName: 'Attachment', + values: [ + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + type: 'string', + default: '', + placeholder: 'e.g. data', + hint: 'The name of the input field containing the binary file data to be attached', + }, + ], + }, + ], + }, + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Comma-separated list of email addresses of BCC recipients', + type: 'string', + default: '', + }, + { + displayName: 'Category Names or IDs', + name: 'categories', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + typeOptions: { + loadOptionsMethod: 'getCategoriesNames', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: 'Comma-separated list of email addresses of CC recipients', + type: 'string', + default: '', + }, + { + displayName: 'Custom Headers', + name: 'internetMessageHeaders', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'headers', + displayName: 'Header', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the header', + }, + ], + }, + ], + }, + { + displayName: 'From', + name: 'from', + description: + 'The owner of the mailbox from which the message is sent. Must correspond to the actual mailbox used.', + type: 'string', + default: '', + }, + { + displayName: 'Importance', + name: 'importance', + description: 'The importance of the message', + type: 'options', + options: [ + { + name: 'Low', + value: 'Low', + }, + { + name: 'Normal', + value: 'Normal', + }, + { + name: 'High', + value: 'High', + }, + ], + default: 'Normal', + }, + { + displayName: 'Message Type', + name: 'bodyContentType', + description: 'Message body content type', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Whether a read receipt is requested for the message', + type: 'boolean', + default: false, + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email address to use when replying', + type: 'string', + default: '', + }, + { + displayName: 'Save To Sent Items', + name: 'saveToSentItems', + description: 'Whether to save the message in Sent Items', + type: 'boolean', + default: true, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['send'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) { + const additionalFields = this.getNodeParameter('additionalFields', index); + const toRecipients = this.getNodeParameter('toRecipients', index) as string; + const subject = this.getNodeParameter('subject', index) as string; + const bodyContent = this.getNodeParameter('bodyContent', index, '') as string; + + additionalFields.subject = subject; + additionalFields.bodyContent = bodyContent || ' '; + additionalFields.toRecipients = toRecipients; + + const saveToSentItems = + additionalFields.saveToSentItems === undefined ? true : additionalFields.saveToSentItems; + delete additionalFields.saveToSentItems; + + // Create message object from optional fields + const message: IDataObject = createMessage(additionalFields); + + if (additionalFields.attachments) { + const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[]; + + // // Handle attachments + message.attachments = attachments.map((attachment) => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[index].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', { + itemIndex: index, + }); + } + + if ( + items[index].binary && + (items[index].binary as IDataObject)[binaryPropertyName] === undefined + ) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: index }, + ); + } + + const binaryData = (items[index].binary as IBinaryKeyData)[binaryPropertyName]; + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: binaryData.fileName, + contentBytes: binaryData.data, + }; + }); + } + + const body: IDataObject = { + message, + saveToSentItems, + }; + + await microsoftApiRequest.call(this, 'POST', '/sendMail', body, {}); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/update.operation.ts new file mode 100644 index 0000000000..e6399489fc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/message/update.operation.ts @@ -0,0 +1,224 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { createMessage, decodeOutlookId } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { folderRLC, messageRLC } from '../../descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + messageRLC, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Comma-separated list of email addresses of BCC recipients', + type: 'string', + default: '', + }, + { + displayName: 'Category Names or IDs', + name: 'categories', + type: 'multiOptions', + description: + 'Choose from the list, or specify IDs using an expression', + typeOptions: { + loadOptionsMethod: 'getCategoriesNames', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: 'Comma-separated list of email addresses of CC recipients', + type: 'string', + default: '', + }, + { + displayName: 'Custom Headers', + name: 'internetMessageHeaders', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'headers', + displayName: 'Header', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the header', + }, + ], + }, + ], + }, + { ...folderRLC, required: false }, + { + displayName: 'Importance', + name: 'importance', + description: 'The importance of the message', + type: 'options', + options: [ + { + name: 'Low', + value: 'Low', + }, + { + name: 'Normal', + value: 'Normal', + }, + { + name: 'High', + value: 'High', + }, + ], + default: 'Normal', + }, + { + displayName: 'Is Read', + name: 'isRead', + description: 'Whether the message must be marked as read', + type: 'boolean', + default: false, + }, + { + displayName: 'Message', + name: 'bodyContent', + description: 'Message body content', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + }, + { + displayName: 'Message Type', + name: 'bodyContentType', + description: 'Message body content type', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Whether a read receipt is requested for the message', + type: 'boolean', + default: false, + }, + { + displayName: 'To', + name: 'toRecipients', + description: 'Comma-separated list of email addresses of recipients', + type: 'string', + default: '', + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email address to use when replying', + type: 'string', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message', + type: 'string', + default: '', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['message'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const updateFields = this.getNodeParameter('updateFields', index); + + const folderId = decodeOutlookId( + this.getNodeParameter('updateFields.folderId', index, '', { + extractValue: true, + }) as string, + ); + + if (folderId) { + const body: IDataObject = { + destinationId: folderId, + }; + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/move`, + body, + ); + + delete updateFields.folderId; + + if (!Object.keys(updateFields).length) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; + } + } + + const body: IDataObject = createMessage(updateFields); + + if (!Object.keys(body).length) { + throw new NodeOperationError(this.getNode(), 'No fields to update got specified'); + } + + responseData = await microsoftApiRequest.call(this, 'PATCH', `/messages/${messageId}`, body, {}); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/add.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/add.operation.ts new file mode 100644 index 0000000000..c4532ba6ac --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/add.operation.ts @@ -0,0 +1,157 @@ +import type { + IBinaryKeyData, + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { messageRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + messageRLC, + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + hint: 'The name of the input field containing the binary file data to be attached', + type: 'string', + required: true, + default: 'data', + placeholder: 'e.g. data', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'File Name', + name: 'fileName', + description: + 'Filename of the attachment. If not set will the file-name of the binary property be used, if it exists.', + type: 'string', + default: '', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['messageAttachment'], + operation: ['add'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) { + let responseData; + + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0); + const options = this.getNodeParameter('options', index); + + if (items[index].binary === undefined) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!'); + } + + if ( + items[index].binary && + (items[index].binary as IDataObject)[binaryPropertyName] === undefined + ) { + throw new NodeOperationError( + this.getNode(), + `No binary data property "${binaryPropertyName}" does not exists on item!`, + { itemIndex: index }, + ); + } + + const binaryData = (items[index].binary as IBinaryKeyData)[binaryPropertyName]; + const dataBuffer = await this.helpers.getBinaryDataBuffer(index, binaryPropertyName); + + const fileName = options.fileName === undefined ? binaryData.fileName : options.fileName; + + if (!fileName) { + throw new NodeOperationError( + this.getNode(), + 'File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!', + { itemIndex: index }, + ); + } + + // Check if the file is over 3MB big + if (dataBuffer.length > 3e6) { + // Maximum chunk size is 4MB + const chunkSize = 4e6; + const body: IDataObject = { + AttachmentItem: { + attachmentType: 'file', + name: fileName, + size: dataBuffer.length, + }, + }; + + // Create upload session + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/attachments/createUploadSession`, + body, + ); + const uploadUrl = responseData.uploadUrl; + + if (uploadUrl === undefined) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: 'Failed to get upload session', + }); + } + + for (let bytesUploaded = 0; bytesUploaded < dataBuffer.length; bytesUploaded += chunkSize) { + // Upload the file chunk by chunk + const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length); + const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`; + + const data = dataBuffer.subarray(bytesUploaded, nextChunk); + + responseData = await this.helpers.request(uploadUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': data.length, + 'Content-Range': contentRange, + }, + body: data, + }); + } + } else { + const body: IDataObject = { + '@odata.type': '#microsoft.graph.fileAttachment', + name: fileName, + contentBytes: binaryData.data, + }; + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/messages/${messageId}/attachments`, + body, + {}, + ); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/download.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/download.operation.ts new file mode 100644 index 0000000000..3a6fbde5e1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/download.operation.ts @@ -0,0 +1,86 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { attachmentRLC, messageRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + messageRLC, + attachmentRLC, + { + displayName: 'Put Output in Field', + name: 'binaryPropertyName', + hint: 'The name of the output field to put the binary file data in', + type: 'string', + required: true, + default: 'data', + }, +]; + +const displayOptions = { + show: { + resource: ['messageAttachment'], + operation: ['download'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) { + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const attachmentId = this.getNodeParameter('attachmentId', index, undefined, { + extractValue: true, + }) as string; + + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', index); + + // Get attachment details first + const attachmentDetails = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments/${attachmentId}`, + undefined, + { $select: 'id,name,contentType' }, + ); + + let mimeType: string | undefined; + if (attachmentDetails.contentType) { + mimeType = attachmentDetails.contentType; + } + const fileName = attachmentDetails.name; + + const response = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments/${attachmentId}/$value`, + undefined, + {}, + undefined, + {}, + { encoding: null, resolveWithFullResponse: true }, + ); + + const newItem: INodeExecutionData = { + json: items[index].json, + binary: {}, + }; + + if (items[index].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary!, items[index].binary); + } + + items[index] = newItem; + const data = Buffer.from(response.body as string, 'utf8'); + items[index].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData( + data as unknown as Buffer, + fileName as string, + mimeType, + ); + + return items; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/get.operation.ts new file mode 100644 index 0000000000..cbc6466cac --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/get.operation.ts @@ -0,0 +1,94 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { attachmentRLC, messageRLC } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + messageRLC, + attachmentRLC, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + default: [], + options: [ + { + name: 'contentType', + value: 'contentType', + }, + { + name: 'isInline', + value: 'isInline', + }, + { + name: 'lastModifiedDateTime', + value: 'lastModifiedDateTime', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'name', + value: 'name', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'size', + value: 'size', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['messageAttachment'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + const qs: IDataObject = {}; + + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const attachmentId = this.getNodeParameter('attachmentId', index, undefined, { + extractValue: true, + }) as string; + + const options = this.getNodeParameter('options', index); + + // Have sane defaults so we don't fetch attachment data in this operation + qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline'; + + if (options.fields && (options.fields as string[]).length) { + qs.$select = (options.fields as string[]).map((field) => field.trim()).join(','); + } + + const responseData = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments/${attachmentId}`, + undefined, + qs, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/getAll.operation.ts new file mode 100644 index 0000000000..f0de6687d6 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/getAll.operation.ts @@ -0,0 +1,100 @@ +import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { messageRLC, returnAllOrLimit } from '../../descriptions'; + +export const properties: INodeProperties[] = [ + messageRLC, + ...returnAllOrLimit, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + description: 'The fields to add to the output', + default: [], + options: [ + { + name: 'contentType', + value: 'contentType', + }, + { + name: 'isInline', + value: 'isInline', + }, + { + name: 'lastModifiedDateTime', + value: 'lastModifiedDateTime', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'name', + value: 'name', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'size', + value: 'size', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['messageAttachment'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, index: number) { + let responseData; + const qs = {} as IDataObject; + + const messageId = this.getNodeParameter('messageId', index, undefined, { + extractValue: true, + }) as string; + + const returnAll = this.getNodeParameter('returnAll', index); + const options = this.getNodeParameter('options', index); + + // Have sane defaults so we don't fetch attachment data in this operation + qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline'; + + if (options.fields && (options.fields as string[]).length) { + qs.$select = (options.fields as string[]).map((field) => field.trim()).join(','); + } + + const endpoint = `/messages/${messageId}/attachments`; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', index); + responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs); + responseData = responseData.value; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: index } }, + ); + + return executionData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/index.ts new file mode 100644 index 0000000000..24beaf050e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/messageAttachment/index.ts @@ -0,0 +1,52 @@ +import type { INodeProperties } from 'n8n-workflow'; +import * as add from './add.operation'; +import * as download from './download.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; + +export { add, download, get, getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['messageAttachment'], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add an attachment to a message', + action: 'Add an attachment', + }, + { + name: 'Download', + value: 'download', + description: 'Download an attachment from a message', + action: 'Download an attachment', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve information about an attachment of a message', + action: 'Get an attachment', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve information about the attachments of a message', + action: 'Get many attachments', + }, + ], + default: 'add', + }, + ...add.description, + ...download.description, + ...get.description, + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts new file mode 100644 index 0000000000..586f3860a5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts @@ -0,0 +1,82 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; +import * as calendar from './calendar'; +import * as contact from './contact'; +import * as draft from './draft'; +import * as event from './event'; +import * as folder from './folder'; +import * as folderMessage from './folderMessage'; +import * as message from './message'; +import * as messageAttachment from './messageAttachment'; + +export const description: INodeTypeDescription = { + displayName: 'Microsoft Outlook', + name: 'microsoftOutlook', + group: ['transform'], + icon: 'file:outlook.svg', + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Outlook API', + defaults: { + name: 'Microsoft Outlook', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftOutlookOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + default: 'message', + options: [ + { + name: 'Calendar', + value: 'calendar', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Event', + value: 'event', + }, + { + name: 'Folder', + value: 'folder', + }, + { + name: 'Folder Message', + value: 'folderMessage', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Message Attachment', + value: 'messageAttachment', + }, + ], + }, + ...calendar.description, + ...contact.description, + ...draft.description, + ...event.description, + ...folder.description, + ...folderMessage.description, + ...message.description, + ...messageAttachment.description, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.type.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.type.ts new file mode 100644 index 0000000000..19b6f02b56 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.type.ts @@ -0,0 +1,14 @@ +import type { AllEntities } from 'n8n-workflow'; + +type NodeMap = { + calendar: 'create' | 'delete' | 'get' | 'getAll' | 'update'; + contact: 'create' | 'delete' | 'get' | 'getAll' | 'update'; + draft: 'create' | 'delete' | 'get' | 'send' | 'update'; + event: 'create' | 'delete' | 'get' | 'getAll' | 'update'; + folder: 'create' | 'delete' | 'get' | 'getAll' | 'update'; + folderMessage: 'getAll'; + message: 'delete' | 'get' | 'getAll' | 'move' | 'update' | 'send' | 'reply'; + messageAttachment: 'add' | 'download' | 'getAll' | 'get'; +}; + +export type MicrosoftOutlook = AllEntities; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts new file mode 100644 index 0000000000..d6033e3ec8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts @@ -0,0 +1,84 @@ +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; + +import type { MicrosoftOutlook } from './node.type'; +import * as calendar from './calendar'; +import * as contact from './contact'; +import * as draft from './draft'; +import * as event from './event'; +import * as folder from './folder'; +import * as folderMessage from './folderMessage'; +import * as message from './message'; +import * as messageAttachment from './messageAttachment'; + +export async function router(this: IExecuteFunctions) { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0); + + let responseData; + + const microsoftOutlook = { + resource, + operation, + } as MicrosoftOutlook; + + for (let i = 0; i < items.length; i++) { + try { + switch (microsoftOutlook.resource) { + case 'calendar': + responseData = await calendar[microsoftOutlook.operation].execute.call(this, i); + break; + case 'contact': + responseData = await contact[microsoftOutlook.operation].execute.call(this, i); + break; + case 'draft': + responseData = await draft[microsoftOutlook.operation].execute.call(this, i, items); + break; + case 'event': + responseData = await event[microsoftOutlook.operation].execute.call(this, i); + break; + case 'folder': + responseData = await folder[microsoftOutlook.operation].execute.call(this, i); + break; + case 'folderMessage': + responseData = await folderMessage[microsoftOutlook.operation].execute.call(this, i); + break; + case 'message': + responseData = await message[microsoftOutlook.operation].execute.call(this, i, items); + break; + case 'messageAttachment': + responseData = await messageAttachment[microsoftOutlook.operation].execute.call( + this, + i, + items, + ); + break; + default: + throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`); + } + + returnData.push(...responseData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + //NodeApiError will be missing the itemIndex, add it + if (error instanceof NodeApiError && error?.context?.itemIndex === undefined) { + if (error.context === undefined) { + error.context = {}; + } + error.context.itemIndex = i; + } + throw error; + } + } + return [returnData]; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/common.descriptions.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/common.descriptions.ts new file mode 100644 index 0000000000..69b4f396cb --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/common.descriptions.ts @@ -0,0 +1,394 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const returnAllOrLimit: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 100, + description: 'Max number of results to return', + }, +]; + +export const folderFields = [ + { + name: 'Child Folder Count', + value: 'childFolderCount', + }, + { + name: 'Display Name', + value: 'displayName', + }, + { + name: 'Is Hidden', + value: 'isHidden', + }, + { + name: 'Parent Folder ID', + value: 'parentFolderId', + }, + { + name: 'Total Item Count', + value: 'totalItemCount', + }, + { + name: 'Unread Item Count', + value: 'unreadItemCount', + }, +]; + +export const contactFields: INodeProperties[] = [ + { + displayName: 'Assistant Name', + name: 'assistantName', + type: 'string', + default: '', + description: "The name of the contact's assistant", + }, + { + displayName: 'Birthday', + name: 'birthday', + type: 'dateTime', + default: '', + }, + { + displayName: 'Business Address', + name: 'businessAddress', + type: 'fixedCollection', + placeholder: 'Add Address', + default: { + values: { sity: '', street: '', postalCode: '', countryOrRegion: '', state: '' }, + }, + options: [ + { + displayName: 'Address', + name: 'values', + values: [ + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'Country/Region', + name: 'countryOrRegion', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Business Home Page', + name: 'businessHomePage', + type: 'string', + default: '', + }, + { + displayName: 'Business Phones', + name: 'businessPhones', + type: 'string', + description: 'Comma-separated list of business phone numbers', + default: '', + }, + { + displayName: 'Categories', + name: 'categories', + description: 'Comma-separated list of categories associated with the contact', + type: 'string', + default: '', + }, + { + displayName: 'Children', + name: 'children', + description: "Comma-separated list of names of the contact's children", + type: 'string', + default: '', + }, + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + }, + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + default: '', + }, + { + displayName: 'Email Address', + name: 'emailAddresses', + type: 'fixedCollection', + placeholder: 'Add Email', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'File As', + name: 'fileAs', + type: 'string', + default: '', + description: 'The name the contact is filed under', + }, + { + displayName: 'Home Address', + name: 'homeAddress', + type: 'fixedCollection', + placeholder: 'Add Address', + default: { + values: { sity: '', street: '', postalCode: '', countryOrRegion: '', state: '' }, + }, + options: [ + { + displayName: 'Address', + name: 'values', + values: [ + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'Country/Region', + name: 'countryOrRegion', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Home Phones', + name: 'homePhones', + type: 'string', + default: '', + hint: 'Multiple phones can be added separated by ,', + }, + { + displayName: 'Instant Messaging Addresses', + name: 'imAddresses', + description: "The contact's instant messaging (IM) addresses", + type: 'string', + default: '', + hint: 'Multiple addresses can be added separated by ,', + }, + { + displayName: 'Initials', + name: 'initials', + type: 'string', + default: '', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + }, + { + displayName: 'Manager', + name: 'manager', + type: 'string', + default: '', + description: "The name of the contact's manager", + }, + { + displayName: 'Middle Name', + name: 'middleName', + type: 'string', + default: '', + }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + type: 'string', + default: '', + }, + { + displayName: 'Name', + name: 'givenName', + type: 'string', + default: '', + displayOptions: { + show: { + '/operation': ['update'], + }, + }, + }, + { + displayName: 'Nickname', + name: 'nickName', + type: 'string', + default: '', + }, + { + displayName: 'Office Location', + name: 'officeLocation', + type: 'string', + default: '', + }, + { + displayName: 'Other Address', + name: 'otherAddress', + type: 'fixedCollection', + placeholder: 'Add Address', + default: { + values: { sity: '', street: '', postalCode: '', countryOrRegion: '', state: '' }, + }, + options: [ + { + displayName: 'Address', + name: 'values', + values: [ + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'Country/Region', + name: 'countryOrRegion', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Personal Notes', + name: 'personalNotes', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Profession', + name: 'profession', + type: 'string', + default: '', + }, + { + displayName: 'Spouse Name', + name: 'spouseName', + type: 'string', + default: '', + }, + { + displayName: 'Surname', + name: 'surname', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/index.ts new file mode 100644 index 0000000000..d1d564cad8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/index.ts @@ -0,0 +1,2 @@ +export * from './rlc.description'; +export * from './common.descriptions'; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/rlc.description.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/rlc.description.ts new file mode 100644 index 0000000000..df3ce0fde0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/descriptions/rlc.description.ts @@ -0,0 +1,229 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const calendarRLC: INodeProperties = { + displayName: 'Calendar', + name: 'calendarId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a calendar...', + typeOptions: { + searchListMethod: 'searchCalendars', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; + +export const contactRLC: INodeProperties = { + displayName: 'Contact', + name: 'contactId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a contact...', + typeOptions: { + searchListMethod: 'searchContacts', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; + +export const draftRLC: INodeProperties = { + displayName: 'Draft', + name: 'draftId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a draft...', + typeOptions: { + searchListMethod: 'searchDrafts', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; + +export const messageRLC: INodeProperties = { + displayName: 'Message', + name: 'messageId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a message...', + typeOptions: { + searchListMethod: 'searchMessages', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; + +export const eventRLC: INodeProperties = { + displayName: 'Event', + name: 'eventId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['calendarId.value'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a event...', + typeOptions: { + searchListMethod: 'searchEvents', + searchable: true, + }, + }, + { + displayName: 'Link', + name: 'url', + type: 'string', + placeholder: 'e.g. https://outlook.office365.com/calendar/item/AAMkADlhOTA0M...UAAA%3D', + extractValue: { + type: 'regex', + regex: + 'https:\\/\\/outlook\\.office365\\.com\\/calendar\\/item\\/([A-Za-z0-9%]+)(?:\\/.*|)', + }, + validation: [ + { + type: 'regex', + properties: { + regex: + 'https:\\/\\/outlook\\.office365\\.com\\/calendar\\/item\\/([A-Za-z0-9%]+)(?:\\/.*|)', + errorMessage: 'Not a valid Outlook Event URL', + }, + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; + +export const folderRLC: INodeProperties = { + displayName: 'Folder', + name: 'folderId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a folder...', + typeOptions: { + searchListMethod: 'searchFolders', + searchable: true, + }, + }, + { + displayName: 'Link', + name: 'url', + type: 'string', + placeholder: 'e.g. https://outlook.office365.com/mail/AAMkADlhOT...AAA%3D', + extractValue: { + type: 'regex', + regex: 'https:\\/\\/outlook\\.office365\\.com\\/mail\\/([A-Za-z0-9%]+)(?:\\/.*|)', + }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/outlook\\.office365\\.com\\/mail\\/([A-Za-z0-9%]+)(?:\\/.*|)', + errorMessage: 'Not a valid Outlook Folder URL', + }, + }, + ], + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; + +export const attachmentRLC: INodeProperties = { + displayName: 'Attachment', + name: 'attachmentId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['messageId.value'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a attachment...', + typeOptions: { + searchListMethod: 'searchAttachments', + searchable: false, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...', + }, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts new file mode 100644 index 0000000000..3ea607c1d2 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts @@ -0,0 +1,302 @@ +import type { + IDataObject, + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, + JsonObject, +} from 'n8n-workflow'; +import { jsonParse, NodeApiError } from 'n8n-workflow'; + +export const messageFields = [ + 'bccRecipients', + 'body', + 'bodyPreview', + 'categories', + 'ccRecipients', + 'changeKey', + 'conversationId', + 'createdDateTime', + 'flag', + 'from', + 'hasAttachments', + 'importance', + 'inferenceClassification', + 'internetMessageId', + 'isDeliveryReceiptRequested', + 'isDraft', + 'isRead', + 'isReadReceiptRequested', + 'lastModifiedDateTime', + 'parentFolderId', + 'receivedDateTime', + 'replyTo', + 'sender', + 'sentDateTime', + 'subject', + 'toRecipients', + 'webLink', +].map((field) => ({ name: field, value: field })); + +export const eventfields = [ + 'allowNewTimeProposals', + 'attendees', + 'body', + 'bodyPreview', + 'categories', + 'changeKey', + 'createdDateTime', + 'end', + 'hasAttachments', + 'hideAttendees', + 'iCalUId', + 'importance', + 'isAllDay', + 'isCancelled', + 'isDraft', + 'isOnlineMeeting', + 'isOrganizer', + 'isReminderOn', + 'lastModifiedDateTime', + 'location', + 'locations', + 'onlineMeeting', + 'onlineMeetingProvider', + 'onlineMeetingUrl', + 'organizer', + 'originalEndTimeZone', + 'originalStartTimeZone', + 'recurrence', + 'reminderMinutesBeforeStart', + 'responseRequested', + 'responseStatus', + 'sensitivity', + 'seriesMasterId', + 'showAs', + 'start', + 'subject', + 'transactionId', + 'type', + 'webLink', +].map((field) => ({ name: field, value: field })); + +export const contactFields = [ + 'createdDateTime', + 'lastModifiedDateTime', + 'changeKey', + 'categories', + 'parentFolderId', + 'birthday', + 'fileAs', + 'displayName', + 'givenName', + 'initials', + 'middleName', + 'nickName', + 'surname', + 'title', + 'yomiGivenName', + 'yomiSurname', + 'yomiCompanyName', + 'generation', + 'imAddresses', + 'jobTitle', + 'companyName', + 'department', + 'officeLocation', + 'profession', + 'businessHomePage', + 'assistantName', + 'manager', + 'homePhones', + 'mobilePhone', + 'businessPhones', + 'spouseName', + 'personalNotes', + 'children', + 'emailAddresses', + 'homeAddress', + 'businessAddress', + 'otherAddress', +].map((field) => ({ name: field, value: field })); + +export function makeRecipient(email: string) { + return { + emailAddress: { + address: email, + }, + }; +} + +export function createMessage(fields: IDataObject) { + const message: IDataObject = {}; + + // Create body object + if (fields.bodyContent || fields.bodyContentType) { + const bodyObject = { + content: fields.bodyContent, + contentType: fields.bodyContentType, + }; + + message.body = bodyObject; + delete fields.bodyContent; + delete fields.bodyContentType; + } + + // Handle custom headers + if ( + 'internetMessageHeaders' in fields && + 'headers' in (fields.internetMessageHeaders as IDataObject) + ) { + fields.internetMessageHeaders = (fields.internetMessageHeaders as IDataObject).headers; + } + + for (const [key, value] of Object.entries(fields)) { + if (['bccRecipients', 'ccRecipients', 'replyTo', 'sender', 'toRecipients'].includes(key)) { + if (Array.isArray(value)) { + message[key] = (value as string[]).map((email) => makeRecipient(email)); + } else if (typeof value === 'string') { + message[key] = value.split(',').map((recipient: string) => makeRecipient(recipient.trim())); + } else { + throw new Error(`The "${key}" field must be a string or an array of strings`); + } + continue; + } + + if (['from', 'sender'].includes(key)) { + if (value) { + message[key] = makeRecipient(value as string); + } + continue; + } + + message[key] = value; + } + + return message; +} + +export function simplifyOutputMessages(data: IDataObject[]) { + return data.map((item: IDataObject) => { + return { + id: item.id, + conversationId: item.conversationId, + subject: item.subject, + bodyPreview: item.bodyPreview, + from: ((item.from as IDataObject)?.emailAddress as IDataObject)?.address, + to: (item.toRecipients as IDataObject[]).map( + (recipient: IDataObject) => (recipient.emailAddress as IDataObject)?.address, + ), + categories: item.categories, + hasAttachments: item.hasAttachments, + }; + }); +} + +export function prepareContactFields(fields: IDataObject) { + const returnData: IDataObject = {}; + + const typeStringCollection = [ + 'businessPhones', + 'categories', + 'children', + 'homePhones', + 'imAddresses', + ]; + const typeValuesToExtract = ['businessAddress', 'emailAddresses', 'homePhones', 'otherAddress']; + + for (const [key, value] of Object.entries(fields)) { + if (value === undefined || value === '') { + continue; + } + + if (typeStringCollection.includes(key) && !Array.isArray(value)) { + returnData[key] = (value as string).split(',').map((item) => item.trim()); + continue; + } + + if (typeValuesToExtract.includes(key)) { + if ((value as IDataObject).values === undefined) continue; + returnData[key] = (value as IDataObject).values; + continue; + } + + returnData[key] = value; + } + + return returnData; +} + +export function prepareFilterString(filters: IDataObject) { + const selectedFilters = filters.filters as IDataObject; + const filterString: string[] = []; + + if (selectedFilters.foldersToInclude) { + const folders = (selectedFilters.foldersToInclude as string[]) + .filter((folder) => folder !== '') + .map((folder) => `parentFolderId eq '${folder}'`) + .join(' or '); + + filterString.push(folders); + } + + if (selectedFilters.foldersToExclude) { + for (const folder of selectedFilters.foldersToExclude as string[]) { + filterString.push(`parentFolderId ne '${folder}'`); + } + } + + if (selectedFilters.sender) { + const sender = selectedFilters.sender as string; + const byMailAddress = `from/emailAddress/address eq '${sender}'`; + const byName = `from/emailAddress/name eq '${sender}'`; + filterString.push(`(${byMailAddress} or ${byName})`); + } + + if (selectedFilters.hasAttachments) { + filterString.push(`hasAttachments eq ${selectedFilters.hasAttachments}`); + } + + if (selectedFilters.readStatus && selectedFilters.readStatus !== 'both') { + filterString.push(`isRead eq ${selectedFilters.readStatus === 'read'}`); + } + + if (selectedFilters.receivedAfter) { + filterString.push(`receivedDateTime ge ${selectedFilters.receivedAfter}`); + } + + if (selectedFilters.receivedBefore) { + filterString.push(`receivedDateTime le ${selectedFilters.receivedBefore}`); + } + + if (selectedFilters.custom) { + filterString.push(selectedFilters.custom as string); + } + + return filterString.length ? filterString.join(' and ') : undefined; +} + +export function prepareApiError( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + error: IDataObject, + itemIndex = 0, +) { + const [httpCode, err, message] = (error.description as string).split(' - '); + const json = jsonParse(err); + return new NodeApiError(this.getNode(), json as JsonObject, { + itemIndex, + httpCode, + //In UI we are replacing some of the field names to make them more user friendly, updating error message to reflect that + message: message + .replace(/toRecipients/g, 'toRecipients (To)') + .replace(/bodyContent/g, 'bodyContent (Message)') + .replace(/bodyContentType/g, 'bodyContentType (Message Type)'), + }); +} + +export const encodeOutlookId = (id: string) => { + return id.replace(/-/g, '%2F').replace(/=/g, '%3D').replace(/\+/g, '%2B'); +}; + +export const decodeOutlookId = (id: string) => { + return id.replace(/%2F/g, '-').replace(/%3D/g, '=').replace(/%2B/g, '+'); +}; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/index.ts new file mode 100644 index 0000000000..a5508a3e0f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/index.ts @@ -0,0 +1,2 @@ +export * as loadOptions from './loadOptions'; +export * as listSearch from './listSearch'; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/listSearch.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/listSearch.ts new file mode 100644 index 0000000000..c0ded584c0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/listSearch.ts @@ -0,0 +1,289 @@ +import type { IDataObject, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; +import { getSubfolders, microsoftApiRequest } from '../transport'; +import { encodeOutlookId } from '../helpers/utils'; + +async function search( + this: ILoadOptionsFunctions, + resource: string, + nameProperty: string, + filter?: string, + paginationToken?: string, +): Promise { + let response: IDataObject = {}; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + const qs: IDataObject = { + $select: `id,${nameProperty}`, + $top: 100, + }; + + if (filter) { + const filterValue = encodeURI(filter); + qs.$filter = `contains(${nameProperty}, '${filterValue}')`; + } + + response = await microsoftApiRequest.call(this, 'GET', resource, undefined, qs); + } + + return { + results: (response.value as IDataObject[]).map((entry: IDataObject) => { + return { + name: entry[nameProperty] as string, + value: entry.id as string, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function searchContacts( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return search.call(this, '/contacts', 'displayName', filter, paginationToken); +} + +export async function searchCalendars( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + return search.call(this, '/calendars', 'name', filter, paginationToken); +} + +export async function searchDrafts( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let response: IDataObject = {}; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + const qs: IDataObject = { + $select: 'id,subject,bodyPreview,webLink', + $top: 100, + $filter: 'isDraft eq true', + }; + + if (filter) { + const filterValue = encodeURI(filter); + qs.$filter += ` AND contains(${'subject'}, '${filterValue}')`; + } + + response = await microsoftApiRequest.call(this, 'GET', '/messages', undefined, qs); + } + + return { + results: (response.value as IDataObject[]).map((entry: IDataObject) => { + return { + name: (entry.subject || entry.bodyPreview) as string, + value: entry.id as string, + url: entry.webLink as string, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function searchMessages( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let response: IDataObject = {}; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + const qs: IDataObject = { + $select: 'id,subject,bodyPreview,webLink', + $top: 100, + }; + + if (filter) { + const filterValue = encodeURI(filter); + qs.$filter = `contains(${'subject'}, '${filterValue}')`; + } + + response = await microsoftApiRequest.call(this, 'GET', '/messages', undefined, qs); + } + + return { + results: (response.value as IDataObject[]).map((entry: IDataObject) => { + return { + name: (entry.subject || entry.bodyPreview) as string, + value: entry.id as string, + url: entry.webLink as string, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function searchEvents( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let response: IDataObject = {}; + + const calendarId = this.getNodeParameter('calendarId', undefined, { + extractValue: true, + }) as string; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + const qs: IDataObject = { + $select: 'id,subject,bodyPreview', + $top: 100, + }; + + if (filter) { + const filterValue = encodeURI(filter); + qs.$filter = `contains(${'subject'}, '${filterValue}')`; + } + + response = await microsoftApiRequest.call( + this, + 'GET', + `/calendars/${calendarId}/events`, + undefined, + qs, + ); + } + + return { + results: (response.value as IDataObject[]).map((entry: IDataObject) => { + return { + name: (entry.subject || entry.bodyPreview) as string, + value: entry.id as string, + url: `https://outlook.office365.com/calendar/item/${encodeOutlookId(entry.id as string)}`, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function searchFolders( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let response: IDataObject = {}; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + const qs: IDataObject = { + $top: 100, + }; + + response = await microsoftApiRequest.call(this, 'GET', '/mailFolders', undefined, qs); + } + + let folders = await getSubfolders.call(this, response.value as IDataObject[]); + + if (filter) { + filter = filter.toLowerCase(); + folders = folders.filter((folder) => + ((folder.displayName as string) || '').toLowerCase().includes(filter as string), + ); + } + + return { + results: folders.map((entry: IDataObject) => { + return { + name: entry.displayName as string, + value: entry.id as string, + url: `https://outlook.office365.com/mail/${encodeOutlookId(entry.id as string)}`, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function searchAttachments( + this: ILoadOptionsFunctions, + paginationToken?: string, +): Promise { + let response: IDataObject = {}; + + const messageId = this.getNodeParameter('messageId', undefined, { + extractValue: true, + }) as string; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + const qs: IDataObject = { + $select: 'id,name', + $top: 100, + }; + + response = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/attachments`, + undefined, + qs, + ); + } + + return { + results: (response.value as IDataObject[]).map((entry: IDataObject) => { + return { + name: entry.name as string, + value: entry.id as string, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/loadOptions.ts new file mode 100644 index 0000000000..389d8b49a0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/methods/loadOptions.ts @@ -0,0 +1,54 @@ +import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; +import { getSubfolders, microsoftApiRequestAllItems } from '../transport'; + +export async function getCategoriesNames( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const categories = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + '/outlook/masterCategories', + ); + for (const category of categories) { + returnData.push({ + name: category.displayName as string, + value: category.displayName as string, + }); + } + return returnData; +} + +export async function getFolders(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const response = await microsoftApiRequestAllItems.call(this, 'value', 'GET', '/mailFolders', {}); + const folders = await getSubfolders.call(this, response); + for (const folder of folders) { + returnData.push({ + name: folder.displayName as string, + value: folder.id as string, + }); + } + return returnData; +} + +export async function getCalendarGroups( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const calendars = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + '/calendarGroups', + {}, + ); + for (const calendar of calendars) { + returnData.push({ + name: calendar.name as string, + value: calendar.id as string, + }); + } + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts new file mode 100644 index 0000000000..f6bf061295 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts @@ -0,0 +1,224 @@ +import type { OptionsWithUri } from 'request'; + +import { + type IDataObject, + type IExecuteFunctions, + type IExecuteSingleFunctions, + type ILoadOptionsFunctions, + type INodeExecutionData, +} from 'n8n-workflow'; +import { prepareApiError } from '../helpers/utils'; + +export async function microsoftApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, + option: IDataObject = { json: true }, +) { + const credentials = await this.getCredentials('microsoftOutlookOAuth2Api'); + + let apiUrl = `https://graph.microsoft.com/v1.0/me${resource}`; + // If accessing shared mailbox + if (credentials.useShared && credentials.userPrincipalName) { + apiUrl = `https://graph.microsoft.com/v1.0/users/${credentials.userPrincipalName}${resource}`; + } + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || apiUrl, + }; + try { + Object.assign(options, option); + + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + return await this.helpers.requestWithAuthentication.call( + this, + 'microsoftOutlookOAuth2Api', + options, + ); + } catch (error) { + if ( + ((error.message || '').toLowerCase().includes('bad request') || + (error.message || '').toLowerCase().includes('unknown error')) && + error.description + ) { + let updatedError; + // Try to return the error prettier, otherwise return the original one repalcing the message with the description + try { + updatedError = prepareApiError.call(this, error); + } catch (e) {} + + if (updatedError) throw updatedError; + + error.message = error.description; + error.description = ''; + } + + throw error; + } +} + +export async function microsoftApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: IDataObject = {}, + query: IDataObject = {}, + headers: IDataObject = {}, +) { + const returnData: IDataObject[] = []; + + let responseData; + let nextLink: string | undefined; + query.$top = 100; + + do { + responseData = await microsoftApiRequest.call( + this, + method, + endpoint, + body, + nextLink ? undefined : query, // Do not add query parameters as nextLink already contains them + nextLink, + headers, + ); + nextLink = responseData['@odata.nextLink']; + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData['@odata.nextLink'] !== undefined); + + return returnData; +} + +export async function downloadAttachments( + this: IExecuteFunctions, + messages: IDataObject[] | IDataObject, + prefix: string, +) { + const elements: INodeExecutionData[] = []; + if (!Array.isArray(messages)) { + messages = [messages]; + } + for (const message of messages) { + const element: INodeExecutionData = { + json: message, + binary: {}, + }; + if (message.hasAttachments === true) { + const attachments = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/messages/${message.id}/attachments`, + {}, + ); + for (const [index, attachment] of attachments.entries()) { + const response = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${message.id}/attachments/${attachment.id}/$value`, + undefined, + {}, + undefined, + {}, + { encoding: null, resolveWithFullResponse: true }, + ); + + const data = Buffer.from(response.body as string, 'utf8'); + element.binary![`${prefix}${index}`] = await this.helpers.prepareBinaryData( + data as unknown as Buffer, + attachment.name as string, + attachment.contentType as string, + ); + } + } + if (Object.keys(element.binary!).length === 0) { + delete element.binary; + } + elements.push(element); + } + return elements; +} + +export async function getMimeContent( + this: IExecuteFunctions, + messageId: string, + binaryPropertyName: string, + outputFileName?: string, +) { + const response = await microsoftApiRequest.call( + this, + 'GET', + `/messages/${messageId}/$value`, + undefined, + {}, + undefined, + {}, + { encoding: null, resolveWithFullResponse: true }, + ); + + let mimeType: string | undefined; + if (response.headers['content-type']) { + mimeType = response.headers['content-type']; + } + + const fileName = `${outputFileName || messageId}.eml`; + const data = Buffer.from(response.body as string, 'utf8'); + const binary: IDataObject = {}; + binary[binaryPropertyName] = await this.helpers.prepareBinaryData( + data as unknown as Buffer, + fileName, + mimeType, + ); + + return binary; +} + +export async function getSubfolders( + this: IExecuteFunctions | ILoadOptionsFunctions, + folders: IDataObject[], + addPathToDisplayName = false, +) { + const returnData: IDataObject[] = [...folders]; + for (const folder of folders) { + if ((folder.childFolderCount as number) > 0) { + let subfolders = await microsoftApiRequest.call( + this, + 'GET', + `/mailFolders/${folder.id}/childFolders`, + ); + + if (addPathToDisplayName) { + subfolders = subfolders.value.map((subfolder: IDataObject) => { + return { + ...subfolder, + displayName: `${folder.displayName}/${subfolder.displayName}`, + }; + }); + } else { + subfolders = subfolders.value; + } + + returnData.push( + ...(await getSubfolders.call(this, subfolders as IDataObject[], addPathToDisplayName)), + ); + } + } + return returnData; +}