@carlgo11/simpleimfparser 0.0.1-a → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -674
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/parsers/parseEnvelope.ts → dist/parsers/parseEnvelope.d.ts} +2 -5
- package/dist/parsers/parseEnvelope.d.ts.map +1 -0
- package/dist/parsers/parseHeaders.d.ts +10 -0
- package/dist/parsers/parseHeaders.d.ts.map +1 -0
- package/dist/parsers/parseMessage.d.ts +11 -0
- package/dist/parsers/parseMessage.d.ts.map +1 -0
- package/dist/types/Attachment.d.ts +56 -0
- package/dist/types/Attachment.d.ts.map +1 -0
- package/dist/types/Email.d.ts +114 -0
- package/dist/types/Email.d.ts.map +1 -0
- package/dist/types/Envelope.d.ts +45 -0
- package/dist/types/Envelope.d.ts.map +1 -0
- package/dist/types/Sender.d.ts +75 -0
- package/dist/types/Sender.d.ts.map +1 -0
- package/dist/utils/streamToString.d.ts +8 -0
- package/dist/utils/streamToString.d.ts.map +1 -0
- package/package.json +12 -4
- package/.github/workflows/publish.yaml +0 -51
- package/.github/workflows/test.yaml +0 -14
- package/.gitignore +0 -6
- package/dist/index.js +0 -8
- package/dist/parsers/parseEnvelope.js +0 -9
- package/dist/parsers/parseHeaders.js +0 -65
- package/dist/parsers/parseMessage.js +0 -30
- package/dist/types/Attachment.js +0 -1
- package/dist/types/Email.js +0 -55
- package/dist/types/Envelope.js +0 -1
- package/dist/types/Sender.js +0 -1
- package/dist/utils/streamToString.js +0 -13
- package/src/index.ts +0 -14
- package/src/parsers/parseHeaders.ts +0 -65
- package/src/parsers/parseMessage.ts +0 -32
- package/src/types/Attachment.ts +0 -6
- package/src/types/Email.ts +0 -77
- package/src/types/Envelope.ts +0 -9
- package/src/types/Sender.ts +0 -10
- package/src/utils/streamToString.ts +0 -15
- package/test/index.test.ts +0 -40
- package/test/parsers/parseHeaders.test.ts +0 -32
- package/test/parsers/parseMessage.test.ts +0 -34
- package/tsconfig.json +0 -15
package/dist/types/Email.js
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
export class Email {
|
2
|
-
envelope;
|
3
|
-
headers;
|
4
|
-
body;
|
5
|
-
attachments;
|
6
|
-
constructor(envelope, headers, body, attachments) {
|
7
|
-
this.envelope = envelope;
|
8
|
-
this.headers = headers;
|
9
|
-
this.body = body;
|
10
|
-
this.attachments = attachments || [];
|
11
|
-
}
|
12
|
-
/**
|
13
|
-
* Get a header value (case-insensitive).
|
14
|
-
*/
|
15
|
-
getHeader(name) {
|
16
|
-
return this.headers[name.toLowerCase()];
|
17
|
-
}
|
18
|
-
/**
|
19
|
-
* Add or update a header.
|
20
|
-
*/
|
21
|
-
addHeader(name, value) {
|
22
|
-
const key = name.toLowerCase();
|
23
|
-
if (this.headers[key]) {
|
24
|
-
if (Array.isArray(this.headers[key])) {
|
25
|
-
this.headers[key].push(value);
|
26
|
-
}
|
27
|
-
else {
|
28
|
-
this.headers[key] = [this.headers[key], value];
|
29
|
-
}
|
30
|
-
}
|
31
|
-
else {
|
32
|
-
this.headers[key] = value;
|
33
|
-
}
|
34
|
-
}
|
35
|
-
/**
|
36
|
-
* Remove a header.
|
37
|
-
*/
|
38
|
-
removeHeader(name) {
|
39
|
-
delete this.headers[name.toLowerCase()];
|
40
|
-
}
|
41
|
-
/**
|
42
|
-
* Get the email body (prefers text, fallback to HTML).
|
43
|
-
*/
|
44
|
-
getBody() {
|
45
|
-
return this.body || '';
|
46
|
-
}
|
47
|
-
toString() {
|
48
|
-
let _headers = '';
|
49
|
-
Object.entries(this.headers).forEach(([k, v]) => _headers += `${camelize(k)}: ${v}\r\n`);
|
50
|
-
return `${_headers}\r\n\r\n${this.body}`;
|
51
|
-
}
|
52
|
-
}
|
53
|
-
function camelize(str) {
|
54
|
-
return str.replace(/(\w)(\w*)/g, function (g0, g1, g2) { return g1.toUpperCase() + g2.toLowerCase(); });
|
55
|
-
}
|
package/dist/types/Envelope.js
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export {};
|
package/dist/types/Sender.js
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export {};
|
@@ -1,13 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Converts a Readable Stream to a string.
|
3
|
-
* @param stream - Input stream
|
4
|
-
* @returns Promise resolving to the string content
|
5
|
-
*/
|
6
|
-
export default async function streamToString(stream) {
|
7
|
-
return new Promise((resolve, reject) => {
|
8
|
-
let data = '';
|
9
|
-
stream.on('data', chunk => (data += chunk));
|
10
|
-
stream.on('end', () => resolve(data));
|
11
|
-
stream.on('error', reject);
|
12
|
-
});
|
13
|
-
}
|
package/src/index.ts
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
import { Readable } from 'node:stream';
|
2
|
-
import { Email } from './types/Email.js';
|
3
|
-
import parseEnvelope from './parsers/parseEnvelope.js';
|
4
|
-
import parseMessage from './parsers/parseMessage.js';
|
5
|
-
|
6
|
-
export default async function parseEmail(
|
7
|
-
rawData: string | Readable,
|
8
|
-
envelope: object,
|
9
|
-
): Promise<Email> {
|
10
|
-
const parsedEnvelope = await parseEnvelope(envelope);
|
11
|
-
const { headers, body } = await parseMessage(rawData);
|
12
|
-
|
13
|
-
return new Email(parsedEnvelope, headers, body);
|
14
|
-
}
|
@@ -1,65 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Parses headers from a raw IMF header string.
|
3
|
-
* @param rawHeaders - The raw headers as a string
|
4
|
-
* @returns A dictionary of headers (handling duplicates)
|
5
|
-
*/
|
6
|
-
export default function parseHeaders(rawHeaders: string): Record<string, string | string[]> {
|
7
|
-
const headers: Record<string, string | string[]> = {};
|
8
|
-
const lines = rawHeaders.split(/\r\n|\n/);
|
9
|
-
|
10
|
-
let currentKey: string | null = null;
|
11
|
-
let currentValue: string = '';
|
12
|
-
|
13
|
-
for (const line of lines) {
|
14
|
-
if (line.startsWith(' ') || line.startsWith('\t')) {
|
15
|
-
// Handle folded headers (continuation lines)
|
16
|
-
if (currentKey) {
|
17
|
-
if (Array.isArray(headers[currentKey])) {
|
18
|
-
(headers[currentKey] as string[])[(headers[currentKey] as string[]).length - 1] += ' ' + line.trim();
|
19
|
-
} else {
|
20
|
-
headers[currentKey] += ' ' + line.trim();
|
21
|
-
}
|
22
|
-
} else {
|
23
|
-
throw new Error('Header folding found without a preceding header field.');
|
24
|
-
}
|
25
|
-
} else {
|
26
|
-
// Store previous header before starting a new one
|
27
|
-
if (currentKey) {
|
28
|
-
if (headers[currentKey]) {
|
29
|
-
if (Array.isArray(headers[currentKey])) {
|
30
|
-
(headers[currentKey] as string[]).push(currentValue);
|
31
|
-
} else {
|
32
|
-
headers[currentKey] = [headers[currentKey] as string, currentValue];
|
33
|
-
}
|
34
|
-
} else {
|
35
|
-
headers[currentKey] = currentValue;
|
36
|
-
}
|
37
|
-
}
|
38
|
-
|
39
|
-
// Extract new header key-value pair
|
40
|
-
const match = line.match(/^([!#$%&'*+\-.0-9A-Z^_`a-z|~]+):\s*(.*)$/);
|
41
|
-
|
42
|
-
if (!match) {
|
43
|
-
throw new Error(`Invalid header format: "${line}"`);
|
44
|
-
}
|
45
|
-
|
46
|
-
currentKey = match[1].toLowerCase(); // Convert key to lowercase
|
47
|
-
currentValue = match[2];
|
48
|
-
}
|
49
|
-
}
|
50
|
-
|
51
|
-
// Store the last header
|
52
|
-
if (currentKey) {
|
53
|
-
if (headers[currentKey]) {
|
54
|
-
if (Array.isArray(headers[currentKey])) {
|
55
|
-
(headers[currentKey] as string[]).push(currentValue);
|
56
|
-
} else {
|
57
|
-
headers[currentKey] = [headers[currentKey] as string, currentValue];
|
58
|
-
}
|
59
|
-
} else {
|
60
|
-
headers[currentKey] = currentValue;
|
61
|
-
}
|
62
|
-
}
|
63
|
-
|
64
|
-
return headers;
|
65
|
-
}
|
@@ -1,32 +0,0 @@
|
|
1
|
-
import { Readable } from 'node:stream';
|
2
|
-
import parseHeaders from './parseHeaders.js';
|
3
|
-
import streamToString from '../utils/streamToString.js';
|
4
|
-
|
5
|
-
/**
|
6
|
-
* Parses the raw email message (headers + body).
|
7
|
-
* @param rawData - Raw IMF email (string or stream)
|
8
|
-
* @returns Parsed email components
|
9
|
-
*/
|
10
|
-
export default async function parseMessage(rawData: string | Readable): Promise<{ headers: Record<string, string | string[]>, body: string }> {
|
11
|
-
let rawEmail: string;
|
12
|
-
|
13
|
-
if (typeof rawData === 'string') {
|
14
|
-
rawEmail = rawData;
|
15
|
-
} else if (rawData instanceof Readable) {
|
16
|
-
rawEmail = await streamToString(rawData);
|
17
|
-
} else {
|
18
|
-
throw new TypeError('Invalid rawData type. Expected a string or a Readable stream.');
|
19
|
-
}
|
20
|
-
|
21
|
-
const headerBoundary = rawEmail.indexOf('\r\n\r\n');
|
22
|
-
const rawHeaders = rawEmail.slice(0, headerBoundary);
|
23
|
-
const body = rawEmail.slice(headerBoundary + 4)
|
24
|
-
if (body.length === 0) {
|
25
|
-
throw new Error('Header and Body must be separated by \\r\\n\\r\\n');
|
26
|
-
}
|
27
|
-
|
28
|
-
return {
|
29
|
-
headers: parseHeaders(rawHeaders),
|
30
|
-
body,
|
31
|
-
};
|
32
|
-
}
|
package/src/types/Attachment.ts
DELETED
package/src/types/Email.ts
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
import Envelope from './Envelope.js';
|
2
|
-
|
3
|
-
export class Email {
|
4
|
-
envelope: Envelope;
|
5
|
-
headers: Record<string, string | string[]>;
|
6
|
-
body: string;
|
7
|
-
attachments?: Attachment[];
|
8
|
-
|
9
|
-
constructor(
|
10
|
-
envelope: Envelope,
|
11
|
-
headers: Record<string, string | string[]>,
|
12
|
-
body: string,
|
13
|
-
attachments?: Attachment[],
|
14
|
-
) {
|
15
|
-
this.envelope = envelope;
|
16
|
-
this.headers = headers;
|
17
|
-
this.body = body;
|
18
|
-
this.attachments = attachments || [];
|
19
|
-
}
|
20
|
-
|
21
|
-
/**
|
22
|
-
* Get a header value (case-insensitive).
|
23
|
-
*/
|
24
|
-
getHeader(name: string): string | string[] | undefined {
|
25
|
-
return this.headers[name.toLowerCase()];
|
26
|
-
}
|
27
|
-
|
28
|
-
/**
|
29
|
-
* Add or update a header.
|
30
|
-
*/
|
31
|
-
addHeader(name: string, value: string): void {
|
32
|
-
const key = name.toLowerCase();
|
33
|
-
if (this.headers[key]) {
|
34
|
-
if (Array.isArray(this.headers[key])) {
|
35
|
-
(this.headers[key] as string[]).push(value);
|
36
|
-
} else {
|
37
|
-
this.headers[key] = [this.headers[key] as string, value];
|
38
|
-
}
|
39
|
-
} else {
|
40
|
-
this.headers[key] = value;
|
41
|
-
}
|
42
|
-
}
|
43
|
-
|
44
|
-
/**
|
45
|
-
* Remove a header.
|
46
|
-
*/
|
47
|
-
removeHeader(name: string): void {
|
48
|
-
delete this.headers[name.toLowerCase()];
|
49
|
-
}
|
50
|
-
|
51
|
-
/**
|
52
|
-
* Get the email body (prefers text, fallback to HTML).
|
53
|
-
*/
|
54
|
-
getBody(): string {
|
55
|
-
return this.body || '';
|
56
|
-
}
|
57
|
-
|
58
|
-
toString(): string {
|
59
|
-
let _headers = ''
|
60
|
-
Object.entries(this.headers).forEach(([k, v]) => _headers += `${camelize(k)}: ${v}\r\n`)
|
61
|
-
|
62
|
-
|
63
|
-
return `${_headers}\r\n\r\n${this.body}`;
|
64
|
-
}
|
65
|
-
}
|
66
|
-
|
67
|
-
function camelize(str: string):string {
|
68
|
-
return str.replace(/(\w)(\w*)/g,
|
69
|
-
function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();});
|
70
|
-
}
|
71
|
-
|
72
|
-
export interface Attachment {
|
73
|
-
filename: string;
|
74
|
-
contentType: string;
|
75
|
-
size: number;
|
76
|
-
content: Buffer;
|
77
|
-
}
|
package/src/types/Envelope.ts
DELETED
package/src/types/Sender.ts
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
import { Readable } from 'node:stream';
|
2
|
-
|
3
|
-
/**
|
4
|
-
* Converts a Readable Stream to a string.
|
5
|
-
* @param stream - Input stream
|
6
|
-
* @returns Promise resolving to the string content
|
7
|
-
*/
|
8
|
-
export default async function streamToString(stream: Readable): Promise<string> {
|
9
|
-
return new Promise((resolve, reject) => {
|
10
|
-
let data = '';
|
11
|
-
stream.on('data', chunk => (data += chunk));
|
12
|
-
stream.on('end', () => resolve(data));
|
13
|
-
stream.on('error', reject);
|
14
|
-
});
|
15
|
-
}
|
package/test/index.test.ts
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
import { describe, it } from 'node:test';
|
2
|
-
import { faker } from '@faker-js/faker';
|
3
|
-
import assert from 'node:assert';
|
4
|
-
import parseEmail from '../src';
|
5
|
-
import { Readable } from 'node:stream';
|
6
|
-
|
7
|
-
describe('simpleIMFParser', () => {
|
8
|
-
it('Should parse valid message (string)', async () => {
|
9
|
-
const headers: Record<string, string> = {
|
10
|
-
'Subject': faker.lorem.sentence(),
|
11
|
-
'MESSAGE-ID': `${faker.string.uuid()}@${faker.internet.domainName()}`,
|
12
|
-
'from': `"${faker.person.fullName()}" <${faker.internet.email()}>`,
|
13
|
-
'To': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
14
|
-
};
|
15
|
-
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
16
|
-
const body = faker.lorem.paragraphs(5, '\r\n');
|
17
|
-
|
18
|
-
const full_email = `${headersString}\r\n\r\n${body}`;
|
19
|
-
const email = await parseEmail(full_email, {});
|
20
|
-
assert.ok(email.toString());
|
21
|
-
});
|
22
|
-
|
23
|
-
it('Should parse valid message (stream)', async () => {
|
24
|
-
const headers: Record<string, string> = {
|
25
|
-
'from': `"${faker.person.fullName()}" <${faker.internet.email()}>`,
|
26
|
-
'To': `${faker.person.fullName()} <${faker.internet.email({allowSpecialCharacters: true})}>`,
|
27
|
-
'Subject': faker.lorem.sentence(),
|
28
|
-
'MESSAGE-ID': `${faker.string.uuid()}@${faker.internet.domainName()}`,
|
29
|
-
};
|
30
|
-
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
31
|
-
const body = faker.lorem.paragraphs(5, '\r\n');
|
32
|
-
|
33
|
-
const full_email = `${headersString}\r\n\r\n${body}`;
|
34
|
-
|
35
|
-
const email = await parseEmail(Readable.from(full_email), {});
|
36
|
-
assert.ok(email.toString());
|
37
|
-
});
|
38
|
-
|
39
|
-
|
40
|
-
});
|
@@ -1,32 +0,0 @@
|
|
1
|
-
import test, { describe, it } from 'node:test';
|
2
|
-
import { faker } from '@faker-js/faker';
|
3
|
-
import assert from 'node:assert';
|
4
|
-
import parseMessage from '../../src/parsers/parseMessage';
|
5
|
-
import parseHeaders from '../../src/parsers/parseHeaders';
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
describe('Parse Headers', () => {
|
10
|
-
it('Various case header names', () => {
|
11
|
-
const headers: Record<string, string> = {
|
12
|
-
'subject': faker.lorem.sentence(),
|
13
|
-
'MESSAGE-ID': `${faker.string.uuid()}@${faker.internet.domainName()}`,
|
14
|
-
'FroM': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
15
|
-
'to': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
16
|
-
};
|
17
|
-
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
18
|
-
assert.ok(parseHeaders(headersString));
|
19
|
-
});
|
20
|
-
/*
|
21
|
-
it('Extra text after email address', async () => {
|
22
|
-
const headers: Record<string, string> = {
|
23
|
-
'To': `${faker.person.fullName()} <${faker.internet.email()}> ${faker.lorem.sentence()}`,
|
24
|
-
};
|
25
|
-
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
26
|
-
await assert.rejects(async () => parseHeaders(headersString));
|
27
|
-
});*/
|
28
|
-
|
29
|
-
it('Missing : separator.', async () => {
|
30
|
-
await assert.rejects(async () => parseHeaders(`Subject ${faker.lorem.sentence()}`));
|
31
|
-
});
|
32
|
-
});
|
@@ -1,34 +0,0 @@
|
|
1
|
-
import { describe, it } from 'node:test';
|
2
|
-
import { faker } from '@faker-js/faker';
|
3
|
-
import assert from 'node:assert';
|
4
|
-
import parseMessage from '../../src/parsers/parseMessage';
|
5
|
-
|
6
|
-
describe('Parse Message', () => {
|
7
|
-
it('Should parse valid message', async () => {
|
8
|
-
const headers: Record<string, string> = {
|
9
|
-
'Subject': faker.lorem.sentence(),
|
10
|
-
'Message-ID': `${faker.string.uuid()}@${faker.internet.domainName()}`,
|
11
|
-
'From': `"${faker.person.fullName()}" <${faker.internet.email()}>`,
|
12
|
-
'To': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
13
|
-
};
|
14
|
-
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
15
|
-
const body = faker.lorem.paragraphs(5).replace('\n', '\r\n');
|
16
|
-
|
17
|
-
const full_email = `${headersString}\r\n\r\n${body}`;
|
18
|
-
assert.ok(await parseMessage(full_email));
|
19
|
-
});
|
20
|
-
|
21
|
-
it('Should reject invalid message divider', async () => {
|
22
|
-
const headers: Record<string, string> = {
|
23
|
-
'Subject': faker.lorem.sentence(),
|
24
|
-
'Message-ID': `${faker.string.uuid()}@${faker.internet.domainName()}`,
|
25
|
-
'From': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
26
|
-
'To': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
27
|
-
};
|
28
|
-
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
29
|
-
const body = faker.lorem.paragraphs(5).replace('\n', '\r\n');
|
30
|
-
|
31
|
-
const full_email = `${headersString}\r\n${body}`;
|
32
|
-
await assert.rejects(() => parseMessage(full_email));
|
33
|
-
});
|
34
|
-
});
|
package/tsconfig.json
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"compilerOptions": {
|
3
|
-
"target": "ES2024",
|
4
|
-
"module": "nodenext",
|
5
|
-
"esModuleInterop": true,
|
6
|
-
"forceConsistentCasingInFileNames": true,
|
7
|
-
"strict": true,
|
8
|
-
"skipLibCheck": true,
|
9
|
-
"outDir": "./dist"
|
10
|
-
},
|
11
|
-
"include": [
|
12
|
-
"./src/**/*",
|
13
|
-
"./src/*"
|
14
|
-
]
|
15
|
-
}
|