Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Email Trigger (IMAP) Node): Handle attachments correctly #9410

Merged
merged 2 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
2 changes: 1 addition & 1 deletion packages/@n8n/imap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 3 additions & 41 deletions packages/@n8n/imap/src/ImapSimple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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) => {
Expand Down
83 changes: 83 additions & 0 deletions packages/@n8n/imap/src/PartData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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);

Check failure on line 15 in packages/@n8n/imap/src/PartData.ts

View workflow job for this annotation

GitHub Actions / Lint changes

'Base64PartData' was used before it was defined
}

if (encoding === 'QUOTED-PRINTABLE') {
return new QuotedPrintablePartData(data, charset);

Check failure on line 19 in packages/@n8n/imap/src/PartData.ts

View workflow job for this annotation

GitHub Actions / Lint changes

'QuotedPrintablePartData' was used before it was defined
}

if (encoding === '7BIT') {
return new SevenBitPartData(data);

Check failure on line 23 in packages/@n8n/imap/src/PartData.ts

View workflow job for this annotation

GitHub Actions / Lint changes

'SevenBitPartData' was used before it was defined
}

if (encoding === '8BIT' || encoding === 'BINARY') {
return new BinaryPartData(data, charset);

Check failure on line 27 in packages/@n8n/imap/src/PartData.ts

View workflow job for this annotation

GitHub Actions / Lint changes

'BinaryPartData' was used before it was defined
}

if (encoding === 'UUENCODE') {
return new UuencodedPartData(data);

Check failure on line 31 in packages/@n8n/imap/src/PartData.ts

View workflow job for this annotation

GitHub Actions / Lint changes

'UuencodedPartData' was used before it was defined
}

// 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 changes: 88 additions & 0 deletions packages/@n8n/imap/test/PartData.test.ts
Original file line number Diff line number Diff line change
@@ -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!');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand All @@ -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 '';
}
Expand Down Expand Up @@ -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,
);
});
Expand Down
14 changes: 10 additions & 4 deletions packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand All @@ -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 '';
}
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions patches/@types__uuencode@0.0.3.patch
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 9 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading