From 720ae1b96b4c6fd644bad60191c35d8d598ad666 Mon Sep 17 00:00:00 2001 From: Bram Kn Date: Wed, 28 Feb 2024 10:23:58 +0100 Subject: [PATCH] feat: Add Outlook Trigger Node (#8656) Co-authored-by: Jonathan Bennetts --- .../Outlook/MicrosoftOutlookTrigger.node.json | 19 +++ .../Outlook/MicrosoftOutlookTrigger.node.ts | 97 ++++++++++++ .../Outlook/trigger/GenericFunctions.ts | 85 +++++++++++ .../Outlook/trigger/MessageDescription.ts | 139 ++++++++++++++++++ .../Microsoft/Outlook/v2/helpers/utils.ts | 3 +- .../Microsoft/Outlook/v2/transport/index.ts | 9 +- packages/nodes-base/package.json | 1 + 7 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.json create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/trigger/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Outlook/trigger/MessageDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.json b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.json new file mode 100644 index 0000000000000..8d0e83cf5c713 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.microsoftOutlookTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Communication"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/microsoft" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.microsoftOutlookTrigger/" + } + ] + }, + "alias": ["email"] +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.ts b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.ts new file mode 100644 index 0000000000000..92f5a0422a85a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.ts @@ -0,0 +1,97 @@ +import type { + IPollFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { DateTime } from 'luxon'; + +import { properties as messageProperties } from './trigger/MessageDescription'; + +import { getPollResponse } from './trigger/GenericFunctions'; +import { loadOptions } from './v2/methods'; + +export class MicrosoftOutlookTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Microsoft Outlook Trigger', + name: 'microsoftOutlookTrigger', + icon: 'file:outlook.svg', + group: ['trigger'], + version: 1, + description: + 'Fetches emails from Microsoft Outlook and starts the workflow on specified polling intervals.', + subtitle: '={{"Microsoft Outlook Trigger"}}', + defaults: { + name: 'Microsoft Outlook Trigger', + }, + credentials: [ + { + name: 'microsoftOutlookOAuth2Api', + required: true, + }, + ], + polling: true, + inputs: [], + outputs: ['main'], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + default: 'messageReceived', + options: [ + { + name: 'Message Received', + value: 'messageReceived', + }, + ], + }, + ...messageProperties, + ], + }; + + methods = { loadOptions }; + + async poll(this: IPollFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + let responseData; + + const now = DateTime.now().toISO(); + const startDate = (webhookData.lastTimeChecked as string) || now; + const endDate = now; + try { + const pollStartDate = startDate; + const pollEndDate = endDate; + + responseData = await getPollResponse.call(this, pollStartDate, pollEndDate); + + if (!responseData?.length) { + webhookData.lastTimeChecked = endDate; + return null; + } + } catch (error) { + if (this.getMode() === 'manual' || !webhookData.lastTimeChecked) { + throw error; + } + const workflow = this.getWorkflow(); + const node = this.getNode(); + this.logger.error( + `There was a problem in '${node.name}' node in workflow '${workflow.id}': '${error.description}'`, + { + node: node.name, + workflowId: workflow.id, + error, + }, + ); + } + + webhookData.lastTimeChecked = endDate; + + if (Array.isArray(responseData) && responseData.length) { + return [responseData]; + } + + return null; + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/trigger/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Outlook/trigger/GenericFunctions.ts new file mode 100644 index 0000000000000..7999c1b8a56dc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/trigger/GenericFunctions.ts @@ -0,0 +1,85 @@ +import { NodeApiError } from 'n8n-workflow'; +import type { JsonObject, IDataObject, INodeExecutionData, IPollFunctions } from 'n8n-workflow'; + +import { + downloadAttachments, + microsoftApiRequest, + microsoftApiRequestAllItems, +} from '../v2/transport'; + +import { prepareFilterString, simplifyOutputMessages } from '../v2/helpers/utils'; + +export async function getPollResponse( + this: IPollFunctions, + pollStartDate: string, + pollEndDate: string, +) { + let responseData; + const qs = {} as IDataObject; + try { + const filters = this.getNodeParameter('filters', {}) as IDataObject; + const options = this.getNodeParameter('options', {}) as IDataObject; + const output = this.getNodeParameter('output') as string; + if (output === 'fields') { + const fields = this.getNodeParameter('fields') 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'; + } + + const filterString = prepareFilterString({ filters }); + + if (filterString) { + qs.$filter = filterString; + } + + const endpoint = '/messages'; + if (this.getMode() !== 'manual') { + if (qs.$filter) { + qs.$filter = `${qs.$filter} and receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`; + } else { + qs.$filter = `receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`; + } + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs.$top = 1; + 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.returnJsonArray(responseData as IDataObject[]); + } + + return executionData; + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject, { + message: error.message, + description: error.description, + }); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/trigger/MessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/trigger/MessageDescription.ts new file mode 100644 index 0000000000000..9ff9af88bf6c0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/trigger/MessageDescription.ts @@ -0,0 +1,139 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { messageFields } from '../v2/helpers/utils'; + +export const properties: INodeProperties[] = [ + { + 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: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + 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: '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", + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts index 85ebecdd9f6ea..56bf596d40b7a 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/helpers/utils.ts @@ -3,6 +3,7 @@ import type { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions, + IPollFunctions, JsonObject, } from 'n8n-workflow'; import { ApplicationError, jsonParse, NodeApiError } from 'n8n-workflow'; @@ -278,7 +279,7 @@ export function prepareFilterString(filters: IDataObject) { } export function prepareApiError( - this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, error: IDataObject, itemIndex = 0, ) { diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts index 772e519c05303..db85f5b60aa76 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/transport/index.ts @@ -6,11 +6,12 @@ import type { IExecuteSingleFunctions, ILoadOptionsFunctions, INodeExecutionData, + IPollFunctions, } from 'n8n-workflow'; import { prepareApiError } from '../helpers/utils'; export async function microsoftApiRequest( - this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: IHttpRequestMethods, resource: string, body: IDataObject = {}, @@ -59,7 +60,7 @@ export async function microsoftApiRequest( error.description ) { let updatedError; - // Try to return the error prettier, otherwise return the original one repalcing the message with the description + // Try to return the error prettier, otherwise return the original one replacing the message with the description try { updatedError = prepareApiError.call(this, error); } catch (e) {} @@ -75,7 +76,7 @@ export async function microsoftApiRequest( } export async function microsoftApiRequestAllItems( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: IHttpRequestMethods, endpoint: string, @@ -107,7 +108,7 @@ export async function microsoftApiRequestAllItems( } export async function downloadAttachments( - this: IExecuteFunctions, + this: IExecuteFunctions | IPollFunctions, messages: IDataObject[] | IDataObject, prefix: string, ) { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index eb0c2196e05cd..2219f0da63597 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -617,6 +617,7 @@ "dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", "dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js", + "dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js", "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js", "dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js", "dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js",