mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-27 14:13:33 +03:00
fix(Email Trigger (IMAP) Node): Handle attachments correctly (#9410)
This commit is contained in:
parent
bf549301df
commit
68a6c81729
@ -92,7 +92,8 @@
|
||||
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
||||
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
||||
"@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch",
|
||||
"vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch"
|
||||
"vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch",
|
||||
"@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "echo \"Error: no test created yet\""
|
||||
"test": "jest"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
|
@ -2,13 +2,10 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type Imap from 'imap';
|
||||
import { type ImapMessage } from 'imap';
|
||||
import * as qp from 'quoted-printable';
|
||||
import * as iconvlite from 'iconv-lite';
|
||||
import * as utf8 from 'utf8';
|
||||
import * as uuencode from 'uuencode';
|
||||
|
||||
import { getMessage } from './helpers/getMessage';
|
||||
import type { Message, MessagePart } from './types';
|
||||
import { PartData } from './PartData';
|
||||
|
||||
const IMAP_EVENTS = ['alert', 'mail', 'expunge', 'uidvalidity', 'update', 'close', 'end'] as const;
|
||||
|
||||
@ -124,7 +121,7 @@ export class ImapSimple extends EventEmitter {
|
||||
/** The message part to be downloaded, from the `message.attributes.struct` Array */
|
||||
part: MessagePart,
|
||||
) {
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
return await new Promise<PartData>((resolve, reject) => {
|
||||
const fetch = this.imap.fetch(message.attributes.uid, {
|
||||
bodies: [part.partID],
|
||||
struct: true,
|
||||
@ -138,43 +135,8 @@ export class ImapSimple extends EventEmitter {
|
||||
}
|
||||
|
||||
const data = result.parts[0].body as string;
|
||||
|
||||
const encoding = part.encoding.toUpperCase();
|
||||
|
||||
if (encoding === 'BASE64') {
|
||||
resolve(Buffer.from(data, 'base64').toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === 'QUOTED-PRINTABLE') {
|
||||
if (part.params?.charset?.toUpperCase() === 'UTF-8') {
|
||||
resolve(Buffer.from(utf8.decode(qp.decode(data))).toString());
|
||||
} else {
|
||||
resolve(Buffer.from(qp.decode(data)).toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === '7BIT') {
|
||||
resolve(Buffer.from(data).toString('ascii'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === '8BIT' || encoding === 'BINARY') {
|
||||
const charset = part.params?.charset ?? 'utf-8';
|
||||
resolve(iconvlite.decode(Buffer.from(data), charset));
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === 'UUENCODE') {
|
||||
const parts = data.toString().split('\n'); // remove newline characters
|
||||
const merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string
|
||||
resolve(uuencode.decode(merged));
|
||||
return;
|
||||
}
|
||||
|
||||
// if it gets here, the encoding is not currently supported
|
||||
reject(new Error('Unknown encoding ' + part.encoding));
|
||||
resolve(PartData.fromData(data, encoding));
|
||||
};
|
||||
|
||||
const fetchOnError = (error: Error) => {
|
||||
|
84
packages/@n8n/imap/src/PartData.ts
Normal file
84
packages/@n8n/imap/src/PartData.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import * as qp from 'quoted-printable';
|
||||
import * as iconvlite from 'iconv-lite';
|
||||
import * as utf8 from 'utf8';
|
||||
import * as uuencode from 'uuencode';
|
||||
|
||||
export abstract class PartData {
|
||||
constructor(readonly buffer: Buffer) {}
|
||||
|
||||
toString() {
|
||||
return this.buffer.toString();
|
||||
}
|
||||
|
||||
static fromData(data: string, encoding: string, charset?: string): PartData {
|
||||
if (encoding === 'BASE64') {
|
||||
return new Base64PartData(data);
|
||||
}
|
||||
|
||||
if (encoding === 'QUOTED-PRINTABLE') {
|
||||
return new QuotedPrintablePartData(data, charset);
|
||||
}
|
||||
|
||||
if (encoding === '7BIT') {
|
||||
return new SevenBitPartData(data);
|
||||
}
|
||||
|
||||
if (encoding === '8BIT' || encoding === 'BINARY') {
|
||||
return new BinaryPartData(data, charset);
|
||||
}
|
||||
|
||||
if (encoding === 'UUENCODE') {
|
||||
return new UuencodedPartData(data);
|
||||
}
|
||||
|
||||
// if it gets here, the encoding is not currently supported
|
||||
throw new Error('Unknown encoding ' + encoding);
|
||||
}
|
||||
}
|
||||
|
||||
export class Base64PartData extends PartData {
|
||||
constructor(data: string) {
|
||||
super(Buffer.from(data, 'base64'));
|
||||
}
|
||||
}
|
||||
|
||||
export class QuotedPrintablePartData extends PartData {
|
||||
constructor(data: string, charset?: string) {
|
||||
const decoded =
|
||||
charset?.toUpperCase() === 'UTF-8' ? utf8.decode(qp.decode(data)) : qp.decode(data);
|
||||
super(Buffer.from(decoded));
|
||||
}
|
||||
}
|
||||
|
||||
export class SevenBitPartData extends PartData {
|
||||
constructor(data: string) {
|
||||
super(Buffer.from(data));
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.buffer.toString('ascii');
|
||||
}
|
||||
}
|
||||
|
||||
export class BinaryPartData extends PartData {
|
||||
constructor(
|
||||
data: string,
|
||||
readonly charset: string = 'utf-8',
|
||||
) {
|
||||
super(Buffer.from(data));
|
||||
}
|
||||
|
||||
toString() {
|
||||
return iconvlite.decode(this.buffer, this.charset);
|
||||
}
|
||||
}
|
||||
|
||||
export class UuencodedPartData extends PartData {
|
||||
constructor(data: string) {
|
||||
const parts = data.split('\n'); // remove newline characters
|
||||
const merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string
|
||||
const decoded = uuencode.decode(merged);
|
||||
super(decoded);
|
||||
}
|
||||
}
|
88
packages/@n8n/imap/test/PartData.test.ts
Normal file
88
packages/@n8n/imap/test/PartData.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {
|
||||
PartData,
|
||||
Base64PartData,
|
||||
QuotedPrintablePartData,
|
||||
SevenBitPartData,
|
||||
BinaryPartData,
|
||||
UuencodedPartData,
|
||||
} from '../src/PartData';
|
||||
|
||||
describe('PartData', () => {
|
||||
describe('fromData', () => {
|
||||
it('should return an instance of Base64PartData when encoding is BASE64', () => {
|
||||
const result = PartData.fromData('data', 'BASE64');
|
||||
expect(result).toBeInstanceOf(Base64PartData);
|
||||
});
|
||||
|
||||
it('should return an instance of QuotedPrintablePartData when encoding is QUOTED-PRINTABLE', () => {
|
||||
const result = PartData.fromData('data', 'QUOTED-PRINTABLE');
|
||||
expect(result).toBeInstanceOf(QuotedPrintablePartData);
|
||||
});
|
||||
|
||||
it('should return an instance of SevenBitPartData when encoding is 7BIT', () => {
|
||||
const result = PartData.fromData('data', '7BIT');
|
||||
expect(result).toBeInstanceOf(SevenBitPartData);
|
||||
});
|
||||
|
||||
it('should return an instance of BinaryPartData when encoding is 8BIT or BINARY', () => {
|
||||
let result = PartData.fromData('data', '8BIT');
|
||||
expect(result).toBeInstanceOf(BinaryPartData);
|
||||
result = PartData.fromData('data', 'BINARY');
|
||||
expect(result).toBeInstanceOf(BinaryPartData);
|
||||
});
|
||||
|
||||
it('should return an instance of UuencodedPartData when encoding is UUENCODE', () => {
|
||||
const result = PartData.fromData('data', 'UUENCODE');
|
||||
expect(result).toBeInstanceOf(UuencodedPartData);
|
||||
});
|
||||
|
||||
it('should throw an error when encoding is not supported', () => {
|
||||
expect(() => PartData.fromData('data', 'UNSUPPORTED')).toThrow(
|
||||
'Unknown encoding UNSUPPORTED',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Base64PartData', () => {
|
||||
it('should correctly decode base64 data', () => {
|
||||
const data = Buffer.from('Hello, world!', 'utf-8').toString('base64');
|
||||
const partData = new Base64PartData(data);
|
||||
expect(partData.toString()).toBe('Hello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('QuotedPrintablePartData', () => {
|
||||
it('should correctly decode quoted-printable data', () => {
|
||||
const data = '=48=65=6C=6C=6F=2C=20=77=6F=72=6C=64=21'; // 'Hello, world!' in quoted-printable
|
||||
const partData = new QuotedPrintablePartData(data);
|
||||
expect(partData.toString()).toBe('Hello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SevenBitPartData', () => {
|
||||
it('should correctly decode 7bit data', () => {
|
||||
const data = 'Hello, world!';
|
||||
const partData = new SevenBitPartData(data);
|
||||
expect(partData.toString()).toBe('Hello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BinaryPartData', () => {
|
||||
it('should correctly decode binary data', () => {
|
||||
const data = Buffer.from('Hello, world!', 'utf-8').toString();
|
||||
const partData = new BinaryPartData(data);
|
||||
expect(partData.toString()).toBe('Hello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UuencodedPartData', () => {
|
||||
it('should correctly decode uuencoded data', () => {
|
||||
const data = Buffer.from(
|
||||
'YmVnaW4gNjQ0IGRhdGEKLTImNUw7JlxMKCc9TzxGUUQoMGBgCmAKZW5kCg==',
|
||||
'base64',
|
||||
).toString('binary');
|
||||
const partData = new UuencodedPartData(data);
|
||||
expect(partData.toString()).toBe('Hello, world!');
|
||||
});
|
||||
});
|
@ -285,7 +285,7 @@ export class EmailReadImapV1 implements INodeType {
|
||||
|
||||
// Returns the email text
|
||||
|
||||
const getText = async (parts: any[], message: Message, subtype: string) => {
|
||||
const getText = async (parts: any[], message: Message, subtype: string): Promise<string> => {
|
||||
if (!message.attributes.struct) {
|
||||
return '';
|
||||
}
|
||||
@ -296,12 +296,14 @@ export class EmailReadImapV1 implements INodeType {
|
||||
);
|
||||
});
|
||||
|
||||
if (textParts.length === 0) {
|
||||
const part = textParts[0];
|
||||
if (!part) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return await connection.getPartData(message, textParts[0]);
|
||||
const partData = await connection.getPartData(message, part);
|
||||
return partData.toString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@ -330,7 +332,7 @@ export class EmailReadImapV1 implements INodeType {
|
||||
.then(async (partData) => {
|
||||
// Return it in the format n8n expects
|
||||
return await this.helpers.prepareBinaryData(
|
||||
Buffer.from(partData),
|
||||
partData.buffer,
|
||||
attachmentPart.disposition.params.filename as string,
|
||||
);
|
||||
});
|
||||
|
@ -298,7 +298,11 @@ export class EmailReadImapV2 implements INodeType {
|
||||
|
||||
// Returns the email text
|
||||
|
||||
const getText = async (parts: MessagePart[], message: Message, subtype: string) => {
|
||||
const getText = async (
|
||||
parts: MessagePart[],
|
||||
message: Message,
|
||||
subtype: string,
|
||||
): Promise<string> => {
|
||||
if (!message.attributes.struct) {
|
||||
return '';
|
||||
}
|
||||
@ -309,12 +313,14 @@ export class EmailReadImapV2 implements INodeType {
|
||||
);
|
||||
});
|
||||
|
||||
if (textParts.length === 0) {
|
||||
const part = textParts[0];
|
||||
if (!part) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return await connection.getPartData(message, textParts[0]);
|
||||
const partData = await connection.getPartData(message, part);
|
||||
return partData.toString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@ -355,7 +361,7 @@ export class EmailReadImapV2 implements INodeType {
|
||||
?.filename as string,
|
||||
);
|
||||
// Return it in the format n8n expects
|
||||
return await this.helpers.prepareBinaryData(Buffer.from(partData), fileName);
|
||||
return await this.helpers.prepareBinaryData(partData.buffer, fileName);
|
||||
});
|
||||
|
||||
attachmentPromises.push(attachmentPromise);
|
||||
|
10
patches/@types__uuencode@0.0.3.patch
Normal file
10
patches/@types__uuencode@0.0.3.patch
Normal file
@ -0,0 +1,10 @@
|
||||
diff --git a/index.d.ts b/index.d.ts
|
||||
index f8f89c567f394a538018bfdf11c28dc15e9c9fdc..f3d1cd426711f1f714744474604bd7e321073983 100644
|
||||
--- a/index.d.ts
|
||||
+++ b/index.d.ts
|
||||
@@ -1,4 +1,4 @@
|
||||
/// <reference types="node"/>
|
||||
|
||||
-export function decode(str: string | Buffer): string;
|
||||
+export function decode(str: string | Buffer): Buffer;
|
||||
export function encode(str: string | Buffer): string;
|
@ -25,6 +25,9 @@ patchedDependencies:
|
||||
'@types/express-serve-static-core@4.17.43':
|
||||
hash: 5orrj4qleu2iko5t27vl44u4we
|
||||
path: patches/@types__express-serve-static-core@4.17.43.patch
|
||||
'@types/uuencode@0.0.3':
|
||||
hash: 3i7wecddkama6vhpu5o37g24u4
|
||||
path: patches/@types__uuencode@0.0.3.patch
|
||||
'@types/ws@8.5.4':
|
||||
hash: nbzuqaoyqbrfwipijj5qriqqju
|
||||
path: patches/@types__ws@8.5.4.patch
|
||||
@ -227,7 +230,7 @@ importers:
|
||||
version: 3.0.3
|
||||
'@types/uuencode':
|
||||
specifier: ^0.0.3
|
||||
version: 0.0.3
|
||||
version: 0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4)
|
||||
|
||||
packages/@n8n/nodes-langchain:
|
||||
dependencies:
|
||||
@ -9258,7 +9261,7 @@ packages:
|
||||
ts-dedent: 2.2.0
|
||||
type-fest: 2.19.0
|
||||
vue: 3.4.21(typescript@5.4.2)
|
||||
vue-component-type-helpers: 2.0.17
|
||||
vue-component-type-helpers: 2.0.18
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
@ -10161,11 +10164,12 @@ packages:
|
||||
resolution: {integrity: sha512-+lqLGxWZsEe4Z6OrzBI7Ym4SMUTaMS5yOrHZ0/IL0bpIye1Qbs4PpobJL2mLDbftUXlPFZR7fu6d1yM+bHLX1w==}
|
||||
dev: true
|
||||
|
||||
/@types/uuencode@0.0.3:
|
||||
/@types/uuencode@0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4):
|
||||
resolution: {integrity: sha512-NaBWHPPQvcXqiSaMAGa2Ea/XaFcK/nHwGe2akwJBXRLkCNa2+izx/F1aKJrzFH+L68D88VLYIATTYP7B2k4zVA==}
|
||||
dependencies:
|
||||
'@types/node': 18.16.16
|
||||
dev: true
|
||||
patched: true
|
||||
|
||||
/@types/uuid@8.3.4:
|
||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||
@ -24340,8 +24344,8 @@ packages:
|
||||
resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
|
||||
dev: true
|
||||
|
||||
/vue-component-type-helpers@2.0.17:
|
||||
resolution: {integrity: sha512-2car49m8ciqg/JjgMBkx7o/Fd2A7fHESxNqL/2vJYFLXm4VwYO4yH0rexOi4a35vwNgDyvt17B07Vj126l9rAQ==}
|
||||
/vue-component-type-helpers@2.0.18:
|
||||
resolution: {integrity: sha512-zi1QaDBhSb3oeHJh55aTCrosFNKEQsOL9j3XCAjpF9dwxDUUtd85RkJVzO+YpJqy1LNoCWLU8gwuZ7HW2iDN/A==}
|
||||
dev: true
|
||||
|
||||
/vue-demi@0.14.5(vue@3.4.21):
|
||||
|
Loading…
Reference in New Issue
Block a user