mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(Email Trigger (IMAP) Node): Handle attachments correctly (#9410)
This commit is contained in:
committed by
GitHub
parent
bf549301df
commit
68a6c81729
@@ -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!');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user