mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 18:49:20 +02:00
fix(Send Email Node): Allow non-inline file attachments (#31071)
This commit is contained in:
parent
d6457bd4bc
commit
c1856aff8d
|
|
@ -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' } }];
|
||||
|
|
|
|||
|
|
@ -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. <img src="cid:image_1">',
|
||||
'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. <img src="cid:image_1">. 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) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type EmailSendOptions = {
|
|||
appendAttribution?: boolean;
|
||||
allowUnauthorizedCerts?: boolean;
|
||||
attachments?: string;
|
||||
fileAttachments?: string;
|
||||
ccEmail?: string;
|
||||
bccEmail?: string;
|
||||
replyTo?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user