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,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;
}
@@ -0,0 +1,31 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { draftRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [draftRLC];
const displayOptions = {
show: {
resource: ['draft'],
operation: ['delete'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const draftId = this.getNodeParameter('draftId', index, undefined, {
extractValue: true,
}) as string;
await microsoftApiRequest.call(this, 'DELETE', `/messages/${draftId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,129 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { draftRLC } from '../../descriptions';
import { messageFields, simplifyOutputMessages } from '../../helpers/utils';
import { downloadAttachments, microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
draftRLC,
{
displayName: 'Output',
name: 'output',
type: 'options',
default: 'simple',
options: [
{
name: 'Simplified',
value: 'simple',
},
{
name: 'Raw',
value: 'raw',
},
{
name: 'Select Included Fields',
value: 'fields',
},
],
},
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
displayOptions: {
show: {
output: ['fields'],
},
},
options: messageFields,
default: [],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Attachments Prefix',
name: 'attachmentsPrefix',
type: 'string',
default: 'attachment_',
description:
'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".',
},
{
displayName: 'Download Attachments',
name: 'downloadAttachments',
type: 'boolean',
default: false,
description:
"Whether the message's attachments will be downloaded and included in the output",
},
],
},
];
const displayOptions = {
show: {
resource: ['draft'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs: IDataObject = {};
const draftId = this.getNodeParameter('draftId', index, undefined, {
extractValue: true,
}) as string;
const options = this.getNodeParameter('options', index, {});
const output = this.getNodeParameter('output', index) as string;
if (output === 'fields') {
const fields = this.getNodeParameter('fields', index) as string[];
if (options.downloadAttachments) {
fields.push('hasAttachments');
}
qs.$select = fields.join(',');
}
if (output === 'simple') {
qs.$select =
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments';
}
responseData = await microsoftApiRequest.call(this, 'GET', `/messages/${draftId}`, undefined, qs);
if (output === 'simple') {
responseData = simplifyOutputMessages([responseData as IDataObject]);
}
let executionData: INodeExecutionData[] = [];
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
}
return executionData;
}
@@ -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 send from './send.operation';
import * as update from './update.operation';
export { create, del as delete, get, send, update };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['draft'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new email draft',
action: 'Create a draft',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an email draft',
action: 'Delete a draft',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve an email draft',
action: 'Get a draft',
},
{
name: 'Send',
value: 'send',
description: 'Send an existing email draft',
action: 'Send a draft',
},
{
name: 'Update',
value: 'update',
description: 'Update an email draft',
action: 'Update a draft',
},
],
default: 'create',
},
...create.description,
...del.description,
...get.description,
...send.description,
...update.description,
];
@@ -0,0 +1,54 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { draftRLC } from '../../descriptions';
import { makeRecipient } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
draftRLC,
{
displayName: 'To',
name: 'to',
description: 'Comma-separated list of email addresses of recipients',
type: 'string',
default: '',
},
];
const displayOptions = {
show: {
resource: ['draft'],
operation: ['send'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const draftId = this.getNodeParameter('draftId', index, undefined, { extractValue: true });
const to = this.getNodeParameter('to', index) as string;
if (to) {
const recipients = to
.split(',')
.map((s) => s.trim())
.filter((email) => email);
if (recipients.length !== 0) {
await microsoftApiRequest.call(this, 'PATCH', `/messages/${draftId}`, {
toRecipients: recipients.map((recipient: string) => makeRecipient(recipient)),
});
}
}
await microsoftApiRequest.call(this, 'POST', `/messages/${draftId}/send`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,208 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { draftRLC } from '../../descriptions';
import { createMessage } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
draftRLC,
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Comma-separated list of email addresses of BCC recipients',
type: 'string',
placeholder: 'e.g. john@example.com',
default: '',
},
{
displayName: 'Category Names or IDs',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <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: 'Is Read',
name: 'isRead',
description: 'Whether the message must be marked as read',
type: 'boolean',
default: false,
},
{
displayName: 'Message',
name: 'bodyContent',
description: 'Message body content',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
},
{
displayName: 'Message Type',
name: 'bodyContentType',
description: 'Message body content type',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Whether a read receipt is requested for the message',
type: 'boolean',
default: false,
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email address to use when replying',
type: 'string',
placeholder: 'e.g. replyto@example.com',
default: '',
},
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message',
type: 'string',
default: '',
},
{
displayName: 'To',
name: 'toRecipients',
description: 'Comma-separated list of email addresses of recipients',
type: 'string',
placeholder: 'e.g. john@example.com',
default: '',
},
],
},
];
const displayOptions = {
show: {
resource: ['draft'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const draftId = this.getNodeParameter('draftId', index, undefined, {
extractValue: true,
}) as string;
const updateFields = this.getNodeParameter('updateFields', index);
// Create message from optional fields
const body: IDataObject = createMessage(updateFields);
const responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/messages/${draftId}`,
body,
{},
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,292 @@
import { DateTime } from 'luxon';
import moment from 'moment-timezone';
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
calendarRLC,
{
displayName: 'Title',
name: 'subject',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Start',
name: 'startDateTime',
type: 'dateTime',
default: DateTime.now().toISO(),
required: true,
},
{
displayName: 'End',
name: 'endDateTime',
type: 'dateTime',
required: true,
default: DateTime.now().plus({ minutes: 30 }).toISO(),
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Categories',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getCategoriesNames',
},
default: [],
},
{
displayName: 'Description',
name: 'body',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
},
{
displayName: 'Description Preview',
name: 'bodyPreview',
type: 'string',
default: '',
},
{
displayName: 'Hide Attendees',
name: 'hideAttendees',
type: 'boolean',
default: false,
description:
'Whether to allow each attendee to only see themselves in the meeting request and meeting tracking list',
},
{
displayName: 'Importance',
name: 'importance',
type: 'options',
options: [
{
name: 'Low',
value: 'low',
},
{
name: 'Normal',
value: 'normal',
},
{
name: 'High',
value: 'high',
},
],
default: 'normal',
},
{
displayName: 'Is All Day',
name: 'isAllDay',
type: 'boolean',
default: false,
},
{
displayName: 'Is Cancelled',
name: 'isCancelled',
type: 'boolean',
default: false,
},
{
displayName: 'Is Draft',
name: 'isDraft',
type: 'boolean',
default: false,
},
{
displayName: 'Is Online Meeting',
name: 'isOnlineMeeting',
type: 'boolean',
default: false,
},
{
displayName: 'Sensitivity',
name: 'sensitivity',
type: 'options',
default: 'normal',
options: [
{
name: 'Normal',
value: 'normal',
},
{
name: 'Personal',
value: 'personal',
},
{
name: 'Private',
value: 'private',
},
{
name: 'Confidential',
value: 'confidential',
},
],
},
{
displayName: 'Show As',
name: 'showAs',
type: 'options',
default: 'free',
options: [
{
name: 'Busy',
value: 'busy',
},
{
name: 'Free',
value: 'free',
},
{
name: 'Oof',
value: 'oof',
},
{
name: 'Tentative',
value: 'tentative',
},
{
name: 'Working Elsewhere',
value: 'workingElsewhere',
},
],
},
{
displayName: 'Timezone',
name: 'timeZone',
type: 'options',
default: 'UTC',
options: moment.tz.names().map((name) => ({
name,
value: name,
})),
},
{
displayName: 'Type',
name: 'type',
type: 'options',
default: 'singleInstance',
options: [
{
name: 'Single Instance',
value: 'singleInstance',
},
{
name: 'Occurrence',
value: 'occurrence',
},
{
name: 'Exception',
value: 'exception',
},
{
name: 'Series Master',
value: 'seriesMaster',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['event'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let additionalFields = this.getNodeParameter('additionalFields', index);
additionalFields = Object.keys(additionalFields).reduce((acc: IDataObject, key: string) => {
if (additionalFields[key] !== '' || additionalFields[key] !== undefined) {
acc[key] = additionalFields[key];
}
return acc;
}, {});
const calendarId = this.getNodeParameter('calendarId', index, '', {
extractValue: true,
}) as string;
if (calendarId === '') {
throw new NodeOperationError(this.getNode(), 'Calendar ID is required');
}
const subject = this.getNodeParameter('subject', index) as string;
const endpoint = `/calendars/${calendarId}/events`;
let timeZone = 'UTC';
if (additionalFields.timeZone) {
timeZone = additionalFields.timeZone as string;
delete additionalFields.timeZone;
}
if (additionalFields.body) {
additionalFields.body = {
content: additionalFields.body,
contentType: 'html',
};
}
let startDateTime = this.getNodeParameter('startDateTime', index) as string;
let endDateTime = this.getNodeParameter('endDateTime', index) as string;
if (additionalFields.isAllDay) {
startDateTime = DateTime.fromISO(startDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd');
endDateTime = DateTime.fromISO(endDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd');
const minimalWholeDayDuration = 24;
const duration = DateTime.fromISO(startDateTime, { zone: timeZone }).diff(
DateTime.fromISO(endDateTime, { zone: timeZone }),
).hours;
if (duration < minimalWholeDayDuration) {
endDateTime = DateTime.fromISO(startDateTime, { zone: timeZone }).plus({ hours: 24 }).toISO();
}
}
const body: IDataObject = {
subject,
start: {
dateTime: startDateTime,
timeZone,
},
end: {
dateTime: endDateTime,
timeZone,
},
...additionalFields,
};
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,35 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC, eventRLC } from '../../descriptions';
import { decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [calendarRLC, eventRLC];
const displayOptions = {
show: {
resource: ['event'],
operation: ['delete'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const eventId = decodeOutlookId(
this.getNodeParameter('eventId', index, undefined, {
extractValue: true,
}) as string,
);
await microsoftApiRequest.call(this, 'DELETE', `/calendar/events/${eventId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,86 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC, eventRLC } from '../../descriptions';
import { decodeOutlookId, eventfields } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
calendarRLC,
eventRLC,
{
displayName: 'Output',
name: 'output',
type: 'options',
default: 'simple',
options: [
{
name: 'Simplified',
value: 'simple',
},
{
name: 'Raw',
value: 'raw',
},
{
name: 'Select Included Fields',
value: 'fields',
},
],
},
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
displayOptions: {
show: {
output: ['fields'],
},
},
options: eventfields,
default: [],
},
];
const displayOptions = {
show: {
resource: ['event'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const qs = {} as IDataObject;
const eventId = decodeOutlookId(
this.getNodeParameter('eventId', index, undefined, {
extractValue: true,
}) as string,
);
const output = this.getNodeParameter('output', index) as string;
if (output === 'fields') {
const fields = this.getNodeParameter('fields', index) as string[];
qs.$select = fields.join(',');
}
if (output === 'simple') {
qs.$select = 'id,subject,bodyPreview,start,end,organizer,attendees,webLink';
}
const endpoint = `/calendar/events/${eventId}`;
const responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,164 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC, returnAllOrLimit } from '../../descriptions';
import { eventfields } from '../../helpers/utils';
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
export const properties: INodeProperties[] = [
{
displayName: 'From All Calendars',
name: 'fromAllCalendars',
type: 'boolean',
default: true,
},
{
...calendarRLC,
displayOptions: {
show: {
fromAllCalendars: [false],
},
},
},
...returnAllOrLimit,
{
displayName: 'Output',
name: 'output',
type: 'options',
default: 'simple',
options: [
{
name: 'Simplified',
value: 'simple',
},
{
name: 'Raw',
value: 'raw',
},
{
name: 'Select Included Fields',
value: 'fields',
},
],
},
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
displayOptions: {
show: {
output: ['fields'],
},
},
options: eventfields,
default: [],
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
options: [
{
displayName: 'Filter Query',
name: 'custom',
type: 'string',
default: '',
placeholder: "e.g. contains(subject,'Hello')",
hint: 'Search query to filter events. <a href="https://learn.microsoft.com/en-us/graph/filter-query-parameter">More info</a>.',
},
],
},
];
const displayOptions = {
show: {
resource: ['event'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const responseData: IDataObject[] = [];
const qs = {} as IDataObject;
const returnAll = this.getNodeParameter('returnAll', index);
const filters = this.getNodeParameter('filters', index, {});
const output = this.getNodeParameter('output', index) as string;
if (output === 'fields') {
const fields = this.getNodeParameter('fields', index) as string[];
qs.$select = fields.join(',');
}
if (output === 'simple') {
qs.$select = 'id,subject,bodyPreview,start,end,organizer,attendees,webLink';
}
if (Object.keys(filters).length) {
const filterString: string[] = [];
if (filters.custom) {
filterString.push(filters.custom as string);
}
if (filterString.length) {
qs.$filter = filterString.join(' and ');
}
}
const calendars: string[] = [];
const fromAllCalendars = this.getNodeParameter('fromAllCalendars', index) as boolean;
if (fromAllCalendars) {
const response = await microsoftApiRequest.call(this, 'GET', '/calendars', undefined, {
$select: 'id',
});
for (const calendar of response.value) {
calendars.push(calendar.id as string);
}
} else {
const calendarId = this.getNodeParameter('calendarId', index, undefined, {
extractValue: true,
}) as string;
calendars.push(calendarId);
}
const limit = this.getNodeParameter('limit', index, 0);
for (const calendarId of calendars) {
const endpoint = `/calendars/${calendarId}/events`;
if (returnAll) {
const response = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
responseData.push(...response);
} else {
qs.$top = limit - responseData.length;
if (qs.$top <= 0) break;
const response = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData.push(...response.value);
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData: { item: index } },
);
return executionData;
}
@@ -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: ['event'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new event',
action: 'Create an event',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an event',
action: 'Delete an event',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve an event',
action: 'Get an event',
},
{
name: 'Get Many',
value: 'getAll',
description: 'List and search events',
action: 'Get many events',
},
{
name: 'Update',
value: 'update',
description: 'Update an event',
action: 'Update an event',
},
],
default: 'getAll',
},
...create.description,
...del.description,
...get.description,
...getAll.description,
...update.description,
];
@@ -0,0 +1,284 @@
import { DateTime } from 'luxon';
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { calendarRLC, eventRLC } from '../../descriptions';
import { decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
calendarRLC,
eventRLC,
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Categories',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getCategoriesNames',
},
default: [],
},
{
displayName: 'Description',
name: 'body',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
},
{
displayName: 'Description Preview',
name: 'bodyPreview',
type: 'string',
default: '',
},
{
displayName: 'End',
name: 'end',
type: 'dateTime',
default: '',
},
{
displayName: 'Hide Attendees',
name: 'hideAttendees',
type: 'boolean',
default: false,
description:
'Whether to allow each attendee to only see themselves in the meeting request and meeting tracking list',
},
{
displayName: 'Importance',
name: 'importance',
type: 'options',
default: 'low',
options: [
{
name: 'Low',
value: 'low',
},
{
name: 'Normal',
value: 'normal',
},
{
name: 'High',
value: 'high',
},
],
},
{
displayName: 'Is All Day',
name: 'isAllDay',
type: 'boolean',
default: false,
},
{
displayName: 'Is Cancelled',
name: 'isCancelled',
type: 'boolean',
default: false,
},
{
displayName: 'Is Draft',
name: 'isDraft',
type: 'boolean',
default: false,
},
{
displayName: 'Is Online Meeting',
name: 'isOnlineMeeting',
type: 'boolean',
default: true,
},
{
displayName: 'Sensitivity',
name: 'sensitivity',
type: 'options',
default: 'normal',
options: [
{
name: 'Normal',
value: 'normal',
},
{
name: 'Personal',
value: 'personal',
},
{
name: 'Private',
value: 'private',
},
{
name: 'Confidential',
value: 'confidential',
},
],
},
{
displayName: 'Show As',
name: 'showAs',
type: 'options',
default: 'free',
options: [
{
name: 'Busy',
value: 'busy',
},
{
name: 'Free',
value: 'free',
},
{
name: 'Oof',
value: 'oof',
},
{
name: 'Tentative',
value: 'tentative',
},
{
name: 'Working Elsewhere',
value: 'workingElsewhere',
},
],
},
{
displayName: 'Start',
name: 'start',
type: 'dateTime',
default: '',
},
{
displayName: 'Timezone',
name: 'timeZone',
type: 'string',
default: '',
},
{
displayName: 'Title',
name: 'subject',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
default: 'singleInstance',
options: [
{
name: 'Single Instance',
value: 'singleInstance',
},
{
name: 'Occurrence',
value: 'occurrence',
},
{
name: 'Exception',
value: 'exception',
},
{
name: 'Series Master',
value: 'seriesMaster',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['event'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const additionalFields = this.getNodeParameter('additionalFields', index);
const eventId = decodeOutlookId(
this.getNodeParameter('eventId', index, undefined, {
extractValue: true,
}) as string,
);
let timeZone = 'UTC';
if (additionalFields.timeZone) {
timeZone = additionalFields.timeZone as string;
delete additionalFields.timeZone;
}
if (additionalFields.body) {
additionalFields.body = {
content: additionalFields.body,
contentType: 'html',
};
}
let startDateTime = additionalFields.start as string;
let endDateTime = additionalFields.end as string;
if (additionalFields.isAllDay) {
startDateTime =
DateTime.fromISO(startDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd') ||
DateTime.utc().toFormat('yyyy-MM-dd');
endDateTime =
DateTime.fromISO(endDateTime, { zone: timeZone }).toFormat('yyyy-MM-dd') ||
DateTime.utc().toFormat('yyyy-MM-dd');
const minimalWholeDayDuration = 24;
const duration = DateTime.fromISO(startDateTime, { zone: timeZone }).diff(
DateTime.fromISO(endDateTime, { zone: timeZone }),
).hours;
if (duration < minimalWholeDayDuration) {
endDateTime = DateTime.fromISO(startDateTime, { zone: timeZone }).plus({ hours: 24 }).toISO();
}
}
const body: IDataObject = {
...additionalFields,
};
if (startDateTime) {
body.start = {
dateTime: startDateTime,
timeZone,
};
}
if (endDateTime) {
body.end = {
dateTime: endDateTime,
timeZone,
};
}
const endpoint = `/calendar/events/${eventId}`;
const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,67 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderRLC } from '../../descriptions';
import { decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
{
displayName: 'Name',
name: 'displayName',
description: 'Name of the folder',
type: 'string',
required: true,
default: '',
placeholder: 'e.g. My Folder',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [{ ...folderRLC, displayName: 'Parent Folder', required: false }],
},
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const displayName = this.getNodeParameter('displayName', index) as string;
const folderId = decodeOutlookId(
this.getNodeParameter('options.folderId', index, '', {
extractValue: true,
}) as string,
);
const body: IDataObject = {
displayName,
};
let endpoint;
if (folderId) {
endpoint = `/mailFolders/${folderId}/childFolders`;
} else {
endpoint = '/mailFolders';
}
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,35 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderRLC } from '../../descriptions';
import { decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [folderRLC];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['delete'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const folderId = decodeOutlookId(
this.getNodeParameter('folderId', index, undefined, {
extractValue: true,
}) as string,
);
await microsoftApiRequest.call(this, 'DELETE', `/mailFolders/${folderId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,71 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderFields, folderRLC } from '../../descriptions';
import { decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
folderRLC,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
options: folderFields,
default: [],
},
],
},
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const qs: IDataObject = {};
const folderId = decodeOutlookId(
this.getNodeParameter('folderId', index, undefined, {
extractValue: true,
}) as string,
);
const options = this.getNodeParameter('options', index);
if (options.fields) {
qs.$select = (options.fields as string[]).join(',');
}
if (options.filter) {
qs.$filter = options.filter;
}
const responseData = await microsoftApiRequest.call(
this,
'GET',
`/mailFolders/${folderId}`,
{},
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,113 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderFields, folderRLC, returnAllOrLimit } from '../../descriptions';
import { getSubfolders, microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
export const properties: INodeProperties[] = [
...returnAllOrLimit,
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
options: [
{
displayName: 'Filter Query',
name: 'filter',
type: 'string',
default: '',
placeholder: "e.g. displayName eq 'My Folder'",
hint: 'Search query to filter folders. <a href="https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter">More info</a>.',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
options: folderFields,
default: [],
},
{
displayName: 'Include Child Folders',
name: 'includeChildFolders',
type: 'boolean',
default: false,
description: 'Whether to include child folders in the response',
},
{
...folderRLC,
displayName: 'Parent Folder',
required: false,
description: 'The folder you want to search in',
},
],
},
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs: IDataObject = {};
const returnAll = this.getNodeParameter('returnAll', index);
const options = this.getNodeParameter('options', index);
const filter = this.getNodeParameter('filters.filter', index, '') as string;
const parentFolderId = this.getNodeParameter('options.folderId', index, '', {
extractValue: true,
}) as string;
if (options.fields) {
qs.$select = (options.fields as string[]).join(',');
}
if (filter) {
qs.$filter = filter;
}
let endpoint;
if (parentFolderId) {
endpoint = `/mailFolders/${parentFolderId}/childFolders`;
} else {
endpoint = '/mailFolders';
}
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {}, qs);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.value;
}
if (options.includeChildFolders) {
responseData = await getSubfolders.call(this, responseData as IDataObject[]);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: index } },
);
return executionData;
}
@@ -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: ['folder'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: "Create a mail folder in the root folder of the user's mailbox",
action: 'Create a folder',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a folder',
action: 'Delete a folder',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a folder',
action: 'Get a folder',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many folders',
action: 'Get many folders',
},
{
name: 'Update',
value: 'update',
description: 'Update a folder',
action: 'Update a folder',
},
],
default: 'create',
},
...create.description,
...del.description,
...get.description,
...getAll.description,
...update.description,
];
@@ -0,0 +1,48 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderRLC } from '../../descriptions';
import { decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
folderRLC,
{
displayName: 'Name',
name: 'displayName',
description: 'Name of the folder',
type: 'string',
default: '',
required: true,
},
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const folderId = decodeOutlookId(
this.getNodeParameter('folderId', index, undefined, {
extractValue: true,
}) as string,
);
const displayName = this.getNodeParameter('displayName', index, undefined) as string;
const responseData = await microsoftApiRequest.call(this, 'PATCH', `/mailFolders/${folderId}`, {
displayName,
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,303 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderRLC, returnAllOrLimit } from '../../descriptions';
import {
decodeOutlookId,
messageFields,
prepareFilterString,
simplifyOutputMessages,
} from '../../helpers/utils';
import {
downloadAttachments,
microsoftApiRequest,
microsoftApiRequestAllItems,
} from '../../transport';
export const properties: INodeProperties[] = [
folderRLC,
...returnAllOrLimit,
{
displayName: 'Output',
name: 'output',
type: 'options',
default: 'simple',
options: [
{
name: 'Simplified',
value: 'simple',
},
{
name: 'Raw',
value: 'raw',
},
{
name: 'Select Included Fields',
value: 'fields',
},
],
},
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
displayOptions: {
show: {
output: ['fields'],
},
},
options: messageFields,
default: [],
},
{
displayName:
'Fetching a lot of messages may take a long time. Consider using filters to speed things up',
name: 'filtersNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
returnAll: [true],
},
},
},
{
displayName: 'Filters',
name: 'filtersUI',
type: 'fixedCollection',
placeholder: 'Add Filters',
default: {},
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Filter By',
name: 'filterBy',
type: 'options',
options: [
{
name: 'Filters',
value: 'filters',
},
{
name: 'Search',
value: 'search',
},
],
default: 'filters',
},
{
displayName: 'Search',
name: 'search',
type: 'string',
default: '',
placeholder: 'e.g. automation',
description:
'Only return messages that contains search term. Without specific message properties, the search is carried out on the default search properties of from, subject, and body. <a href="https://docs.microsoft.com/en-us/graph/query-parameters#search-parameter target="_blank">More info</a>.',
displayOptions: {
show: {
filterBy: ['search'],
},
},
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
filterBy: ['filters'],
},
},
options: [
{
displayName: 'Filter Query',
name: 'custom',
type: 'string',
default: '',
placeholder: 'e.g. isRead eq false',
hint: 'Search query to filter messages. <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: 'Read Status',
name: 'readStatus',
type: 'options',
default: 'unread',
hint: 'Filter messages by whether they have been read or not',
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread and read messages',
value: 'both',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread messages only',
value: 'unread',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Read messages only',
value: 'read',
},
],
},
{
displayName: 'Received After',
name: 'receivedAfter',
type: 'dateTime',
default: '',
description:
'Get all messages received after the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.',
},
{
displayName: 'Received Before',
name: 'receivedBefore',
type: 'dateTime',
default: '',
description:
'Get all messages received before the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.',
},
{
displayName: 'Sender',
name: 'sender',
type: 'string',
default: '',
description: 'Sender name or email to filter by',
},
],
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Attachments Prefix',
name: 'attachmentsPrefix',
type: 'string',
default: 'attachment_',
description:
'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".',
},
{
displayName: 'Download Attachments',
name: 'downloadAttachments',
type: 'boolean',
default: false,
description:
"Whether the message's attachments will be downloaded and included in the output",
},
],
},
];
const displayOptions = {
show: {
resource: ['folderMessage'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs: IDataObject = {};
const folderId = decodeOutlookId(
this.getNodeParameter('folderId', index, undefined, {
extractValue: true,
}) as string,
);
const returnAll = this.getNodeParameter('returnAll', index);
const filters = this.getNodeParameter('filtersUI.values', index, {}) as IDataObject;
const options = this.getNodeParameter('options', index, {});
const output = this.getNodeParameter('output', index) as string;
if (output === 'fields') {
const fields = this.getNodeParameter('fields', index) as string[];
if (options.downloadAttachments) {
fields.push('hasAttachments');
}
qs.$select = fields.join(',');
}
if (output === 'simple') {
qs.$select =
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments';
}
if (filters.filterBy === 'search' && filters.search !== '') {
qs.$search = `"${filters.search}"`;
}
if (filters.filterBy === 'filters') {
const filterString = prepareFilterString(filters);
if (filterString) {
qs.$filter = filterString;
}
}
const endpoint = `/mailFolders/${folderId}/messages`;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = responseData.value;
}
if (output === 'simple') {
responseData = simplifyOutputMessages(responseData as IDataObject[]);
}
let executionData: INodeExecutionData[] = [];
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: index } },
);
}
return executionData;
}
@@ -0,0 +1,30 @@
import type { INodeProperties } from 'n8n-workflow';
import * as getAll from './getAll.operation';
export { getAll };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['folderMessage'],
},
},
options: [
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieves the messages in a folder',
action: 'Get many folder messages',
},
],
default: 'getAll',
},
...getAll.description,
];
@@ -0,0 +1,31 @@
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [messageRLC];
const displayOptions = {
show: {
resource: ['message'],
operation: ['delete'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
await microsoftApiRequest.call(this, 'DELETE', `/messages/${messageId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,183 @@
import type {
IBinaryKeyData,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC } from '../../descriptions';
import { messageFields, simplifyOutputMessages } from '../../helpers/utils';
import { downloadAttachments, getMimeContent, microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
{
displayName: 'Output',
name: 'output',
type: 'options',
default: 'simple',
options: [
{
name: 'Simplified',
value: 'simple',
},
{
name: 'Raw',
value: 'raw',
},
{
name: 'Select Included Fields',
value: 'fields',
},
],
},
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
displayOptions: {
show: {
output: ['fields'],
},
},
options: messageFields,
default: [],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Attachments Prefix',
name: 'attachmentsPrefix',
type: 'string',
default: 'attachment_',
description:
'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".',
},
{
displayName: 'Download Attachments',
name: 'downloadAttachments',
type: 'boolean',
default: false,
description:
"Whether the message's attachments will be downloaded and included in the output",
},
{
displayName: 'Get MIME Content',
name: 'getMimeContent',
type: 'fixedCollection',
default: { values: { binaryPropertyName: 'data' } },
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Put Output in Field',
name: 'binaryPropertyName',
type: 'string',
default: '',
hint: 'The name of the output field to put the binary file data in',
},
{
displayName: 'File Name',
name: 'outputFileName',
type: 'string',
placeholder: 'message',
default: '',
description: 'Optional name of the output file, if not set message ID is used',
},
],
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs: IDataObject = {};
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const options = this.getNodeParameter('options', index, {});
const output = this.getNodeParameter('output', index) as string;
if (output === 'fields') {
const fields = this.getNodeParameter('fields', index) as string[];
if (options.downloadAttachments) {
fields.push('hasAttachments');
}
qs.$select = fields.join(',');
}
if (output === 'simple') {
qs.$select =
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments';
}
responseData = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}`,
undefined,
qs,
);
if (output === 'simple') {
responseData = simplifyOutputMessages([responseData as IDataObject]);
}
let executionData: INodeExecutionData[] = [];
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
}
if (options.getMimeContent) {
const { binaryPropertyName, outputFileName } = (options.getMimeContent as IDataObject)
.values as IDataObject;
const binary = await getMimeContent.call(
this,
messageId,
binaryPropertyName as string,
outputFileName as string,
);
executionData[0].binary = {
...(executionData[0].binary || {}),
...(binary as IBinaryKeyData),
};
}
return executionData;
}
@@ -0,0 +1,313 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { returnAllOrLimit } from '../../descriptions';
import { messageFields, prepareFilterString, simplifyOutputMessages } from '../../helpers/utils';
import {
downloadAttachments,
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: messageFields,
default: [],
},
{
displayName:
'Fetching a lot of messages may take a long time. Consider using filters to speed things up',
name: 'filtersNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
returnAll: [true],
},
},
},
{
displayName: 'Filters',
name: 'filtersUI',
type: 'fixedCollection',
placeholder: 'Add Filters',
default: {},
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Filter By',
name: 'filterBy',
type: 'options',
options: [
{
name: 'Filters',
value: 'filters',
},
{
name: 'Search',
value: 'search',
},
],
default: 'filters',
},
{
displayName: 'Search',
name: 'search',
type: 'string',
default: '',
placeholder: 'e.g. automation',
description:
'Only return messages that contains search term. Without specific message properties, the search is carried out on the default search properties of from, subject, and body. <a href="https://docs.microsoft.com/en-us/graph/query-parameters#search-parameter target="_blank">More info</a>.',
displayOptions: {
show: {
filterBy: ['search'],
},
},
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
filterBy: ['filters'],
},
},
options: [
{
displayName: 'Filter Query',
name: 'custom',
type: 'string',
default: '',
placeholder: 'e.g. isRead eq false',
hint: 'Search query to filter messages. <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: 'Received After',
name: 'receivedAfter',
type: 'dateTime',
default: '',
description:
'Get all messages received after the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.',
},
{
displayName: 'Received Before',
name: 'receivedBefore',
type: 'dateTime',
default: '',
description:
'Get all messages received before the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.',
},
{
displayName: 'Sender',
name: 'sender',
type: 'string',
default: '',
description: 'Sender name or email to filter by',
},
],
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Attachments Prefix',
name: 'attachmentsPrefix',
type: 'string',
default: 'attachment_',
description:
'Prefix for name of the output fields to put the binary files data in. An index starting from 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".',
},
{
displayName: 'Download Attachments',
name: 'downloadAttachments',
type: 'boolean',
default: false,
description:
"Whether the message's attachments will be downloaded and included in the output",
},
],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs = {} as IDataObject;
const returnAll = this.getNodeParameter('returnAll', index);
const filters = this.getNodeParameter('filtersUI.values', index, {}) as IDataObject;
const options = this.getNodeParameter('options', index, {});
const output = this.getNodeParameter('output', index) as string;
if (output === 'fields') {
const fields = this.getNodeParameter('fields', index) as string[];
if (options.downloadAttachments) {
fields.push('hasAttachments');
}
qs.$select = fields.join(',');
}
if (output === 'simple') {
qs.$select =
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments';
}
if (filters.filterBy === 'search' && filters.search !== '') {
qs.$search = `"${filters.search}"`;
}
if (filters.filterBy === 'filters') {
const filterString = prepareFilterString(filters);
if (filterString) {
qs.$filter = filterString;
}
}
const endpoint = '/messages';
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = responseData.value;
}
if (output === 'simple') {
responseData = simplifyOutputMessages(responseData as IDataObject[]);
}
let executionData: INodeExecutionData[] = [];
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: index } },
);
}
return executionData;
}
@@ -0,0 +1,86 @@
import { SEND_AND_WAIT_OPERATION, type INodeProperties } from 'n8n-workflow';
import * as del from './delete.operation';
import * as get from './get.operation';
import * as getAll from './getAll.operation';
import * as move from './move.operation';
import * as reply from './reply.operation';
import * as send from './send.operation';
import * as sendAndWait from './sendAndWait.operation';
import * as update from './update.operation';
export { del as delete, get, getAll, move, reply, send, sendAndWait, update };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['message'],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a message',
action: 'Delete a message',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a single message',
action: 'Get a message',
},
{
name: 'Get Many',
value: 'getAll',
description: 'List and search messages',
action: 'Get many messages',
},
{
name: 'Move',
value: 'move',
description: 'Move a message to a folder',
action: 'Move a message',
},
{
name: 'Reply',
value: 'reply',
description: 'Create a reply to a message',
action: 'Reply to a message',
},
{
name: 'Send',
value: 'send',
description: 'Send a message',
action: 'Send a message',
},
{
name: 'Send and Wait for Response',
value: SEND_AND_WAIT_OPERATION,
description: 'Send a message and wait for response',
action: 'Send message and wait for response',
},
{
name: 'Update',
value: 'update',
description: 'Update a message',
action: 'Update a message',
},
],
default: 'send',
},
...del.description,
...get.description,
...getAll.description,
...move.description,
...reply.description,
...send.description,
...sendAndWait.description,
...update.description,
];
@@ -0,0 +1,43 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderRLC, messageRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
{ ...folderRLC, displayName: 'Parent Folder' },
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['move'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const destinationId = this.getNodeParameter('folderId', index, undefined, {
extractValue: true,
}) as string;
const body: IDataObject = {
destinationId,
};
await microsoftApiRequest.call(this, 'POST', `/messages/${messageId}/move`, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,302 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC } from '../../descriptions';
import { createMessage } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
{
displayName: 'Reply to Sender Only',
name: 'replyToSenderOnly',
type: 'boolean',
default: false,
description: 'Whether to reply to the sender only or to the entire list of recipients',
},
{
displayName: 'Message',
name: 'message',
// name: 'bodyContent',
description: 'Message body content',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
replyToSenderOnly: [true],
},
},
options: [
{
displayName: 'Attachments',
name: 'attachments',
type: 'fixedCollection',
placeholder: 'Add Attachment',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachments',
displayName: 'Attachment',
values: [
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: '',
placeholder: 'e.g. data',
hint: 'The name of the input field containing the binary file data to be attached',
},
],
},
],
},
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Comma-separated list of email addresses of BCC recipients',
type: 'string',
default: '',
},
{
displayName: 'CC Recipients',
name: 'ccRecipients',
description: 'Comma-separated list of email addresses of CC recipients',
type: 'string',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header',
},
],
},
],
},
{
displayName: 'From',
name: 'from',
description:
'The owner of the mailbox from which the message is sent. Must correspond to the actual mailbox used.',
type: 'string',
default: '',
},
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Normal',
},
{
displayName: 'Message Type',
name: 'bodyContentType',
description: 'Message body content type',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Whether a read receipt is requested for the message',
type: 'boolean',
default: false,
},
{
displayName: 'To',
name: 'toRecipients',
description: 'Comma-separated list of email addresses of recipients',
type: 'string',
default: '',
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email address to use when replying',
type: 'string',
default: '',
},
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message',
type: 'string',
default: '',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Save as Draft',
name: 'saveAsDraft',
description:
'Whether to save the message as a draft. If false, the message is sent immediately.',
type: 'boolean',
default: false,
},
],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['reply'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number, _: INodeExecutionData[]) {
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const replyToSenderOnly = this.getNodeParameter('replyToSenderOnly', index, false) as string;
const message = this.getNodeParameter('message', index) as string;
const saveAsDraft = this.getNodeParameter('options.saveAsDraft', index, false) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', index, {});
const body: IDataObject = {};
let action = 'createReply';
if (!replyToSenderOnly) {
body.comment = message;
action = 'createReplyAll';
} else {
// body.comment = comment;
body.message = {} as IDataObject;
additionalFields.bodyContent = message;
Object.assign(body.message, createMessage(additionalFields));
delete (body.message as IDataObject).attachments;
}
const responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/${action}`,
body,
);
if (additionalFields.attachments) {
const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[];
// // Handle attachments
const data: IDataObject[] = [];
for (const attachment of attachments) {
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;
}
data.push({
'@odata.type': '#microsoft.graph.fileAttachment',
name: binaryData.fileName,
contentBytes: fileBase64,
});
}
for (const attachment of data) {
await microsoftApiRequest.call(
this,
'POST',
`/messages/${responseData.id}/attachments`,
attachment,
{},
);
}
}
if (!saveAsDraft) {
await microsoftApiRequest.call(this, 'POST', `/messages/${responseData.id}/send`);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,269 @@
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: 'To',
name: 'toRecipients',
description: 'Comma-separated list of email addresses of recipients',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message',
type: 'string',
default: '',
},
{
displayName: 'Message',
name: 'bodyContent',
description: 'Message body content',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Attachments',
name: 'attachments',
type: 'fixedCollection',
placeholder: 'Add Attachment',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachments',
displayName: 'Attachment',
values: [
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: '',
placeholder: 'e.g. data',
hint: 'The name of the input field containing the binary file data to be attached',
},
],
},
],
},
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Comma-separated list of email addresses of BCC recipients',
type: 'string',
default: '',
},
{
displayName: 'Category Names or IDs',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <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',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header',
},
],
},
],
},
{
displayName: 'From',
name: 'from',
description:
'The owner of the mailbox from which the message is sent. Must correspond to the actual mailbox used.',
type: 'string',
default: '',
},
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Normal',
},
{
displayName: 'Message Type',
name: 'bodyContentType',
description: 'Message body content type',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Whether a read receipt is requested for the message',
type: 'boolean',
default: false,
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email address to use when replying',
type: 'string',
default: '',
},
{
displayName: 'Save To Sent Items',
name: 'saveToSentItems',
description: 'Whether to save the message in Sent Items',
type: 'boolean',
default: true,
},
],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['send'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number, _: INodeExecutionData[]) {
const additionalFields = this.getNodeParameter('additionalFields', index);
const toRecipients = this.getNodeParameter('toRecipients', index) as string;
const subject = this.getNodeParameter('subject', index) as string;
const bodyContent = this.getNodeParameter('bodyContent', index, '') as string;
additionalFields.subject = subject;
additionalFields.bodyContent = bodyContent || ' ';
additionalFields.toRecipients = toRecipients;
const saveToSentItems =
additionalFields.saveToSentItems === undefined ? true : additionalFields.saveToSentItems;
delete additionalFields.saveToSentItems;
// Create message object from optional fields
const message: IDataObject = createMessage(additionalFields);
if (additionalFields.attachments) {
const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[];
const messageAttachments: IDataObject[] = [];
for (const attachment of attachments) {
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;
}
messageAttachments.push({
'@odata.type': '#microsoft.graph.fileAttachment',
name: binaryData.fileName,
contentBytes: fileBase64,
});
}
message.attachments = messageAttachments;
}
const body: IDataObject = {
message,
saveToSentItems,
};
await microsoftApiRequest.call(this, 'POST', '/sendMail', body, {});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,62 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import {
createEmailBodyWithN8nAttribution,
createEmailBodyWithoutN8nAttribution,
} from '../../../../../../utils/sendAndWait/email-templates';
import {
getSendAndWaitConfig,
getSendAndWaitProperties,
createButton,
} from '../../../../../../utils/sendAndWait/utils';
import { createMessage } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const description: INodeProperties[] = getSendAndWaitProperties([
{
displayName: 'To',
name: 'toRecipients',
description: 'Comma-separated list of email addresses of recipients',
type: 'string',
required: true,
default: '',
},
]);
export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) {
const toRecipients = this.getNodeParameter('toRecipients', index) as string;
const config = getSendAndWaitConfig(this);
const buttons: string[] = [];
for (const option of config.options) {
buttons.push(createButton(option.url, option.label, option.style));
}
let bodyContent: string;
if (config.appendAttribution !== false) {
const instanceId = this.getInstanceId();
bodyContent = createEmailBodyWithN8nAttribution(config.message, buttons.join('\n'), instanceId);
} else {
bodyContent = createEmailBodyWithoutN8nAttribution(config.message, buttons.join('\n'));
}
const fields: IDataObject = {
subject: config.title,
bodyContent,
toRecipients,
bodyContentType: 'html',
};
const message: IDataObject = createMessage(fields);
const body: IDataObject = { message };
await microsoftApiRequest.call(this, 'POST', '/sendMail', body);
return items;
}
@@ -0,0 +1,226 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { folderRLC, messageRLC } from '../../descriptions';
import { createMessage, decodeOutlookId } from '../../helpers/utils';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Comma-separated list of email addresses of BCC recipients',
type: 'string',
default: '',
},
{
displayName: 'Category Names or IDs',
name: 'categories',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <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',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header',
},
],
},
],
},
{ ...folderRLC, required: false },
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Normal',
},
{
displayName: 'Is Read',
name: 'isRead',
description: 'Whether the message must be marked as read',
type: 'boolean',
default: false,
},
{
displayName: 'Message',
name: 'bodyContent',
description: 'Message body content',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
},
{
displayName: 'Message Type',
name: 'bodyContentType',
description: 'Message body content type',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Whether a read receipt is requested for the message',
type: 'boolean',
default: false,
},
{
displayName: 'To',
name: 'toRecipients',
description: 'Comma-separated list of email addresses of recipients',
type: 'string',
default: '',
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email address to use when replying',
type: 'string',
default: '',
},
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message',
type: 'string',
default: '',
},
],
},
];
const displayOptions = {
show: {
resource: ['message'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const updateFields = this.getNodeParameter('updateFields', index);
const folderId = decodeOutlookId(
this.getNodeParameter('updateFields.folderId', index, '', {
extractValue: true,
}) as string,
);
if (folderId) {
const body: IDataObject = {
destinationId: folderId,
};
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/move`,
body,
);
delete updateFields.folderId;
if (!Object.keys(updateFields).length) {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
}
const body: IDataObject = createMessage(updateFields);
if (!Object.keys(body).length) {
throw new NodeOperationError(this.getNode(), 'No fields to update got specified');
}
responseData = await microsoftApiRequest.call(this, 'PATCH', `/messages/${messageId}`, body, {});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,143 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
hint: 'The name of the input field containing the binary file data to be attached',
type: 'string',
required: true,
default: 'data',
placeholder: 'e.g. data',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'File Name',
name: 'fileName',
description:
'Filename of the attachment. If not set will the file-name of the binary property be used, if it exists.',
type: 'string',
default: '',
},
],
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['add'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number, _: INodeExecutionData[]) {
let responseData;
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0);
const options = this.getNodeParameter('options', index);
const binaryData = this.helpers.assertBinaryData(index, binaryPropertyName);
const dataBuffer = await this.helpers.getBinaryDataBuffer(index, binaryPropertyName);
const fileName = options.fileName === undefined ? binaryData.fileName : options.fileName;
if (!fileName) {
throw new NodeOperationError(
this.getNode(),
'File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!',
{ itemIndex: index },
);
}
// Check if the file is over 3MB big
if (dataBuffer.length > 3e6) {
// Maximum chunk size is 4MB
const chunkSize = 4e6;
const body: IDataObject = {
AttachmentItem: {
attachmentType: 'file',
name: fileName,
size: dataBuffer.length,
},
};
// Create upload session
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/attachments/createUploadSession`,
body,
);
const uploadUrl = responseData.uploadUrl;
if (uploadUrl === undefined) {
throw new NodeApiError(this.getNode(), responseData as JsonObject, {
message: 'Failed to get upload session',
});
}
for (let bytesUploaded = 0; bytesUploaded < dataBuffer.length; bytesUploaded += chunkSize) {
// Upload the file chunk by chunk
const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length);
const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`;
const data = dataBuffer.subarray(bytesUploaded, nextChunk);
responseData = await this.helpers.request(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': data.length,
'Content-Range': contentRange,
},
body: data,
});
}
} else {
const body: IDataObject = {
'@odata.type': '#microsoft.graph.fileAttachment',
name: fileName,
contentBytes: binaryData.data,
};
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/attachments`,
body,
{},
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,92 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { attachmentRLC, messageRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
attachmentRLC,
{
displayName: 'Put Output in Field',
name: 'binaryPropertyName',
hint: 'The name of the output field to put the binary file data in',
type: 'string',
required: true,
default: 'data',
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['download'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) {
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const attachmentId = this.getNodeParameter('attachmentId', index, undefined, {
extractValue: true,
}) as string;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', index);
// Get attachment details first
const attachmentDetails = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
undefined,
{ $select: 'id,name,contentType' },
);
let mimeType: string | undefined;
if (attachmentDetails.contentType) {
mimeType = attachmentDetails.contentType;
}
const fileName = attachmentDetails.name as string;
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
const newItem: INodeExecutionData = {
json: items[index].json,
binary: {},
};
if (items[index].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary!, items[index].binary);
}
const data = Buffer.from(response.body as string, 'utf8');
newItem.binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
data,
fileName,
mimeType,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(newItem),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,96 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { attachmentRLC, messageRLC } from '../../descriptions';
import { microsoftApiRequest } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
attachmentRLC,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
default: [],
options: [
{
name: 'contentType',
value: 'contentType',
},
{
name: 'isInline',
value: 'isInline',
},
{
name: 'lastModifiedDateTime',
value: 'lastModifiedDateTime',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'name',
value: 'name',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'size',
value: 'size',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const qs: IDataObject = {};
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const attachmentId = this.getNodeParameter('attachmentId', index, undefined, {
extractValue: true,
}) as string;
const options = this.getNodeParameter('options', index);
// Have sane defaults so we don't fetch attachment data in this operation
qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline';
if (options.fields && (options.fields as string[]).length) {
qs.$select = (options.fields as string[]).map((field) => field.trim()).join(',');
}
const responseData = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
undefined,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,102 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC, returnAllOrLimit } from '../../descriptions';
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
export const properties: INodeProperties[] = [
messageRLC,
...returnAllOrLimit,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
default: [],
options: [
{
name: 'contentType',
value: 'contentType',
},
{
name: 'isInline',
value: 'isInline',
},
{
name: 'lastModifiedDateTime',
value: 'lastModifiedDateTime',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'name',
value: 'name',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'size',
value: 'size',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs = {} as IDataObject;
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const returnAll = this.getNodeParameter('returnAll', index);
const options = this.getNodeParameter('options', index);
// Have sane defaults so we don't fetch attachment data in this operation
qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline';
if (options.fields && (options.fields as string[]).length) {
qs.$select = (options.fields as string[]).map((field) => field.trim()).join(',');
}
const endpoint = `/messages/${messageId}/attachments`;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = responseData.value;
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}
@@ -0,0 +1,53 @@
import type { INodeProperties } from 'n8n-workflow';
import * as add from './add.operation';
import * as download from './download.operation';
import * as get from './get.operation';
import * as getAll from './getAll.operation';
export { add, download, get, getAll };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['messageAttachment'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add an attachment to a message',
action: 'Add an attachment',
},
{
name: 'Download',
value: 'download',
description: 'Download an attachment from a message',
action: 'Download an attachment',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve information about an attachment of a message',
action: 'Get an attachment',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve information about the attachments of a message',
action: 'Get many attachments',
},
],
default: 'add',
},
...add.description,
...download.description,
...get.description,
...getAll.description,
];
@@ -0,0 +1,88 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import * as calendar from './calendar';
import * as contact from './contact';
import * as draft from './draft';
import * as event from './event';
import * as folder from './folder';
import * as folderMessage from './folderMessage';
import * as message from './message';
import * as messageAttachment from './messageAttachment';
import { sendAndWaitWebhooksDescription } from '../../../../../utils/sendAndWait/descriptions';
import { SEND_AND_WAIT_WAITING_TOOLTIP } from '../../../../../utils/sendAndWait/utils';
export const description: INodeTypeDescription = {
displayName: 'Microsoft Outlook',
name: 'microsoftOutlook',
group: ['transform'],
icon: 'file:outlook.svg',
version: 2,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Microsoft Outlook API',
defaults: {
name: 'Microsoft Outlook',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
credentials: [
{
name: 'microsoftOutlookOAuth2Api',
required: true,
},
],
waitingNodeTooltip: SEND_AND_WAIT_WAITING_TOOLTIP,
webhooks: sendAndWaitWebhooksDescription,
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
default: 'message',
options: [
{
name: 'Calendar',
value: 'calendar',
},
{
name: 'Contact',
value: 'contact',
},
{
name: 'Draft',
value: 'draft',
},
{
name: 'Event',
value: 'event',
},
{
name: 'Folder',
value: 'folder',
},
{
name: 'Folder Message',
value: 'folderMessage',
},
{
name: 'Message',
value: 'message',
},
{
name: 'Message Attachment',
value: 'messageAttachment',
},
],
},
...calendar.description,
...contact.description,
...draft.description,
...event.description,
...folder.description,
...folderMessage.description,
...message.description,
...messageAttachment.description,
],
};
@@ -0,0 +1,14 @@
import type { AllEntities } from 'n8n-workflow';
type NodeMap = {
calendar: 'create' | 'delete' | 'get' | 'getAll' | 'update';
contact: 'create' | 'delete' | 'get' | 'getAll' | 'update';
draft: 'create' | 'delete' | 'get' | 'send' | 'update';
event: 'create' | 'delete' | 'get' | 'getAll' | 'update';
folder: 'create' | 'delete' | 'get' | 'getAll' | 'update';
folderMessage: 'getAll';
message: 'delete' | 'get' | 'getAll' | 'move' | 'update' | 'send' | 'reply' | 'sendAndWait';
messageAttachment: 'add' | 'download' | 'getAll' | 'get';
};
export type MicrosoftOutlook = AllEntities<NodeMap>;
@@ -0,0 +1,97 @@
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeApiError, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
import * as calendar from './calendar';
import * as contact from './contact';
import * as draft from './draft';
import * as event from './event';
import * as folder from './folder';
import * as folderMessage from './folderMessage';
import * as message from './message';
import * as messageAttachment from './messageAttachment';
import type { MicrosoftOutlook } from './node.type';
import { configureWaitTillDate } from '../../../../../utils/sendAndWait/configureWaitTillDate.util';
export async function router(this: IExecuteFunctions) {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter<MicrosoftOutlook>('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0);
let responseData;
const microsoftOutlook = {
resource,
operation,
} as MicrosoftOutlook;
if (
microsoftOutlook.resource === 'message' &&
microsoftOutlook.operation === SEND_AND_WAIT_OPERATION
) {
await message[microsoftOutlook.operation].execute.call(this, 0, items);
const waitTill = configureWaitTillDate(this);
await this.putExecutionToWait(waitTill);
return [items];
}
for (let i = 0; i < items.length; i++) {
try {
switch (microsoftOutlook.resource) {
case 'calendar':
responseData = await calendar[microsoftOutlook.operation].execute.call(this, i);
break;
case 'contact':
responseData = await contact[microsoftOutlook.operation].execute.call(this, i);
break;
case 'draft':
responseData = await draft[microsoftOutlook.operation].execute.call(this, i, items);
break;
case 'event':
responseData = await event[microsoftOutlook.operation].execute.call(this, i);
break;
case 'folder':
responseData = await folder[microsoftOutlook.operation].execute.call(this, i);
break;
case 'folderMessage':
responseData = await folderMessage[microsoftOutlook.operation].execute.call(this, i);
break;
case 'message':
responseData = await message[microsoftOutlook.operation].execute.call(this, i, items);
break;
case 'messageAttachment':
responseData = await messageAttachment[microsoftOutlook.operation].execute.call(
this,
i,
items,
);
break;
default:
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`);
}
returnData.push(...responseData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
//NodeApiError will be missing the itemIndex, add it
if (error instanceof NodeApiError && error?.context?.itemIndex === undefined) {
if (error.context === undefined) {
error.context = {};
}
error.context.itemIndex = i;
}
throw error;
}
}
return [returnData];
}
@@ -0,0 +1,394 @@
import type { INodeProperties } from 'n8n-workflow';
export const returnAllOrLimit: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
},
default: 100,
description: 'Max number of results to return',
},
];
export const folderFields = [
{
name: 'Child Folder Count',
value: 'childFolderCount',
},
{
name: 'Display Name',
value: 'displayName',
},
{
name: 'Is Hidden',
value: 'isHidden',
},
{
name: 'Parent Folder ID',
value: 'parentFolderId',
},
{
name: 'Total Item Count',
value: 'totalItemCount',
},
{
name: 'Unread Item Count',
value: 'unreadItemCount',
},
];
export const contactFields: INodeProperties[] = [
{
displayName: 'Assistant Name',
name: 'assistantName',
type: 'string',
default: '',
description: "The name of the contact's assistant",
},
{
displayName: 'Birthday',
name: 'birthday',
type: 'dateTime',
default: '',
},
{
displayName: 'Business Address',
name: 'businessAddress',
type: 'fixedCollection',
placeholder: 'Add Address',
default: {
values: { sity: '', street: '', postalCode: '', countryOrRegion: '', state: '' },
},
options: [
{
displayName: 'Address',
name: 'values',
values: [
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'Country/Region',
name: 'countryOrRegion',
type: 'string',
default: '',
},
{
displayName: 'Postal Code',
name: 'postalCode',
type: 'string',
default: '',
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
},
{
displayName: 'Street',
name: 'street',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Business Home Page',
name: 'businessHomePage',
type: 'string',
default: '',
},
{
displayName: 'Business Phones',
name: 'businessPhones',
type: 'string',
description: 'Comma-separated list of business phone numbers',
default: '',
},
{
displayName: 'Categories',
name: 'categories',
description: 'Comma-separated list of categories associated with the contact',
type: 'string',
default: '',
},
{
displayName: 'Children',
name: 'children',
description: "Comma-separated list of names of the contact's children",
type: 'string',
default: '',
},
{
displayName: 'Company Name',
name: 'companyName',
type: 'string',
default: '',
},
{
displayName: 'Department',
name: 'department',
type: 'string',
default: '',
},
{
displayName: 'Display Name',
name: 'displayName',
type: 'string',
default: '',
},
{
displayName: 'Email Address',
name: 'emailAddresses',
type: 'fixedCollection',
placeholder: 'Add Email',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Email',
name: 'values',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Address',
name: 'address',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'File As',
name: 'fileAs',
type: 'string',
default: '',
description: 'The name the contact is filed under',
},
{
displayName: 'Home Address',
name: 'homeAddress',
type: 'fixedCollection',
placeholder: 'Add Address',
default: {
values: { sity: '', street: '', postalCode: '', countryOrRegion: '', state: '' },
},
options: [
{
displayName: 'Address',
name: 'values',
values: [
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'Country/Region',
name: 'countryOrRegion',
type: 'string',
default: '',
},
{
displayName: 'Postal Code',
name: 'postalCode',
type: 'string',
default: '',
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
},
{
displayName: 'Street',
name: 'street',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Home Phones',
name: 'homePhones',
type: 'string',
default: '',
hint: 'Multiple phones can be added separated by ,',
},
{
displayName: 'Instant Messaging Addresses',
name: 'imAddresses',
description: "The contact's instant messaging (IM) addresses",
type: 'string',
default: '',
hint: 'Multiple addresses can be added separated by ,',
},
{
displayName: 'Initials',
name: 'initials',
type: 'string',
default: '',
},
{
displayName: 'Job Title',
name: 'jobTitle',
type: 'string',
default: '',
},
{
displayName: 'Manager',
name: 'manager',
type: 'string',
default: '',
description: "The name of the contact's manager",
},
{
displayName: 'Middle Name',
name: 'middleName',
type: 'string',
default: '',
},
{
displayName: 'Mobile Phone',
name: 'mobilePhone',
type: 'string',
default: '',
},
{
displayName: 'Name',
name: 'givenName',
type: 'string',
default: '',
displayOptions: {
show: {
'/operation': ['update'],
},
},
},
{
displayName: 'Nickname',
name: 'nickName',
type: 'string',
default: '',
},
{
displayName: 'Office Location',
name: 'officeLocation',
type: 'string',
default: '',
},
{
displayName: 'Other Address',
name: 'otherAddress',
type: 'fixedCollection',
placeholder: 'Add Address',
default: {
values: { sity: '', street: '', postalCode: '', countryOrRegion: '', state: '' },
},
options: [
{
displayName: 'Address',
name: 'values',
values: [
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'Country/Region',
name: 'countryOrRegion',
type: 'string',
default: '',
},
{
displayName: 'Postal Code',
name: 'postalCode',
type: 'string',
default: '',
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
},
{
displayName: 'Street',
name: 'street',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Personal Notes',
name: 'personalNotes',
type: 'string',
default: '',
typeOptions: {
alwaysOpenEditWindow: true,
},
},
{
displayName: 'Profession',
name: 'profession',
type: 'string',
default: '',
},
{
displayName: 'Spouse Name',
name: 'spouseName',
type: 'string',
default: '',
},
{
displayName: 'Surname',
name: 'surname',
type: 'string',
default: '',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
},
];
@@ -0,0 +1,2 @@
export * from './rlc.description';
export * from './common.descriptions';
@@ -0,0 +1,229 @@
import type { INodeProperties } from 'n8n-workflow';
export const calendarRLC: INodeProperties = {
displayName: 'Calendar',
name: 'calendarId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a calendar...',
typeOptions: {
searchListMethod: 'searchCalendars',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
export const contactRLC: INodeProperties = {
displayName: 'Contact',
name: 'contactId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a contact...',
typeOptions: {
searchListMethod: 'searchContacts',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
export const draftRLC: INodeProperties = {
displayName: 'Draft',
name: 'draftId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a draft...',
typeOptions: {
searchListMethod: 'searchDrafts',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
export const messageRLC: INodeProperties = {
displayName: 'Message',
name: 'messageId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a message...',
typeOptions: {
searchListMethod: 'searchMessages',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
export const eventRLC: INodeProperties = {
displayName: 'Event',
name: 'eventId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
typeOptions: {
loadOptionsDependsOn: ['calendarId.value'],
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a event...',
typeOptions: {
searchListMethod: 'searchEvents',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://outlook.office365.com/calendar/item/AAMkADlhOTA0M...UAAA%3D',
extractValue: {
type: 'regex',
regex:
'https:\\/\\/outlook\\.office365\\.com\\/calendar\\/item\\/([A-Za-z0-9%]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/outlook\\.office365\\.com\\/calendar\\/item\\/([A-Za-z0-9%]+)(?:\\/.*|)',
errorMessage: 'Not a valid Outlook Event URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
export const folderRLC: INodeProperties = {
displayName: 'Folder',
name: 'folderId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a folder...',
typeOptions: {
searchListMethod: 'searchFolders',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://outlook.office365.com/mail/AAMkADlhOT...AAA%3D',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/outlook\\.office365\\.com\\/mail\\/([A-Za-z0-9%]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/outlook\\.office365\\.com\\/mail\\/([A-Za-z0-9%]+)(?:\\/.*|)',
errorMessage: 'Not a valid Outlook Folder URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
export const attachmentRLC: INodeProperties = {
displayName: 'Attachment',
name: 'attachmentId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
typeOptions: {
loadOptionsDependsOn: ['messageId.value'],
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a attachment...',
typeOptions: {
searchListMethod: 'searchAttachments',
searchable: false,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. AAAkAAAhAAA0BBc5LLLwOOOtNNNkZS05Nz...',
},
],
};
@@ -0,0 +1,305 @@
import type {
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IPollFunctions,
JsonObject,
} from 'n8n-workflow';
import { ApplicationError, jsonParse, NodeApiError } from 'n8n-workflow';
export const messageFields = [
'bccRecipients',
'body',
'bodyPreview',
'categories',
'ccRecipients',
'changeKey',
'conversationId',
'createdDateTime',
'flag',
'from',
'hasAttachments',
'importance',
'inferenceClassification',
'internetMessageId',
'isDeliveryReceiptRequested',
'isDraft',
'isRead',
'isReadReceiptRequested',
'lastModifiedDateTime',
'parentFolderId',
'receivedDateTime',
'replyTo',
'sender',
'sentDateTime',
'subject',
'toRecipients',
'webLink',
].map((field) => ({ name: field, value: field }));
export const eventfields = [
'allowNewTimeProposals',
'attendees',
'body',
'bodyPreview',
'categories',
'changeKey',
'createdDateTime',
'end',
'hasAttachments',
'hideAttendees',
'iCalUId',
'importance',
'isAllDay',
'isCancelled',
'isDraft',
'isOnlineMeeting',
'isOrganizer',
'isReminderOn',
'lastModifiedDateTime',
'location',
'locations',
'onlineMeeting',
'onlineMeetingProvider',
'onlineMeetingUrl',
'organizer',
'originalEndTimeZone',
'originalStartTimeZone',
'recurrence',
'reminderMinutesBeforeStart',
'responseRequested',
'responseStatus',
'sensitivity',
'seriesMasterId',
'showAs',
'start',
'subject',
'transactionId',
'type',
'webLink',
].map((field) => ({ name: field, value: field }));
export const contactFields = [
'createdDateTime',
'lastModifiedDateTime',
'changeKey',
'categories',
'parentFolderId',
'birthday',
'fileAs',
'displayName',
'givenName',
'initials',
'middleName',
'nickName',
'surname',
'title',
'yomiGivenName',
'yomiSurname',
'yomiCompanyName',
'generation',
'imAddresses',
'jobTitle',
'companyName',
'department',
'officeLocation',
'profession',
'businessHomePage',
'assistantName',
'manager',
'homePhones',
'mobilePhone',
'businessPhones',
'spouseName',
'personalNotes',
'children',
'emailAddresses',
'homeAddress',
'businessAddress',
'otherAddress',
].map((field) => ({ name: field, value: field }));
export function makeRecipient(email: string) {
return {
emailAddress: {
address: email,
},
};
}
export function createMessage(fields: IDataObject) {
const message: IDataObject = {};
// Create body object
if (fields.bodyContent || fields.bodyContentType) {
const bodyObject = {
content: fields.bodyContent,
contentType: fields.bodyContentType,
};
message.body = bodyObject;
delete fields.bodyContent;
delete fields.bodyContentType;
}
// Handle custom headers
if (
'internetMessageHeaders' in fields &&
'headers' in (fields.internetMessageHeaders as IDataObject)
) {
fields.internetMessageHeaders = (fields.internetMessageHeaders as IDataObject).headers;
}
for (const [key, value] of Object.entries(fields)) {
if (['bccRecipients', 'ccRecipients', 'replyTo', 'sender', 'toRecipients'].includes(key)) {
if (Array.isArray(value)) {
message[key] = (value as string[]).map((email) => makeRecipient(email));
} else if (typeof value === 'string') {
message[key] = value.split(',').map((recipient: string) => makeRecipient(recipient.trim()));
} else {
throw new ApplicationError(`The "${key}" field must be a string or an array of strings`, {
level: 'warning',
});
}
continue;
}
if (['from', 'sender'].includes(key)) {
if (value) {
message[key] = makeRecipient(value as string);
}
continue;
}
message[key] = value;
}
return message;
}
export function simplifyOutputMessages(data: IDataObject[]) {
return data.map((item: IDataObject) => {
return {
id: item.id,
conversationId: item.conversationId,
subject: item.subject,
bodyPreview: item.bodyPreview,
from: ((item.from as IDataObject)?.emailAddress as IDataObject)?.address,
to: (item.toRecipients as IDataObject[]).map(
(recipient: IDataObject) => (recipient.emailAddress as IDataObject)?.address,
),
categories: item.categories,
hasAttachments: item.hasAttachments,
};
});
}
export function prepareContactFields(fields: IDataObject) {
const returnData: IDataObject = {};
const typeStringCollection = [
'businessPhones',
'categories',
'children',
'homePhones',
'imAddresses',
];
const typeValuesToExtract = ['businessAddress', 'emailAddresses', 'homePhones', 'otherAddress'];
for (const [key, value] of Object.entries(fields)) {
if (value === undefined || value === '') {
continue;
}
if (typeStringCollection.includes(key) && !Array.isArray(value)) {
returnData[key] = (value as string).split(',').map((item) => item.trim());
continue;
}
if (typeValuesToExtract.includes(key)) {
if ((value as IDataObject).values === undefined) continue;
returnData[key] = (value as IDataObject).values;
continue;
}
returnData[key] = value;
}
return returnData;
}
export function prepareFilterString(filters: IDataObject) {
const selectedFilters = filters.filters as IDataObject;
const filterString: string[] = [];
if (selectedFilters.foldersToInclude) {
const folders = (selectedFilters.foldersToInclude as string[])
.filter((folder) => folder !== '')
.map((folder) => `parentFolderId eq '${folder}'`)
.join(' or ');
filterString.push(folders);
}
if (selectedFilters.foldersToExclude) {
for (const folder of selectedFilters.foldersToExclude as string[]) {
filterString.push(`parentFolderId ne '${folder}'`);
}
}
if (selectedFilters.sender) {
const sender = selectedFilters.sender as string;
const byMailAddress = `from/emailAddress/address eq '${sender}'`;
const byName = `from/emailAddress/name eq '${sender}'`;
filterString.push(`(${byMailAddress} or ${byName})`);
}
if (selectedFilters.hasAttachments) {
filterString.push(`hasAttachments eq ${selectedFilters.hasAttachments}`);
}
if (selectedFilters.readStatus && selectedFilters.readStatus !== 'both') {
filterString.push(`isRead eq ${selectedFilters.readStatus === 'read'}`);
}
if (selectedFilters.receivedAfter) {
filterString.push(`receivedDateTime ge ${selectedFilters.receivedAfter}`);
}
if (selectedFilters.receivedBefore) {
filterString.push(`receivedDateTime le ${selectedFilters.receivedBefore}`);
}
if (selectedFilters.custom) {
filterString.push(selectedFilters.custom as string);
}
return filterString.length ? filterString.join(' and ') : undefined;
}
export function prepareApiError(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,
error: IDataObject,
itemIndex = 0,
) {
const [httpCode, err, message] = (error.description as string).split(' - ');
const json = jsonParse(err);
return new NodeApiError(this.getNode(), json as JsonObject, {
itemIndex,
httpCode,
//In UI we are replacing some of the field names to make them more user friendly, updating error message to reflect that
message: message
.replace(/toRecipients/g, 'toRecipients (To)')
.replace(/bodyContent/g, 'bodyContent (Message)')
.replace(/bodyContentType/g, 'bodyContentType (Message Type)'),
});
}
export const encodeOutlookId = (id: string) => {
return id.replace(/-/g, '%2F').replace(/=/g, '%3D').replace(/\+/g, '%2B');
};
export const decodeOutlookId = (id: string) => {
return id.replace(/%2F/g, '-').replace(/%3D/g, '=').replace(/%2B/g, '+');
};
@@ -0,0 +1,2 @@
export * as loadOptions from './loadOptions';
export * as listSearch from './listSearch';
@@ -0,0 +1,290 @@
import type { IDataObject, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
import { encodeOutlookId } from '../helpers/utils';
import { getSubfolders, microsoftApiRequest } from '../transport';
async function search(
this: ILoadOptionsFunctions,
resource: string,
nameProperty: string,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: IDataObject = {};
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'',
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
);
} else {
const qs: IDataObject = {
$select: `id,${nameProperty}`,
$top: 100,
};
if (filter) {
const filterValue = encodeURI(filter);
qs.$filter = `contains(${nameProperty}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(this, 'GET', resource, undefined, qs);
}
return {
results: (response.value as IDataObject[]).map((entry: IDataObject) => {
return {
name: entry[nameProperty] as string,
value: entry.id as string,
};
}),
paginationToken: response['@odata.nextLink'],
};
}
export async function searchContacts(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
return await search.call(this, '/contacts', 'displayName', filter, paginationToken);
}
export async function searchCalendars(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
return await search.call(this, '/calendars', 'name', filter, paginationToken);
}
export async function searchDrafts(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: IDataObject = {};
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'',
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
);
} else {
const qs: IDataObject = {
$select: 'id,subject,bodyPreview,webLink',
$top: 100,
$filter: 'isDraft eq true',
};
if (filter) {
const filterValue = encodeURI(filter);
qs.$filter += ` AND contains(${'subject'}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(this, 'GET', '/messages', undefined, qs);
}
return {
results: (response.value as IDataObject[]).map((entry: IDataObject) => {
return {
name: (entry.subject || entry.bodyPreview) as string,
value: entry.id as string,
url: entry.webLink as string,
};
}),
paginationToken: response['@odata.nextLink'],
};
}
export async function searchMessages(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: IDataObject = {};
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'',
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
);
} else {
const qs: IDataObject = {
$select: 'id,subject,bodyPreview,webLink',
$top: 100,
};
if (filter) {
const filterValue = encodeURI(filter);
qs.$filter = `contains(${'subject'}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(this, 'GET', '/messages', undefined, qs);
}
return {
results: (response.value as IDataObject[]).map((entry: IDataObject) => {
return {
name: (entry.subject || entry.bodyPreview) as string,
value: entry.id as string,
url: entry.webLink as string,
};
}),
paginationToken: response['@odata.nextLink'],
};
}
export async function searchEvents(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: IDataObject = {};
const calendarId = this.getNodeParameter('calendarId', undefined, {
extractValue: true,
}) as string;
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'',
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
);
} else {
const qs: IDataObject = {
$select: 'id,subject,bodyPreview',
$top: 100,
};
if (filter) {
const filterValue = encodeURI(filter);
qs.$filter = `contains(${'subject'}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(
this,
'GET',
`/calendars/${calendarId}/events`,
undefined,
qs,
);
}
return {
results: (response.value as IDataObject[]).map((entry: IDataObject) => {
return {
name: (entry.subject || entry.bodyPreview) as string,
value: entry.id as string,
url: `https://outlook.office365.com/calendar/item/${encodeOutlookId(entry.id as string)}`,
};
}),
paginationToken: response['@odata.nextLink'],
};
}
export async function searchFolders(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: IDataObject = {};
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'',
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
);
} else {
const qs: IDataObject = {
$top: 100,
};
response = await microsoftApiRequest.call(this, 'GET', '/mailFolders', undefined, qs);
}
let folders = await getSubfolders.call(this, response.value as IDataObject[]);
if (filter) {
filter = filter.toLowerCase();
folders = folders.filter((folder) =>
((folder.displayName as string) || '').toLowerCase().includes(filter as string),
);
}
return {
results: folders.map((entry: IDataObject) => {
return {
name: entry.displayName as string,
value: entry.id as string,
url: `https://outlook.office365.com/mail/${encodeOutlookId(entry.id as string)}`,
};
}),
paginationToken: response['@odata.nextLink'],
};
}
export async function searchAttachments(
this: ILoadOptionsFunctions,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: IDataObject = {};
const messageId = this.getNodeParameter('messageId', undefined, {
extractValue: true,
}) as string;
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'',
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
);
} else {
const qs: IDataObject = {
$select: 'id,name',
$top: 100,
};
response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments`,
undefined,
qs,
);
}
return {
results: (response.value as IDataObject[]).map((entry: IDataObject) => {
return {
name: entry.name as string,
value: entry.id as string,
};
}),
paginationToken: response['@odata.nextLink'],
};
}
@@ -0,0 +1,55 @@
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
import { getSubfolders, microsoftApiRequestAllItems } from '../transport';
export async function getCategoriesNames(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const categories = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
'/outlook/masterCategories',
);
for (const category of categories) {
returnData.push({
name: category.displayName as string,
value: category.displayName as string,
});
}
return returnData;
}
export async function getFolders(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const response = await microsoftApiRequestAllItems.call(this, 'value', 'GET', '/mailFolders', {});
const folders = await getSubfolders.call(this, response);
for (const folder of folders) {
returnData.push({
name: folder.displayName as string,
value: folder.id as string,
});
}
return returnData;
}
export async function getCalendarGroups(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const calendars = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
'/calendarGroups',
{},
);
for (const calendar of calendars) {
returnData.push({
name: calendar.name as string,
value: calendar.id as string,
});
}
return returnData;
}
@@ -0,0 +1,210 @@
import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { microsoftApiRequest } from '../../transport/index';
describe('Microsoft Outlook Transport', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
let mockNode: INode;
let mockRequestWithAuthentication: jest.Mock;
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
mockRequestWithAuthentication = jest.fn();
mockExecuteFunctions.helpers.requestWithAuthentication = mockRequestWithAuthentication;
mockNode = {
id: 'test-node',
name: 'Test Outlook Node',
type: 'n8n-nodes-base.microsoftOutlook',
typeVersion: 2,
position: [0, 0],
parameters: {},
};
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
jest.clearAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('microsoftApiRequest', () => {
describe('graphApiBaseUrl from credentials', () => {
it('should use base URL from credentials', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: 'https://graph.microsoft.us',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://graph.microsoft.us/v1.0/me/messages',
json: true,
}),
);
});
it('should fall back to default when credentials.graphApiBaseUrl is empty', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: '',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://graph.microsoft.com/v1.0/me/messages',
json: true,
}),
);
});
it('should fall back to default when credentials.graphApiBaseUrl is undefined', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://graph.microsoft.com/v1.0/me/messages',
json: true,
}),
);
});
it('should strip trailing slashes from base URL using regex', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: 'https://graph.microsoft.com/',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://graph.microsoft.com/v1.0/me/messages',
json: true,
}),
);
});
it('should strip multiple trailing slashes from base URL', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: 'https://graph.microsoft.com///',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://graph.microsoft.com/v1.0/me/messages',
json: true,
}),
);
});
it('should use US Government cloud endpoint', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: 'https://graph.microsoft.us',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://graph.microsoft.us/v1.0/me/messages',
json: true,
}),
);
});
it('should use US Government DOD cloud endpoint', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: 'https://dod-graph.microsoft.us',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://dod-graph.microsoft.us/v1.0/me/messages',
json: true,
}),
);
});
it('should use China cloud endpoint', async () => {
const mockResponse = { data: 'test' };
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
mockExecuteFunctions.getCredentials.mockResolvedValue({
oauthTokenData: {
access_token: 'test-access-token',
},
graphApiBaseUrl: 'https://microsoftgraph.chinacloudapi.cn',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: 'https://microsoftgraph.chinacloudapi.cn/v1.0/me/messages',
json: true,
}),
);
});
});
});
});
@@ -0,0 +1,232 @@
import type {
IHttpRequestMethods,
IRequestOptions,
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
IPollFunctions,
} from 'n8n-workflow';
import { prepareApiError } from '../helpers/utils';
export async function microsoftApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
option: IDataObject = { json: true },
) {
const credentials = await this.getCredentials('microsoftOutlookOAuth2Api');
const baseUrl = (
typeof credentials.graphApiBaseUrl === 'string' && credentials.graphApiBaseUrl !== ''
? credentials.graphApiBaseUrl
: 'https://graph.microsoft.com'
).replace(/\/+$/, '');
let apiUrl = `${baseUrl}/v1.0/me${resource}`;
// If accessing shared mailbox
if (credentials.useShared && credentials.userPrincipalName) {
apiUrl = `${baseUrl}/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).length === 0) {
delete options.body;
}
return await this.helpers.requestWithAuthentication.call(
this,
'microsoftOutlookOAuth2Api',
options,
);
} catch (error) {
if (
((error.message || '').toLowerCase().includes('bad request') ||
(error.message || '').toLowerCase().includes('unknown error')) &&
error.description
) {
let updatedError;
// Try to return the error prettier, otherwise return the original one replacing the message with the description
try {
updatedError = prepareApiError.call(this, error);
} catch (e) {}
if (updatedError) throw updatedError;
error.message = error.description;
error.description = '';
}
throw error;
}
}
export async function microsoftApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
query: IDataObject = {},
headers: IDataObject = {},
) {
const returnData: IDataObject[] = [];
let responseData;
let nextLink: string | undefined;
query.$top = 100;
do {
responseData = await microsoftApiRequest.call(
this,
method,
endpoint,
body,
nextLink ? undefined : query, // Do not add query parameters as nextLink already contains them
nextLink,
headers,
);
nextLink = responseData['@odata.nextLink'];
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
} while (responseData['@odata.nextLink'] !== undefined);
return returnData;
}
export async function downloadAttachments(
this: IExecuteFunctions | IPollFunctions,
messages: IDataObject[] | IDataObject,
prefix: string,
) {
const elements: INodeExecutionData[] = [];
if (!Array.isArray(messages)) {
messages = [messages];
}
for (const message of messages) {
const element: INodeExecutionData = {
json: message,
binary: {},
};
if (message.hasAttachments === true) {
const attachments = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/messages/${message.id}/attachments`,
{},
);
for (const [index, attachment] of attachments.entries()) {
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${message.id}/attachments/${attachment.id}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
const data = Buffer.from(response.body as string, 'utf8');
element.binary![`${prefix}${index}`] = await this.helpers.prepareBinaryData(
data as unknown as Buffer,
attachment.name as string,
attachment.contentType as string,
);
}
}
if (Object.keys(element.binary!).length === 0) {
delete element.binary;
}
elements.push(element);
}
return elements;
}
export async function getMimeContent(
this: IExecuteFunctions,
messageId: string,
binaryPropertyName: string,
outputFileName?: string,
) {
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
let mimeType: string | undefined;
if (response.headers['content-type']) {
mimeType = response.headers['content-type'];
}
const fileName = `${outputFileName || messageId}.eml`;
const data = Buffer.from(response.body as string, 'utf8');
const binary: IDataObject = {};
binary[binaryPropertyName] = await this.helpers.prepareBinaryData(
data as unknown as Buffer,
fileName,
mimeType,
);
return binary;
}
export async function getSubfolders(
this: IExecuteFunctions | ILoadOptionsFunctions,
folders: IDataObject[],
addPathToDisplayName = false,
) {
const returnData: IDataObject[] = [...folders];
for (const folder of folders) {
if ((folder.childFolderCount as number) > 0) {
let subfolders = await microsoftApiRequest.call(
this,
'GET',
`/mailFolders/${folder.id}/childFolders`,
);
if (addPathToDisplayName) {
subfolders = subfolders.value.map((subfolder: IDataObject) => {
return {
...subfolder,
displayName: `${folder.displayName}/${subfolder.displayName}`,
};
});
} else {
subfolders = subfolders.value;
}
returnData.push(
...(await getSubfolders.call(this, subfolders as IDataObject[], addPathToDisplayName)),
);
}
}
return returnData;
}