import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } 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, items: 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);
// --- FIX START: Logic updated to match send.operation.ts ---
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;
if (items[index].binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', {
itemIndex: index,
});
}
if (
items[index].binary &&
(items[index].binary as IDataObject)[binaryPropertyName] === undefined
) {
throw new NodeOperationError(
this.getNode(),
`No binary data property "${binaryPropertyName}" does not exists on item!`,
{ itemIndex: index },
);
}
// Use helper to assert data exists
const binaryData = this.helpers.assertBinaryData(index, binaryPropertyName);
let fileBase64;
// Check if data is stored by reference (ID) or value (Data)
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,
});
}
body.attachments = messageAttachments;
}
// --- FIX END ---
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;
}
Bug Description
Hello team,
I have update to 2.0 this morning and now it's impossible to create a draft in outlook with an attachment inside, i get this error 400 :
But if try to use the node outlook to send an email, i get no error with the same exact workflow with same exact attachment.
I asked Gemini 3.0 with the two file and maybe found something that is super simple to patch :
create.operation.ts (not working draft)
send.operation.ts (working send email).
Gemini response :
Details
The problem lies in how the **binary data** (the attachment file) is processed in `create.operation.ts`.The Cause
In modern versions of n8n, binary data is often stored by reference (an
idpointing to a file on disk) rather than keeping the raw Base64 string in memory (in the.dataproperty).send.operation.ts(Working): The code checks ifbinaryData.idexists. If it does, it streams the file, converts it to a buffer, and then to a Base64 string.create.operation.ts(Broken): The code assumesbinaryData.dataalways contains the Base64 string. If n8n has offloaded the data to disk,binaryData.datais empty/undefined. When this is sent to Microsoft Graph, thecontentBytesfield is missing or invalid, causing theUnableToDeserializePostBodyerror (status 400).The Solution
You need to replace the attachment handling logic in
create.operation.tsto match the logic used insend.operation.ts. Specifically, replace the synchronous.map()function with an asynchronousfor...ofloop that correctly handles binary streams.Here is the corrected
create.operation.ts:I really need this node to work, if you need ANY information to patch the issue faster, feel free to ask anythings 😅
To Reproduce
Add an attachment to a draft in outlook
Expected behavior
To be able to use draft node with attachment
Debug Info
Debug info
core
storage
pruning
client
Generated at: 2025-12-17T11:18:18.345Z
Operating System
Debian 12 (bookworm) on host, latest n8n container image from this morning
n8n Version
2.0.2
Node.js Version
22.21.0
Database
SQLite (default)
Execution mode
main (default)
Hosting
self hosted