@carlgo11/simpleimfparser 0.0.0 → 0.0.1-a

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.
@@ -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
@@ -3,3 +3,4 @@ dist
3
3
  package-lock.json
4
4
  .*
5
5
  !.gitignore
6
+ !.github
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Simple IMF Parser
2
+
3
+ A TypeScript library for parsing Internet Message Format (IMF) emails, extracting headers, envelope details, and message bodies while providing useful utilities for email manipulation.
4
+
5
+ ## Features
6
+ - Parses raw IMF emails from **string** or **stream** input
7
+ - Supports **header parsing** (including multi-line folding)
8
+ - Provides a structured **Email class** with helper functions:
9
+ - `getHeader(name)` – Retrieve a header
10
+ - `addHeader(name, value)` – Add a new header
11
+ - `removeHeader(name)` – Remove a header
12
+ - `getBody()` – Get the email body
13
+ - Handles **multiple headers** with the same name (e.g., `Received` headers)
14
+ - Supports **UTF-8 names & addresses** (RFC 6532)
15
+
16
+ ## Installation
17
+ ```sh
18
+ npm install @carlgo11/simpleimfparser
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Parsing an Email
24
+ ```ts
25
+ import parseEmail from '@carlgo11/simpleimfparser';
26
+ import fs from 'fs';
27
+
28
+ (async () => {
29
+ const rawData = fs.readFileSync('test-email.eml', 'utf-8');
30
+ const envelope = {
31
+ from: 'alice@example.com',
32
+ to: ['bob@example.com'],
33
+ senderIP: '192.168.1.1'
34
+ };
35
+
36
+ const email = await parseEmail(rawData, envelope);
37
+
38
+ console.log('Subject:', email.getHeader('subject'));
39
+ console.log('Body:', email.getBody());
40
+ })();
41
+ ```
42
+
43
+ ### Working with Headers
44
+ ```ts
45
+ email.addHeader('X-Custom-Header', 'Hello World');
46
+ email.removeHeader('Received');
47
+ console.log(email.getHeader('from'));
48
+ ```
49
+
50
+ ### Extracting Envelope Details
51
+ ```ts
52
+ console.log(email.envelope.senderIP); // 192.168.1.1
53
+ console.log(email.envelope.to); // ['bob@example.com']
54
+ ```
55
+
56
+ ### Recompiling Email Object to IMF Format
57
+ ```ts
58
+ email.toString();
59
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { Email } from './types/Email.js';
2
+ import parseEnvelope from './parsers/parseEnvelope.js';
3
+ import parseMessage from './parsers/parseMessage.js';
4
+ export default async function parseEmail(rawData, envelope) {
5
+ const parsedEnvelope = await parseEnvelope(envelope);
6
+ const { headers, body } = await parseMessage(rawData);
7
+ return new Email(parsedEnvelope, headers, body);
8
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Parses envelope metadata from the SMTP transaction.
3
+ * @param envelope - Envelope data object
4
+ * @returns Parsed Envelope object
5
+ */
6
+ export default async function parseEnvelope(envelope) {
7
+ // TODO: Implement envelope parsing logic
8
+ return envelope;
9
+ }
@@ -0,0 +1,65 @@
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) {
7
+ const headers = {};
8
+ const lines = rawHeaders.split(/\r\n|\n/);
9
+ let currentKey = null;
10
+ let currentValue = '';
11
+ for (const line of lines) {
12
+ if (line.startsWith(' ') || line.startsWith('\t')) {
13
+ // Handle folded headers (continuation lines)
14
+ if (currentKey) {
15
+ if (Array.isArray(headers[currentKey])) {
16
+ headers[currentKey][headers[currentKey].length - 1] += ' ' + line.trim();
17
+ }
18
+ else {
19
+ headers[currentKey] += ' ' + line.trim();
20
+ }
21
+ }
22
+ else {
23
+ throw new Error('Header folding found without a preceding header field.');
24
+ }
25
+ }
26
+ else {
27
+ // Store previous header before starting a new one
28
+ if (currentKey) {
29
+ if (headers[currentKey]) {
30
+ if (Array.isArray(headers[currentKey])) {
31
+ headers[currentKey].push(currentValue);
32
+ }
33
+ else {
34
+ headers[currentKey] = [headers[currentKey], currentValue];
35
+ }
36
+ }
37
+ else {
38
+ headers[currentKey] = currentValue;
39
+ }
40
+ }
41
+ // Extract new header key-value pair
42
+ const match = line.match(/^([!#$%&'*+\-.0-9A-Z^_`a-z|~]+):\s*(.*)$/);
43
+ if (!match) {
44
+ throw new Error(`Invalid header format: "${line}"`);
45
+ }
46
+ currentKey = match[1].toLowerCase(); // Convert key to lowercase
47
+ currentValue = match[2];
48
+ }
49
+ }
50
+ // Store the last header
51
+ if (currentKey) {
52
+ if (headers[currentKey]) {
53
+ if (Array.isArray(headers[currentKey])) {
54
+ headers[currentKey].push(currentValue);
55
+ }
56
+ else {
57
+ headers[currentKey] = [headers[currentKey], currentValue];
58
+ }
59
+ }
60
+ else {
61
+ headers[currentKey] = currentValue;
62
+ }
63
+ }
64
+ return headers;
65
+ }
@@ -0,0 +1,30 @@
1
+ import { Readable } from 'node:stream';
2
+ import parseHeaders from './parseHeaders.js';
3
+ import streamToString from '../utils/streamToString.js';
4
+ /**
5
+ * Parses the raw email message (headers + body).
6
+ * @param rawData - Raw IMF email (string or stream)
7
+ * @returns Parsed email components
8
+ */
9
+ export default async function parseMessage(rawData) {
10
+ let rawEmail;
11
+ if (typeof rawData === 'string') {
12
+ rawEmail = rawData;
13
+ }
14
+ else if (rawData instanceof Readable) {
15
+ rawEmail = await streamToString(rawData);
16
+ }
17
+ else {
18
+ throw new TypeError('Invalid rawData type. Expected a string or a Readable stream.');
19
+ }
20
+ const headerBoundary = rawEmail.indexOf('\r\n\r\n');
21
+ const rawHeaders = rawEmail.slice(0, headerBoundary);
22
+ const body = rawEmail.slice(headerBoundary + 4);
23
+ if (body.length === 0) {
24
+ throw new Error('Header and Body must be separated by \\r\\n\\r\\n');
25
+ }
26
+ return {
27
+ headers: parseHeaders(rawHeaders),
28
+ body,
29
+ };
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
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/package.json CHANGED
@@ -1,18 +1,35 @@
1
1
  {
2
2
  "type": "module",
3
- "main": "src/index.ts",
3
+ "main": "dist/index.js",
4
4
  "name": "@carlgo11/simpleimfparser",
5
- "version": "0.0.0",
5
+ "description": "Simple IMF parser",
6
+ "version": "0.0.1a",
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,
23
+ "dependencies": {
24
+ "@types/node": "^22.10.7"
25
+ },
11
26
  "devDependencies": {
12
27
  "@faker-js/faker": "^9.4.0",
13
- "@types/node": "^22.10.7",
14
28
  "ts-node": "^10.9.2",
15
29
  "tsx": "^4.19.2",
16
30
  "typescript": "^5.7.3"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
17
34
  }
18
35
  }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Readable } from 'node:stream';
2
- import { Email } from './types/Email';
3
- import parseEnvelope from './parsers/parseEnvelope';
4
- import parseMessage from './parsers/parseMessage';
2
+ import { Email } from './types/Email.js';
3
+ import parseEnvelope from './parsers/parseEnvelope.js';
4
+ import parseMessage from './parsers/parseMessage.js';
5
5
 
6
6
  export default async function parseEmail(
7
7
  rawData: string | Readable,
@@ -1,4 +1,4 @@
1
- import Envelope from '../types/Envelope';
1
+ import Envelope from '../types/Envelope.js';
2
2
 
3
3
  /**
4
4
  * Parses envelope metadata from the SMTP transaction.
@@ -1,6 +1,6 @@
1
1
  import { Readable } from 'node:stream';
2
- import parseHeaders from './parseHeaders';
3
- import streamToString from '../utils/streamToString';
2
+ import parseHeaders from './parseHeaders.js';
3
+ import streamToString from '../utils/streamToString.js';
4
4
 
5
5
  /**
6
6
  * Parses the raw email message (headers + body).
@@ -1,4 +1,4 @@
1
- import Envelope from './Envelope';
1
+ import Envelope from './Envelope.js';
2
2
 
3
3
  export class Email {
4
4
  envelope: Envelope;
@@ -1,4 +1,4 @@
1
- import Sender from './Sender';
1
+ import Sender from './Sender.js';
2
2
 
3
3
  export default interface Envelope {
4
4
  id: string;
@@ -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).replace('\n', '\r\n');
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
  });
package/tsconfig.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2024", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
4
- "module": "commonjs", /* Specify what module code is generated. */
5
- "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
6
- "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
7
- "strict": true, /* Enable all strict type-checking options. */
8
- "skipLibCheck": true /* Skip type checking all .d.ts files. */
9
- }
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
+ ]
10
15
  }