first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,22 @@
{
"node": "n8n-nodes-base.microsoftOutlook",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "HITL"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftoutlook/"
}
]
},
"alias": ["email", "human", "form", "wait", "hitl", "approval"]
}
@@ -0,0 +1,27 @@
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,
schemaPath: 'Microsoft/Outlook',
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new MicrosoftOutlookV1(baseDescription),
2: new MicrosoftOutlookV2(baseDescription),
};
super(nodeVersions, baseDescription);
}
}
@@ -0,0 +1,19 @@
{
"node": "n8n-nodes-base.microsoftOutlookTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.microsoftoutlooktrigger/"
}
]
},
"alias": ["email"]
}
@@ -0,0 +1,96 @@
import { DateTime } from 'luxon';
import {
type IPollFunctions,
type INodeExecutionData,
type INodeType,
type INodeTypeDescription,
NodeConnectionTypes,
} from 'n8n-workflow';
import { getPollResponse } from './trigger/GenericFunctions';
import { properties as messageProperties } from './trigger/MessageDescription';
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: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'Trigger On',
name: 'event',
type: 'options',
default: 'messageReceived',
options: [
{
name: 'Message Received',
value: 'messageReceived',
},
],
},
...messageProperties,
],
};
methods = { loadOptions };
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
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;
}
}
@@ -0,0 +1,62 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"allowedOnlineMeetingProviders": {
"type": "array",
"items": {
"type": "string"
}
},
"canEdit": {
"type": "boolean"
},
"canShare": {
"type": "boolean"
},
"canViewPrivateItems": {
"type": "boolean"
},
"changeKey": {
"type": "string"
},
"color": {
"type": "string"
},
"defaultOnlineMeetingProvider": {
"type": "string"
},
"hexColor": {
"type": "string"
},
"id": {
"type": "string"
},
"isDefaultCalendar": {
"type": "boolean"
},
"isRemovable": {
"type": "boolean"
},
"isTallyingResponses": {
"type": "boolean"
},
"name": {
"type": "string"
},
"owner": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,62 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"allowedOnlineMeetingProviders": {
"type": "array",
"items": {
"type": "string"
}
},
"canEdit": {
"type": "boolean"
},
"canShare": {
"type": "boolean"
},
"canViewPrivateItems": {
"type": "boolean"
},
"changeKey": {
"type": "string"
},
"color": {
"type": "string"
},
"defaultOnlineMeetingProvider": {
"type": "string"
},
"hexColor": {
"type": "string"
},
"id": {
"type": "string"
},
"isDefaultCalendar": {
"type": "boolean"
},
"isRemovable": {
"type": "boolean"
},
"isTallyingResponses": {
"type": "boolean"
},
"name": {
"type": "string"
},
"owner": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,59 @@
{
"type": "object",
"properties": {
"allowedOnlineMeetingProviders": {
"type": "array",
"items": {
"type": "string"
}
},
"canEdit": {
"type": "boolean"
},
"canShare": {
"type": "boolean"
},
"canViewPrivateItems": {
"type": "boolean"
},
"changeKey": {
"type": "string"
},
"color": {
"type": "string"
},
"defaultOnlineMeetingProvider": {
"type": "string"
},
"hexColor": {
"type": "string"
},
"id": {
"type": "string"
},
"isDefaultCalendar": {
"type": "boolean"
},
"isRemovable": {
"type": "boolean"
},
"isTallyingResponses": {
"type": "boolean"
},
"name": {
"type": "string"
},
"owner": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,89 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"businessPhones": {
"type": "array",
"items": {
"type": "string"
}
},
"changeKey": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"displayName": {
"type": "string"
},
"emailAddresses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"fileAs": {
"type": "string"
},
"generation": {
"type": "null"
},
"homePhones": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"initials": {
"type": "null"
},
"lastModifiedDateTime": {
"type": "string"
},
"middleName": {
"type": "null"
},
"nickName": {
"type": "null"
},
"parentFolderId": {
"type": "string"
},
"personalNotes": {
"type": "string"
},
"profession": {
"type": "null"
},
"spouseName": {
"type": "null"
},
"yomiCompanyName": {
"type": "null"
},
"yomiGivenName": {
"type": "null"
},
"yomiSurname": {
"type": "null"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,38 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"businessPhones": {
"type": "array",
"items": {
"type": "string"
}
},
"displayName": {
"type": "string"
},
"emailAddresses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"id": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,35 @@
{
"type": "object",
"properties": {
"@odata.etag": {
"type": "string"
},
"businessPhones": {
"type": "array",
"items": {
"type": "string"
}
},
"displayName": {
"type": "string"
},
"emailAddresses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"id": {
"type": "string"
}
},
"version": 5
}
@@ -0,0 +1,151 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"bccRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"changeKey": {
"type": "string"
},
"conversationId": {
"type": "string"
},
"conversationIndex": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"flag": {
"type": "object",
"properties": {
"flagStatus": {
"type": "string"
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"inferenceClassification": {
"type": "string"
},
"internetMessageId": {
"type": "string"
},
"isDeliveryReceiptRequested": {
"type": "boolean"
},
"isDraft": {
"type": "boolean"
},
"isRead": {
"type": "boolean"
},
"isReadReceiptRequested": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"parentFolderId": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sentDateTime": {
"type": "string"
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,44 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"conversationId": {
"type": "string"
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"subject": {
"type": "string"
}
},
"version": 2
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,177 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"ccRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"changeKey": {
"type": "string"
},
"conversationId": {
"type": "string"
},
"conversationIndex": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"flag": {
"type": "object",
"properties": {
"flagStatus": {
"type": "string"
}
}
},
"from": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"inferenceClassification": {
"type": "string"
},
"internetMessageId": {
"type": "string"
},
"isDraft": {
"type": "boolean"
},
"isRead": {
"type": "boolean"
},
"isReadReceiptRequested": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"parentFolderId": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sender": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"sentDateTime": {
"type": "string"
},
"subject": {
"type": "string"
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,202 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"allowNewTimeProposals": {
"type": "boolean"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"changeKey": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"end": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"hasAttachments": {
"type": "boolean"
},
"hideAttendees": {
"type": "boolean"
},
"iCalUId": {
"type": "string"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"isAllDay": {
"type": "boolean"
},
"isCancelled": {
"type": "boolean"
},
"isDraft": {
"type": "boolean"
},
"isOnlineMeeting": {
"type": "boolean"
},
"isOrganizer": {
"type": "boolean"
},
"isReminderOn": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"location": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"locationType": {
"type": "string"
},
"uniqueIdType": {
"type": "string"
}
}
},
"locations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"locationType": {
"type": "string"
},
"uniqueId": {
"type": "string"
},
"uniqueIdType": {
"type": "string"
}
}
}
},
"occurrenceId": {
"type": "null"
},
"onlineMeetingProvider": {
"type": "string"
},
"organizer": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"originalEndTimeZone": {
"type": "string"
},
"originalStartTimeZone": {
"type": "string"
},
"recurrence": {
"type": "null"
},
"reminderMinutesBeforeStart": {
"type": "integer"
},
"responseRequested": {
"type": "boolean"
},
"responseStatus": {
"type": "object",
"properties": {
"response": {
"type": "string"
},
"time": {
"type": "string"
}
}
},
"sensitivity": {
"type": "string"
},
"seriesMasterId": {
"type": "null"
},
"showAs": {
"type": "string"
},
"start": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"transactionId": {
"type": "null"
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,101 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"attendees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"status": {
"type": "object",
"properties": {
"response": {
"type": "string"
},
"time": {
"type": "string"
}
}
},
"type": {
"type": "string"
}
}
}
},
"bodyPreview": {
"type": "string"
},
"calendar@odata.associationLink": {
"type": "string"
},
"calendar@odata.navigationLink": {
"type": "string"
},
"end": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"organizer": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"start": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"subject": {
"type": "string"
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,92 @@
{
"type": "object",
"properties": {
"@odata.etag": {
"type": "string"
},
"attendees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"status": {
"type": "object",
"properties": {
"response": {
"type": "string"
},
"time": {
"type": "string"
}
}
},
"type": {
"type": "string"
}
}
}
},
"bodyPreview": {
"type": "string"
},
"end": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"organizer": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"start": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"subject": {
"type": "string"
},
"webLink": {
"type": "string"
}
},
"version": 4
}
@@ -0,0 +1,235 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"allowNewTimeProposals": {
"type": "boolean"
},
"attendees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"status": {
"type": "object",
"properties": {
"response": {
"type": "string"
},
"time": {
"type": "string"
}
}
},
"type": {
"type": "string"
}
}
}
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"changeKey": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"end": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"hasAttachments": {
"type": "boolean"
},
"hideAttendees": {
"type": "boolean"
},
"iCalUId": {
"type": "string"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"isAllDay": {
"type": "boolean"
},
"isCancelled": {
"type": "boolean"
},
"isDraft": {
"type": "boolean"
},
"isOnlineMeeting": {
"type": "boolean"
},
"isOrganizer": {
"type": "boolean"
},
"isReminderOn": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"location": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"locationType": {
"type": "string"
},
"uniqueIdType": {
"type": "string"
}
}
},
"locations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"locationType": {
"type": "string"
},
"uniqueId": {
"type": "string"
},
"uniqueIdType": {
"type": "string"
}
}
}
},
"occurrenceId": {
"type": "null"
},
"onlineMeetingProvider": {
"type": "string"
},
"organizer": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"originalEndTimeZone": {
"type": "string"
},
"originalStartTimeZone": {
"type": "string"
},
"recurrence": {
"type": "null"
},
"reminderMinutesBeforeStart": {
"type": "integer"
},
"responseRequested": {
"type": "boolean"
},
"responseStatus": {
"type": "object",
"properties": {
"response": {
"type": "string"
},
"time": {
"type": "string"
}
}
},
"sensitivity": {
"type": "string"
},
"seriesMasterId": {
"type": "null"
},
"showAs": {
"type": "string"
},
"start": {
"type": "object",
"properties": {
"dateTime": {
"type": "string"
},
"timeZone": {
"type": "string"
}
}
},
"subject": {
"type": "string"
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,33 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"childFolderCount": {
"type": "integer"
},
"displayName": {
"type": "string"
},
"id": {
"type": "string"
},
"isHidden": {
"type": "boolean"
},
"parentFolderId": {
"type": "string"
},
"sizeInBytes": {
"type": "integer"
},
"totalItemCount": {
"type": "integer"
},
"unreadItemCount": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,33 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"childFolderCount": {
"type": "integer"
},
"displayName": {
"type": "string"
},
"id": {
"type": "string"
},
"isHidden": {
"type": "boolean"
},
"parentFolderId": {
"type": "string"
},
"sizeInBytes": {
"type": "integer"
},
"totalItemCount": {
"type": "integer"
},
"unreadItemCount": {
"type": "integer"
}
},
"version": 3
}
@@ -0,0 +1,30 @@
{
"type": "object",
"properties": {
"childFolderCount": {
"type": "integer"
},
"displayName": {
"type": "string"
},
"id": {
"type": "string"
},
"isHidden": {
"type": "boolean"
},
"parentFolderId": {
"type": "string"
},
"sizeInBytes": {
"type": "integer"
},
"totalItemCount": {
"type": "integer"
},
"unreadItemCount": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,82 @@
{
"type": "object",
"properties": {
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"conversationId": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"sentDateTime": {
"type": "string"
},
"subject": {
"type": "string"
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 8
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,189 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"ccRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"changeKey": {
"type": "string"
},
"conversationId": {
"type": "string"
},
"conversationIndex": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"flag": {
"type": "object",
"properties": {
"flagStatus": {
"type": "string"
}
}
},
"from": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"inferenceClassification": {
"type": "string"
},
"internetMessageId": {
"type": "string"
},
"isDraft": {
"type": "boolean"
},
"isRead": {
"type": "boolean"
},
"isReadReceiptRequested": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"parentFolderId": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sender": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"sentDateTime": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "array",
"items": {
"type": "string"
}
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"webLink": {
"type": "string"
}
},
"version": 9
}
@@ -0,0 +1,186 @@
{
"type": "object",
"properties": {
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"ccRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"changeKey": {
"type": "string"
},
"conversationId": {
"type": "string"
},
"conversationIndex": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"flag": {
"type": "object",
"properties": {
"flagStatus": {
"type": "string"
}
}
},
"from": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"inferenceClassification": {
"type": "string"
},
"internetMessageId": {
"type": "string"
},
"isDraft": {
"type": "boolean"
},
"isRead": {
"type": "boolean"
},
"isReadReceiptRequested": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"parentFolderId": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sender": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"sentDateTime": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "array",
"items": {
"type": "string"
}
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"webLink": {
"type": "string"
}
},
"version": 10
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,161 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"changeKey": {
"type": "string"
},
"conversationId": {
"type": "string"
},
"conversationIndex": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"flag": {
"type": "object",
"properties": {
"flagStatus": {
"type": "string"
}
}
},
"from": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"inferenceClassification": {
"type": "string"
},
"internetMessageId": {
"type": "string"
},
"isDeliveryReceiptRequested": {
"type": "boolean"
},
"isDraft": {
"type": "boolean"
},
"isRead": {
"type": "boolean"
},
"isReadReceiptRequested": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"parentFolderId": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sender": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"sentDateTime": {
"type": "string"
},
"subject": {
"type": "string"
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,17 @@
{
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"approved": {
"type": "boolean"
},
"text": {
"type": "string"
}
}
}
},
"version": 2
}
@@ -0,0 +1,183 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.etag": {
"type": "string"
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"bodyPreview": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"ccRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"changeKey": {
"type": "string"
},
"conversationId": {
"type": "string"
},
"conversationIndex": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"flag": {
"type": "object",
"properties": {
"flagStatus": {
"type": "string"
}
}
},
"from": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"hasAttachments": {
"type": "boolean"
},
"id": {
"type": "string"
},
"importance": {
"type": "string"
},
"inferenceClassification": {
"type": "string"
},
"internetMessageId": {
"type": "string"
},
"isDraft": {
"type": "boolean"
},
"isRead": {
"type": "boolean"
},
"isReadReceiptRequested": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"parentFolderId": {
"type": "string"
},
"receivedDateTime": {
"type": "string"
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sender": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
},
"sentDateTime": {
"type": "string"
},
"subject": {
"type": "string"
},
"toRecipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"emailAddress": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"webLink": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,30 @@
{
"type": "object",
"properties": {
"@odata.mediaContentType": {
"type": "string"
},
"@odata.type": {
"type": "string"
},
"contentType": {
"type": "string"
},
"id": {
"type": "string"
},
"isInline": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"version": 3
}
@@ -0,0 +1,33 @@
{
"type": "object",
"properties": {
"@odata.context": {
"type": "string"
},
"@odata.mediaContentType": {
"type": "string"
},
"@odata.type": {
"type": "string"
},
"contentType": {
"type": "string"
},
"id": {
"type": "string"
},
"isInline": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"version": 7
}
@@ -0,0 +1,30 @@
{
"type": "object",
"properties": {
"@odata.mediaContentType": {
"type": "string"
},
"@odata.type": {
"type": "string"
},
"contentType": {
"type": "string"
},
"id": {
"type": "string"
},
"isInline": {
"type": "boolean"
},
"lastModifiedDateTime": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 81 81"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><path fill="#2372ba" stroke="none" d="M77.9 19.2H46.86V0L0 8.232V71.98L46.86 80V61.847H77.9c1.266 0 2.1-1.055 2.1-2.1V21.32c0-1.266-.844-2.1-2.1-2.1zM21.952 54.46c-15.4-1.055-14.776-29.13.422-29.974 16.675-.844 16.253 31.03-.422 29.974m55.515 4.644H46.86V35.04l9.5 9.077c.422.422.844.633 1.477.633s1.055-.2 1.478-.633L77.467 27.23zm0-35.462-19.63 18.153L46.86 31.45v-9.7h30.396v1.9h.2zm-55.092 6.543c-8.232.422-8.443 18.364-.2 18.575 8.654.422 8.654-18.997.2-18.575"/></symbol></svg>

After

Width:  |  Height:  |  Size: 749 B

@@ -0,0 +1,610 @@
import { mockDeep } from 'jest-mock-extended';
import type { IDataObject, INode, IPollFunctions } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { getPollResponse } from '../../trigger/GenericFunctions';
jest.mock('../../v2/helpers/utils', () => ({
prepareFilterString: jest.fn(),
simplifyOutputMessages: jest.fn(),
}));
jest.mock('../../v2/transport', () => ({
downloadAttachments: jest.fn(),
microsoftApiRequest: jest.fn(),
microsoftApiRequestAllItems: jest.fn(),
}));
import { prepareFilterString, simplifyOutputMessages } from '../../v2/helpers/utils';
import {
downloadAttachments,
microsoftApiRequest,
microsoftApiRequestAllItems,
} from '../../v2/transport';
describe('Microsoft Outlook Trigger GenericFunctions', () => {
let mockPollFunctions: jest.Mocked<IPollFunctions>;
let mockNode: INode;
beforeEach(() => {
mockPollFunctions = mockDeep<IPollFunctions>();
mockNode = {
id: 'test-node',
name: 'Test Outlook Trigger Node',
type: 'n8n-nodes-base.microsoftOutlookTrigger',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
mockPollFunctions.getNode.mockReturnValue(mockNode);
jest.clearAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('getPollResponse', () => {
const pollStartDate = '2023-01-01T00:00:00Z';
const pollEndDate = '2023-01-02T00:00:00Z';
const mockMessages = [
{
id: 'msg1',
subject: 'Test Message 1',
from: { emailAddress: { address: 'sender1@example.com' } },
receivedDateTime: '2023-01-01T12:00:00Z',
},
{
id: 'msg2',
subject: 'Test Message 2',
from: { emailAddress: { address: 'sender2@example.com' } },
receivedDateTime: '2023-01-01T15:00:00Z',
},
];
beforeEach(() => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: {},
output: 'simple',
fields: ['id', 'subject', 'from'],
};
return params[paramName] ?? defaultValue;
},
);
const mockJsonArray = jest
.fn()
.mockImplementation((data: IDataObject[]) =>
data.map((item, index) => ({ json: item, pairedItem: { item: index } })),
);
mockPollFunctions.helpers.returnJsonArray = mockJsonArray;
(prepareFilterString as jest.Mock).mockReturnValue('');
(simplifyOutputMessages as jest.Mock).mockReturnValue(mockMessages);
});
describe('successful execution', () => {
describe('manual mode', () => {
beforeEach(() => {
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
});
it('should handle simple output format in manual mode', async () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
$select:
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments',
$top: 1,
});
expect(simplifyOutputMessages).toHaveBeenCalledWith(mockMessages);
expect(result).toHaveLength(2);
expect(result[0].json).toEqual(mockMessages[0]);
});
it('should handle fields output format in manual mode', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: {},
output: 'fields',
fields: ['id', 'subject', 'from'],
};
return params[paramName] ?? defaultValue;
},
);
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
$select: 'id,subject,from',
$top: 1,
});
expect(simplifyOutputMessages).not.toHaveBeenCalled();
expect(result).toHaveLength(2);
});
it('should handle downloadAttachments option in manual mode', async () => {
const mockExecutionData = [
{ json: { ...mockMessages[0], attachments: ['file1.pdf'] }, pairedItem: { item: 0 } },
{ json: { ...mockMessages[1], attachments: ['file2.doc'] }, pairedItem: { item: 1 } },
];
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: { downloadAttachments: true, attachmentsPrefix: 'attachment_' },
output: 'fields',
fields: ['id', 'subject', 'from'],
};
return params[paramName] ?? defaultValue;
},
);
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
(downloadAttachments as jest.Mock).mockResolvedValue(mockExecutionData);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
$select: 'id,subject,from,hasAttachments',
$top: 1,
});
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'attachment_');
expect(result).toEqual(mockExecutionData);
});
});
describe('trigger mode', () => {
beforeEach(() => {
mockPollFunctions.getMode.mockReturnValue('trigger');
(microsoftApiRequestAllItems as jest.Mock).mockResolvedValue(mockMessages);
});
it('should handle simple output format in trigger mode', async () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/messages',
undefined,
{
$select:
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments',
$filter: `receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`,
},
);
expect(simplifyOutputMessages).toHaveBeenCalledWith(mockMessages);
expect(result).toHaveLength(2);
});
it('should handle fields output format in trigger mode', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: {},
output: 'fields',
fields: ['id', 'subject', 'receivedDateTime'],
};
return params[paramName] ?? defaultValue;
},
);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/messages',
undefined,
{
$select: 'id,subject,receivedDateTime',
$filter: `receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`,
},
);
expect(simplifyOutputMessages).not.toHaveBeenCalled();
expect(result).toHaveLength(2);
});
it('should combine custom filters with date filters in trigger mode', async () => {
const customFilter = 'isRead eq false';
(prepareFilterString as jest.Mock).mockReturnValue(customFilter);
await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/messages',
undefined,
{
$select:
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments',
$filter: `${customFilter} and receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`,
},
);
});
it('should handle downloadAttachments option with custom prefix in trigger mode', async () => {
const mockExecutionData = [
{
json: { ...mockMessages[0], attachments: ['custom_file1.pdf'] },
pairedItem: { item: 0 },
},
];
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: { downloadAttachments: true, attachmentsPrefix: 'custom_' },
output: 'simple',
};
return params[paramName] ?? defaultValue;
},
);
(downloadAttachments as jest.Mock).mockResolvedValue(mockExecutionData);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'custom_');
expect(result).toEqual(mockExecutionData);
});
it('should use default attachment prefix when not specified', async () => {
const mockExecutionData = [
{
json: { ...mockMessages[0], attachments: ['attachment_file1.pdf'] },
pairedItem: { item: 0 },
},
];
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: { downloadAttachments: true },
output: 'simple',
};
return params[paramName] ?? defaultValue;
},
);
(downloadAttachments as jest.Mock).mockResolvedValue(mockExecutionData);
await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'attachment_');
});
});
describe('output formats', () => {
beforeEach(() => {
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
});
it('should handle full output format', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: {},
output: 'full',
};
return params[paramName] ?? defaultValue;
},
);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
$top: 1,
});
expect(simplifyOutputMessages).not.toHaveBeenCalled();
expect(result).toHaveLength(2);
});
});
describe('edge cases', () => {
beforeEach(() => {
mockPollFunctions.getMode.mockReturnValue('manual');
});
it('should handle empty response', async () => {
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: [] });
(simplifyOutputMessages as jest.Mock).mockReturnValue([]);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(result).toHaveLength(0);
expect(result).toEqual([]);
});
it('should handle null response values', async () => {
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: null });
const mockJsonArray = jest.fn().mockReturnValue([]);
mockPollFunctions.helpers.returnJsonArray = mockJsonArray;
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(result).toHaveLength(0);
});
it('should include hasAttachments when downloadAttachments is enabled', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: { downloadAttachments: true },
output: 'fields',
fields: ['id', 'subject'],
};
return params[paramName] ?? defaultValue;
},
);
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
(downloadAttachments as jest.Mock).mockResolvedValue([]);
await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
$select: 'id,subject,hasAttachments',
$top: 1,
});
});
});
});
describe('error handling', () => {
it('should throw NodeApiError when microsoftApiRequest fails', async () => {
const originalError = new Error('API request failed');
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockRejectedValue(originalError);
await expect(
getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate),
).rejects.toThrow(NodeApiError);
expect(mockPollFunctions.getNode).toHaveBeenCalled();
});
it('should throw NodeApiError when microsoftApiRequestAllItems fails', async () => {
const originalError = new Error('All items request failed');
mockPollFunctions.getMode.mockReturnValue('trigger');
(microsoftApiRequestAllItems as jest.Mock).mockRejectedValue(originalError);
await expect(
getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate),
).rejects.toThrow(NodeApiError);
expect(mockPollFunctions.getNode).toHaveBeenCalled();
});
it('should throw NodeApiError when downloadAttachments fails', async () => {
const originalError = new Error('Download attachments failed');
mockPollFunctions.getMode.mockReturnValue('manual');
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: {},
options: { downloadAttachments: true },
output: 'simple',
};
return params[paramName] ?? defaultValue;
},
);
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
(downloadAttachments as jest.Mock).mockRejectedValue(originalError);
await expect(
getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate),
).rejects.toThrow(NodeApiError);
});
it('should throw NodeApiError when simplifyOutputMessages fails', async () => {
const originalError = new Error('Simplify output failed');
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
(simplifyOutputMessages as jest.Mock).mockImplementation(() => {
throw originalError;
});
await expect(
getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate),
).rejects.toThrow(NodeApiError);
});
it('should throw NodeApiError when prepareFilterString fails', async () => {
const originalError = new Error('Filter preparation failed');
mockPollFunctions.getMode.mockReturnValue('trigger');
(prepareFilterString as jest.Mock).mockImplementation(() => {
throw originalError;
});
await expect(
getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate),
).rejects.toThrow(NodeApiError);
});
it('should preserve error message and description in NodeApiError', async () => {
const originalError = {
message: 'Custom error message',
description: 'Custom error description',
};
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockRejectedValue(originalError);
await expect(
getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate),
).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining('Custom error message'),
}),
);
});
it('should handle errors without description', async () => {
const originalError = {
message: 'Error without description',
};
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockRejectedValue(originalError);
try {
await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
} catch (error) {
expect(error).toBeInstanceOf(NodeApiError);
expect((error as NodeApiError).message).toContain('Error without description');
}
});
});
describe('parameter validation', () => {
beforeEach(() => {
mockPollFunctions.getMode.mockReturnValue('manual');
(microsoftApiRequest as jest.Mock).mockResolvedValue({ value: mockMessages });
});
it('should handle missing filters parameter', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
if (paramName === 'filters') return undefined;
const params: Record<string, any> = {
options: {},
output: 'simple',
};
return params[paramName] ?? defaultValue;
},
);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(prepareFilterString).toHaveBeenCalledWith({ filters: undefined });
expect(result).toHaveLength(2);
});
it('should handle missing options parameter', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
if (paramName === 'options') return defaultValue || {};
const params: Record<string, any> = {
filters: {},
output: 'simple',
};
return params[paramName] ?? defaultValue;
},
);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(result).toHaveLength(2);
expect(downloadAttachments).not.toHaveBeenCalled();
});
it('should handle missing fields parameter when output is fields', async () => {
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
if (paramName === 'fields') return [];
const params: Record<string, any> = {
filters: {},
options: {},
output: 'fields',
};
return params[paramName] ?? defaultValue;
},
);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
$select: '',
$top: 1,
});
expect(result).toHaveLength(2);
});
});
describe('integration scenarios', () => {
it('should handle complex filter combinations', async () => {
const complexFilter = "isRead eq false and from/emailAddress/address eq 'test@example.com'";
mockPollFunctions.getMode.mockReturnValue('trigger');
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: { isRead: false, fromAddress: 'test@example.com' },
options: {},
output: 'fields',
fields: ['id', 'subject', 'from', 'isRead'],
};
return params[paramName] ?? defaultValue;
},
);
(prepareFilterString as jest.Mock).mockReturnValue(complexFilter);
(microsoftApiRequestAllItems as jest.Mock).mockResolvedValue(mockMessages);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(prepareFilterString).toHaveBeenCalledWith({
filters: { isRead: false, fromAddress: 'test@example.com' },
});
expect(microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/messages',
undefined,
{
$select: 'id,subject,from,isRead',
$filter: `${complexFilter} and receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`,
},
);
expect(result).toHaveLength(2);
});
it('should handle all options together', async () => {
const mockExecutionData = [
{
json: { ...mockMessages[0], attachments: ['prefix_file1.pdf'] },
pairedItem: { item: 0 },
},
];
mockPollFunctions.getMode.mockReturnValue('trigger');
mockPollFunctions.getNodeParameter.mockImplementation(
(paramName: string, defaultValue?: any) => {
const params: Record<string, any> = {
filters: { isRead: false },
options: { downloadAttachments: true, attachmentsPrefix: 'prefix_' },
output: 'fields',
fields: ['id', 'subject'],
};
return params[paramName] ?? defaultValue;
},
);
(prepareFilterString as jest.Mock).mockReturnValue('isRead eq false');
(microsoftApiRequestAllItems as jest.Mock).mockResolvedValue(mockMessages);
(downloadAttachments as jest.Mock).mockResolvedValue(mockExecutionData);
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/messages',
undefined,
{
$select: 'id,subject,hasAttachments',
$filter: `isRead eq false and receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`,
},
);
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'prefix_');
expect(result).toEqual(mockExecutionData);
});
});
});
});
@@ -0,0 +1,617 @@
import { mockDeep } from 'jest-mock-extended';
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import {
searchContacts,
searchCalendars,
searchDrafts,
searchMessages,
searchEvents,
searchFolders,
searchAttachments,
} from '../../../v2/methods/listSearch';
import * as transport from '../../../v2/transport';
import * as utils from '../../../v2/helpers/utils';
jest.mock('../../../v2/transport');
jest.mock('../../../v2/helpers/utils');
const mockTransport = transport as jest.Mocked<typeof transport>;
const mockUtils = utils as jest.Mocked<typeof utils>;
describe('MicrosoftOutlookV2 - listSearch methods', () => {
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
beforeEach(() => {
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
jest.clearAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('searchContacts', () => {
it('should search contacts without filter', async () => {
const mockResponse = {
value: [
{ id: 'contact1', displayName: 'John Doe' },
{ id: 'contact2', displayName: 'Jane Smith' },
],
'@odata.nextLink': 'https://graph.microsoft.com/v1.0/me/contacts?$skip=100',
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchContacts.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/contacts',
undefined,
{
$select: 'id,displayName',
$top: 100,
},
);
expect(result).toEqual({
results: [
{ name: 'John Doe', value: 'contact1' },
{ name: 'Jane Smith', value: 'contact2' },
],
paginationToken: 'https://graph.microsoft.com/v1.0/me/contacts?$skip=100',
});
});
it('should search contacts with filter', async () => {
const mockResponse = {
value: [{ id: 'contact1', displayName: 'John Doe' }],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchContacts.call(mockLoadOptionsFunctions, 'John');
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/contacts',
undefined,
{
$select: 'id,displayName',
$top: 100,
$filter: "contains(displayName, 'John')",
},
);
expect(result).toEqual({
results: [{ name: 'John Doe', value: 'contact1' }],
paginationToken: undefined,
});
});
it('should handle pagination token', async () => {
const mockResponse = {
value: [{ id: 'contact1', displayName: 'John Doe' }],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const paginationToken = 'https://graph.microsoft.com/v1.0/me/contacts?$skip=100';
await searchContacts.call(mockLoadOptionsFunctions, undefined, paginationToken);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'',
undefined,
undefined,
paginationToken,
);
});
});
describe('searchCalendars', () => {
it('should search calendars successfully', async () => {
const mockResponse = {
value: [
{ id: 'cal1', name: 'Work Calendar' },
{ id: 'cal2', name: 'Personal Calendar' },
],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchCalendars.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/calendars',
undefined,
{
$select: 'id,name',
$top: 100,
},
);
expect(result).toEqual({
results: [
{ name: 'Work Calendar', value: 'cal1' },
{ name: 'Personal Calendar', value: 'cal2' },
],
paginationToken: undefined,
});
});
});
describe('searchDrafts', () => {
it('should search drafts without filter', async () => {
const mockResponse = {
value: [
{
id: 'draft1',
subject: 'Draft Email',
bodyPreview: 'This is a draft',
webLink: 'https://outlook.office365.com/mail/draft1',
},
],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchDrafts.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
$top: 100,
$filter: 'isDraft eq true',
},
);
expect(result).toEqual({
results: [
{
name: 'Draft Email',
value: 'draft1',
url: 'https://outlook.office365.com/mail/draft1',
},
],
paginationToken: undefined,
});
});
it('should search drafts with filter', async () => {
const mockResponse = {
value: [
{
id: 'draft1',
subject: 'Important Draft',
bodyPreview: 'This is an important draft',
webLink: 'https://outlook.office365.com/mail/draft1',
},
],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
await searchDrafts.call(mockLoadOptionsFunctions, 'Important');
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
$top: 100,
$filter: "isDraft eq true AND contains(subject, 'Important')",
},
);
});
it('should fallback to bodyPreview when subject is empty', async () => {
const mockResponse = {
value: [
{
id: 'draft1',
subject: '',
bodyPreview: 'This is a draft without subject',
webLink: 'https://outlook.office365.com/mail/draft1',
},
],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchDrafts.call(mockLoadOptionsFunctions);
expect(result.results[0].name).toBe('This is a draft without subject');
});
});
describe('searchMessages', () => {
it('should search messages successfully', async () => {
const mockResponse = {
value: [
{
id: 'msg1',
subject: 'Hello World',
bodyPreview: 'This is a message',
webLink: 'https://outlook.office365.com/mail/msg1',
},
],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchMessages.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
$top: 100,
},
);
expect(result).toEqual({
results: [
{
name: 'Hello World',
value: 'msg1',
url: 'https://outlook.office365.com/mail/msg1',
},
],
paginationToken: undefined,
});
});
it('should search messages with filter', async () => {
const mockResponse = {
value: [
{
id: 'msg1',
subject: 'Meeting Invite',
bodyPreview: 'You are invited to a meeting',
webLink: 'https://outlook.office365.com/mail/msg1',
},
],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
await searchMessages.call(mockLoadOptionsFunctions, 'Meeting');
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
$top: 100,
$filter: "contains(subject, 'Meeting')",
},
);
});
});
describe('searchEvents', () => {
beforeEach(() => {
mockUtils.encodeOutlookId.mockReturnValue('encoded-id');
});
it('should search events successfully', async () => {
const calendarId = 'cal123';
const mockResponse = {
value: [
{
id: 'event1',
subject: 'Team Meeting',
bodyPreview: 'Weekly team sync',
},
],
};
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue(calendarId);
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchEvents.call(mockLoadOptionsFunctions);
expect(mockLoadOptionsFunctions.getNodeParameter).toHaveBeenCalledWith(
'calendarId',
undefined,
{
extractValue: true,
},
);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/calendars/${calendarId}/events`,
undefined,
{
$select: 'id,subject,bodyPreview',
$top: 100,
},
);
expect(result).toEqual({
results: [
{
name: 'Team Meeting',
value: 'event1',
url: 'https://outlook.office365.com/calendar/item/encoded-id',
},
],
paginationToken: undefined,
});
});
it('should search events with filter', async () => {
const calendarId = 'cal123';
const mockResponse = {
value: [
{
id: 'event1',
subject: 'Project Review',
bodyPreview: 'Review project progress',
},
],
};
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue(calendarId);
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
await searchEvents.call(mockLoadOptionsFunctions, 'Project');
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/calendars/${calendarId}/events`,
undefined,
{
$select: 'id,subject,bodyPreview',
$top: 100,
$filter: "contains(subject, 'Project')",
},
);
});
});
describe('searchFolders', () => {
beforeEach(() => {
mockUtils.encodeOutlookId.mockReturnValue('encoded-folder-id');
});
it('should search folders successfully', async () => {
const mockResponse = {
value: [
{ id: 'folder1', displayName: 'Inbox' },
{ id: 'folder2', displayName: 'Sent Items' },
],
};
const mockFolders = [
{ id: 'folder1', displayName: 'Inbox' },
{ id: 'folder2', displayName: 'Sent Items' },
{ id: 'subfolder1', displayName: 'Work' },
];
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockResolvedValue(mockFolders);
const result = await searchFolders.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/mailFolders',
undefined,
{
$top: 100,
},
);
expect(mockTransport.getSubfolders).toHaveBeenCalledWith(mockResponse.value);
expect(result).toEqual({
results: [
{
name: 'Inbox',
value: 'folder1',
url: 'https://outlook.office365.com/mail/encoded-folder-id',
},
{
name: 'Sent Items',
value: 'folder2',
url: 'https://outlook.office365.com/mail/encoded-folder-id',
},
{
name: 'Work',
value: 'subfolder1',
url: 'https://outlook.office365.com/mail/encoded-folder-id',
},
],
paginationToken: undefined,
});
});
it('should filter folders by name', async () => {
const mockResponse = {
value: [
{ id: 'folder1', displayName: 'Inbox' },
{ id: 'folder2', displayName: 'Sent Items' },
],
};
const mockFolders = [
{ id: 'folder1', displayName: 'Inbox' },
{ id: 'folder2', displayName: 'Sent Items' },
{ id: 'folder3', displayName: 'Work Folder' },
];
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockResolvedValue(mockFolders);
const result = await searchFolders.call(mockLoadOptionsFunctions, 'work');
expect(result.results).toHaveLength(1);
expect(result.results[0].name).toBe('Work Folder');
});
it('should handle case-insensitive filtering', async () => {
const mockResponse = {
value: [{ id: 'folder1', displayName: 'IMPORTANT' }],
};
const mockFolders = [{ id: 'folder1', displayName: 'IMPORTANT' }];
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockResolvedValue(mockFolders);
const result = await searchFolders.call(mockLoadOptionsFunctions, 'important');
expect(result.results).toHaveLength(1);
expect(result.results[0].name).toBe('IMPORTANT');
});
it('should handle empty displayName gracefully', async () => {
const mockResponse = {
value: [{ id: 'folder1', displayName: null }],
};
const mockFolders = [{ id: 'folder1', displayName: null }];
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockResolvedValue(mockFolders);
const result = await searchFolders.call(mockLoadOptionsFunctions, 'test');
expect(result.results).toHaveLength(0);
});
});
describe('searchAttachments', () => {
it('should search attachments successfully', async () => {
const messageId = 'msg123';
const mockResponse = {
value: [
{ id: 'att1', name: 'document.pdf' },
{ id: 'att2', name: 'image.jpg' },
],
};
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue(messageId);
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchAttachments.call(mockLoadOptionsFunctions);
expect(mockLoadOptionsFunctions.getNodeParameter).toHaveBeenCalledWith(
'messageId',
undefined,
{
extractValue: true,
},
);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/messages/${messageId}/attachments`,
undefined,
{
$select: 'id,name',
$top: 100,
},
);
expect(result).toEqual({
results: [
{ name: 'document.pdf', value: 'att1' },
{ name: 'image.jpg', value: 'att2' },
],
paginationToken: undefined,
});
});
it('should handle pagination for attachments', async () => {
const messageId = 'msg123';
const paginationToken =
'https://graph.microsoft.com/v1.0/me/messages/msg123/attachments?$skip=100';
const mockResponse = {
value: [{ id: 'att1', name: 'document.pdf' }],
};
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue(messageId);
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
await searchAttachments.call(mockLoadOptionsFunctions, paginationToken);
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'',
undefined,
undefined,
paginationToken,
);
});
});
describe('Error Handling', () => {
it('should handle API errors in searchContacts', async () => {
const apiError = new Error('API Error');
mockTransport.microsoftApiRequest.mockRejectedValue(apiError);
await expect(searchContacts.call(mockLoadOptionsFunctions)).rejects.toThrow('API Error');
});
it('should handle API errors in searchEvents', async () => {
const apiError = new Error('Calendar not found');
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('invalid-calendar');
mockTransport.microsoftApiRequest.mockRejectedValue(apiError);
await expect(searchEvents.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Calendar not found',
);
});
it('should handle API errors in searchFolders', async () => {
const apiError = new Error('Folders not accessible');
mockTransport.microsoftApiRequest.mockRejectedValue(apiError);
await expect(searchFolders.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Folders not accessible',
);
});
});
describe('Edge Cases', () => {
it('should handle empty response arrays', async () => {
const mockResponse = {
value: [],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchContacts.call(mockLoadOptionsFunctions);
expect(result).toEqual({
results: [],
paginationToken: undefined,
});
});
it('should handle missing properties in response', async () => {
const mockResponse = {
value: [{ id: 'contact1' }, { displayName: 'Jane Smith' }],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchContacts.call(mockLoadOptionsFunctions);
expect(result.results).toEqual([
{ name: undefined, value: 'contact1' },
{ name: 'Jane Smith', value: undefined },
]);
});
it('should handle response without @odata.nextLink', async () => {
const mockResponse = {
value: [{ id: 'contact1', displayName: 'John Doe' }],
};
mockTransport.microsoftApiRequest.mockResolvedValue(mockResponse);
const result = await searchContacts.call(mockLoadOptionsFunctions);
expect(result.paginationToken).toBeUndefined();
});
});
});
@@ -0,0 +1,370 @@
import { mockDeep } from 'jest-mock-extended';
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import { getCategoriesNames, getFolders, getCalendarGroups } from '../../../v2/methods/loadOptions';
import * as transport from '../../../v2/transport';
jest.mock('../../../v2/transport');
const mockTransport = transport as jest.Mocked<typeof transport>;
describe('MicrosoftOutlookV2 - loadOptions methods', () => {
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
beforeEach(() => {
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
jest.clearAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('getCategoriesNames', () => {
it('should get categories names successfully', async () => {
const mockCategories = [
{ displayName: 'Red Category' },
{ displayName: 'Blue Category' },
{ displayName: 'Green Category' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCategories);
const result = await getCategoriesNames.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/outlook/masterCategories',
);
expect(result).toEqual([
{ name: 'Red Category', value: 'Red Category' },
{ name: 'Blue Category', value: 'Blue Category' },
{ name: 'Green Category', value: 'Green Category' },
]);
});
it('should handle empty categories array', async () => {
mockTransport.microsoftApiRequestAllItems.mockResolvedValue([]);
const result = await getCategoriesNames.call(mockLoadOptionsFunctions);
expect(result).toEqual([]);
});
it('should handle categories with missing displayName', async () => {
const mockCategories = [
{ displayName: 'Valid Category' },
{ id: 'category-without-name' },
{ displayName: null },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCategories);
const result = await getCategoriesNames.call(mockLoadOptionsFunctions);
expect(result).toEqual([
{ name: 'Valid Category', value: 'Valid Category' },
{ name: undefined, value: undefined },
{ name: null, value: null },
]);
});
it('should handle API errors', async () => {
const apiError = new Error('Failed to fetch categories');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(apiError);
await expect(getCategoriesNames.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Failed to fetch categories',
);
});
it('should handle special characters in category names', async () => {
const mockCategories = [
{ displayName: 'Category with "quotes"' },
{ displayName: "Category with 'apostrophes'" },
{ displayName: 'Category with & symbols' },
{ displayName: 'Category With Unicode: 🔥' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCategories);
const result = await getCategoriesNames.call(mockLoadOptionsFunctions);
expect(result).toEqual([
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
{ name: 'Category with "quotes"', value: 'Category with "quotes"' },
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
{ name: "Category with 'apostrophes'", value: "Category with 'apostrophes'" },
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
{ name: 'Category with & symbols', value: 'Category with & symbols' },
{ name: 'Category With Unicode: 🔥', value: 'Category With Unicode: 🔥' },
]);
});
});
describe('getFolders', () => {
it('should get folders successfully', async () => {
const mockResponse = [
{ id: 'folder1', displayName: 'Inbox' },
{ id: 'folder2', displayName: 'Sent Items' },
];
const mockFolders = [
{ id: 'folder1', displayName: 'Inbox' },
{ id: 'folder2', displayName: 'Sent Items' },
{ id: 'subfolder1', displayName: 'Work/Projects' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockResolvedValue(mockFolders);
const result = await getFolders.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/mailFolders',
{},
);
expect(mockTransport.getSubfolders).toHaveBeenCalledWith(mockResponse);
expect(result).toEqual([
{ name: 'Inbox', value: 'folder1' },
{ name: 'Sent Items', value: 'folder2' },
{ name: 'Work/Projects', value: 'subfolder1' },
]);
});
it('should handle empty folders response', async () => {
mockTransport.microsoftApiRequestAllItems.mockResolvedValue([]);
mockTransport.getSubfolders.mockResolvedValue([]);
const result = await getFolders.call(mockLoadOptionsFunctions);
expect(result).toEqual([]);
});
it('should handle folders with missing properties', async () => {
const mockResponse = [
{ id: 'folder1', displayName: 'Valid Folder' },
{ displayName: 'Folder without ID' },
{ id: 'folder3' },
];
const mockFolders = [
{ id: 'folder1', displayName: 'Valid Folder' },
{ displayName: 'Folder without ID' },
{ id: 'folder3' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockResolvedValue(mockFolders);
const result = await getFolders.call(mockLoadOptionsFunctions);
expect(result).toEqual([
{ name: 'Valid Folder', value: 'folder1' },
{ name: 'Folder without ID', value: undefined },
{ name: undefined, value: 'folder3' },
]);
});
it('should handle API errors from microsoftApiRequestAllItems', async () => {
const apiError = new Error('Failed to fetch mail folders');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(apiError);
await expect(getFolders.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Failed to fetch mail folders',
);
});
it('should handle errors from getSubfolders', async () => {
const mockResponse = [{ id: 'folder1', displayName: 'Inbox' }];
const subfolderError = new Error('Failed to get subfolders');
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockResponse);
mockTransport.getSubfolders.mockRejectedValue(subfolderError);
await expect(getFolders.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Failed to get subfolders',
);
});
it('should handle large number of folders', async () => {
const largeFolderList = Array.from({ length: 1000 }, (_, i) => ({
id: `folder${i}`,
displayName: `Folder ${i}`,
}));
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(largeFolderList);
mockTransport.getSubfolders.mockResolvedValue(largeFolderList);
const result = await getFolders.call(mockLoadOptionsFunctions);
expect(result).toHaveLength(1000);
expect(result[0]).toEqual({ name: 'Folder 0', value: 'folder0' });
expect(result[999]).toEqual({ name: 'Folder 999', value: 'folder999' });
});
});
describe('getCalendarGroups', () => {
it('should get calendar groups successfully', async () => {
const mockCalendars = [
{ id: 'group1', name: 'My Calendars' },
{ id: 'group2', name: 'Work Calendars' },
{ id: 'group3', name: 'Shared Calendars' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCalendars);
const result = await getCalendarGroups.call(mockLoadOptionsFunctions);
expect(mockTransport.microsoftApiRequestAllItems).toHaveBeenCalledWith(
'value',
'GET',
'/calendarGroups',
{},
);
expect(result).toEqual([
{ name: 'My Calendars', value: 'group1' },
{ name: 'Work Calendars', value: 'group2' },
{ name: 'Shared Calendars', value: 'group3' },
]);
});
it('should handle empty calendar groups', async () => {
mockTransport.microsoftApiRequestAllItems.mockResolvedValue([]);
const result = await getCalendarGroups.call(mockLoadOptionsFunctions);
expect(result).toEqual([]);
});
it('should handle calendar groups with missing properties', async () => {
const mockCalendars = [
{ id: 'group1', name: 'Valid Group' },
{ name: 'Group without ID' },
{ id: 'group3' },
{ id: 'group4', name: null },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCalendars);
const result = await getCalendarGroups.call(mockLoadOptionsFunctions);
expect(result).toEqual([
{ name: 'Valid Group', value: 'group1' },
{ name: 'Group without ID', value: undefined },
{ name: undefined, value: 'group3' },
{ name: null, value: 'group4' },
]);
});
it('should handle API errors', async () => {
const apiError = new Error('Failed to fetch calendar groups');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(apiError);
await expect(getCalendarGroups.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Failed to fetch calendar groups',
);
});
it('should handle special characters in calendar group names', async () => {
const mockCalendars = [
{ id: 'group1', name: 'My "Work" Calendar' },
{ id: 'group2', name: "John's Calendar" },
{ id: 'group3', name: 'Team & Projects' },
{ id: 'group4', name: 'Calendar with unicode: =' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCalendars);
const result = await getCalendarGroups.call(mockLoadOptionsFunctions);
expect(result).toEqual([
{ name: 'My "Work" Calendar', value: 'group1' },
{ name: "John's Calendar", value: 'group2' },
{ name: 'Team & Projects', value: 'group3' },
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
{ name: 'Calendar with unicode: =', value: 'group4' },
]);
});
it('should handle timeout scenarios', async () => {
const timeoutError = new Error('Request timeout');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(timeoutError);
await expect(getCalendarGroups.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Request timeout',
);
});
});
describe('Edge Cases and Integration', () => {
it('should handle network connectivity issues', async () => {
const networkError = new Error('Network error');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(networkError);
await expect(getCategoriesNames.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Network error',
);
await expect(getFolders.call(mockLoadOptionsFunctions)).rejects.toThrow('Network error');
await expect(getCalendarGroups.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Network error',
);
});
it('should handle malformed API responses', async () => {
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(null as any);
await expect(getCategoriesNames.call(mockLoadOptionsFunctions)).rejects.toThrow();
});
it('should handle authentication errors', async () => {
const authError = new Error('Authentication failed');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(authError);
await expect(getCategoriesNames.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Authentication failed',
);
});
it('should handle rate limiting errors', async () => {
const rateLimitError = new Error('Rate limit exceeded');
mockTransport.microsoftApiRequestAllItems.mockRejectedValue(rateLimitError);
await expect(getCalendarGroups.call(mockLoadOptionsFunctions)).rejects.toThrow(
'Rate limit exceeded',
);
});
});
describe('Performance and Memory', () => {
it('should handle very long displayNames efficiently', async () => {
const longName = 'A'.repeat(10000);
const mockCategories = [{ displayName: longName }];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCategories);
const result = await getCategoriesNames.call(mockLoadOptionsFunctions);
expect(result).toEqual([{ name: longName, value: longName }]);
});
it('should handle empty strings in response data', async () => {
const mockCategories = [
{ displayName: '' },
{ displayName: '' },
{ displayName: 'Valid Category' },
];
mockTransport.microsoftApiRequestAllItems.mockResolvedValue(mockCategories);
const result = await getCategoriesNames.call(mockLoadOptionsFunctions);
expect(result).toEqual([
{ name: '', value: '' },
{ name: '', value: '' },
{ name: 'Valid Category', value: 'Valid Category' },
]);
});
});
});
@@ -0,0 +1,35 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, calendar => create', () => {
nock('https://graph.microsoft.com/v1.0/me')
.post(
'/calendarGroups/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRu_AAA=/calendars',
{ color: 'lightOrange', name: 'New Calendar' },
)
.reply(200, {
'@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',
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,14 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, calendar => delete', () => {
nock('https://graph.microsoft.com/v1.0/me')
.delete(
'/calendars/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvIAAA=',
)
.reply(200);
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,34 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, calendar => get', () => {
nock('https://graph.microsoft.com/v1.0/me')
.get(
'/calendars/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=',
)
.reply(200, {
'@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',
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,53 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, calendar => getAll', () => {
nock('https://graph.microsoft.com/v1.0/me')
.get('/calendars?%24filter=canEdit%20eq%20true&%24top=2')
.reply(200, {
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',
},
},
],
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,35 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, calendar => update', () => {
nock('https://graph.microsoft.com/v1.0/me')
.patch(
'/calendars/AAAXXXYYYnnnT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEGAABZf4De-LkrSqpPI8eyjUmAAACLtRvGAAA=',
{ color: 'lightOrange', isDefaultCalendar: false, name: 'Foo' },
)
.reply(200, {
'@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',
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,65 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, contact => create', () => {
nock('https://graph.microsoft.com/v1.0/me')
.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',
})
.reply(200, {
'@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: {},
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,75 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, contact => update', () => {
nock('https://graph.microsoft.com/v1.0/me')
.patch(
'/contacts/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEOAABZf4De-LkrSqpPI8eyjUmAAAFXBCQuAAA=',
{
businessAddress: {
city: 'City',
countryOrRegion: 'Country',
postalCode: '777777',
state: 'State',
street: 'Street',
},
businessPhones: ['999000555777'],
displayName: 'Username',
manager: 'Manager',
},
)
.reply(200, {
'@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: {},
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,318 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import nock from 'nock';
import { Readable } from 'stream';
import { execute } from '../../../../v2/actions/draft/create.operation';
import * as transport from '../../../../v2/transport';
describe('Test MicrosoftOutlookV2, draft => create', () => {
nock('https://graph.microsoft.com/v1.0/me')
.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' } }],
})
.reply(200, {
'@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:
'<AM0PR10MB21003DD359041CBE7D64DDB6DDE9A@AM0PR10MB2100.EURPRD10.PROD.OUTLOOK.COM>',
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',
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['create.workflow.json'],
});
describe('with attachments', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
let microsoftApiRequestSpy: jest.SpyInstance;
const mockNode: INode = {
id: 'test-node-id',
name: 'Microsoft Outlook Test',
type: 'n8n-nodes-base.microsoftOutlook',
typeVersion: 2,
position: [0, 0],
parameters: {},
};
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest');
jest.clearAllMocks();
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
(mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation(
(data: any, options: any) => {
return data.map((item: any) => ({
...item,
pairedItem: { item: options?.itemData?.item ?? 0 },
}));
},
);
(mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation(
(data: any) => {
return [{ json: data }];
},
);
});
afterEach(() => {
jest.resetAllMocks();
});
it('should properly await attachments with binary data stream', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
subject: 'Test Draft with Attachment',
bodyContent: 'Test message',
additionalFields: {
attachments: {
attachments: [
{
binaryPropertyName: 'data',
},
],
},
},
};
return params[paramName];
});
const mockBinaryData = {
id: 'binary-data-id-123',
mimeType: 'text/plain',
fileName: 'test-file.txt',
fileExtension: 'txt',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
const mockStream = Readable.from([Buffer.from('Hello World')]);
(mockExecuteFunctions.helpers.getBinaryStream as jest.Mock).mockResolvedValue(mockStream);
(mockExecuteFunctions.helpers.binaryToBuffer as jest.Mock).mockResolvedValue(
Buffer.from('Hello World'),
);
const mockResponse = {
id: 'draft-id-123',
subject: 'Test Draft with Attachment',
};
microsoftApiRequestSpy.mockResolvedValue(mockResponse);
const result = await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages',
expect.objectContaining({
subject: 'Test Draft with Attachment',
body: expect.objectContaining({
content: 'Test message',
}),
attachments: [
{
'@odata.type': '#microsoft.graph.fileAttachment',
name: 'test-file.txt',
contentBytes: Buffer.from('Hello World').toString('base64'),
},
],
}),
{},
);
expect(result).toEqual([
{
json: mockResponse,
pairedItem: { item: 0 },
},
]);
});
it('should properly await attachments with direct binary data', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
subject: 'Test Draft',
bodyContent: 'Test',
additionalFields: {
attachments: {
attachments: [
{
binaryPropertyName: 'data',
},
],
},
},
};
return params[paramName];
});
const mockBinaryData = {
data: Buffer.from('Direct data').toString('base64'),
mimeType: 'text/plain',
fileName: 'direct-file.txt',
fileExtension: 'txt',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
microsoftApiRequestSpy.mockResolvedValue({});
await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages',
expect.objectContaining({
attachments: [
{
'@odata.type': '#microsoft.graph.fileAttachment',
name: 'direct-file.txt',
contentBytes: mockBinaryData.data,
},
],
}),
{},
);
});
it('should properly await multiple attachments', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
subject: 'Test Draft',
bodyContent: 'Test',
additionalFields: {
attachments: {
attachments: [{ binaryPropertyName: 'file1' }, { binaryPropertyName: 'file2' }],
},
},
};
return params[paramName];
});
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock)
.mockReturnValueOnce({
id: 'id1',
fileName: 'file1.txt',
})
.mockReturnValueOnce({
data: 'ZmlsZTI=',
fileName: 'file2.txt',
});
const mockStream1 = Readable.from([Buffer.from('file1')]);
(mockExecuteFunctions.helpers.getBinaryStream as jest.Mock).mockResolvedValue(mockStream1);
(mockExecuteFunctions.helpers.binaryToBuffer as jest.Mock).mockResolvedValue(
Buffer.from('file1'),
);
microsoftApiRequestSpy.mockResolvedValue({});
await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages',
expect.objectContaining({
attachments: expect.arrayContaining([
expect.objectContaining({
name: 'file1.txt',
contentBytes: Buffer.from('file1').toString('base64'),
}),
expect.objectContaining({
name: 'file2.txt',
contentBytes: 'ZmlsZTI=',
}),
]),
}),
{},
);
});
});
});
@@ -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": "<AM0PR10MB21003DD359041CBE7D64DDB6DDE9A@AM0PR10MB2100.EURPRD10.PROD.OUTLOOK.COM>",
"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": []
}
@@ -0,0 +1,19 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, draft => send', () => {
nock('https://graph.microsoft.com/v1.0/me')
.post(
'/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=/send',
)
.reply(200)
.patch(
'/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDupAAA=',
{ toRecipients: [{ emailAddress: { address: 'michael.k@radency.com' } }] },
)
.reply(200);
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,112 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, contact => event', () => {
nock('https://graph.microsoft.com/v1.0/me')
.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',
},
)
.reply(200, {
'@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:
'<html>\r\n<head>\r\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">\r\n</head>\r\n<body>\r\nevent description<br>\r\n<div style="width:100%"><span style="white-space:nowrap; color:#5F5F5F; opacity:.36">________________________________________________________________________________</span>\r\n</div>\r\n<div class="me-email-text" lang="en-US" style="color:#252424; font-family:\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif">\r\n<div style="margin-top:24px; margin-bottom:20px"><span style="font-size:24px; color:#252424">Microsoft Teams meeting</span>\r\n</div>\r\n<div style="margin-bottom:20px">\r\n<div style="margin-top:0px; margin-bottom:0px; font-weight:bold"><span style="font-size:14px; color:#252424">Join on your computer, mobile app or room device</span>\r\n</div>\r\n<a href="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" class="me-email-headline" style="font-size:14px; font-family:\'Segoe UI Semibold\',\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7">Click\r\n here to join the meeting</a> </div>\r\n<div style="margin-bottom:20px; margin-top:20px">\r\n<div style="margin-bottom:4px"><span data-tid="meeting-code" style="font-size:14px; color:#252424">Meeting ID:\r\n<span style="font-size:16px; color:#252424">355 132 640 047</span> </span><br>\r\n<span style="font-size:14px; color:#252424">Passcode: </span><span style="font-size:16px; color:#252424">xgUo7v\r\n</span>\r\n<div style="font-size:14px"><a href="https://www.microsoft.com/en-us/microsoft-teams/download-app" class="me-email-link" style="font-size:14px; text-decoration:underline; color:#6264a7; font-family:\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif">Download\r\n Teams</a> | <a href="https://www.microsoft.com/microsoft-teams/join-a-meeting" class="me-email-link" style="font-size:14px; text-decoration:underline; color:#6264a7; font-family:\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif">\r\nJoin on the web</a></div>\r\n</div>\r\n</div>\r\n<div style="margin-bottom:24px; margin-top:20px"><a href="https://aka.ms/JoinTeamsMeeting" class="me-email-link" style="font-size:14px; text-decoration:underline; color:#6264a7; font-family:\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif">Learn More</a>\r\n | <a href="https://teams.microsoft.com/meetingOptions/?organizerId=b834447b-6848-4af9-8390-d2259ce46b74&amp;tenantId=23786ca6-7ff2-4672-87d0-5c649ee0a337&amp;threadId=19_meeting_MDZmMzZmYzYtMDc4Yi00NTA2LWE3MTMtZDc5ZDI1M2JmY2M3@thread.v2&amp;messageId=0&amp;language=en-US" class="me-email-link" style="font-size:14px; text-decoration:underline; color:#6264a7; font-family:\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif">\r\nMeeting options</a> </div>\r\n</div>\r\n<div style="font-size:14px; margin-bottom:4px; font-family:\'Segoe UI\',\'Helvetica Neue\',Helvetica,Arial,sans-serif">\r\n</div>\r\n<div style="font-size:12px"></div>\r\n<div></div>\r\n<div style="width:100%"><span style="white-space:nowrap; color:#5F5F5F; opacity:.36">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\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',
},
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\nevent description<br>\r\n<div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n<div class=\"me-email-text\" lang=\"en-US\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Microsoft Teams meeting</span>\r\n</div>\r\n<div style=\"margin-bottom:20px\">\r\n<div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Join on your computer, mobile app or room device</span>\r\n</div>\r\n<a href=\"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\" class=\"me-email-headline\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Click\r\n here to join the meeting</a> </div>\r\n<div style=\"margin-bottom:20px; margin-top:20px\">\r\n<div style=\"margin-bottom:4px\"><span data-tid=\"meeting-code\" style=\"font-size:14px; color:#252424\">Meeting ID:\r\n<span style=\"font-size:16px; color:#252424\">355 132 640 047</span> </span><br>\r\n<span style=\"font-size:14px; color:#252424\">Passcode: </span><span style=\"font-size:16px; color:#252424\">xgUo7v\r\n</span>\r\n<div style=\"font-size:14px\"><a href=\"https://www.microsoft.com/en-us/microsoft-teams/download-app\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Download\r\n Teams</a> | <a href=\"https://www.microsoft.com/microsoft-teams/join-a-meeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nJoin on the web</a></div>\r\n</div>\r\n</div>\r\n<div style=\"margin-bottom:24px; margin-top:20px\"><a href=\"https://aka.ms/JoinTeamsMeeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Learn More</a>\r\n | <a href=\"https://teams.microsoft.com/meetingOptions/?organizerId=b834447b-6848-4af9-8390-d2259ce46b74&amp;tenantId=23786ca6-7ff2-4672-87d0-5c649ee0a337&amp;threadId=19_meeting_MDZmMzZmYzYtMDc4Yi00NTA2LWE3MTMtZDc5ZDI1M2JmY2M3@thread.v2&amp;messageId=0&amp;language=en-US\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nMeeting options</a> </div>\r\n</div>\r\n<div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n</div>\r\n<div style=\"font-size:12px\"></div>\r\n<div></div>\r\n<div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\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": []
}
@@ -0,0 +1,27 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, contact => folder', () => {
nock('https://graph.microsoft.com')
.post(
'/v1.0/me/mailFolders/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEGAAA=/childFolders',
{ displayName: 'Folder 42' },
)
.reply(200, {
'@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,
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,23 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, folderMessage => getAll', () => {
nock('https://graph.microsoft.com/v1.0/me')
.get(
'/mailFolders/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=/messages?%24select=bodyPreview%2Csubject&%24top=100',
)
.reply(200, {
value: [
{
'@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFW3CAj"',
id: 'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAABZf4De-LkrSqpPI8eyjUmAAAFXBGDUAAA=',
subject: 'XXXX',
bodyPreview: 'test',
},
],
});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,59 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, message => get', () => {
const credentials = {
microsoftOutlookOAuth2Api: {
accessToken: 'test-access-token',
refreshToken: 'test-refresh-token',
},
};
beforeAll(() => {
const messageId =
'AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKecAAA=';
nock('https://graph.microsoft.com')
.get(`/v1.0/me/messages/${messageId}`)
.query({
$select:
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments',
})
.reply(200, {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/messages/$entity',
'@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFSpKec"',
id: messageId,
conversationId:
'AAQkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgAQALc9G-OGBp9Hshu8JZGiSFs=',
subject: 'Test Email Subject',
bodyPreview: 'This is a test email body preview for the get operation',
from: {
emailAddress: {
name: 'John Doe',
address: 'john.doe@example.com',
},
},
toRecipients: [
{
emailAddress: {
name: 'Jane Smith',
address: 'jane.smith@example.com',
},
},
{
emailAddress: {
name: 'Bob Wilson',
address: 'bob.wilson@example.com',
},
},
],
categories: ['Blue category', 'Important'],
hasAttachments: true,
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['get.workflow.json'],
});
});
@@ -0,0 +1,75 @@
{
"name": "Microsoft Outlook Message Get Test",
"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": "message",
"operation": "get",
"messageId": {
"__rl": true,
"value": "AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKecAAA=",
"mode": "id"
},
"output": "simple"
},
"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 OAuth2 Test"
}
}
}
],
"pinData": {
"Microsoft Outlook": [
{
"json": {
"id": "AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKecAAA=",
"conversationId": "AAQkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgAQALc9G-OGBp9Hshu8JZGiSFs=",
"subject": "Test Email Subject",
"bodyPreview": "This is a test email body preview for the get operation",
"from": "john.doe@example.com",
"to": ["jane.smith@example.com", "bob.wilson@example.com"],
"categories": ["Blue category", "Important"],
"hasAttachments": 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": []
}
@@ -0,0 +1,79 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, message => getAll', () => {
const credentials = {
microsoftOutlookOAuth2Api: {
accessToken: 'test-access-token',
refreshToken: 'test-refresh-token',
},
};
beforeAll(() => {
nock('https://graph.microsoft.com')
.get('/v1.0/me/messages')
.query({
$select:
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments',
$top: 50,
})
.reply(200, {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/messages',
value: [
{
'@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFSpKec"',
id: 'AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKecAAA=',
conversationId:
'AAQkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgAQALc9G-OGBp9Hshu8JZGiSFs=',
subject: 'Test Email 1',
bodyPreview: 'This is the first test email body preview',
from: {
emailAddress: {
name: 'John Doe',
address: 'john.doe@example.com',
},
},
toRecipients: [
{
emailAddress: {
name: 'Jane Smith',
address: 'jane.smith@example.com',
},
},
],
categories: ['Blue category'],
hasAttachments: false,
},
{
'@odata.etag': 'W/"CQAAABYAAABZf4De/LkrSqpPI8eyjUmAAAFSpKed"',
id: 'AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKedAAA=',
conversationId:
'AAQkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgAQALc9G-OGBp9Hshu8JZGiSFt=',
subject: 'Test Email 2',
bodyPreview: 'This is the second test email body preview',
from: {
emailAddress: {
name: 'Alice Johnson',
address: 'alice.johnson@example.com',
},
},
toRecipients: [
{
emailAddress: {
name: 'Bob Wilson',
address: 'bob.wilson@example.com',
},
},
],
categories: ['Red category'],
hasAttachments: true,
},
],
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getAll.workflow.json'],
});
});
@@ -0,0 +1,84 @@
{
"name": "Microsoft Outlook Message Get All Test",
"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": "message",
"operation": "getAll",
"returnAll": false,
"limit": 50,
"output": "simple"
},
"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 OAuth2 Test"
}
}
}
],
"pinData": {
"Microsoft Outlook": [
{
"json": {
"id": "AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKecAAA=",
"conversationId": "AAQkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgAQALc9G-OGBp9Hshu8JZGiSFs=",
"subject": "Test Email 1",
"bodyPreview": "This is the first test email body preview",
"from": "john.doe@example.com",
"to": ["jane.smith@example.com"],
"categories": ["Blue category"],
"hasAttachments": false
}
},
{
"json": {
"id": "AAMkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgBGAAAAAAA_iGOFSpJvRJ8V0rRW_J9FBwBZf4De_LkrSqpPI8eyjUmAAAACEDdwAABZf4De_LkrSqpPI8eyjUmAAAFSpKedAAA=",
"conversationId": "AAQkAGVmMDEzMTM4LTExMWMtNDAxYy05MWNjLWQ5YTY3NWUzNzE4ZgAQALc9G-OGBp9Hshu8JZGiSFt=",
"subject": "Test Email 2",
"bodyPreview": "This is the second test email body preview",
"from": "alice.johnson@example.com",
"to": ["bob.wilson@example.com"],
"categories": ["Red category"],
"hasAttachments": 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": []
}
@@ -0,0 +1,18 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, message => move', () => {
nock('https://graph.microsoft.com/v1.0/me')
.post(
'/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEJAABZf4De-LkrSqpPI8eyjUmAAAFXBEVwAAA=/move',
{
destinationId:
'AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OAAuAAAAAABPLqzvT6b9RLP0CKzHiJrRAQBZf4De-LkrSqpPI8eyjUmAAAFXBAEHAAA=',
},
)
.reply(200);
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,85 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, message => reply', () => {
nock('https://graph.microsoft.com/v1.0/me')
.post(
'/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEJAABZf4De-LkrSqpPI8eyjUmAAAFXBEVwAAA=/createReply',
{
message: {
body: { content: 'Reply message', contentType: 'html' },
importance: 'High',
subject: 'Reply Subject',
},
},
)
.reply(200, {
'@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:
'<AM0PR10MB2100903A148F1623165004E3DDE9A@AM0PR10MB2100.EURPRD10.PROD.OUTLOOK.COM>',
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:
'<html><head>\r\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>Reply message </body></html>',
},
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',
},
})
.post(
'/messages/AAMkADlhOTA0MTc5LWUwOTMtNDRkZS05NzE0LTNlYmI0ZWM5OWI5OABGAAAAAABPLqzvT6b9RLP0CKzHiJrRBwBZf4De-LkrSqpPI8eyjUmAAAAAAAEPAABZf4De-LkrSqpPI8eyjUmAAAFXBDurAAA=/send',
)
.reply(200);
new NodeTestHarness().setupTests({
workflowFiles: ['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": "<AM0PR10MB2100903A148F1623165004E3DDE9A@AM0PR10MB2100.EURPRD10.PROD.OUTLOOK.COM>",
"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": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body>Reply message </body></html>"
},
"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": []
}
@@ -0,0 +1,20 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test MicrosoftOutlookV2, message => send', () => {
nock('https://graph.microsoft.com')
.post('/v1.0/me/sendMail', {
message: {
body: { content: 'message description', contentType: 'Text' },
replyTo: [{ emailAddress: { address: 'reply@mail.com' } }],
subject: 'Hello',
toRecipients: [{ emailAddress: { address: 'to@mail.com' } }],
},
saveToSentItems: true,
})
.reply(200, {});
new NodeTestHarness().setupTests({
workflowFiles: ['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": []
}
@@ -0,0 +1,78 @@
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import { SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow';
import { description } from '../../../../v2/actions/node.description';
import { MicrosoftOutlookV2 } from '../../../../v2/MicrosoftOutlookV2.node';
import * as transport from '../../../../v2/transport';
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 => sendAndWait', () => {
let microsoftOutlook: MicrosoftOutlookV2;
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
beforeEach(() => {
microsoftOutlook = new MicrosoftOutlookV2(description);
mockExecuteFunctions = mock<IExecuteFunctions>();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should send message and put execution to wait', async () => {
const items = [{ json: { data: 'test' } }];
//router
mockExecuteFunctions.getInputData.mockReturnValue(items);
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
mockExecuteFunctions.putExecutionToWait.mockImplementation();
//operation
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my@outlook.com');
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
//getSendAndWaitConfig
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
);
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // approvalOptions
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // options
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
// configureWaitTillDate
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options.limitWaitTime.values
const result = await microsoftOutlook.execute.call(mockExecuteFunctions);
expect(result).toEqual([items]);
expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1);
expect(transport.microsoftApiRequest).toHaveBeenCalledWith('POST', '/sendMail', {
message: {
body: {
content: expect.stringContaining(
'href="http://localhost/waiting-webhook/nodeID?approved=true&signature=abc"',
),
contentType: 'html',
},
subject: 'my subject',
toRecipients: [{ emailAddress: { address: 'my@outlook.com' } }],
},
});
});
});
@@ -0,0 +1,264 @@
import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { execute } from '../../../../v2/actions/messageAttachment/add.operation';
import * as transport from '../../../../v2/transport';
describe('Microsoft Outlook V2 - MessageAttachment:add', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
let microsoftApiRequestSpy: jest.SpyInstance;
const mockNode: INode = {
id: 'test-node-id',
name: 'Microsoft Outlook Test',
type: 'n8n-nodes-base.microsoftOutlook',
typeVersion: 2,
position: [0, 0],
parameters: {},
};
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest');
jest.clearAllMocks();
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
(mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation(
(data: any, options: any) => {
return data.map((item: any, index: number) => ({
...item,
pairedItem: { item: options?.itemData?.item ?? index },
}));
},
);
(mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: any) => {
return [{ json: data }];
});
});
afterEach(() => {
jest.resetAllMocks();
});
describe('Small file (under 3MB)', () => {
beforeEach(() => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
messageId: 'AAMkAGI2TG93AAA=',
binaryPropertyName: 'data',
options: {},
};
return params[paramName];
});
const mockBinaryData = {
data: 'SGVsbG8gV29ybGQ=',
mimeType: 'text/plain',
fileName: 'test-file.txt',
fileExtension: 'txt',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
Buffer.from('Hello World'),
);
});
it('should add small attachment successfully', async () => {
const mockResponse = {
'@odata.context':
"https://graph.microsoft.com/v1.0/$metadata#users('user-id')/messages('AAMkAGI2TG93AAA%3D')/attachments/$entity",
'@odata.type': '#microsoft.graph.fileAttachment',
id: 'AAMkAGI2TG93AAA=attachment-id',
lastModifiedDateTime: '2023-12-19T12:00:00Z',
name: 'test-file.txt',
contentType: 'text/plain',
size: 11,
isInline: false,
};
microsoftApiRequestSpy.mockResolvedValue(mockResponse);
const result = await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages/AAMkAGI2TG93AAA=/attachments',
{
'@odata.type': '#microsoft.graph.fileAttachment',
name: 'test-file.txt',
contentBytes: 'SGVsbG8gV29ybGQ=',
},
{},
);
expect(result).toEqual([
{
json: { success: true },
pairedItem: { item: 0 },
},
]);
});
it('should use custom filename when provided', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
messageId: 'AAMkAGI2TG93AAA=',
binaryPropertyName: 'data',
options: {
fileName: 'custom-name.pdf',
},
};
return params[paramName];
});
const mockBinaryData = {
data: 'JVBERi0xLjQK',
mimeType: 'application/pdf',
fileName: 'original-name.pdf',
fileExtension: 'pdf',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
Buffer.from('%PDF-1.4\n'),
);
microsoftApiRequestSpy.mockResolvedValue({});
await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages/AAMkAGI2TG93AAA=/attachments',
{
'@odata.type': '#microsoft.graph.fileAttachment',
name: 'custom-name.pdf',
contentBytes: 'JVBERi0xLjQK',
},
{},
);
});
it('should throw error when fileName is not set', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
messageId: 'AAMkAGI2TG93AAA=',
binaryPropertyName: 'data',
options: {},
};
return params[paramName];
});
const mockBinaryData = {
data: 'SGVsbG8gV29ybGQ=',
mimeType: 'text/plain',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
Buffer.from('Hello World'),
);
await expect(execute.call(mockExecuteFunctions, 0, [{ json: {} }])).rejects.toThrow(
NodeOperationError,
);
await expect(execute.call(mockExecuteFunctions, 0, [{ json: {} }])).rejects.toThrow(
'File name is not set',
);
});
});
describe('Large file (over 3MB) with chunked upload', () => {
beforeEach(() => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
messageId: 'AAMkAGI2TG93BBB=',
binaryPropertyName: 'data',
options: {},
};
return params[paramName];
});
const largeBinaryData = Buffer.alloc(4 * 1024 * 1024);
const mockBinaryData = {
data: largeBinaryData.toString('base64'),
mimeType: 'application/octet-stream',
fileName: 'large-file.bin',
fileExtension: 'bin',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
largeBinaryData,
);
});
it('should create upload session and upload large file in chunks', async () => {
const uploadUrl =
"https://outlook.office.com/api/v2.0/Users('user-id')/Messages('AAMkAGI2TG93BBB=')/AttachmentSessions('session-id')?authtoken=token";
microsoftApiRequestSpy.mockResolvedValueOnce({
'@odata.context':
'https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession',
expirationDateTime: '2023-12-19T13:00:00.0000000Z',
nextExpectedRanges: ['0-'],
uploadUrl,
});
(mockExecuteFunctions.helpers.request as jest.Mock).mockResolvedValue({
'@odata.type': '#microsoft.graph.fileAttachment',
id: 'attachment-id',
name: 'large-file.bin',
size: 4 * 1024 * 1024,
});
const result = await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages/AAMkAGI2TG93BBB=/attachments/createUploadSession',
{
AttachmentItem: {
attachmentType: 'file',
name: 'large-file.bin',
size: 4 * 1024 * 1024,
},
},
);
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith(
uploadUrl,
expect.objectContaining({
method: 'PUT',
headers: expect.objectContaining({
'Content-Type': 'application/octet-stream',
}),
}),
);
expect(result).toEqual([
{
json: { success: true },
pairedItem: { item: 0 },
},
]);
});
it('should throw error when upload session fails', async () => {
microsoftApiRequestSpy.mockResolvedValueOnce({
'@odata.context':
'https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession',
});
await expect(execute.call(mockExecuteFunctions, 0, [{ json: {} }])).rejects.toThrow(
'Failed to get upload session',
);
});
});
});
@@ -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 <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 <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);
});
});
@@ -0,0 +1,84 @@
import { NodeApiError } from 'n8n-workflow';
import type { JsonObject, IDataObject, INodeExecutionData, IPollFunctions } from 'n8n-workflow';
import { prepareFilterString, simplifyOutputMessages } from '../v2/helpers/utils';
import {
downloadAttachments,
microsoftApiRequest,
microsoftApiRequestAllItems,
} from '../v2/transport';
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,
});
}
}
@@ -0,0 +1,140 @@
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. <a href="https://learn.microsoft.com/en-us/graph/filter-query-parameter">More info</a>.',
},
{
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 <a href="https://docs.n8n.io/code/expressions/">expression</a>',
},
{
displayName: 'Folders to Include',
name: 'foldersToInclude',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getFolders',
},
default: [],
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
},
{
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",
},
],
},
];
@@ -0,0 +1,281 @@
import type { INodeProperties } from 'n8n-workflow';
export const draftOperations: 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 a draft',
action: 'Delete a draft',
},
{
name: 'Get',
value: 'get',
description: 'Get a single draft',
action: 'Get a draft',
},
{
name: 'Send',
value: 'send',
description: 'Send an existing draft message',
action: 'Send a draft',
},
{
name: 'Update',
value: 'update',
description: 'Update a draft',
action: 'Update a draft',
},
],
default: 'create',
},
];
export const draftFields: INodeProperties[] = [
{
displayName: 'Message ID',
name: 'messageId',
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: 'Category Names or IDs',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
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: '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: '',
},
],
},
];
@@ -0,0 +1,206 @@
import type { INodeProperties } from 'n8n-workflow';
export const draftMessageSharedFields: INodeProperties[] = [
// 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 <a href="https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter">here</a>.',
},
],
},
// 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: 'Category Names or IDs',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
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: 'Is Read',
name: 'isRead',
description: 'Whether the message has been read',
type: 'boolean',
default: false,
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: '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: '',
},
],
},
];
@@ -0,0 +1,258 @@
import type { INodeProperties } from 'n8n-workflow';
export const folderOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['folder'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: "Create a new 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: 'Get a single folder details',
action: 'Get a folder',
},
{
name: 'Get Children',
value: 'getChildren',
description: 'Lists all child folders under the folder',
action: 'Get items in a folder',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many folders under the root folder of the signed-in user',
action: 'Get many folders',
},
],
default: 'create',
},
];
export const folderFields: INodeProperties[] = [
{
displayName: 'Folder ID',
name: 'folderId',
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: 'Whether to return all results 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: 'Max number of 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: 'Whether to 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. Information about the syntax can be found <a href="https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter">here</a>.',
},
],
},
// 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: 'Whether to 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: [],
},
],
},
];
@@ -0,0 +1,101 @@
import type { INodeProperties } from 'n8n-workflow';
export const folderMessageOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['folderMessage'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Get many messages in a folder',
action: 'Get many folder messages',
},
],
default: 'create',
},
];
export const folderMessageFields: INodeProperties[] = [
{
displayName: 'Folder ID',
name: 'folderId',
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: 'Whether to return all results 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: 'Max number of 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. Information about the syntax can be found <a href="https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter">here</a>.',
},
],
},
];
@@ -0,0 +1,224 @@
import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
INodeExecutionData,
IRequestOptions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export async function microsoftApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
option: IDataObject = { json: true },
): Promise<any> {
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: IRequestOptions = {
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 as IDataObject).length === 0) {
delete options.body;
}
return await this.helpers.requestOAuth2.call(this, 'microsoftOutlookOAuth2Api', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function microsoftApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
headers: IDataObject = {},
): Promise<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] as IDataObject[]);
} while (responseData['@odata.nextLink'] !== undefined);
return returnData;
}
export async function microsoftApiRequestAllItemsSkip(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
headers: IDataObject = {},
): Promise<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] as IDataObject[]);
} 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 as string,
attachment.contentType as string,
);
}
}
if (Object.keys(element.binary!).length === 0) {
delete element.binary;
}
elements.push(element);
}
return elements;
}
@@ -0,0 +1,178 @@
import type { INodeProperties } from 'n8n-workflow';
export const messageAttachmentOperations: 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 a message attachment',
},
{
name: 'Download',
value: 'download',
description: 'Download attachment content',
action: 'Download a message attachment',
},
{
name: 'Get',
value: 'get',
description: 'Get an attachment from a message',
action: 'Get a message attachment',
},
{
name: 'Get Many',
value: 'getAll',
description: "Get many message's attachments",
action: 'Get many message attachments',
},
],
default: 'add',
},
];
export const messageAttachmentFields: INodeProperties[] = [
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['messageAttachment'],
operation: ['add', 'download', 'get', 'getAll'],
},
},
},
{
displayName: 'Attachment ID',
name: 'attachmentId',
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: 'Whether to return all results 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: 'Max number of results to return',
},
// messageAttachment:create, messageAttachment:update, messageAttachment:send
// File operations
{
displayName: 'Put Output File in Field',
name: 'binaryPropertyName',
hint: 'The name of the output binary field to put the file in',
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',
},
],
},
];
@@ -0,0 +1,591 @@
import type { INodeProperties } from 'n8n-workflow';
export const messageOperations: 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: 'Get a single message',
action: 'Get a message',
},
{
name: 'Get Many',
value: 'getAll',
description: "Get many messages in the signed-in user's mailbox",
action: 'Get many messages',
},
{
name: 'Get MIME Content',
value: 'getMime',
description: 'Get MIME content of a message',
action: 'Get MIME Content of a message',
},
{
name: 'Move',
value: 'move',
description: 'Move a message',
action: 'Move a message',
},
{
name: 'Reply',
value: 'reply',
description: 'Create 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',
},
];
export const messageFields: INodeProperties[] = [
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['message'],
operation: [
'addAttachment',
'delete',
'get',
'getAttachment',
'getMime',
'move',
'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:
'Whether to 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: '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: 'Whether to return all results 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: 'Max number of 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: 'Category Names or IDs',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
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: '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: 'Whether to save the message in Sent Items',
type: 'boolean',
default: true,
},
],
},
// File operations
{
displayName: 'Put Output File in Field',
name: 'binaryPropertyName',
hint: 'The name of the output binary field to put the file in',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
resource: ['message'],
operation: ['getMime'],
},
},
},
// message:move
{
displayName: 'Folder ID',
name: 'folderId',
description: 'Target Folder ID',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['move'],
},
},
},
];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
import type {
IExecuteFunctions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { description } from './actions/node.description';
import { router } from './actions/router';
import { loadOptions, listSearch } from './methods';
import { sendAndWaitWebhook } from '../../../../utils/sendAndWait/utils';
export class MicrosoftOutlookV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...description,
};
}
methods = { loadOptions, listSearch };
webhook = sendAndWaitWebhook;
async execute(this: IExecuteFunctions) {
return await router.call(this);
}
}
@@ -0,0 +1,118 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { microsoftApiRequest } from '../../transport';
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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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;
}
@@ -0,0 +1,32 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,40 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,80 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { returnAllOrLimit } from '../../descriptions';
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
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. <a href="https://learn.microsoft.com/en-us/graph/filter-query-parameter">More info</a>.',
},
],
},
];
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;
}
@@ -0,0 +1,62 @@
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: ['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,
];
@@ -0,0 +1,110 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,64 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { contactFields } from '../../descriptions';
import { prepareContactFields } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,31 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { contactRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,87 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { contactRLC } from '../../descriptions';
import { contactFields } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,139 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { returnAllOrLimit } from '../../descriptions';
import { contactFields } from '../../helpers/utils';
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
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. <a href="https://learn.microsoft.com/en-us/graph/filter-query-parameter">More info</a>.',
},
{
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;
}
@@ -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 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: ['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,
];
@@ -0,0 +1,51 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { contactFields, contactRLC } from '../../descriptions';
import { prepareContactFields } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
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;
}
@@ -0,0 +1,255 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { createMessage } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
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 <a href="https://docs.n8n.io/code/expressions/">expression</a>',
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, _: 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 = await Promise.all(
attachments.map(async (attachment) => {
const binaryPropertyName = attachment.binaryPropertyName as string;
const binaryData = this.helpers.assertBinaryData(index, binaryPropertyName);
let fileBase64;
if (binaryData.id) {
const chunkSize = 256 * 1024;
const stream = await this.helpers.getBinaryStream(binaryData.id, chunkSize);
const buffer = await this.helpers.binaryToBuffer(stream);
fileBase64 = buffer.toString('base64');
} else {
fileBase64 = binaryData.data;
}
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: binaryData.fileName,
contentBytes: fileBase64,
};
}),
);
}
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;
}

Some files were not shown because too many files have changed in this diff Show More