Skip to content

Outlook Draft impossible to add an attachement (2.0 Bug) #23347

Description

@TheNexter

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 :

{ "error": { "code": "UnableToDeserializePostBody", "message": "were unable to deserialize " } }

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 id pointing to a file on disk) rather than keeping the raw Base64 string in memory (in the .data property).

  1. In send.operation.ts (Working): The code checks if binaryData.id exists. If it does, it streams the file, converts it to a buffer, and then to a Base64 string.
  2. In create.operation.ts (Broken): The code assumes binaryData.data always contains the Base64 string. If n8n has offloaded the data to disk, binaryData.data is empty/undefined. When this is sent to Microsoft Graph, the contentBytes field is missing or invalid, causing the UnableToDeserializePostBody error (status 400).

The Solution

You need to replace the attachment handling logic in create.operation.ts to match the logic used in send.operation.ts. Specifically, replace the synchronous .map() function with an asynchronous for...of loop that correctly handles binary streams.

Here is the corrected create.operation.ts:

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;
}

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

  • n8nVersion: 2.0.2
  • platform: docker (self-hosted)
  • nodeJsVersion: 22.21.0
  • nodeEnv: production
  • database: sqlite
  • executionMode: regular
  • concurrency: -1
  • license: enterprise (production)
  • consumerId: f7073216-ac59-411a-8c3b-29a19ba54de0

storage

  • success: all
  • error: all
  • progress: false
  • manual: true
  • binaryMode: filesystem

pruning

  • enabled: true
  • maxAge: 336 hours
  • maxCount: 10000 executions

client

  • userAgent: mozilla/5.0 (x11; linux x86_64; rv:146.0) gecko/20100101 firefox/146.0
  • isTouchDevice: false

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions