@carlgo11/simpleimfparser 0.0.0 → 0.0.1
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/.github/workflows/publish.yaml +51 -0
- package/.github/workflows/test.yaml +14 -0
- package/.gitignore +1 -0
- package/dist/index.js +14 -0
- package/dist/parsers/parseEnvelope.js +12 -0
- package/dist/parsers/parseHeaders.js +68 -0
- package/dist/parsers/parseMessage.js +36 -0
- package/dist/types/Attachment.js +2 -0
- package/dist/types/Email.js +59 -0
- package/dist/types/Envelope.js +2 -0
- package/dist/types/Sender.js +2 -0
- package/dist/utils/streamToString.js +16 -0
- package/package.json +18 -3
- package/src/index.js +14 -0
- package/src/parsers/parseEnvelope.js +12 -0
- package/src/parsers/parseHeaders.js +68 -0
- package/src/parsers/parseMessage.js +36 -0
- package/src/types/Attachment.js +2 -0
- package/src/types/Email.js +59 -0
- package/src/types/Envelope.js +2 -0
- package/src/types/Sender.js +2 -0
- package/src/utils/streamToString.js +16 -0
- package/test/index.test.js +38 -0
- package/test/index.test.ts +21 -2
- package/test/parsers/parseHeaders.test.js +32 -0
- package/test/parsers/parseMessage.test.js +35 -0
- package/tsconfig.json +8 -2
@@ -0,0 +1,51 @@
|
|
1
|
+
name: Publish package
|
2
|
+
on:
|
3
|
+
release:
|
4
|
+
types:
|
5
|
+
- published
|
6
|
+
jobs:
|
7
|
+
test:
|
8
|
+
name: Test code
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
steps:
|
11
|
+
- uses: actions/checkout@v4
|
12
|
+
- uses: actions/setup-node@v4
|
13
|
+
with:
|
14
|
+
node-version: 20
|
15
|
+
- run: npm install
|
16
|
+
- run: npm test
|
17
|
+
|
18
|
+
publish:
|
19
|
+
name: Publish package
|
20
|
+
needs: test
|
21
|
+
runs-on: ubuntu-latest
|
22
|
+
permissions:
|
23
|
+
contents: read
|
24
|
+
packages: write
|
25
|
+
strategy:
|
26
|
+
matrix:
|
27
|
+
registry:
|
28
|
+
- gpr
|
29
|
+
- npm
|
30
|
+
steps:
|
31
|
+
- uses: actions/checkout@v4
|
32
|
+
- uses: actions/setup-node@v4
|
33
|
+
- uses: actions/setup-node@v4
|
34
|
+
with:
|
35
|
+
node-version: 22
|
36
|
+
registry-url: ${{ matrix.registry == 'gpr' && 'https://npm.pkg.github.com/' || 'https://registry.npmjs.org/' }}
|
37
|
+
- name: Extract version from tag
|
38
|
+
id: extract_version
|
39
|
+
run: |
|
40
|
+
VERSION=${GITHUB_REF#refs/tags/v}
|
41
|
+
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
42
|
+
- name: Update package.json version
|
43
|
+
run: |
|
44
|
+
jq ".version = \"$VERSION\"" package.json > package.tmp.json
|
45
|
+
mv package.tmp.json package.json
|
46
|
+
- run: npm install
|
47
|
+
- run: npm run build
|
48
|
+
- name: Publish package
|
49
|
+
run: npm publish
|
50
|
+
env:
|
51
|
+
NODE_AUTH_TOKEN: ${{ matrix.registry == 'gpr' && secrets.GITHUB_TOKEN || secrets.npm_token }}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
name: Test package
|
2
|
+
on: push
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
name: Test package
|
6
|
+
runs-on: ubuntu-latest
|
7
|
+
steps:
|
8
|
+
- uses: actions/checkout@v4
|
9
|
+
- uses: actions/setup-node@v4
|
10
|
+
with:
|
11
|
+
node-version: 20
|
12
|
+
- run: npm install
|
13
|
+
- run: npm test
|
14
|
+
- run: npm run build
|
package/.gitignore
CHANGED
package/dist/index.js
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.default = parseEmail;
|
7
|
+
const Email_1 = require("./types/Email");
|
8
|
+
const parseEnvelope_1 = __importDefault(require("./parsers/parseEnvelope"));
|
9
|
+
const parseMessage_1 = __importDefault(require("./parsers/parseMessage"));
|
10
|
+
async function parseEmail(rawData, envelope) {
|
11
|
+
const parsedEnvelope = await (0, parseEnvelope_1.default)(envelope);
|
12
|
+
const { headers, body } = await (0, parseMessage_1.default)(rawData);
|
13
|
+
return new Email_1.Email(parsedEnvelope, headers, body);
|
14
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = parseEnvelope;
|
4
|
+
/**
|
5
|
+
* Parses envelope metadata from the SMTP transaction.
|
6
|
+
* @param envelope - Envelope data object
|
7
|
+
* @returns Parsed Envelope object
|
8
|
+
*/
|
9
|
+
async function parseEnvelope(envelope) {
|
10
|
+
// TODO: Implement envelope parsing logic
|
11
|
+
return envelope;
|
12
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = parseHeaders;
|
4
|
+
/**
|
5
|
+
* Parses headers from a raw IMF header string.
|
6
|
+
* @param rawHeaders - The raw headers as a string
|
7
|
+
* @returns A dictionary of headers (handling duplicates)
|
8
|
+
*/
|
9
|
+
function parseHeaders(rawHeaders) {
|
10
|
+
const headers = {};
|
11
|
+
const lines = rawHeaders.split(/\r\n|\n/);
|
12
|
+
let currentKey = null;
|
13
|
+
let currentValue = '';
|
14
|
+
for (const line of lines) {
|
15
|
+
if (line.startsWith(' ') || line.startsWith('\t')) {
|
16
|
+
// Handle folded headers (continuation lines)
|
17
|
+
if (currentKey) {
|
18
|
+
if (Array.isArray(headers[currentKey])) {
|
19
|
+
headers[currentKey][headers[currentKey].length - 1] += ' ' + line.trim();
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
headers[currentKey] += ' ' + line.trim();
|
23
|
+
}
|
24
|
+
}
|
25
|
+
else {
|
26
|
+
throw new Error('Header folding found without a preceding header field.');
|
27
|
+
}
|
28
|
+
}
|
29
|
+
else {
|
30
|
+
// Store previous header before starting a new one
|
31
|
+
if (currentKey) {
|
32
|
+
if (headers[currentKey]) {
|
33
|
+
if (Array.isArray(headers[currentKey])) {
|
34
|
+
headers[currentKey].push(currentValue);
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
headers[currentKey] = [headers[currentKey], currentValue];
|
38
|
+
}
|
39
|
+
}
|
40
|
+
else {
|
41
|
+
headers[currentKey] = currentValue;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
// Extract new header key-value pair
|
45
|
+
const match = line.match(/^([!#$%&'*+\-.0-9A-Z^_`a-z|~]+):\s*(.*)$/);
|
46
|
+
if (!match) {
|
47
|
+
throw new Error(`Invalid header format: "${line}"`);
|
48
|
+
}
|
49
|
+
currentKey = match[1].toLowerCase(); // Convert key to lowercase
|
50
|
+
currentValue = match[2];
|
51
|
+
}
|
52
|
+
}
|
53
|
+
// Store the last header
|
54
|
+
if (currentKey) {
|
55
|
+
if (headers[currentKey]) {
|
56
|
+
if (Array.isArray(headers[currentKey])) {
|
57
|
+
headers[currentKey].push(currentValue);
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
headers[currentKey] = [headers[currentKey], currentValue];
|
61
|
+
}
|
62
|
+
}
|
63
|
+
else {
|
64
|
+
headers[currentKey] = currentValue;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
return headers;
|
68
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.default = parseMessage;
|
7
|
+
const node_stream_1 = require("node:stream");
|
8
|
+
const parseHeaders_1 = __importDefault(require("./parseHeaders"));
|
9
|
+
const streamToString_1 = __importDefault(require("../utils/streamToString"));
|
10
|
+
/**
|
11
|
+
* Parses the raw email message (headers + body).
|
12
|
+
* @param rawData - Raw IMF email (string or stream)
|
13
|
+
* @returns Parsed email components
|
14
|
+
*/
|
15
|
+
async function parseMessage(rawData) {
|
16
|
+
let rawEmail;
|
17
|
+
if (typeof rawData === 'string') {
|
18
|
+
rawEmail = rawData;
|
19
|
+
}
|
20
|
+
else if (rawData instanceof node_stream_1.Readable) {
|
21
|
+
rawEmail = await (0, streamToString_1.default)(rawData);
|
22
|
+
}
|
23
|
+
else {
|
24
|
+
throw new TypeError('Invalid rawData type. Expected a string or a Readable stream.');
|
25
|
+
}
|
26
|
+
const headerBoundary = rawEmail.indexOf('\r\n\r\n');
|
27
|
+
const rawHeaders = rawEmail.slice(0, headerBoundary);
|
28
|
+
const body = rawEmail.slice(headerBoundary + 4);
|
29
|
+
if (body.length === 0) {
|
30
|
+
throw new Error('Header and Body must be separated by \\r\\n\\r\\n');
|
31
|
+
}
|
32
|
+
return {
|
33
|
+
headers: (0, parseHeaders_1.default)(rawHeaders),
|
34
|
+
body,
|
35
|
+
};
|
36
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.Email = void 0;
|
4
|
+
class Email {
|
5
|
+
envelope;
|
6
|
+
headers;
|
7
|
+
body;
|
8
|
+
attachments;
|
9
|
+
constructor(envelope, headers, body, attachments) {
|
10
|
+
this.envelope = envelope;
|
11
|
+
this.headers = headers;
|
12
|
+
this.body = body;
|
13
|
+
this.attachments = attachments || [];
|
14
|
+
}
|
15
|
+
/**
|
16
|
+
* Get a header value (case-insensitive).
|
17
|
+
*/
|
18
|
+
getHeader(name) {
|
19
|
+
return this.headers[name.toLowerCase()];
|
20
|
+
}
|
21
|
+
/**
|
22
|
+
* Add or update a header.
|
23
|
+
*/
|
24
|
+
addHeader(name, value) {
|
25
|
+
const key = name.toLowerCase();
|
26
|
+
if (this.headers[key]) {
|
27
|
+
if (Array.isArray(this.headers[key])) {
|
28
|
+
this.headers[key].push(value);
|
29
|
+
}
|
30
|
+
else {
|
31
|
+
this.headers[key] = [this.headers[key], value];
|
32
|
+
}
|
33
|
+
}
|
34
|
+
else {
|
35
|
+
this.headers[key] = value;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
/**
|
39
|
+
* Remove a header.
|
40
|
+
*/
|
41
|
+
removeHeader(name) {
|
42
|
+
delete this.headers[name.toLowerCase()];
|
43
|
+
}
|
44
|
+
/**
|
45
|
+
* Get the email body (prefers text, fallback to HTML).
|
46
|
+
*/
|
47
|
+
getBody() {
|
48
|
+
return this.body || '';
|
49
|
+
}
|
50
|
+
toString() {
|
51
|
+
let _headers = '';
|
52
|
+
Object.entries(this.headers).forEach(([k, v]) => _headers += `${camelize(k)}: ${v}\r\n`);
|
53
|
+
return `${_headers}\r\n\r\n${this.body}`;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
exports.Email = Email;
|
57
|
+
function camelize(str) {
|
58
|
+
return str.replace(/(\w)(\w*)/g, function (g0, g1, g2) { return g1.toUpperCase() + g2.toLowerCase(); });
|
59
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = streamToString;
|
4
|
+
/**
|
5
|
+
* Converts a Readable Stream to a string.
|
6
|
+
* @param stream - Input stream
|
7
|
+
* @returns Promise resolving to the string content
|
8
|
+
*/
|
9
|
+
async function streamToString(stream) {
|
10
|
+
return new Promise((resolve, reject) => {
|
11
|
+
let data = '';
|
12
|
+
stream.on('data', chunk => (data += chunk));
|
13
|
+
stream.on('end', () => resolve(data));
|
14
|
+
stream.on('error', reject);
|
15
|
+
});
|
16
|
+
}
|
package/package.json
CHANGED
@@ -1,11 +1,23 @@
|
|
1
1
|
{
|
2
2
|
"type": "module",
|
3
|
-
"main": "
|
3
|
+
"main": "dist/index.js",
|
4
4
|
"name": "@carlgo11/simpleimfparser",
|
5
|
-
"
|
5
|
+
"description": "Simple IMF parser",
|
6
|
+
"version": "0.0.1",
|
7
|
+
"license": "GPL-3.0",
|
8
|
+
"author": "Carlgo11",
|
9
|
+
"repository": {
|
10
|
+
"type": "git",
|
11
|
+
"url": "https://github.com/carlgo11/SimpleIMFParser.git"
|
12
|
+
},
|
13
|
+
"bugs": {
|
14
|
+
"url": "https://github.com/carlgo11/SimpleIMFParser/issues"
|
15
|
+
},
|
16
|
+
"homepage": "https://github.com/carlgo11/SimpleIMFParser#readme",
|
6
17
|
"scripts": {
|
7
18
|
"start": "node src/index.ts",
|
8
|
-
"test": "node --import tsx --test test/*.test.ts test/**/*.test.ts"
|
19
|
+
"test": "node --import tsx --test test/*.test.ts test/**/*.test.ts",
|
20
|
+
"build": "npx tsc"
|
9
21
|
},
|
10
22
|
"private": false,
|
11
23
|
"devDependencies": {
|
@@ -14,5 +26,8 @@
|
|
14
26
|
"ts-node": "^10.9.2",
|
15
27
|
"tsx": "^4.19.2",
|
16
28
|
"typescript": "^5.7.3"
|
29
|
+
},
|
30
|
+
"engines": {
|
31
|
+
"node": ">=20"
|
17
32
|
}
|
18
33
|
}
|
package/src/index.js
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.default = parseEmail;
|
7
|
+
const Email_1 = require("./types/Email");
|
8
|
+
const parseEnvelope_1 = __importDefault(require("./parsers/parseEnvelope"));
|
9
|
+
const parseMessage_1 = __importDefault(require("./parsers/parseMessage"));
|
10
|
+
async function parseEmail(rawData, envelope) {
|
11
|
+
const parsedEnvelope = await (0, parseEnvelope_1.default)(envelope);
|
12
|
+
const { headers, body } = await (0, parseMessage_1.default)(rawData);
|
13
|
+
return new Email_1.Email(parsedEnvelope, headers, body);
|
14
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = parseEnvelope;
|
4
|
+
/**
|
5
|
+
* Parses envelope metadata from the SMTP transaction.
|
6
|
+
* @param envelope - Envelope data object
|
7
|
+
* @returns Parsed Envelope object
|
8
|
+
*/
|
9
|
+
async function parseEnvelope(envelope) {
|
10
|
+
// TODO: Implement envelope parsing logic
|
11
|
+
return envelope;
|
12
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = parseHeaders;
|
4
|
+
/**
|
5
|
+
* Parses headers from a raw IMF header string.
|
6
|
+
* @param rawHeaders - The raw headers as a string
|
7
|
+
* @returns A dictionary of headers (handling duplicates)
|
8
|
+
*/
|
9
|
+
function parseHeaders(rawHeaders) {
|
10
|
+
const headers = {};
|
11
|
+
const lines = rawHeaders.split(/\r\n|\n/);
|
12
|
+
let currentKey = null;
|
13
|
+
let currentValue = '';
|
14
|
+
for (const line of lines) {
|
15
|
+
if (line.startsWith(' ') || line.startsWith('\t')) {
|
16
|
+
// Handle folded headers (continuation lines)
|
17
|
+
if (currentKey) {
|
18
|
+
if (Array.isArray(headers[currentKey])) {
|
19
|
+
headers[currentKey][headers[currentKey].length - 1] += ' ' + line.trim();
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
headers[currentKey] += ' ' + line.trim();
|
23
|
+
}
|
24
|
+
}
|
25
|
+
else {
|
26
|
+
throw new Error('Header folding found without a preceding header field.');
|
27
|
+
}
|
28
|
+
}
|
29
|
+
else {
|
30
|
+
// Store previous header before starting a new one
|
31
|
+
if (currentKey) {
|
32
|
+
if (headers[currentKey]) {
|
33
|
+
if (Array.isArray(headers[currentKey])) {
|
34
|
+
headers[currentKey].push(currentValue);
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
headers[currentKey] = [headers[currentKey], currentValue];
|
38
|
+
}
|
39
|
+
}
|
40
|
+
else {
|
41
|
+
headers[currentKey] = currentValue;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
// Extract new header key-value pair
|
45
|
+
const match = line.match(/^([!#$%&'*+\-.0-9A-Z^_`a-z|~]+):\s*(.*)$/);
|
46
|
+
if (!match) {
|
47
|
+
throw new Error(`Invalid header format: "${line}"`);
|
48
|
+
}
|
49
|
+
currentKey = match[1].toLowerCase(); // Convert key to lowercase
|
50
|
+
currentValue = match[2];
|
51
|
+
}
|
52
|
+
}
|
53
|
+
// Store the last header
|
54
|
+
if (currentKey) {
|
55
|
+
if (headers[currentKey]) {
|
56
|
+
if (Array.isArray(headers[currentKey])) {
|
57
|
+
headers[currentKey].push(currentValue);
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
headers[currentKey] = [headers[currentKey], currentValue];
|
61
|
+
}
|
62
|
+
}
|
63
|
+
else {
|
64
|
+
headers[currentKey] = currentValue;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
return headers;
|
68
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.default = parseMessage;
|
7
|
+
const node_stream_1 = require("node:stream");
|
8
|
+
const parseHeaders_1 = __importDefault(require("./parseHeaders"));
|
9
|
+
const streamToString_1 = __importDefault(require("../utils/streamToString"));
|
10
|
+
/**
|
11
|
+
* Parses the raw email message (headers + body).
|
12
|
+
* @param rawData - Raw IMF email (string or stream)
|
13
|
+
* @returns Parsed email components
|
14
|
+
*/
|
15
|
+
async function parseMessage(rawData) {
|
16
|
+
let rawEmail;
|
17
|
+
if (typeof rawData === 'string') {
|
18
|
+
rawEmail = rawData;
|
19
|
+
}
|
20
|
+
else if (rawData instanceof node_stream_1.Readable) {
|
21
|
+
rawEmail = await (0, streamToString_1.default)(rawData);
|
22
|
+
}
|
23
|
+
else {
|
24
|
+
throw new TypeError('Invalid rawData type. Expected a string or a Readable stream.');
|
25
|
+
}
|
26
|
+
const headerBoundary = rawEmail.indexOf('\r\n\r\n');
|
27
|
+
const rawHeaders = rawEmail.slice(0, headerBoundary);
|
28
|
+
const body = rawEmail.slice(headerBoundary + 4);
|
29
|
+
if (body.length === 0) {
|
30
|
+
throw new Error('Header and Body must be separated by \\r\\n\\r\\n');
|
31
|
+
}
|
32
|
+
return {
|
33
|
+
headers: (0, parseHeaders_1.default)(rawHeaders),
|
34
|
+
body,
|
35
|
+
};
|
36
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.Email = void 0;
|
4
|
+
class Email {
|
5
|
+
envelope;
|
6
|
+
headers;
|
7
|
+
body;
|
8
|
+
attachments;
|
9
|
+
constructor(envelope, headers, body, attachments) {
|
10
|
+
this.envelope = envelope;
|
11
|
+
this.headers = headers;
|
12
|
+
this.body = body;
|
13
|
+
this.attachments = attachments || [];
|
14
|
+
}
|
15
|
+
/**
|
16
|
+
* Get a header value (case-insensitive).
|
17
|
+
*/
|
18
|
+
getHeader(name) {
|
19
|
+
return this.headers[name.toLowerCase()];
|
20
|
+
}
|
21
|
+
/**
|
22
|
+
* Add or update a header.
|
23
|
+
*/
|
24
|
+
addHeader(name, value) {
|
25
|
+
const key = name.toLowerCase();
|
26
|
+
if (this.headers[key]) {
|
27
|
+
if (Array.isArray(this.headers[key])) {
|
28
|
+
this.headers[key].push(value);
|
29
|
+
}
|
30
|
+
else {
|
31
|
+
this.headers[key] = [this.headers[key], value];
|
32
|
+
}
|
33
|
+
}
|
34
|
+
else {
|
35
|
+
this.headers[key] = value;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
/**
|
39
|
+
* Remove a header.
|
40
|
+
*/
|
41
|
+
removeHeader(name) {
|
42
|
+
delete this.headers[name.toLowerCase()];
|
43
|
+
}
|
44
|
+
/**
|
45
|
+
* Get the email body (prefers text, fallback to HTML).
|
46
|
+
*/
|
47
|
+
getBody() {
|
48
|
+
return this.body || '';
|
49
|
+
}
|
50
|
+
toString() {
|
51
|
+
let _headers = '';
|
52
|
+
Object.entries(this.headers).forEach(([k, v]) => _headers += `${camelize(k)}: ${v}\r\n`);
|
53
|
+
return `${_headers}\r\n\r\n${this.body}`;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
exports.Email = Email;
|
57
|
+
function camelize(str) {
|
58
|
+
return str.replace(/(\w)(\w*)/g, function (g0, g1, g2) { return g1.toUpperCase() + g2.toLowerCase(); });
|
59
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = streamToString;
|
4
|
+
/**
|
5
|
+
* Converts a Readable Stream to a string.
|
6
|
+
* @param stream - Input stream
|
7
|
+
* @returns Promise resolving to the string content
|
8
|
+
*/
|
9
|
+
async function streamToString(stream) {
|
10
|
+
return new Promise((resolve, reject) => {
|
11
|
+
let data = '';
|
12
|
+
stream.on('data', chunk => (data += chunk));
|
13
|
+
stream.on('end', () => resolve(data));
|
14
|
+
stream.on('error', reject);
|
15
|
+
});
|
16
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const node_test_1 = require("node:test");
|
7
|
+
const faker_1 = require("@faker-js/faker");
|
8
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
9
|
+
const src_1 = __importDefault(require("../src"));
|
10
|
+
const node_stream_1 = require("node:stream");
|
11
|
+
(0, node_test_1.describe)('simpleIMFParser', () => {
|
12
|
+
(0, node_test_1.it)('Should parse valid message (string)', async () => {
|
13
|
+
const headers = {
|
14
|
+
'Subject': faker_1.faker.lorem.sentence(),
|
15
|
+
'MESSAGE-ID': `${faker_1.faker.string.uuid()}@${faker_1.faker.internet.domainName()}`,
|
16
|
+
'from': `"${faker_1.faker.person.fullName()}" <${faker_1.faker.internet.email()}>`,
|
17
|
+
'To': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email()}>`,
|
18
|
+
};
|
19
|
+
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
20
|
+
const body = faker_1.faker.lorem.paragraphs(5, '\r\n');
|
21
|
+
const full_email = `${headersString}\r\n\r\n${body}`;
|
22
|
+
const email = await (0, src_1.default)(full_email, {});
|
23
|
+
node_assert_1.default.ok(email.toString());
|
24
|
+
});
|
25
|
+
(0, node_test_1.it)('Should parse valid message (stream)', async () => {
|
26
|
+
const headers = {
|
27
|
+
'from': `"${faker_1.faker.person.fullName()}" <${faker_1.faker.internet.email()}>`,
|
28
|
+
'To': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email({ allowSpecialCharacters: true })}>`,
|
29
|
+
'Subject': faker_1.faker.lorem.sentence(),
|
30
|
+
'MESSAGE-ID': `${faker_1.faker.string.uuid()}@${faker_1.faker.internet.domainName()}`,
|
31
|
+
};
|
32
|
+
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
33
|
+
const body = faker_1.faker.lorem.paragraphs(5, '\r\n');
|
34
|
+
const full_email = `${headersString}\r\n\r\n${body}`;
|
35
|
+
const email = await (0, src_1.default)(node_stream_1.Readable.from(full_email), {});
|
36
|
+
node_assert_1.default.ok(email.toString());
|
37
|
+
});
|
38
|
+
});
|
package/test/index.test.ts
CHANGED
@@ -2,9 +2,10 @@ import { describe, it } from 'node:test';
|
|
2
2
|
import { faker } from '@faker-js/faker';
|
3
3
|
import assert from 'node:assert';
|
4
4
|
import parseEmail from '../src';
|
5
|
+
import { Readable } from 'node:stream';
|
5
6
|
|
6
7
|
describe('simpleIMFParser', () => {
|
7
|
-
it('Should parse valid message', async () => {
|
8
|
+
it('Should parse valid message (string)', async () => {
|
8
9
|
const headers: Record<string, string> = {
|
9
10
|
'Subject': faker.lorem.sentence(),
|
10
11
|
'MESSAGE-ID': `${faker.string.uuid()}@${faker.internet.domainName()}`,
|
@@ -12,10 +13,28 @@ describe('simpleIMFParser', () => {
|
|
12
13
|
'To': `${faker.person.fullName()} <${faker.internet.email()}>`,
|
13
14
|
};
|
14
15
|
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
15
|
-
const body = faker.lorem.paragraphs(5
|
16
|
+
const body = faker.lorem.paragraphs(5, '\r\n');
|
16
17
|
|
17
18
|
const full_email = `${headersString}\r\n\r\n${body}`;
|
18
19
|
const email = await parseEmail(full_email, {});
|
19
20
|
assert.ok(email.toString());
|
20
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
|
+
|
21
40
|
});
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const node_test_1 = require("node:test");
|
7
|
+
const faker_1 = require("@faker-js/faker");
|
8
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
9
|
+
const parseHeaders_1 = __importDefault(require("../../src/parsers/parseHeaders"));
|
10
|
+
(0, node_test_1.describe)('Parse Headers', () => {
|
11
|
+
(0, node_test_1.it)('Various case header names', () => {
|
12
|
+
const headers = {
|
13
|
+
'subject': faker_1.faker.lorem.sentence(),
|
14
|
+
'MESSAGE-ID': `${faker_1.faker.string.uuid()}@${faker_1.faker.internet.domainName()}`,
|
15
|
+
'FroM': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email()}>`,
|
16
|
+
'to': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email()}>`,
|
17
|
+
};
|
18
|
+
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
19
|
+
node_assert_1.default.ok((0, parseHeaders_1.default)(headersString));
|
20
|
+
});
|
21
|
+
/*
|
22
|
+
it('Extra text after email address', async () => {
|
23
|
+
const headers: Record<string, string> = {
|
24
|
+
'To': `${faker.person.fullName()} <${faker.internet.email()}> ${faker.lorem.sentence()}`,
|
25
|
+
};
|
26
|
+
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
27
|
+
await assert.rejects(async () => parseHeaders(headersString));
|
28
|
+
});*/
|
29
|
+
(0, node_test_1.it)('Missing : separator.', async () => {
|
30
|
+
await node_assert_1.default.rejects(async () => (0, parseHeaders_1.default)(`Subject ${faker_1.faker.lorem.sentence()}`));
|
31
|
+
});
|
32
|
+
});
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const node_test_1 = require("node:test");
|
7
|
+
const faker_1 = require("@faker-js/faker");
|
8
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
9
|
+
const parseMessage_1 = __importDefault(require("../../src/parsers/parseMessage"));
|
10
|
+
(0, node_test_1.describe)('Parse Message', () => {
|
11
|
+
(0, node_test_1.it)('Should parse valid message', async () => {
|
12
|
+
const headers = {
|
13
|
+
'Subject': faker_1.faker.lorem.sentence(),
|
14
|
+
'Message-ID': `${faker_1.faker.string.uuid()}@${faker_1.faker.internet.domainName()}`,
|
15
|
+
'From': `"${faker_1.faker.person.fullName()}" <${faker_1.faker.internet.email()}>`,
|
16
|
+
'To': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email()}>`,
|
17
|
+
};
|
18
|
+
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
19
|
+
const body = faker_1.faker.lorem.paragraphs(5).replace('\n', '\r\n');
|
20
|
+
const full_email = `${headersString}\r\n\r\n${body}`;
|
21
|
+
node_assert_1.default.ok(await (0, parseMessage_1.default)(full_email));
|
22
|
+
});
|
23
|
+
(0, node_test_1.it)('Should reject invalid message divider', async () => {
|
24
|
+
const headers = {
|
25
|
+
'Subject': faker_1.faker.lorem.sentence(),
|
26
|
+
'Message-ID': `${faker_1.faker.string.uuid()}@${faker_1.faker.internet.domainName()}`,
|
27
|
+
'From': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email()}>`,
|
28
|
+
'To': `${faker_1.faker.person.fullName()} <${faker_1.faker.internet.email()}>`,
|
29
|
+
};
|
30
|
+
const headersString = Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n');
|
31
|
+
const body = faker_1.faker.lorem.paragraphs(5).replace('\n', '\r\n');
|
32
|
+
const full_email = `${headersString}\r\n${body}`;
|
33
|
+
await node_assert_1.default.rejects(() => (0, parseMessage_1.default)(full_email));
|
34
|
+
});
|
35
|
+
});
|
package/tsconfig.json
CHANGED
@@ -5,6 +5,12 @@
|
|
5
5
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
6
6
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
7
7
|
"strict": true, /* Enable all strict type-checking options. */
|
8
|
-
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
9
|
-
|
8
|
+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
9
|
+
"outDir": "./dist",
|
10
|
+
|
11
|
+
},
|
12
|
+
"include": [
|
13
|
+
"./src/**/*",
|
14
|
+
"./src/*"
|
15
|
+
]
|
10
16
|
}
|