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

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-05-15 15:50:53 +02:00
committed by GitHub
parent bf549301df
commit 68a6c81729
9 changed files with 213 additions and 56 deletions

View File

@@ -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",

View File

@@ -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) => {

View 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);
}
}

View 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!');
});
});