fix(Microsoft Outlook Node): Resolve service principal mailbox per input item (#34110)

This commit is contained in:
Csaba Tuncsik
2026-07-16 05:57:28 +00:00
committed by GitHub
parent 0ca24ba996
commit 831e054e6e
59 changed files with 908 additions and 115 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"issues": 558
"issues": 561
}
@@ -96,7 +96,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
it('should handle simple output format in manual mode', async () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', 0, undefined, {
$select:
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments',
$top: 1,
@@ -122,7 +122,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', 0, undefined, {
$select: 'id,subject,from',
$top: 1,
});
@@ -152,11 +152,11 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', 0, undefined, {
$select: 'id,subject,from,hasAttachments',
$top: 1,
});
expect(downloadAttachments).toHaveBeenCalledWith([mockMessages[0]], 'attachment_');
expect(downloadAttachments).toHaveBeenCalledWith([mockMessages[0]], 'attachment_', 0);
expect(result).toEqual(mockExecutionData);
});
});
@@ -174,6 +174,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
'/messages',
0,
undefined,
{
$select:
@@ -204,6 +205,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,receivedDateTime',
@@ -224,6 +226,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
'/messages',
0,
undefined,
{
$select:
@@ -255,7 +258,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'custom_');
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'custom_', 0);
expect(result).toEqual(mockExecutionData);
});
@@ -281,7 +284,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'attachment_');
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'attachment_', 0);
});
});
@@ -313,6 +316,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
`/mailFolders/${folderId1}/messages`,
0,
undefined,
{
$select:
@@ -352,6 +356,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
`/mailFolders/${folderId1}/messages`,
0,
undefined,
expect.objectContaining({ $filter: expect.any(String) }),
);
@@ -359,6 +364,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
`/mailFolders/${folderId2}/messages`,
0,
undefined,
expect.objectContaining({ $filter: expect.any(String) }),
);
@@ -402,6 +408,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
'/messages',
0,
undefined,
expect.objectContaining({ $filter: expect.any(String) }),
);
@@ -432,6 +439,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
expect(microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/mailFolders/${folderId1}/messages`,
0,
undefined,
{
$select:
@@ -442,6 +450,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
expect(microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/mailFolders/${folderId2}/messages`,
0,
undefined,
{
$select:
@@ -500,7 +509,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', 0, undefined, {
$top: 1,
});
expect(simplifyOutputMessages).not.toHaveBeenCalled();
@@ -550,7 +559,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', 0, undefined, {
$select: 'id,subject,hasAttachments',
$top: 1,
});
@@ -719,7 +728,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
const result = await getPollResponse.call(mockPollFunctions, pollStartDate, pollEndDate);
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', undefined, {
expect(microsoftApiRequest).toHaveBeenCalledWith('GET', '/messages', 0, undefined, {
$select: '',
$top: 1,
});
@@ -754,6 +763,7 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,from,isRead',
@@ -792,13 +802,14 @@ describe('Microsoft Outlook Trigger GenericFunctions', () => {
'value',
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,hasAttachments',
$filter: `isRead eq false and receivedDateTime ge ${pollStartDate} and receivedDateTime lt ${pollEndDate}`,
},
);
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'prefix_');
expect(downloadAttachments).toHaveBeenCalledWith(mockMessages, 'prefix_', 0);
expect(result).toEqual(mockExecutionData);
});
});
@@ -0,0 +1,53 @@
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { Mock, Mocked } from 'vitest';
import { mockDeep } from 'vitest-mock-extended';
import { router } from '../../../v2/actions/router';
describe('MicrosoftOutlookV2 - router error attribution', () => {
it('stamps context.itemIndex on a NodeError from the failing item', async () => {
const mockExecuteFunctions: Mocked<IExecuteFunctions> = mockDeep<IExecuteFunctions>();
const mockNode: INode = {
id: 'test-node',
name: 'Test Outlook Node',
type: 'n8n-nodes-base.microsoftOutlook',
typeVersion: 2,
position: [0, 0],
parameters: {},
};
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }, { json: {} }]);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
mockExecuteFunctions.getCredentials.mockResolvedValue({
accessToken: 'test-access-token',
graphApiBaseUrl: '',
});
mockExecuteFunctions.helpers.requestWithAuthentication = vi.fn().mockResolvedValue({});
(mockExecuteFunctions.helpers.constructExecutionMetaData as Mock).mockReturnValue([]);
// message:delete on two items: item 0's mailbox is valid, item 1's is not — the
// unstamped validator error must leave the router carrying item 1's index.
mockExecuteFunctions.getNodeParameter.mockImplementation((name, itemIndex, fallback) => {
const params: Record<string, unknown> = {
resource: 'message',
operation: 'delete',
authentication: 'microsoftEntraServicePrincipalApi',
messageId: 'MSG1',
};
if (name === 'mailbox')
return (itemIndex === 1 ? 'contoso.com' : '[email protected]') as never;
return (name in params ? params[name] : fallback) as never;
});
let caught: unknown;
try {
await router.call(mockExecuteFunctions);
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(NodeOperationError);
expect((caught as NodeOperationError).message).toBe('The mailbox is not valid');
expect((caught as NodeOperationError).context.itemIndex).toBe(1);
});
});
@@ -49,6 +49,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/contacts',
0,
undefined,
{
$select: 'id,displayName',
@@ -76,6 +77,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/contacts',
0,
undefined,
{
$select: 'id,displayName',
@@ -102,6 +104,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'',
0,
undefined,
undefined,
paginationToken,
@@ -125,6 +128,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/calendars',
0,
undefined,
{
$select: 'id,name',
@@ -161,6 +165,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
@@ -199,6 +204,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
@@ -248,6 +254,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
@@ -285,6 +292,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/messages',
0,
undefined,
{
$select: 'id,subject,bodyPreview,webLink',
@@ -327,6 +335,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/calendars/${calendarId}/events`,
0,
undefined,
{
$select: 'id,subject,bodyPreview',
@@ -365,6 +374,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/calendars/${calendarId}/events`,
0,
undefined,
{
$select: 'id,subject,bodyPreview',
@@ -401,12 +411,13 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'/mailFolders',
0,
undefined,
{
$top: 100,
},
);
expect(mockTransport.getSubfolders).toHaveBeenCalledWith(mockResponse.value, true);
expect(mockTransport.getSubfolders).toHaveBeenCalledWith(mockResponse.value, 0, true);
expect(result).toEqual({
results: [
{
@@ -506,6 +517,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
`/messages/${messageId}/attachments`,
0,
undefined,
{
$select: 'id,name',
@@ -537,6 +549,7 @@ describe('MicrosoftOutlookV2 - listSearch methods', () => {
expect(mockTransport.microsoftApiRequest).toHaveBeenCalledWith(
'GET',
'',
0,
undefined,
undefined,
paginationToken,
@@ -37,6 +37,7 @@ describe('MicrosoftOutlookV2 - loadOptions methods', () => {
'value',
'GET',
'/outlook/masterCategories',
0,
);
expect(result).toEqual([
{ name: 'Red Category', value: 'Red Category' },
@@ -125,9 +126,10 @@ describe('MicrosoftOutlookV2 - loadOptions methods', () => {
'value',
'GET',
'/mailFolders',
0,
{},
);
expect(mockTransport.getSubfolders).toHaveBeenCalledWith(mockResponse, true);
expect(mockTransport.getSubfolders).toHaveBeenCalledWith(mockResponse, 0, true);
expect(result).toEqual([
{ name: 'Inbox', value: 'folder1' },
{ name: 'Sent Items', value: 'folder2' },
@@ -222,6 +224,7 @@ describe('MicrosoftOutlookV2 - loadOptions methods', () => {
'value',
'GET',
'/calendarGroups',
0,
{},
);
expect(result).toEqual([
@@ -190,6 +190,7 @@ describe('Test MicrosoftOutlookV2, draft => create', () => {
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages',
0,
expect.objectContaining({
subject: 'Test Draft with Attachment',
body: expect.objectContaining({
@@ -243,11 +244,14 @@ describe('Test MicrosoftOutlookV2, draft => create', () => {
microsoftApiRequestSpy.mockResolvedValue({});
await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
// Non-zero index: pins that the operation forwards the PASSED index (not a
// hardcoded 0). The getNodeParameter mock is index-insensitive.
await execute.call(mockExecuteFunctions, 2, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages',
2,
expect.objectContaining({
attachments: [
{
@@ -296,6 +300,7 @@ describe('Test MicrosoftOutlookV2, draft => create', () => {
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages',
0,
expect.objectContaining({
attachments: expect.arrayContaining([
expect.objectContaining({
@@ -64,7 +64,7 @@ describe('Test MicrosoftOutlookV2, message => sendAndWait', () => {
expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1);
expect(transport.microsoftApiRequest).toHaveBeenCalledWith('POST', '/sendMail', {
expect(transport.microsoftApiRequest).toHaveBeenCalledWith('POST', '/sendMail', 0, {
message: {
body: {
content: expect.stringContaining(
@@ -90,6 +90,7 @@ describe('Microsoft Outlook V2 - MessageAttachment:add', () => {
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages/AAMkAGI2TG93AAA=/attachments',
0,
{
'@odata.type': '#microsoft.graph.fileAttachment',
name: 'test-file.txt',
@@ -132,11 +133,14 @@ describe('Microsoft Outlook V2 - MessageAttachment:add', () => {
microsoftApiRequestSpy.mockResolvedValue({});
await execute.call(mockExecuteFunctions, 0, [{ json: {} }]);
// Non-zero index: pins that the operation forwards the PASSED index (not a
// hardcoded 0). The getNodeParameter mock is index-insensitive.
await execute.call(mockExecuteFunctions, 2, [{ json: {} }]);
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages/AAMkAGI2TG93AAA=/attachments',
2,
{
'@odata.type': '#microsoft.graph.fileAttachment',
name: 'custom-name.pdf',
@@ -222,6 +226,7 @@ describe('Microsoft Outlook V2 - MessageAttachment:add', () => {
expect(microsoftApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/messages/AAMkAGI2TG93BBB=/attachments/createUploadSession',
0,
{
AttachmentItem: {
attachmentType: 'file',
@@ -0,0 +1,62 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
// folder:getAll with includeChildFolders exercises the helper chain end-to-end:
// operation -> microsoftApiRequest AND getSubfolders -> microsoftApiRequestAllItems ->
// (recursive) getSubfolders. Fanning out to two mailboxes proves the loop index is
// threaded through every hop — the child-folder fetch for item i's parent list must hit
// item i's mailbox, not item 0's (which would cross-mix mailboxes). Consume-once
// interceptors + empty pendingMocks pin the exact request set.
describe('Test MicrosoftOutlookV2, Service Principal => folder:getAll per-item mailbox', () => {
const credentials = {
microsoftEntraServicePrincipalApi: {
accessToken: 'test-access-token',
tenantId: '11111111-1111-1111-1111-111111111111',
clientId: '22222222-2222-2222-2222-222222222222',
clientSecret: 'secret',
graphApiBaseUrl: 'https://graph.microsoft.com',
},
};
beforeAll(() => {
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.get('/v1.0/users/usera%40example.com/mailFolders')
.query({ $top: 50 })
.reply(200, {
value: [{ id: 'folder-a', displayName: 'Inbox A', childFolderCount: 1 }],
});
// childFolderCount > 0 makes getSubfolders recurse into this folder — the child
// fetch must stay on mailbox A.
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.get('/v1.0/users/usera%40example.com/mailFolders/folder-a/childFolders')
.query({ $top: 100 })
.reply(200, {
value: [{ id: 'sub-a', displayName: 'Sub A', childFolderCount: 0 }],
});
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.get('/v1.0/users/userb%40example.com/mailFolders')
.query({ $top: 50 })
.reply(200, {
value: [{ id: 'folder-b', displayName: 'Inbox B', childFolderCount: 1 }],
});
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.get('/v1.0/users/userb%40example.com/mailFolders/folder-b/childFolders')
.query({ $top: 100 })
.reply(200, {
value: [{ id: 'sub-b', displayName: 'Sub B', childFolderCount: 0 }],
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['folderGetAllPerItem.workflow.json'],
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
});
});
@@ -0,0 +1,113 @@
{
"name": "Microsoft Outlook Service Principal Folder Get All Per Item",
"nodes": [
{
"parameters": {},
"id": "e524f588-b6a3-4849-8777-b32a8a755af0",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"data": [{ "mailbox": "[email protected]" }, { "mailbox": "[email protected]" }]
},
"id": "e524f588-b6a3-4849-8777-b32a8a755af1",
"name": "Test Data",
"type": "n8n-nodes-testing.testData",
"typeVersion": 1,
"position": [930, 360]
},
{
"parameters": {
"authentication": "microsoftEntraServicePrincipalApi",
"mailbox": {
"__rl": true,
"mode": "id",
"value": "={{ $json.mailbox }}"
},
"resource": "folder",
"operation": "getAll",
"returnAll": false,
"limit": 50,
"filters": {},
"options": {
"includeChildFolders": true
}
},
"id": "baff6798-0304-4255-bdb0-dd3f2659373d",
"name": "Microsoft Outlook",
"type": "n8n-nodes-base.microsoftOutlook",
"typeVersion": 2,
"position": [1040, 360],
"credentials": {
"microsoftEntraServicePrincipalApi": {
"id": "spCredId1",
"name": "Microsoft Entra Service Principal Test"
}
}
}
],
"pinData": {
"Microsoft Outlook": [
{
"json": {
"id": "folder-a",
"displayName": "Inbox A",
"childFolderCount": 1
}
},
{
"json": {
"id": "sub-a",
"displayName": "Sub A",
"childFolderCount": 0
}
},
{
"json": {
"id": "folder-b",
"displayName": "Inbox B",
"childFolderCount": 1
}
},
{
"json": {
"id": "sub-b",
"displayName": "Sub B",
"childFolderCount": 0
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Test Data",
"type": "main",
"index": 0
}
]
]
},
"Test Data": {
"main": [
[
{
"node": "Microsoft Outlook",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"tags": []
}
@@ -0,0 +1,45 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
// The mailbox RLC accepts expressions, so it must be resolved per input item: a fan-out
// to two mailboxes must produce one /users/{encoded-mailbox}/sendMail POST per mailbox.
// Both interceptors are consume-once and `pendingMocks` must be empty afterwards, so a
// run that sends both items from item 0's mailbox fails in either direction.
describe('Test MicrosoftOutlookV2, Service Principal => message:send per-item mailbox', () => {
const credentials = {
microsoftEntraServicePrincipalApi: {
accessToken: 'test-access-token',
tenantId: '11111111-1111-1111-1111-111111111111',
clientId: '22222222-2222-2222-2222-222222222222',
clientSecret: 'secret',
graphApiBaseUrl: 'https://graph.microsoft.com',
},
};
const expectedBody = {
message: {
body: { content: 'message description', contentType: 'Text' },
subject: 'Hello',
toRecipients: [{ emailAddress: { address: '[email protected]' } }],
},
saveToSentItems: true,
};
beforeAll(() => {
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.post('/v1.0/users/alice%40example.com/sendMail', expectedBody)
.reply(200, {});
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.post('/v1.0/users/bob%40example.com/sendMail', expectedBody)
.reply(200, {});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['sendPerItem.workflow.json'],
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
});
});
@@ -0,0 +1,95 @@
{
"name": "Microsoft Outlook Service Principal Message Send Per Item",
"nodes": [
{
"parameters": {},
"id": "e524f588-b6a3-4849-8777-b32a8a755ae6",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"data": [{ "mailbox": "[email protected]" }, { "mailbox": "[email protected]" }]
},
"id": "e524f588-b6a3-4849-8777-b32a8a755ae7",
"name": "Test Data",
"type": "n8n-nodes-testing.testData",
"typeVersion": 1,
"position": [930, 360]
},
{
"parameters": {
"authentication": "microsoftEntraServicePrincipalApi",
"mailbox": {
"__rl": true,
"mode": "id",
"value": "={{ $json.mailbox }}"
},
"resource": "message",
"operation": "send",
"toRecipients": "[email protected]",
"subject": "Hello",
"bodyContent": "message description",
"additionalFields": {
"bodyContentType": "Text"
}
},
"id": "baff6798-0304-4255-bdb0-dd3f2659373c",
"name": "Microsoft Outlook",
"type": "n8n-nodes-base.microsoftOutlook",
"typeVersion": 2,
"position": [1040, 360],
"credentials": {
"microsoftEntraServicePrincipalApi": {
"id": "spCredId1",
"name": "Microsoft Entra Service Principal Test"
}
}
}
],
"pinData": {
"Microsoft Outlook": [
{
"json": {
"success": true
}
},
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Test Data",
"type": "main",
"index": 0
}
]
]
},
"Test Data": {
"main": [
[
{
"node": "Microsoft Outlook",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"tags": []
}
@@ -0,0 +1,37 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
// Per-item error isolation under continueOnFail: item 0's bogus mailbox ("contoso.com"
// is a bare host, not a GUID/UPN) must route through the router's continueOnFail branch
// as an error item for item 0 only, while items 1 and 2 still send from their own
// mailboxes. Only the two valid mailboxes are mocked; consume-once + empty pendingMocks
// pin the exact request set.
describe('Test MicrosoftOutlookV2, Service Principal => message:send fails only the bad item under continueOnFail', () => {
const credentials = {
microsoftEntraServicePrincipalApi: {
accessToken: 'test-access-token',
tenantId: '11111111-1111-1111-1111-111111111111',
clientId: '22222222-2222-2222-2222-222222222222',
clientSecret: 'secret',
graphApiBaseUrl: 'https://graph.microsoft.com',
},
};
beforeAll(() => {
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.post('/v1.0/users/alice%40example.com/sendMail')
.reply(200, {});
nock('https://graph.microsoft.com')
.matchHeader('Authorization', 'Bearer test-access-token')
.post('/v1.0/users/bob%40example.com/sendMail')
.reply(200, {});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['sendPerItemContinueOnFail.workflow.json'],
customAssertions: () => expect(nock.pendingMocks()).toEqual([]),
});
});
@@ -0,0 +1,105 @@
{
"name": "Microsoft Outlook Service Principal Message Send Per Item ContinueOnFail",
"nodes": [
{
"parameters": {},
"id": "e524f588-b6a3-4849-8777-b32a8a755af5",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [820, 360]
},
{
"parameters": {
"data": [
{ "mailbox": "contoso.com" },
{ "mailbox": "[email protected]" },
{ "mailbox": "[email protected]" }
]
},
"id": "e524f588-b6a3-4849-8777-b32a8a755af6",
"name": "Test Data",
"type": "n8n-nodes-testing.testData",
"typeVersion": 1,
"position": [930, 360]
},
{
"parameters": {
"authentication": "microsoftEntraServicePrincipalApi",
"mailbox": {
"__rl": true,
"mode": "id",
"value": "={{ $json.mailbox }}"
},
"resource": "message",
"operation": "send",
"toRecipients": "[email protected]",
"subject": "Hello",
"bodyContent": "message description",
"additionalFields": {
"bodyContentType": "Text"
}
},
"id": "baff6798-0304-4255-bdb0-dd3f2659373e",
"name": "Microsoft Outlook",
"type": "n8n-nodes-base.microsoftOutlook",
"typeVersion": 2,
"position": [1040, 360],
"onError": "continueRegularOutput",
"credentials": {
"microsoftEntraServicePrincipalApi": {
"id": "spCredId1",
"name": "Microsoft Entra Service Principal Test"
}
}
}
],
"pinData": {
"Microsoft Outlook": [
{
"json": {
"error": "The mailbox is not valid"
}
},
{
"json": {
"success": true
}
},
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Test Data",
"type": "main",
"index": 0
}
]
]
},
"Test Data": {
"main": [
[
{
"node": "Microsoft Outlook",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"tags": []
}
@@ -1,9 +1,11 @@
import type { IDataObject, IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import type { Mock, Mocked } from 'vitest';
import { mockDeep } from 'vitest-mock-extended';
import {
downloadAttachments,
getMimeContent,
getOutlookCredentialType,
getSubfolders,
microsoftApiRequest,
@@ -43,7 +45,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: 'https://graph.microsoft.us',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -62,7 +64,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: '',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -78,7 +80,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
oauthTokenData: { access_token: 'test-access-token' },
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -94,7 +96,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: 'https://graph.microsoft.com/',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -110,7 +112,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: 'https://graph.microsoft.com///',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -126,7 +128,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: 'https://graph.microsoft.us',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -142,7 +144,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: 'https://dod-graph.microsoft.us',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -158,7 +160,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
graphApiBaseUrl: 'https://microsoftgraph.chinacloudapi.cn',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -175,7 +177,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
mockExecuteFunctions.getNodeParameter.mockReturnValue(undefined);
mockExecuteFunctions.getCredentials.mockResolvedValue({});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('microsoftOutlookOAuth2Api');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
@@ -189,7 +191,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
mockExecuteFunctions.getNodeParameter.mockReturnValue('microsoftOAuth2Api');
mockExecuteFunctions.getCredentials.mockResolvedValue({});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('microsoftOAuth2Api');
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
@@ -213,7 +215,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
userPrincipalName: '[email protected]',
});
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
@@ -252,7 +254,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
it('should target /users/{encoded-mailbox} and resolve the SP credential', async () => {
setupSP({ mailbox: '[email protected]' });
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
@@ -272,7 +274,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
// this covers the isResourceLocatorValue(raw) ? raw.value branch directly.
setupSP({ mailbox: { __rl: true, mode: 'id', value: '[email protected]' } });
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
@@ -285,7 +287,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
it('should pass a GUID mailbox through verbatim (nothing to percent-encode)', async () => {
setupSP({ mailbox: '11111111-1111-1111-1111-111111111111' });
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
@@ -298,7 +300,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
it('should carry a B2B guest (#EXT#) mailbox end-to-end into an encoded /users/{id} uri', async () => {
setupSP({ mailbox: 'user_contoso.com#EXT#@tenant.onmicrosoft.com' });
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
@@ -311,7 +313,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
it('should carry a "^"/"!" mailbox end-to-end into an encoded /users/{id} uri', async () => {
setupSP({ mailbox: 'joe^[email protected]' });
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
@@ -324,7 +326,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
it('should target the sovereign cloud base for the SP mailbox', async () => {
setupSP({ mailbox: '[email protected]', graphApiBaseUrl: 'https://graph.microsoft.us' });
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages');
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
@@ -339,7 +341,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
const nextLink =
'https://graph.microsoft.com/v1.0/users/user%40example.com/messages?$skip=10&$top=10';
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', {}, {}, nextLink);
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0, {}, {}, nextLink);
const callArgs = mockRequestWithAuthentication.mock.calls[0][1] as IDataObject;
expect(callArgs.uri).toBe(nextLink);
@@ -351,10 +353,10 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
setupSP({ mailbox: '' });
await expect(
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages'),
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0),
).rejects.toThrow(NodeOperationError);
await expect(
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages'),
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0),
).rejects.toThrow('A mailbox is required for the Service Principal');
expect(mockRequestWithAuthentication).not.toHaveBeenCalled();
});
@@ -364,7 +366,7 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
setupSP({ mailbox: ' ' });
await expect(
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages'),
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0),
).rejects.toThrow('A mailbox is required for the Service Principal');
expect(mockRequestWithAuthentication).not.toHaveBeenCalled();
});
@@ -375,10 +377,140 @@ describe('MicrosoftOutlookV2 - microsoftApiRequest', () => {
setupSP({ mailbox: 'contoso.com' });
await expect(
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages'),
microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/messages', 0),
).rejects.toThrow('The mailbox is not valid');
expect(mockRequestWithAuthentication).not.toHaveBeenCalled();
});
// The mailbox RLC accepts expressions, so execute call sites pass the loop index
// and the transport must read the mailbox at exactly that index. Reuses setupSP
// and only swaps in an index-aware mailbox read.
const setupSPPerItem = () => {
setupSP();
mockExecuteFunctions.getNodeParameter.mockImplementation(
(name: string, itemIndex?: number) => {
if (name === 'authentication') return 'microsoftEntraServicePrincipalApi';
if (name === 'mailbox')
return itemIndex === 1 ? '[email protected]' : '[email protected]';
return undefined;
},
);
};
it('should resolve the mailbox at the passed itemIndex', async () => {
setupSPPerItem();
await microsoftApiRequest.call(
mockExecuteFunctions,
'GET',
'/messages',
0,
{},
{},
undefined,
{},
{ json: true },
);
await microsoftApiRequest.call(
mockExecuteFunctions,
'GET',
'/messages',
1,
{},
{},
undefined,
{},
{ json: true },
);
expect(mockRequestWithAuthentication).toHaveBeenNthCalledWith(
1,
'microsoftEntraServicePrincipalApi',
expect.objectContaining({
uri: 'https://graph.microsoft.com/v1.0/users/user1%40example.com/messages',
}),
);
expect(mockRequestWithAuthentication).toHaveBeenNthCalledWith(
2,
'microsoftEntraServicePrincipalApi',
expect.objectContaining({
uri: 'https://graph.microsoft.com/v1.0/users/user2%40example.com/messages',
}),
);
});
it('should attribute a Graph error to the passed itemIndex (prepareApiError path)', async () => {
setupSP();
// "bad request" message + parseable description route the error through
// prepareApiError, which must receive the forwarded index.
mockRequestWithAuthentication.mockRejectedValue(
Object.assign(new Error('bad request'), {
description: '400 - {"error":{"code":"ErrorInvalidRequest","message":"broken"}} - broken',
}),
);
let caught: unknown;
try {
await microsoftApiRequest.call(
mockExecuteFunctions,
'GET',
'/messages',
2,
{},
{},
undefined,
{},
{ json: true },
);
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(NodeApiError);
expect((caught as NodeApiError).context.itemIndex).toBe(2);
});
it('should download attachments from the mailbox at the passed itemIndex', async () => {
setupSPPerItem();
mockRequestWithAuthentication
.mockResolvedValueOnce({
value: [{ id: 'att1', name: 'file.txt', contentType: 'text/plain' }],
})
.mockResolvedValueOnce({ body: 'DATA', headers: {} });
await downloadAttachments.call(
mockExecuteFunctions,
{ id: 'MSG1', hasAttachments: true },
'attachment_',
1,
);
const uris = mockRequestWithAuthentication.mock.calls.map(
(call) => (call[1] as IDataObject).uri,
);
expect(uris).toEqual([
'https://graph.microsoft.com/v1.0/users/user2%40example.com/messages/MSG1/attachments',
'https://graph.microsoft.com/v1.0/users/user2%40example.com/messages/MSG1/attachments/att1/$value',
]);
});
it('should fetch MIME content from the mailbox at the passed itemIndex', async () => {
setupSPPerItem();
mockRequestWithAuthentication.mockResolvedValue({
body: 'MIME',
// eslint-disable-next-line @typescript-eslint/naming-convention
headers: { 'content-type': 'message/rfc822' },
});
await getMimeContent.call(mockExecuteFunctions, 'MSG1', 'data', 1);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftEntraServicePrincipalApi',
expect.objectContaining({
uri: 'https://graph.microsoft.com/v1.0/users/user2%40example.com/messages/MSG1/$value',
}),
);
});
});
});
@@ -401,7 +533,7 @@ describe('MicrosoftOutlookV2 - getSubfolders', () => {
{ id: 'folder2', displayName: 'Sent Items', childFolderCount: 0 },
];
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders);
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders, 0);
expect(
mockLoadOptionsFunctions.helpers.requestWithAuthentication as Mock,
@@ -422,7 +554,7 @@ describe('MicrosoftOutlookV2 - getSubfolders', () => {
value: [{ id: 'sub2', displayName: 'Projects', childFolderCount: 0 }],
});
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders);
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders, 0);
expect(
mockLoadOptionsFunctions.helpers.requestWithAuthentication as Mock,
@@ -445,7 +577,7 @@ describe('MicrosoftOutlookV2 - getSubfolders', () => {
value: [{ id: 'q2', displayName: 'Q2', childFolderCount: 0 }],
});
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders, true);
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders, 0, true);
expect(result).toEqual([
{ id: 'inbox', displayName: 'Inbox', childFolderCount: 1 },
@@ -461,7 +593,7 @@ describe('MicrosoftOutlookV2 - getSubfolders', () => {
value: [{ id: 'work', displayName: 'Work', childFolderCount: 0 }],
});
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders);
const result = await getSubfolders.call(mockLoadOptionsFunctions, folders, 0);
expect(result).toEqual([
{ id: 'inbox', displayName: 'Inbox', childFolderCount: 1 },
@@ -57,7 +57,8 @@ export async function getPollResponse(
const results = await Promise.all(
endpoints.map(
async (endpoint) =>
await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, undefined, {
// Poll context: 0 is the transport's fallback read, not an item index.
await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, 0, undefined, {
...qs,
}),
),
@@ -71,7 +72,8 @@ export async function getPollResponse(
const results = await Promise.all(
endpoints.map(
async (endpoint) =>
await microsoftApiRequest.call(this, 'GET', endpoint, undefined, { ...qs }),
// Poll context: 0 is the transport's fallback read, not an item index.
await microsoftApiRequest.call(this, 'GET', endpoint, 0, undefined, { ...qs }),
),
);
responseData = results.flatMap((result) => (result.value as IDataObject[]) ?? []).slice(0, 1);
@@ -85,7 +87,13 @@ export async function getPollResponse(
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject[], prefix);
// Poll context: 0 is the transport's fallback read, not an item index.
executionData = await downloadAttachments.call(
this,
responseData as IDataObject[],
prefix,
0,
);
} else {
executionData = this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -107,7 +107,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
...additionalFields,
};
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -21,7 +21,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
extractValue: true,
}) as string;
await microsoftApiRequest.call(this, 'DELETE', `/calendars/${calendarId}`);
await microsoftApiRequest.call(this, 'DELETE', `/calendars/${calendarId}`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -27,6 +27,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'GET',
`/calendars/${calendarId}`,
index,
undefined,
qs,
);
@@ -62,12 +62,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
'value',
'GET',
endpoint,
index,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
responseData = responseData.value;
}
@@ -99,7 +99,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
...updateFields,
};
const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, body);
const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -53,7 +53,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
body.surname = surname;
}
const responseData = await microsoftApiRequest.call(this, 'POST', '/contacts', body);
const responseData = await microsoftApiRequest.call(this, 'POST', '/contacts', index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -20,7 +20,7 @@ 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}`);
await microsoftApiRequest.call(this, 'DELETE', `/contacts/${contactId}`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -74,6 +74,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'GET',
`/contacts/${contactId}`,
index,
undefined,
qs,
);
@@ -121,12 +121,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
'value',
'GET',
endpoint,
index,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
responseData = responseData.value;
}
@@ -39,6 +39,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'PATCH',
`/contacts/${contactId}`,
index,
body,
);
@@ -244,7 +244,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
);
}
const responseData = await microsoftApiRequest.call(this, 'POST', '/messages', body, {});
const responseData = await microsoftApiRequest.call(this, 'POST', '/messages', index, body, {});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -20,7 +20,7 @@ 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}`);
await microsoftApiRequest.call(this, 'DELETE', `/messages/${draftId}`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -107,7 +107,14 @@ export async function execute(this: IExecuteFunctions, index: number) {
'id,conversationId,subject,bodyPreview,from,toRecipients,categories,hasAttachments';
}
responseData = await microsoftApiRequest.call(this, 'GET', `/messages/${draftId}`, undefined, qs);
responseData = await microsoftApiRequest.call(
this,
'GET',
`/messages/${draftId}`,
index,
undefined,
qs,
);
if (output === 'simple') {
responseData = simplifyOutputMessages([responseData as IDataObject]);
@@ -117,7 +124,12 @@ export async function execute(this: IExecuteFunctions, index: number) {
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
executionData = await downloadAttachments.call(
this,
responseData as IDataObject,
prefix,
index,
);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -37,13 +37,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
.filter((email) => email);
if (recipients.length !== 0) {
await microsoftApiRequest.call(this, 'PATCH', `/messages/${draftId}`, {
await microsoftApiRequest.call(this, 'PATCH', `/messages/${draftId}`, index, {
toRecipients: recipients.map((recipient: string) => makeRecipient(recipient)),
});
}
}
await microsoftApiRequest.call(this, 'POST', `/messages/${draftId}/send`);
await microsoftApiRequest.call(this, 'POST', `/messages/${draftId}/send`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -195,6 +195,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'PATCH',
`/messages/${draftId}`,
index,
body,
{},
);
@@ -286,7 +286,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
...additionalFields,
};
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -24,7 +24,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
}) as string,
);
await microsoftApiRequest.call(this, 'DELETE', `/calendar/events/${eventId}`);
await microsoftApiRequest.call(this, 'DELETE', `/calendar/events/${eventId}`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -75,7 +75,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
const endpoint = `/calendar/events/${eventId}`;
const responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
const responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
@@ -166,7 +166,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
const fromAllCalendars = this.getNodeParameter('fromAllCalendars', index) as boolean;
if (fromAllCalendars) {
const response = await microsoftApiRequest.call(this, 'GET', '/calendars', undefined, {
const response = await microsoftApiRequest.call(this, 'GET', '/calendars', index, undefined, {
$select: 'id',
});
for (const calendar of response.value) {
@@ -192,6 +192,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
'value',
'GET',
endpoint,
index,
undefined,
qs,
);
@@ -201,7 +202,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
if (qs.$top <= 0) break;
const response = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
const response = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
responseData.push(...response.value);
}
}
@@ -280,7 +280,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
const endpoint = `/calendar/events/${eventId}`;
const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, body);
const responseData = await microsoftApiRequest.call(this, 'PATCH', endpoint, index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -56,7 +56,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
endpoint = '/mailFolders';
}
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
const responseData = await microsoftApiRequest.call(this, 'POST', endpoint, index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
@@ -24,7 +24,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
}) as string,
);
await microsoftApiRequest.call(this, 'DELETE', `/mailFolders/${folderId}`);
await microsoftApiRequest.call(this, 'DELETE', `/mailFolders/${folderId}`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -58,6 +58,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'GET',
`/mailFolders/${folderId}`,
index,
{},
qs,
);
@@ -93,15 +93,23 @@ export async function execute(this: IExecuteFunctions, index: number) {
}
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {}, qs);
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
index,
{},
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, {}, qs);
responseData = responseData.value;
}
if (options.includeChildFolders) {
responseData = await getSubfolders.call(this, responseData as IDataObject[]);
responseData = await getSubfolders.call(this, responseData as IDataObject[], index);
}
const executionData = this.helpers.constructExecutionMetaData(
@@ -35,9 +35,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
);
const displayName = this.getNodeParameter('displayName', index, undefined) as string;
const responseData = await microsoftApiRequest.call(this, 'PATCH', `/mailFolders/${folderId}`, {
displayName,
});
const responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/mailFolders/${folderId}`,
index,
{ displayName },
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -274,12 +274,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
'value',
'GET',
endpoint,
index,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
responseData = responseData.value;
}
@@ -291,7 +292,12 @@ export async function execute(this: IExecuteFunctions, index: number) {
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
executionData = await downloadAttachments.call(
this,
responseData as IDataObject,
prefix,
index,
);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
@@ -20,7 +20,7 @@ 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}`);
await microsoftApiRequest.call(this, 'DELETE', `/messages/${messageId}`, index);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -142,6 +142,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'GET',
`/messages/${messageId}`,
index,
undefined,
qs,
);
@@ -154,7 +155,12 @@ export async function execute(this: IExecuteFunctions, index: number) {
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
executionData = await downloadAttachments.call(
this,
responseData as IDataObject,
prefix,
index,
);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -170,6 +176,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
messageId,
binaryPropertyName as string,
index,
outputFileName as string,
);
@@ -284,12 +284,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
'value',
'GET',
endpoint,
index,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
responseData = responseData.value;
}
@@ -301,7 +302,12 @@ export async function execute(this: IExecuteFunctions, index: number) {
if (options.downloadAttachments) {
const prefix = (options.attachmentsPrefix as string) || 'attachment_';
executionData = await downloadAttachments.call(this, responseData as IDataObject, prefix);
executionData = await downloadAttachments.call(
this,
responseData as IDataObject,
prefix,
index,
);
} else {
executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
@@ -32,7 +32,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
destinationId,
};
await microsoftApiRequest.call(this, 'POST', `/messages/${messageId}/move`, body);
await microsoftApiRequest.call(this, 'POST', `/messages/${messageId}/move`, index, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -248,6 +248,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
this,
'POST',
`/messages/${messageId}/${action}`,
index,
body,
);
@@ -283,6 +284,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
this,
'POST',
`/messages/${responseData.id}/attachments`,
index,
attachment,
{},
);
@@ -290,7 +292,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
}
if (!saveAsDraft) {
await microsoftApiRequest.call(this, 'POST', `/messages/${responseData.id}/send`);
await microsoftApiRequest.call(this, 'POST', `/messages/${responseData.id}/send`, index);
}
const executionData = this.helpers.constructExecutionMetaData(
@@ -258,7 +258,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
saveToSentItems,
};
await microsoftApiRequest.call(this, 'POST', '/sendMail', body, {});
await microsoftApiRequest.call(this, 'POST', '/sendMail', index, body, {});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
@@ -56,7 +56,7 @@ export async function execute(this: IExecuteFunctions, index: number, items: INo
const body: IDataObject = { message };
await microsoftApiRequest.call(this, 'POST', '/sendMail', body);
await microsoftApiRequest.call(this, 'POST', '/sendMail', index, body);
return items;
}
@@ -194,6 +194,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'POST',
`/messages/${messageId}/move`,
index,
body,
);
@@ -215,7 +216,14 @@ export async function execute(this: IExecuteFunctions, index: number) {
throw new NodeOperationError(this.getNode(), 'No fields to update got specified');
}
responseData = await microsoftApiRequest.call(this, 'PATCH', `/messages/${messageId}`, body, {});
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/messages/${messageId}`,
index,
body,
{},
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
@@ -91,6 +91,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
this,
'POST',
`/messages/${messageId}/attachments/createUploadSession`,
index,
body,
);
const uploadUrl = responseData.uploadUrl;
@@ -129,6 +130,7 @@ export async function execute(this: IExecuteFunctions, index: number, _: INodeEx
this,
'POST',
`/messages/${messageId}/attachments`,
index,
body,
{},
);
@@ -43,6 +43,7 @@ export async function execute(this: IExecuteFunctions, index: number, items: INo
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
index,
undefined,
{ $select: 'id,name,contentType' },
);
@@ -57,6 +58,7 @@ export async function execute(this: IExecuteFunctions, index: number, items: INo
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}/$value`,
index,
undefined,
{},
undefined,
@@ -83,6 +83,7 @@ export async function execute(this: IExecuteFunctions, index: number) {
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
index,
undefined,
qs,
);
@@ -84,12 +84,13 @@ export async function execute(this: IExecuteFunctions, index: number) {
'value',
'GET',
endpoint,
index,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, index, undefined, qs);
responseData = responseData.value;
}
@@ -1,5 +1,5 @@
import type { IExecuteFunctions, INodeExecutionData, JsonObject } from 'n8n-workflow';
import { NodeApiError, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
import { NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
import * as calendar from './calendar';
import * as contact from './contact';
@@ -11,6 +11,7 @@ import * as message from './message';
import * as messageAttachment from './messageAttachment';
import type { MicrosoftOutlook } from './node.type';
import { configureWaitTillDate } from '../../../../../utils/sendAndWait/configureWaitTillDate.util';
import { stampItemIndexOnError } from '../../../GenericFunctions';
export async function router(this: IExecuteFunctions) {
const items = this.getInputData();
@@ -90,14 +91,8 @@ export async function router(this: IExecuteFunctions) {
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;
// A NodeError from the transport may be missing the itemIndex, add it
throw stampItemIndexOnError(error, i);
}
}
return [returnData];
@@ -17,10 +17,10 @@ export const mailboxRLC: INodeProperties = {
type: 'resourceLocator',
default: { mode: 'id', value: '' },
required: true,
// Resolved once per run (from the first item) and reused for every item, so it accepts a fixed
// value or an expression; for per-item targeting, feed a single item or use Loop Over Items.
// Shared with the trigger: the action node evaluates this per input item; the
// trigger resolves it once per poll.
description:
'The mailbox the Service Principal should act on. Applies to the whole node (every item in the execution).',
'The mailbox the Service Principal should act on. In the action node this is evaluated per input item (an expression can target a different mailbox per item); the trigger resolves it once.',
modes: [
{
displayName: 'By ID',
@@ -3,6 +3,9 @@ import type { IDataObject, ILoadOptionsFunctions, INodeListSearchResult } from '
import { encodeOutlookId } from '../helpers/utils';
import { getSubfolders, microsoftApiRequest } from '../transport';
// listSearch context throughout this file: the transport's trailing `0` is its
// fallback read (getNodeParameter's 2nd arg here is a fallback, not an item index).
async function search(
this: ILoadOptionsFunctions,
resource: string,
@@ -17,6 +20,7 @@ async function search(
this,
'GET',
'',
0,
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
@@ -32,7 +36,7 @@ async function search(
qs.$filter = `contains(${nameProperty}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(this, 'GET', resource, undefined, qs);
response = await microsoftApiRequest.call(this, 'GET', resource, 0, undefined, qs);
}
return {
@@ -74,6 +78,7 @@ export async function searchDrafts(
this,
'GET',
'',
0,
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
@@ -90,7 +95,7 @@ export async function searchDrafts(
qs.$filter += ` AND contains(${'subject'}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(this, 'GET', '/messages', undefined, qs);
response = await microsoftApiRequest.call(this, 'GET', '/messages', 0, undefined, qs);
}
return {
@@ -117,6 +122,7 @@ export async function searchMessages(
this,
'GET',
'',
0,
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
@@ -132,7 +138,7 @@ export async function searchMessages(
qs.$filter = `contains(${'subject'}, '${filterValue}')`;
}
response = await microsoftApiRequest.call(this, 'GET', '/messages', undefined, qs);
response = await microsoftApiRequest.call(this, 'GET', '/messages', 0, undefined, qs);
}
return {
@@ -163,6 +169,7 @@ export async function searchEvents(
this,
'GET',
'',
0,
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
@@ -182,6 +189,7 @@ export async function searchEvents(
this,
'GET',
`/calendars/${calendarId}/events`,
0,
undefined,
qs,
);
@@ -211,6 +219,7 @@ export async function searchFolders(
this,
'GET',
'',
0,
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
@@ -220,10 +229,10 @@ export async function searchFolders(
$top: 100,
};
response = await microsoftApiRequest.call(this, 'GET', '/mailFolders', undefined, qs);
response = await microsoftApiRequest.call(this, 'GET', '/mailFolders', 0, undefined, qs);
}
let folders = await getSubfolders.call(this, response.value as IDataObject[], true);
let folders = await getSubfolders.call(this, response.value as IDataObject[], 0, true);
if (filter) {
filter = filter.toLowerCase();
@@ -259,6 +268,7 @@ export async function searchAttachments(
this,
'GET',
'',
0,
undefined,
undefined,
paginationToken, // paginationToken contains the full URL
@@ -273,6 +283,7 @@ export async function searchAttachments(
this,
'GET',
`/messages/${messageId}/attachments`,
0,
undefined,
qs,
);
@@ -2,6 +2,9 @@ import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
import { getSubfolders, microsoftApiRequestAllItems } from '../transport';
// loadOptions context throughout this file: the transport's trailing `0` is its
// fallback read (getNodeParameter's 2nd arg here is a fallback, not an item index).
export async function getCategoriesNames(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
@@ -11,6 +14,7 @@ export async function getCategoriesNames(
'value',
'GET',
'/outlook/masterCategories',
0,
);
for (const category of categories) {
returnData.push({
@@ -23,8 +27,15 @@ export async function getCategoriesNames(
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, true);
const response = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
'/mailFolders',
0,
{},
);
const folders = await getSubfolders.call(this, response, 0, true);
for (const folder of folders) {
returnData.push({
name: folder.displayName as string,
@@ -43,6 +54,7 @@ export async function getCalendarGroups(
'value',
'GET',
'/calendarGroups',
0,
{},
);
for (const calendar of calendars) {
@@ -40,33 +40,40 @@ export function getOutlookCredentialType(
* credential, app-only Graph has no `/me`, so the node's `mailbox` parameter is
* read, validated, and returned as the bare (un-encoded) UPN/ID.
*
* The mailbox is read at the fixed item index 0 and treated as a per-node constant
* for the whole run — the same contract as the `authentication` selector. Encoding
* happens once at the URL-build site in `microsoftApiRequest`.
* The mailbox accepts expressions and is resolved per item: execute call sites pass
* the loop's item index; poll/loadOptions call sites pass a literal 0 — in those
* contexts `getNodeParameter`'s 2nd arg is a fallback, not an index. Encoding happens
* once at the URL-build site in `microsoftApiRequest`.
*/
export function resolveMailbox(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,
credentialType: OutlookCredentialType,
itemIndex: number,
): string | undefined {
if (credentialType !== 'microsoftEntraServicePrincipalApi') return undefined;
// Read at item index 0 (per-node constant, same as the `authentication` selector).
// loadOptions' getNodeParameter has no itemIndex arg (execute/poll do), so the
// { extractValue: true } overload can't be shared across all three contexts.
// loadOptions'/poll's getNodeParameter has no itemIndex arg (only execute does), so
// the { extractValue: true } overload can't be shared across all three contexts.
// Read the raw param and take the id-mode RLC value directly (this RLC has no
// extractValue regex, so .value is already the bare mailbox id).
const raw = this.getNodeParameter('mailbox', 0);
const raw = this.getNodeParameter('mailbox', itemIndex);
const value = isResourceLocatorValue(raw) ? raw.value : raw;
// A non-string or whitespace-only value collapses to '', which validateMailbox
// reports as the "mailbox required" error (not "not valid").
// reports as the "mailbox required" error (not "not valid"). A validation error
// gets its item index stamped in the router's catch.
const mailbox = (typeof value === 'string' ? value : '').trim();
validateMailbox(mailbox, this.getNode());
return mailbox;
}
// `itemIndex` is REQUIRED (and placed before the defaulted params so call sites
// need no positional padding): execute call sites pass the loop index; non-execute
// call sites (poll/loadOptions) pass a literal 0, where `getNodeParameter`'s 2nd
// arg is a fallback, not an index.
export async function microsoftApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
resource: string,
itemIndex: number,
body: IDataObject = {},
qs: IDataObject = {},
uri?: string,
@@ -82,7 +89,7 @@ export async function microsoftApiRequest(
: 'https://graph.microsoft.com'
).replace(/\/+$/, '');
const mailbox = resolveMailbox.call(this, credentialType);
const mailbox = resolveMailbox.call(this, credentialType, itemIndex);
let apiUrl: string;
if (mailbox !== undefined) {
@@ -127,7 +134,7 @@ export async function microsoftApiRequest(
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);
updatedError = prepareApiError.call(this, error, itemIndex);
} catch (e) {}
if (updatedError) throw updatedError;
@@ -145,6 +152,7 @@ export async function microsoftApiRequestAllItems(
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
itemIndex: number,
body: IDataObject = {},
query: IDataObject = {},
headers: IDataObject = {},
@@ -160,6 +168,7 @@ export async function microsoftApiRequestAllItems(
this,
method,
endpoint,
itemIndex,
body,
nextLink ? undefined : query, // Do not add query parameters as nextLink already contains them
nextLink,
@@ -176,6 +185,7 @@ export async function downloadAttachments(
this: IExecuteFunctions | IPollFunctions,
messages: IDataObject[] | IDataObject,
prefix: string,
itemIndex: number,
) {
const elements: INodeExecutionData[] = [];
if (!Array.isArray(messages)) {
@@ -192,6 +202,7 @@ export async function downloadAttachments(
'value',
'GET',
`/messages/${message.id}/attachments`,
itemIndex,
{},
);
for (const [index, attachment] of attachments.entries()) {
@@ -199,6 +210,7 @@ export async function downloadAttachments(
this,
'GET',
`/messages/${message.id}/attachments/${attachment.id}/$value`,
itemIndex,
undefined,
{},
undefined,
@@ -226,12 +238,14 @@ export async function getMimeContent(
this: IExecuteFunctions,
messageId: string,
binaryPropertyName: string,
itemIndex: number,
outputFileName?: string,
) {
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/$value`,
itemIndex,
undefined,
{},
undefined,
@@ -259,6 +273,7 @@ export async function getMimeContent(
export async function getSubfolders(
this: IExecuteFunctions | ILoadOptionsFunctions,
folders: IDataObject[],
itemIndex: number,
addPathToDisplayName = false,
) {
const returnData: IDataObject[] = [...folders];
@@ -269,6 +284,7 @@ export async function getSubfolders(
'value',
'GET',
`/mailFolders/${folder.id}/childFolders`,
itemIndex,
);
if (addPathToDisplayName) {
@@ -281,7 +297,12 @@ export async function getSubfolders(
}
returnData.push(
...(await getSubfolders.call(this, subfolders as IDataObject[], addPathToDisplayName)),
...(await getSubfolders.call(
this,
subfolders as IDataObject[],
itemIndex,
addPathToDisplayName,
)),
);
}
}