fix(Send Email Node): Allow non-inline file attachments (#31071)

This commit is contained in:
Michael Kret 2026-05-27 16:51:25 +03:00 committed by GitHub
parent d6457bd4bc
commit c1856aff8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 151 additions and 11 deletions

View File

@ -451,6 +451,124 @@ describe('Test EmailSendV2, send operation', () => {
});
});
describe('file attachments (non-inline)', () => {
it('should attach file attachments without cid', async () => {
const items = [
{
json: { data: 'test' },
binary: {
file1: {
data: 'data1',
mimeType: 'application/pdf',
fileName: 'doc.pdf',
} as IBinaryData,
file2: { data: 'data2', mimeType: 'text/csv', fileName: 'data.csv' } as IBinaryData,
} as Record<string, IBinaryData>,
},
];
mockExecuteFunctions.getInputData.mockReturnValue(items);
mockExecuteFunctions.getNode.mockReturnValue({ typeVersion: 2.0 } as any);
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
mockExecuteFunctions.getCredentials.mockResolvedValue({
host: 'smtp.example.com',
port: 587,
});
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('from@example.com')
.mockReturnValueOnce('to@example.com')
.mockReturnValueOnce('Test Subject')
.mockReturnValueOnce('html')
.mockReturnValueOnce({ fileAttachments: 'file1, file2', appendAttribution: false })
.mockReturnValueOnce('<p>Test HTML</p>');
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockImplementation(
(itemIndex: number, propertyName: string) => {
return items[itemIndex].binary![propertyName];
},
);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockImplementation(
async (itemIndex: number, propertyName: string) => {
return Buffer.from(items[itemIndex].binary![propertyName].data);
},
);
transporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
await sendOperation.execute.call(mockExecuteFunctions);
const callArg = transporter.sendMail.mock.calls[0][0];
expect(callArg.attachments).toEqual([
expect.objectContaining({ filename: 'doc.pdf' }),
expect.objectContaining({ filename: 'data.csv' }),
]);
expect(callArg.attachments[0]).not.toHaveProperty('cid');
expect(callArg.attachments[1]).not.toHaveProperty('cid');
});
it('should combine inline and file attachments', async () => {
const items = [
{
json: { data: 'test' },
binary: {
logo: { data: 'data1', mimeType: 'image/png', fileName: 'logo.png' } as IBinaryData,
report: {
data: 'data2',
mimeType: 'application/pdf',
fileName: 'report.pdf',
} as IBinaryData,
} as Record<string, IBinaryData>,
},
];
mockExecuteFunctions.getInputData.mockReturnValue(items);
mockExecuteFunctions.getNode.mockReturnValue({ typeVersion: 2.0 } as any);
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
mockExecuteFunctions.getCredentials.mockResolvedValue({
host: 'smtp.example.com',
port: 587,
});
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce('from@example.com')
.mockReturnValueOnce('to@example.com')
.mockReturnValueOnce('Test Subject')
.mockReturnValueOnce('html')
.mockReturnValueOnce({
attachments: 'logo',
fileAttachments: 'report',
appendAttribution: false,
})
.mockReturnValueOnce('<p><img src="cid:logo"></p>');
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockImplementation(
(itemIndex: number, propertyName: string) => {
return items[itemIndex].binary![propertyName];
},
);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockImplementation(
async (itemIndex: number, propertyName: string) => {
return Buffer.from(items[itemIndex].binary![propertyName].data);
},
);
transporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
await sendOperation.execute.call(mockExecuteFunctions);
const callArg = transporter.sendMail.mock.calls[0][0];
expect(callArg.attachments).toHaveLength(2);
expect(callArg.attachments[0]).toEqual(
expect.objectContaining({ filename: 'logo.png', cid: 'logo' }),
);
expect(callArg.attachments[1]).toEqual(expect.objectContaining({ filename: 'report.pdf' }));
expect(callArg.attachments[1]).not.toHaveProperty('cid');
});
});
describe('emails without attachments', () => {
it('should send email when no attachments specified', async () => {
const items = [{ json: { data: 'test' } }];

View File

@ -123,12 +123,20 @@ const properties: INodeProperties[] = [
'Whether to include the phrase “This email was sent automatically with n8n” to the end of the email',
},
{
displayName: 'Attachments',
displayName: 'Attachments (Inline)',
name: 'attachments',
type: 'string',
default: '',
description:
'Name of the binary properties that contain data to add to email as attachment. Multiple ones can be comma-separated. Reference embedded images or other content within the body of an email message, e.g. &lt;img src="cid:image_1"&gt;',
'Binary properties to embed in the email body. Multiple ones can be comma-separated. Reference them in HTML via <code>cid:propertyName</code>, e.g. &lt;img src="cid:image_1"&gt;. Use \'Attachments (File)\' for regular file attachments.',
},
{
displayName: 'Attachments (File)',
name: 'fileAttachments',
type: 'string',
default: '',
description:
"Binary properties to attach to the email as regular files. Multiple ones can be comma-separated. They appear in the recipient's attachments list and are not embedded in the body.",
},
{
displayName: 'CC Email',
@ -234,17 +242,30 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
}
}
if (options.attachments && item.binary) {
if ((options.attachments || options.fileAttachments) && item.binary) {
const attachments = [];
const attachmentProperties = prepareBinariesDataList(options.attachments);
for (const propertyName of attachmentProperties) {
const binaryData = this.helpers.assertBinaryData(itemIndex, propertyName);
attachments.push({
filename: binaryData.fileName || 'unknown',
content: await this.helpers.getBinaryDataBuffer(itemIndex, propertyName),
cid: propertyName,
});
if (options.attachments) {
const inlineProperties = prepareBinariesDataList(options.attachments);
for (const propertyName of inlineProperties) {
const binaryData = this.helpers.assertBinaryData(itemIndex, propertyName);
attachments.push({
filename: binaryData.fileName || 'unknown',
content: await this.helpers.getBinaryDataBuffer(itemIndex, propertyName),
cid: propertyName,
});
}
}
if (options.fileAttachments) {
const fileProperties = prepareBinariesDataList(options.fileAttachments);
for (const propertyName of fileProperties) {
const binaryData = this.helpers.assertBinaryData(itemIndex, propertyName);
attachments.push({
filename: binaryData.fileName || 'unknown',
content: await this.helpers.getBinaryDataBuffer(itemIndex, propertyName),
});
}
}
if (attachments.length) {

View File

@ -11,6 +11,7 @@ export type EmailSendOptions = {
appendAttribution?: boolean;
allowUnauthorizedCerts?: boolean;
attachments?: string;
fileAttachments?: string;
ccEmail?: string;
bccEmail?: string;
replyTo?: string;