diff --git a/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts new file mode 100644 index 0000000000..1c8eafb074 --- /dev/null +++ b/packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts @@ -0,0 +1,22 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MicrosoftOutlookOAuth2Api implements ICredentialType { + name = 'microsoftOutlookOAuth2Api'; + extends = [ + 'microsoftOAuth2Api', + ]; + displayName = 'Microsoft Outlook OAuth2 API'; + documentationUrl = 'microsoft'; + properties = [ + //https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'openid offline_access Mail.ReadWrite Mail.Send MailboxSettings.Read', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/DraftDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/DraftDescription.ts new file mode 100644 index 0000000000..96a5adcff3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/DraftDescription.ts @@ -0,0 +1,304 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const draftOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'draft', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new email draft', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a draft', + }, + { + name: 'Get', + value: 'get', + description: 'Get a single draft', + }, + { + name: 'Send', + value: 'send', + description: 'Send an existing draft message', + }, + { + name: 'Update', + value: 'update', + description: 'Update a draft', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const draftFields = [ + { + displayName: 'Message ID', + name: 'messageId', + description: 'Message ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'delete', + 'get', + 'send', + 'update', + ], + }, + }, + }, + + // draft:create + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message.', + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'create', + ], + }, + }, + type: 'string', + default: '', + }, + { + displayName: 'Body Content', + name: 'bodyContent', + description: 'Message body content.', + type: 'string', + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'fixedCollection', + placeholder: 'Add Attachment', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachments', + displayName: 'Attachment', + values: [ + { + displayName: 'Binary Property Name', + name: 'binaryPropertyName', + type: 'string', + default: '', + description: 'Name of the binary property containing the data to be added to the email as an attachment', + }, + ], + }, + ], + + }, + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Email addresses of BCC recipients.', + type: 'string', + default: '', + }, + { + displayName: 'Body Content Type', + name: 'bodyContentType', + description: 'Message body content type.', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Categories', + name: 'categories', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getCategories', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: '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 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: 'Low', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Indicates whether a read receipt is requested for the message.', + type: 'boolean', + default: false, + }, + { + displayName: 'Recipients', + name: 'toRecipients', + description: 'Email addresses of recipients. Multiple can be added separated by comma.', + type: 'string', + default: '', + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email addresses to use when replying.', + type: 'string', + default: '', + }, + ], + }, + + // draft:send + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'send', + ], + }, + }, + options: [ + { + displayName: 'Recipients', + name: 'recipients', + description: 'Email addresses of recipients. Mutiple can be set separated by comma.', + type: 'string', + default: '', + }, + ], + }, + + + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts new file mode 100644 index 0000000000..faa72220c8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts @@ -0,0 +1,209 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const draftMessageSharedFields = [ + + // Get & Get All operations + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'draft', + 'message', + ], + operation: [ + 'get', + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Attachments Prefix', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + description: 'Prefix for name of the binary property to which to
write the attachments. An index starting with 0 will be added.
So if name is "attachment_" the first attachment is saved to "attachment_0"', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields the response will contain. Multiple can be added separated by comma.', + }, + { + displayName: 'Filter', + name: 'filter', + type: 'string', + default: '', + placeholder: 'isRead eq false', + description: 'Microsoft Graph API OData $filter query. Information about the syntax can be found here.', + }, + ], + }, + + // Update operation + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'draft', + 'message', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Email addresses of BCC recipients.', + type: 'string', + default: '', + }, + { + displayName: 'Body Content', + name: 'bodyContent', + description: 'Message body content.', + type: 'string', + default: '', + }, + { + displayName: 'Body Content Type', + name: 'bodyContentType', + description: 'Message body content type.', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Categories', + name: 'categories', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getCategories', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: '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 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: 'Low', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Indicates whether a read receipt is requested for the message.', + type: 'boolean', + default: false, + }, + { + displayName: 'Recipients', + name: 'toRecipients', + description: 'Email addresses of recipients. Multiple can be added separated by comma.', + type: 'string', + default: '', + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email addresses to use when replying.', + type: 'string', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message.', + type: 'string', + default: '', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/FolderDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/FolderDescription.ts new file mode 100644 index 0000000000..61991adff0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/FolderDescription.ts @@ -0,0 +1,311 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const folderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'folder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new mail folder in the root folder of the user\'s mailbox', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a folder', + }, + { + name: 'Get', + value: 'get', + description: 'Get a single folder details', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all folders under the root folder of the signed-in user', + }, + { + name: 'Get Children', + value: 'getChildren', + description: 'Lists all child folders under the folder', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const folderFields = [ + { + displayName: 'Folder ID', + name: 'folderId', + description: 'Folder ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'delete', + 'get', + 'getChildren', + 'update', + ], + }, + }, + }, + // folder:list, getChildren, listMessages + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'getAll', + 'getChildren', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'getAll', + 'getChildren', + ], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + // folder:create + { + displayName: 'Type', + name: 'folderType', + description: 'Folder Type', + type: 'options', + options: [ + { + name: 'Folder', + value: 'folder', + }, + { + name: 'Search Folder', + value: 'searchFolder', + }, + ], + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'create', + ], + }, + }, + default: 'folder', + }, + { + displayName: 'Display Name', + name: 'displayName', + description: 'Name of the folder.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Include Nested Folders', + name: 'includeNestedFolders', + description: 'Include child folders in the search.', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'create', + ], + folderType: [ + 'searchFolder', + ], + }, + }, + }, + { + displayName: 'Source Folder IDs', + name: 'sourceFolderIds', + description: 'The mailbox folders that should be mined.', + type: 'string', + typeOptions: { + multipleValues: true, + }, + default: [], + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'create', + ], + folderType: [ + 'searchFolder', + ], + }, + }, + }, + { + displayName: 'Filter Query', + name: 'filterQuery', + description: 'The OData query to filter the messages.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'create', + ], + folderType: [ + 'searchFolder', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'get', + 'getAll', + 'getChildren', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields the response will contain. Multiple can be added separated by ,.', + }, + { + displayName: 'Filter', + name: 'filter', + type: 'string', + default: '', + description: 'Microsoft Graph API OData $filter query.', + }, + ], + }, + + // folder:update + { + displayName: 'Update Fields', + name: 'updateFields', + description: 'Fields to update.', + type: 'collection', + default: {}, + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Display Name', + name: 'displayName', + description: 'Name of the folder.', + type: 'string', + default: '', + }, + { + displayName: 'Filter Query', + name: 'filterQuery', + description: 'The OData query to filter the messages. Only for search folders.', + type: 'string', + default: '', + }, + { + displayName: 'Include Nested Folders', + name: 'includeNestedFolders', + description: 'Include child folders in the search. Only for search folders.', + type: 'boolean', + default: false, + }, + { + displayName: 'Source Folder IDs', + name: 'sourceFolderIds', + description: 'The mailbox folders that should be mined. Only for search folders.', + type: 'string', + typeOptions: { + multipleValues: true, + }, + default: [], + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/FolderMessageDecription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/FolderMessageDecription.ts new file mode 100644 index 0000000000..3fdf7fd146 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/FolderMessageDecription.ts @@ -0,0 +1,122 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const folderMessageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'folderMessage', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all the messages in a folder', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const folderMessageFields = [ + { + displayName: 'Folder ID', + name: 'folderId', + description: 'Folder ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'folderMessage', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'folderMessage', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'folderMessage', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'folderMessage', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields the response will contain. Multiple can be added separated by ,.', + }, + { + displayName: 'Filter', + name: 'filter', + type: 'string', + default: '', + description: 'Microsoft Graph API OData $filter query.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Outlook/GenericFunctions.ts new file mode 100644 index 0000000000..fa190a8afd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/GenericFunctions.ts @@ -0,0 +1,175 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, +} from 'n8n-workflow'; + +export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = { json: true }): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`, + }; + 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; + } + + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'microsoftOutlookOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { + // Try to return the error prettier + throw new Error(`Microsoft error response [${error.statusCode}]: ${error.response.body.error.message}`); + } + throw error; + } +} + +export async function microsoftApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query['$top'] = 100; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri, headers); + uri = responseData['@odata.nextLink']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['@odata.nextLink'] !== undefined + ); + + return returnData; +} + +export async function microsoftApiRequestAllItemsSkip(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query['$top'] = 100; + query['$skip'] = 0; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, undefined, headers); + query['$skip'] += query['$top']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['value'].length !== 0 + ); + + return returnData; +} + +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; + } + + // Handle recipient fields + ['bccRecipients', 'ccRecipients', 'replyTo', 'sender', 'toRecipients'].forEach(key => { + if (Array.isArray(fields[key])) { + fields[key] = (fields[key] as string[]).map(email => makeRecipient(email)); + } else if (fields[key] !== undefined) { + fields[key] = (fields[key] as string).split(',').map((recipient: string) => makeRecipient(recipient)); + } + }); + + ['from', 'sender'].forEach(key => { + if (fields[key] !== undefined) { + fields[key] = makeRecipient(fields[key] as string); + } + }); + + + Object.assign(message, fields); + + return message; +} + +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, attachment.contentType); + } + } + if (Object.keys(element.binary!).length === 0) { + delete element.binary; + } + elements.push(element); + } + return elements; +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MessageAttachmentDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/MessageAttachmentDescription.ts new file mode 100644 index 0000000000..61b375f63f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MessageAttachmentDescription.ts @@ -0,0 +1,216 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const messageAttachmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add an attachment to a message', + }, + { + name: 'Download', + value: 'download', + description: 'Download attachment content', + }, + { + name: 'Get', + value: 'get', + description: 'Get an attachment from a message', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all the message\'s attachments', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const messageAttachmentFields = [ + { + displayName: 'Message ID', + name: 'messageId', + description: 'Message ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'add', + 'download', + 'get', + 'getAll', + ], + }, + }, + }, + { + displayName: 'Attachment ID', + name: 'attachmentId', + description: 'Attachment ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'download', + 'get', + ], + }, + }, + }, + + // messageAttachment:getAll, messageAttachment:listAttachments + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + + + // messageAttachment:create, messageAttachment:update, messageAttachment:send + + // File operations + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + description: 'Name of the binary property to which to
write the data of the read file.', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'add', + 'download', + ], + }, + }, + }, + + // messageAttachment:add + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'add', + ], + }, + }, + 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: '', + }, + ], + }, + + // Get & Get All operations + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'messageAttachment', + ], + operation: [ + 'get', + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields the response will contain. Multiple can be added separated by ,.', + }, + { + displayName: 'Filter', + name: 'filter', + type: 'string', + default: '', + description: 'Microsoft Graph API OData $filter query.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts new file mode 100644 index 0000000000..d45f1bae17 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts @@ -0,0 +1,633 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const messageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a message', + }, + { + name: 'Get', + value: 'get', + description: 'Get a single message', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all messages in the signed-in user\'s mailbox', + }, + { + name: 'Get MIME Content', + value: 'getMime', + description: 'Get MIME content of a message', + }, + { + name: 'Reply', + value: 'reply', + description: 'Create reply to a message', + }, + { + name: 'Send', + value: 'send', + description: 'Send a message', + }, + { + name: 'Update', + value: 'update', + description: 'Update a message', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const messageFields = [ + { + displayName: 'Message ID', + name: 'messageId', + description: 'Message ID', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'addAttachment', + 'delete', + 'get', + 'getAttachment', + 'getMime', + 'update', + 'reply', + ], + }, + }, + }, + + // message:reply + { + displayName: 'Reply Type', + name: 'replyType', + type: 'options', + options: [ + { + name: 'Reply', + value: 'reply', + }, + { + name: 'Reply All', + value: 'replyAll', + }, + ], + default: 'reply', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + ], + }, + }, + }, + { + displayName: 'Comment', + name: 'comment', + description: 'A comment to include. Can be an empty string.', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + ], + }, + }, + type: 'string', + default: '', + }, + { + displayName: 'Send', + name: 'send', + description: 'Send the reply message directly. If not set, it will be saved as draft.', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + ], + }, + }, + type: 'boolean', + default: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + ], + replyType: [ + 'reply', + ], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'fixedCollection', + placeholder: 'Add Attachment', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachments', + displayName: 'Attachment', + values: [ + { + displayName: 'Binary Property Name', + name: 'binaryPropertyName', + type: 'string', + default: '', + description: 'Name of the binary property containing the data to be added to the email as an attachment', + }, + ], + }, + ], + + }, + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Email addresses of BCC recipients.', + type: 'string', + default: '', + }, + { + displayName: 'Body Content', + name: 'bodyContent', + description: 'Message body content.', + type: 'string', + default: '', + }, + { + displayName: 'Body Content Type', + name: 'bodyContentType', + description: 'Message body content type.', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: '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 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: 'Low', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Indicates whether a read receipt is requested for the message.', + type: 'boolean', + default: false, + }, + { + displayName: 'Recipients', + name: 'toRecipients', + description: 'Email addresses of recipients. Multiple can be added separated by comma.', + type: 'string', + default: '', + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email addresses to use when replying.', + type: 'string', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message.', + type: 'string', + default: '', + }, + ], + }, + + // message:getAll + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + + // message:create, message:update, message:send + { + displayName: 'Subject', + name: 'subject', + description: 'The subject of the message.', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + 'send', + ], + }, + }, + type: 'string', + default: '', + }, + { + displayName: 'Body Content', + name: 'bodyContent', + description: 'Message body content.', + type: 'string', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + 'send', + ], + }, + }, + default: '', + }, + { + displayName: 'Recipients', + name: 'toRecipients', + description: 'Email addresses of recipients. Multiple can be added separated by comma.', + type: 'string', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'send', + ], + }, + }, + required: true, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'send', + ], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'fixedCollection', + placeholder: 'Add Attachment', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachments', + displayName: 'Attachment', + values: [ + { + displayName: 'Binary Property Name', + name: 'binaryPropertyName', + type: 'string', + default: '', + description: 'Name of the binary property containing the data to be added to the email as an attachment', + }, + ], + }, + ], + + }, + { + displayName: 'BCC Recipients', + name: 'bccRecipients', + description: 'Email addresses of BCC recipients.', + type: 'string', + default: '', + }, + { + displayName: 'Body Content Type', + name: 'bodyContentType', + description: 'Message body content type.', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'Text', + }, + ], + default: 'html', + }, + { + displayName: 'Categories', + name: 'categories', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getCategories', + }, + default: [], + }, + { + displayName: 'CC Recipients', + name: 'ccRecipients', + description: '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 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: 'Low', + }, + { + displayName: 'Read Receipt Requested', + name: 'isReadReceiptRequested', + description: 'Indicates whether a read receipt is requested for the message.', + type: 'boolean', + default: false, + }, + { + displayName: 'Recipients', + name: 'toRecipients', + description: 'Email addresses of recipients. Multiple can be added separated by comma.', + type: 'string', + default: '', + }, + { + displayName: 'Reply To', + name: 'replyTo', + description: 'Email addresses to use when replying.', + type: 'string', + default: '', + }, + { + displayName: 'Save To Sent Items', + name: 'saveToSentItems', + description: 'Indicates whether to save the message in Sent Items.', + type: 'boolean', + default: true, + }, + ], + }, + + // File operations + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + description: 'Name of the binary property to which to
write the data of the read file.', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getMime', + ], + }, + }, + }, + + // message:move + { + displayName: 'Folder ID', + name: 'folderId', + description: 'Folder ID', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'move', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts new file mode 100644 index 0000000000..73e87bc22a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MicrosoftOutlook.node.ts @@ -0,0 +1,961 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} 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'; + +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', + color: '#3a71b5', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftOutlookOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + 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 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): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (['draft', 'message'].includes(resource)) { + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'DELETE', + `/messages/${messageId}`, + ); + + returnData.push({ success: true }); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + 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, prefix); + returnData.push.apply(returnData, data as unknown as IDataObject[]); + } else { + returnData.push(responseData); + } + + if (additionalFields.dataPropertyAttachmentsPrefixName) { + return [returnData as INodeExecutionData[]]; + } + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + // Create message from optional fields + const body: IDataObject = createMessage(updateFields); + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/messages/${messageId}`, + body, + {}, + ); + returnData.push(responseData); + } + } + } + + if (resource === 'draft') { + + if (operation === 'create') { + for (let i = 0; i < length; i++) { + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + 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 Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + 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); + } + } + + if (operation === 'send') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i); + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject; + + if (additionalFields && additionalFields.recipients) { + const recipients = ((additionalFields.recipients as string).split(',') as string[]).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 }); + } + } + } + + if (resource === 'message') { + + if (operation === 'reply') { + for (let i = 0; i < length; i++) { + 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, {}) as IDataObject; + + 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 = attachments.map(attachment => { + const binaryPropertyName = attachment.binaryPropertyName as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + 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 === true) { + await microsoftApiRequest.call( + this, + 'POST', + `/messages/${responseData.id}/send`, + ); + } + + returnData.push(responseData); + } + } + + if (operation === 'getMime') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as 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 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); + } + } + + if (operation === 'getAll') { + let additionalFields: IDataObject = {}; + for (let i = 0; i < length; i++) { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.fields) { + qs['$select'] = additionalFields.fields; + } + + if (additionalFields.filter) { + qs['$filter'] = additionalFields.filter; + } + + const endpoint = '/messages'; + + if (returnAll === true) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + 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, prefix); + returnData.push.apply(returnData, data as unknown as IDataObject[]); + } else { + returnData.push.apply(returnData, responseData); + } + } + + if (additionalFields.dataPropertyAttachmentsPrefixName) { + return [returnData as INodeExecutionData[]]; + } + } + + if (operation === 'move') { + for (let i = 0; i < length; i++) { + 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 }); + } + } + + if (operation === 'send') { + for (let i = 0; i < length; i++) { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + 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 Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + 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 }); + } + } + + } + + if (resource === 'messageAttachment') { + if (operation === 'add') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + const dataBuffer = Buffer.from(binaryData.data, 'base64'); + + const fileName = additionalFields.fileName === undefined ? binaryData.fileName : additionalFields.fileName; + + if (!fileName) { + throw new Error('File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!'); + } + + // 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 Error('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 }); + } + } + + if (operation === 'download') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; + + // 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, mimeType); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + // 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); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + const messageId = this.getNodeParameter('messageId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + // 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 === true) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + undefined, + qs, + ); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call( + this, + 'GET', + endpoint, + undefined, + qs, + ); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + } + + if (resource === 'folder') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + 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); + } + } + + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const folderId = this.getNodeParameter('folderId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'DELETE', + `/mailFolders/${folderId}`, + ); + returnData.push({ success: true }); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + const folderId = this.getNodeParameter('folderId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + 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); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.fields) { + qs['$select'] = additionalFields.fields; + } + + if (additionalFields.filter) { + qs['$filter'] = additionalFields.filter; + } + + if (returnAll === true) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + '/mailFolders', + {}, + qs, + ); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call( + this, + 'GET', + '/mailFolders', + {}, + qs, + ); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + + if (operation === 'getChildren') { + for (let i = 0; i < length; i++) { + const folderId = this.getNodeParameter('folderId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + 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) as number; + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/mailFolders/${folderId}/childFolders`, + undefined, + qs, + ); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + const folderId = this.getNodeParameter('folderId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = { + ...updateFields, + }; + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/mailFolders/${folderId}`, + body, + ); + returnData.push(responseData); + } + } + } + + if (resource === 'folderMessage') { + for (let i = 0; i < length; i++) { + if (operation === 'getAll') { + const folderId = this.getNodeParameter('folderId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.fields) { + qs['$select'] = additionalFields.fields; + } + + const endpoint = `/mailFolders/${folderId}/messages`; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + endpoint, + qs, + ); + } + else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call( + this, + 'GET', + endpoint, + undefined, + qs, + ); + responseData = responseData.value; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + } + + if ((resource === 'message' && operation === 'getMime') || (resource === 'messageAttachment' && operation === 'download')) { + return this.prepareOutputData(items); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/outlook.svg b/packages/nodes-base/nodes/Microsoft/Outlook/outlook.svg new file mode 100644 index 0000000000..a4eae7d657 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Outlook/outlook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c7eb05ec81..bf767c3ba3 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -139,6 +139,7 @@ "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", + "dist/credentials/MicrosoftOutlookOAuth2Api.credentials.js", "dist/credentials/MicrosoftSql.credentials.js", "dist/credentials/MicrosoftTeamsOAuth2Api.credentials.js", "dist/credentials/MindeeReceiptApi.credentials.js", @@ -373,6 +374,7 @@ "dist/nodes/MessageBird/MessageBird.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", + "dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js", "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js", "dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js", "dist/nodes/Mindee/Mindee.node.js",