postal-mime 2.4.6 → 2.5.0
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/.prettierignore +2 -1
- package/CHANGELOG.md +14 -0
- package/README.md +229 -17
- package/package.json +6 -3
- package/postal-mime.d.ts +16 -11
- package/src/address-parser.js +69 -15
- package/tsconfig.json +17 -0
package/.prettierignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.0](https://github.com/postalsys/postal-mime/compare/v2.4.7...v2.5.0) (2025-10-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add comprehensive type validation and TypeScript support ([81a6467](https://github.com/postalsys/postal-mime/commit/81a6467b6c5379c502bac9ee7023e2c2dee976cb))
|
|
9
|
+
|
|
10
|
+
## [2.4.7](https://github.com/postalsys/postal-mime/compare/v2.4.6...v2.4.7) (2025-10-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* prevent email extraction from quoted strings in addressParser ([837d679](https://github.com/postalsys/postal-mime/commit/837d679b48cde95a111b8508a7aea23a28cbc12a))
|
|
16
|
+
|
|
3
17
|
## [2.4.6](https://github.com/postalsys/postal-mime/compare/v2.4.5...v2.4.6) (2025-10-01)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
**postal-mime** is an email parsing library that runs in browser environments (including Web Workers) and serverless functions (like Cloudflare Email Workers). It takes in a raw email message (RFC822 format) and outputs a structured object containing headers, recipients, attachments, and more.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> [!TIP]
|
|
6
6
|
> PostalMime is developed by the makers of [EmailEngine](https://emailengine.app/?utm_source=github&utm_campaign=imapflow&utm_medium=readme-link)—a self-hosted email gateway that provides a REST API for IMAP and SMTP servers and sends webhooks whenever something changes in registered accounts.
|
|
7
7
|
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Browser & Node.js compatible** - Works in browsers, Web Workers, Node.js, and serverless environments
|
|
11
|
+
- **TypeScript support** - Fully typed with comprehensive type definitions
|
|
12
|
+
- **Zero dependencies** - No external dependencies
|
|
13
|
+
- **RFC compliant** - Follows RFC 2822/5322 email standards
|
|
14
|
+
- **Handles complex MIME structures** - Multipart messages, nested parts, attachments
|
|
15
|
+
- **Security limits** - Built-in protection against deeply nested messages and oversized headers
|
|
16
|
+
|
|
8
17
|
## Table of Contents
|
|
9
18
|
|
|
10
19
|
- [Source](#source)
|
|
@@ -14,6 +23,7 @@
|
|
|
14
23
|
- [Browser](#browser)
|
|
15
24
|
- [Node.js](#nodejs)
|
|
16
25
|
- [Cloudflare Email Workers](#cloudflare-email-workers)
|
|
26
|
+
- [TypeScript Support](#typescript-support)
|
|
17
27
|
- [API](#api)
|
|
18
28
|
- [PostalMime.parse()](#postalmimeparse)
|
|
19
29
|
- [Utility Functions](#utility-functions)
|
|
@@ -58,6 +68,23 @@ Content-Type: text/html; charset=utf-8
|
|
|
58
68
|
console.log(email.subject); // "My awesome email 🤓"
|
|
59
69
|
```
|
|
60
70
|
|
|
71
|
+
<details>
|
|
72
|
+
<summary><strong>TypeScript</strong></summary>
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
|
|
76
|
+
import type { Email } from 'postal-mime';
|
|
77
|
+
|
|
78
|
+
const email: Email = await PostalMime.parse(`Subject: My awesome email 🤓
|
|
79
|
+
Content-Type: text/html; charset=utf-8
|
|
80
|
+
|
|
81
|
+
<p>Hello world 😵💫</p>`);
|
|
82
|
+
|
|
83
|
+
console.log(email.subject); // "My awesome email 🤓"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
</details>
|
|
87
|
+
|
|
61
88
|
### Node.js
|
|
62
89
|
|
|
63
90
|
In Node.js (including serverless functions), import it directly from `postal-mime`:
|
|
@@ -75,6 +102,29 @@ Content-Type: text/html; charset=utf-8
|
|
|
75
102
|
console.log(util.inspect(email, false, 22, true));
|
|
76
103
|
```
|
|
77
104
|
|
|
105
|
+
<details>
|
|
106
|
+
<summary><strong>TypeScript</strong></summary>
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import PostalMime from 'postal-mime';
|
|
110
|
+
import type { Email, PostalMimeOptions } from 'postal-mime';
|
|
111
|
+
import util from 'node:util';
|
|
112
|
+
|
|
113
|
+
const options: PostalMimeOptions = {
|
|
114
|
+
attachmentEncoding: 'base64'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const email: Email = await PostalMime.parse(`Subject: My awesome email 🤓
|
|
118
|
+
Content-Type: text/html; charset=utf-8
|
|
119
|
+
|
|
120
|
+
<p>Hello world 😵💫</p>`, options);
|
|
121
|
+
|
|
122
|
+
// Use 'util.inspect' for pretty-printing
|
|
123
|
+
console.log(util.inspect(email, false, 22, true));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
</details>
|
|
127
|
+
|
|
78
128
|
### Cloudflare Email Workers
|
|
79
129
|
|
|
80
130
|
Use the `message.raw` as the raw email data for parsing:
|
|
@@ -93,6 +143,77 @@ export default {
|
|
|
93
143
|
};
|
|
94
144
|
```
|
|
95
145
|
|
|
146
|
+
<details>
|
|
147
|
+
<summary><strong>TypeScript</strong></summary>
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import PostalMime from 'postal-mime';
|
|
151
|
+
import type { Email } from 'postal-mime';
|
|
152
|
+
|
|
153
|
+
export default {
|
|
154
|
+
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
155
|
+
const email: Email = await PostalMime.parse(message.raw);
|
|
156
|
+
|
|
157
|
+
console.log('Subject:', email.subject);
|
|
158
|
+
console.log('HTML:', email.html);
|
|
159
|
+
console.log('Text:', email.text);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
</details>
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## TypeScript Support
|
|
169
|
+
|
|
170
|
+
PostalMime includes comprehensive TypeScript type definitions. All types are exported and can be imported from the main package:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import PostalMime, { addressParser, decodeWords } from 'postal-mime';
|
|
174
|
+
import type {
|
|
175
|
+
Email,
|
|
176
|
+
Address,
|
|
177
|
+
Mailbox,
|
|
178
|
+
Header,
|
|
179
|
+
Attachment,
|
|
180
|
+
PostalMimeOptions,
|
|
181
|
+
AddressParserOptions,
|
|
182
|
+
RawEmail
|
|
183
|
+
} from 'postal-mime';
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
> [!NOTE]
|
|
187
|
+
> PostalMime is written in JavaScript but provides comprehensive TypeScript type definitions. All types are validated through both compile-time type checking and runtime type validation tests to ensure accuracy.
|
|
188
|
+
|
|
189
|
+
### Available Types
|
|
190
|
+
|
|
191
|
+
- **`Email`** - The main parsed email object returned by `PostalMime.parse()`
|
|
192
|
+
- **`Address`** - Union type representing either a `Mailbox` or an address group
|
|
193
|
+
- **`Mailbox`** - Individual email address with name and address fields
|
|
194
|
+
- **`Header`** - Email header with key and value
|
|
195
|
+
- **`Attachment`** - Email attachment with metadata and content
|
|
196
|
+
- **`PostalMimeOptions`** - Configuration options for parsing
|
|
197
|
+
- **`AddressParserOptions`** - Configuration options for address parsing
|
|
198
|
+
- **`RawEmail`** - Union type for all accepted email input formats
|
|
199
|
+
|
|
200
|
+
### Type Narrowing
|
|
201
|
+
|
|
202
|
+
TypeScript users can use type guards to narrow address types:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import type { Address, Mailbox } from 'postal-mime';
|
|
206
|
+
|
|
207
|
+
function isMailbox(addr: Address): addr is Mailbox {
|
|
208
|
+
return !('group' in addr) || addr.group === undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Usage
|
|
212
|
+
if (email.from && isMailbox(email.from)) {
|
|
213
|
+
console.log(email.from.address); // TypeScript knows this is a Mailbox
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
96
217
|
---
|
|
97
218
|
|
|
98
219
|
## API
|
|
@@ -100,7 +221,7 @@ export default {
|
|
|
100
221
|
### PostalMime.parse()
|
|
101
222
|
|
|
102
223
|
```js
|
|
103
|
-
PostalMime.parse(email, options) -> Promise<
|
|
224
|
+
PostalMime.parse(email, options) -> Promise<Email>
|
|
104
225
|
```
|
|
105
226
|
|
|
106
227
|
- **email**: An RFC822 formatted email. This can be a `string`, `ArrayBuffer/Uint8Array`, `Blob` (browser only), `Buffer` (Node.js), or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
|
|
@@ -114,29 +235,86 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
|
|
|
114
235
|
- **maxNestingDepth** (number, default: `256`): Maximum allowed MIME part nesting depth. Throws an error if exceeded.
|
|
115
236
|
- **maxHeadersSize** (number, default: `2097152`): Maximum allowed total header size in bytes (default 2MB). Throws an error if exceeded.
|
|
116
237
|
|
|
117
|
-
|
|
238
|
+
> [!IMPORTANT]
|
|
239
|
+
> The `maxNestingDepth` and `maxHeadersSize` options provide built-in security against malicious emails with deeply nested MIME structures or oversized headers that could cause performance issues or memory exhaustion.
|
|
240
|
+
|
|
241
|
+
**Returns**: A Promise that resolves to a structured `Email` object with the following properties:
|
|
118
242
|
|
|
119
|
-
- **headers**: An array of
|
|
243
|
+
- **headers**: An array of `Header` objects, each containing:
|
|
120
244
|
- `key`: Lowercase header name (e.g., `"dkim-signature"`).
|
|
121
245
|
- `value`: Unprocessed header value as a string.
|
|
122
|
-
- **from**, **sender**: Processed address
|
|
246
|
+
- **from**, **sender**: Processed `Address` objects (can be a `Mailbox` or address group):
|
|
123
247
|
- `name`: Decoded display name, or an empty string if not set.
|
|
124
248
|
- `address`: Email address.
|
|
249
|
+
- `group`: Array of `Mailbox` objects (only for address groups).
|
|
125
250
|
- **deliveredTo**, **returnPath**: Single email addresses as strings.
|
|
126
|
-
- **to**, **cc**, **bcc**, **replyTo**: Arrays of
|
|
251
|
+
- **to**, **cc**, **bcc**, **replyTo**: Arrays of `Address` objects (same structure as `from`).
|
|
127
252
|
- **subject**: Subject line of the email.
|
|
128
253
|
- **messageId**, **inReplyTo**, **references**: Values from their corresponding headers.
|
|
129
|
-
- **date**: The email
|
|
254
|
+
- **date**: The email's sending time in ISO 8601 format (or the original string if parsing fails).
|
|
130
255
|
- **html**: String containing the HTML content of the email.
|
|
131
256
|
- **text**: String containing the plain text content of the email.
|
|
132
|
-
- **attachments**: Array of
|
|
133
|
-
- `filename`
|
|
134
|
-
- `mimeType
|
|
135
|
-
- `disposition
|
|
136
|
-
- `related
|
|
137
|
-
- `contentId
|
|
138
|
-
- `content`
|
|
139
|
-
- `encoding`
|
|
257
|
+
- **attachments**: Array of `Attachment` objects:
|
|
258
|
+
- `filename`: String or `null`
|
|
259
|
+
- `mimeType`: String
|
|
260
|
+
- `disposition`: `"attachment"`, `"inline"`, or `null`
|
|
261
|
+
- `related`: Boolean (optional, `true` if it's an inline image)
|
|
262
|
+
- `contentId`: String (optional)
|
|
263
|
+
- `content`: `ArrayBuffer` or string, depending on `attachmentEncoding`
|
|
264
|
+
- `encoding`: `"base64"` or `"utf8"` (optional)
|
|
265
|
+
|
|
266
|
+
<details>
|
|
267
|
+
<summary><strong>TypeScript Types</strong></summary>
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import type {
|
|
271
|
+
Email,
|
|
272
|
+
Address,
|
|
273
|
+
Mailbox,
|
|
274
|
+
Header,
|
|
275
|
+
Attachment,
|
|
276
|
+
PostalMimeOptions,
|
|
277
|
+
RawEmail
|
|
278
|
+
} from 'postal-mime';
|
|
279
|
+
|
|
280
|
+
// Main email parsing
|
|
281
|
+
const email: Email = await PostalMime.parse(rawEmail);
|
|
282
|
+
|
|
283
|
+
// With options
|
|
284
|
+
const options: PostalMimeOptions = {
|
|
285
|
+
attachmentEncoding: 'base64',
|
|
286
|
+
maxNestingDepth: 100
|
|
287
|
+
};
|
|
288
|
+
const email: Email = await PostalMime.parse(rawEmail, options);
|
|
289
|
+
|
|
290
|
+
// Working with addresses
|
|
291
|
+
if (email.from) {
|
|
292
|
+
// Address can be either a Mailbox or a Group
|
|
293
|
+
if ('group' in email.from && email.from.group) {
|
|
294
|
+
// It's a group
|
|
295
|
+
email.from.group.forEach((member: Mailbox) => {
|
|
296
|
+
console.log(member.address);
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
// It's a mailbox
|
|
300
|
+
const mailbox = email.from as Mailbox;
|
|
301
|
+
console.log(mailbox.address);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Working with attachments
|
|
306
|
+
email.attachments.forEach((att: Attachment) => {
|
|
307
|
+
if (att.encoding === 'base64') {
|
|
308
|
+
// content is a string
|
|
309
|
+
const base64Content: string = att.content as string;
|
|
310
|
+
} else {
|
|
311
|
+
// content is ArrayBuffer (default)
|
|
312
|
+
const buffer: ArrayBuffer = att.content as ArrayBuffer;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
</details>
|
|
140
318
|
|
|
141
319
|
---
|
|
142
320
|
|
|
@@ -147,23 +325,42 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
|
|
|
147
325
|
```js
|
|
148
326
|
import { addressParser } from 'postal-mime';
|
|
149
327
|
|
|
150
|
-
addressParser(addressStr, opts) ->
|
|
328
|
+
addressParser(addressStr, opts) -> Address[]
|
|
151
329
|
```
|
|
152
330
|
|
|
153
331
|
- **addressStr**: A raw address header string.
|
|
154
332
|
- **opts**: Optional configuration:
|
|
155
333
|
- **flatten** (boolean, default: `false`): If `true`, ignores address groups and returns a flat array of addresses.
|
|
156
334
|
|
|
157
|
-
**Returns**: An array of
|
|
335
|
+
**Returns**: An array of `Address` objects, which can be nested if address groups are present.
|
|
158
336
|
|
|
159
337
|
**Example**:
|
|
160
338
|
|
|
161
339
|
```js
|
|
340
|
+
import { addressParser } from 'postal-mime';
|
|
341
|
+
|
|
162
342
|
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
|
|
163
343
|
console.log(addressParser(addressStr));
|
|
164
344
|
// [ { name: 'エポスカード', address: 'support@example.com' } ]
|
|
165
345
|
```
|
|
166
346
|
|
|
347
|
+
<details>
|
|
348
|
+
<summary><strong>TypeScript</strong></summary>
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { addressParser } from 'postal-mime';
|
|
352
|
+
import type { Address, AddressParserOptions } from 'postal-mime';
|
|
353
|
+
|
|
354
|
+
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
|
|
355
|
+
const addresses: Address[] = addressParser(addressStr);
|
|
356
|
+
|
|
357
|
+
// With options
|
|
358
|
+
const options: AddressParserOptions = { flatten: true };
|
|
359
|
+
const flatAddresses: Address[] = addressParser(addressStr, options);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
</details>
|
|
363
|
+
|
|
167
364
|
#### decodeWords()
|
|
168
365
|
|
|
169
366
|
```js
|
|
@@ -179,11 +376,26 @@ decodeWords(encodedStr) -> string
|
|
|
179
376
|
**Example**:
|
|
180
377
|
|
|
181
378
|
```js
|
|
379
|
+
import { decodeWords } from 'postal-mime';
|
|
380
|
+
|
|
182
381
|
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
|
|
183
382
|
console.log(decodeWords(encodedStr));
|
|
184
383
|
// Hello, エポスカード
|
|
185
384
|
```
|
|
186
385
|
|
|
386
|
+
<details>
|
|
387
|
+
<summary><strong>TypeScript</strong></summary>
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { decodeWords } from 'postal-mime';
|
|
391
|
+
|
|
392
|
+
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
|
|
393
|
+
const decoded: string = decodeWords(encodedStr);
|
|
394
|
+
console.log(decoded); // Hello, エポスカード
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
</details>
|
|
398
|
+
|
|
187
399
|
---
|
|
188
400
|
|
|
189
401
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postal-mime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Email parser for browser environments",
|
|
5
5
|
"main": "./src/postal-mime.js",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"type": "module",
|
|
12
12
|
"types": "postal-mime.d.ts",
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "
|
|
14
|
+
"test": "npm run lint && npm run type-check && node --test",
|
|
15
|
+
"lint": "eslint",
|
|
16
|
+
"type-check": "tsc --noEmit",
|
|
15
17
|
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
|
|
16
18
|
"format": "prettier --write \"**/*.{js,mjs,json}\" --ignore-path .prettierignore",
|
|
17
19
|
"format:check": "prettier --check \"**/*.{js,mjs,json}\" --ignore-path .prettierignore"
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"eslint": "8.57.0",
|
|
37
39
|
"eslint-cli": "1.1.1",
|
|
38
40
|
"iframe-resizer": "4.3.6",
|
|
39
|
-
"prettier": "^3.6.2"
|
|
41
|
+
"prettier": "^3.6.2",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
40
43
|
}
|
|
41
44
|
}
|
package/postal-mime.d.ts
CHANGED
|
@@ -5,12 +5,20 @@ export type Header = {
|
|
|
5
5
|
value: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type Mailbox = {
|
|
9
9
|
name: string;
|
|
10
|
-
address
|
|
11
|
-
group?:
|
|
10
|
+
address: string;
|
|
11
|
+
group?: undefined;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export type Address =
|
|
15
|
+
| Mailbox
|
|
16
|
+
| {
|
|
17
|
+
name: string;
|
|
18
|
+
address?: undefined;
|
|
19
|
+
group: Mailbox[];
|
|
20
|
+
};
|
|
21
|
+
|
|
14
22
|
export type Attachment = {
|
|
15
23
|
filename: string | null;
|
|
16
24
|
mimeType: string;
|
|
@@ -43,20 +51,20 @@ export type Email = {
|
|
|
43
51
|
attachments: Attachment[];
|
|
44
52
|
};
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
export type AddressParserOptions = {
|
|
47
55
|
flatten?: boolean
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
export function addressParser (
|
|
51
59
|
str: string,
|
|
52
60
|
options?: AddressParserOptions
|
|
53
61
|
): Address[];
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
export function decodeWords (
|
|
56
64
|
str: string
|
|
57
65
|
): string;
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
export type PostalMimeOptions = {
|
|
60
68
|
rfc822Attachments?: boolean,
|
|
61
69
|
forceRfc822Attachments?: boolean,
|
|
62
70
|
attachmentEncoding?: "base64" | "utf8" | "arraybuffer",
|
|
@@ -64,7 +72,7 @@ declare type PostalMimeOptions = {
|
|
|
64
72
|
maxHeadersSize?: number
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
export default class PostalMime {
|
|
68
76
|
constructor(options?: PostalMimeOptions);
|
|
69
77
|
static parse(
|
|
70
78
|
email: RawEmail,
|
|
@@ -72,6 +80,3 @@ declare class PostalMime {
|
|
|
72
80
|
): Promise<Email>;
|
|
73
81
|
parse(email: RawEmail): Promise<Email>;
|
|
74
82
|
}
|
|
75
|
-
|
|
76
|
-
export { addressParser, decodeWords };
|
|
77
|
-
export default PostalMime;
|
package/src/address-parser.js
CHANGED
|
@@ -7,7 +7,6 @@ import { decodeWords } from './decode-strings.js';
|
|
|
7
7
|
* @return {Object} Address object
|
|
8
8
|
*/
|
|
9
9
|
function _handleAddress(tokens) {
|
|
10
|
-
let token;
|
|
11
10
|
let isGroup = false;
|
|
12
11
|
let state = 'text';
|
|
13
12
|
let address;
|
|
@@ -16,28 +15,41 @@ function _handleAddress(tokens) {
|
|
|
16
15
|
address: [],
|
|
17
16
|
comment: [],
|
|
18
17
|
group: [],
|
|
19
|
-
text: []
|
|
18
|
+
text: [],
|
|
19
|
+
textWasQuoted: [] // Track which text tokens came from inside quotes
|
|
20
20
|
};
|
|
21
21
|
let i;
|
|
22
22
|
let len;
|
|
23
|
+
let insideQuotes = false; // Track if we're currently inside a quoted string
|
|
23
24
|
|
|
24
25
|
// Filter out <addresses>, (comments) and regular text
|
|
25
26
|
for (i = 0, len = tokens.length; i < len; i++) {
|
|
26
|
-
token = tokens[i];
|
|
27
|
+
let token = tokens[i];
|
|
28
|
+
let prevToken = i ? tokens[i - 1] : null;
|
|
27
29
|
if (token.type === 'operator') {
|
|
28
30
|
switch (token.value) {
|
|
29
31
|
case '<':
|
|
30
32
|
state = 'address';
|
|
33
|
+
insideQuotes = false;
|
|
31
34
|
break;
|
|
32
35
|
case '(':
|
|
33
36
|
state = 'comment';
|
|
37
|
+
insideQuotes = false;
|
|
34
38
|
break;
|
|
35
39
|
case ':':
|
|
36
40
|
state = 'group';
|
|
37
41
|
isGroup = true;
|
|
42
|
+
insideQuotes = false;
|
|
43
|
+
break;
|
|
44
|
+
case '"':
|
|
45
|
+
// Track quote state for text tokens
|
|
46
|
+
insideQuotes = !insideQuotes;
|
|
47
|
+
state = 'text';
|
|
38
48
|
break;
|
|
39
49
|
default:
|
|
40
50
|
state = 'text';
|
|
51
|
+
insideQuotes = false;
|
|
52
|
+
break;
|
|
41
53
|
}
|
|
42
54
|
} else if (token.value) {
|
|
43
55
|
if (state === 'address') {
|
|
@@ -46,7 +58,19 @@ function _handleAddress(tokens) {
|
|
|
46
58
|
// and so will we
|
|
47
59
|
token.value = token.value.replace(/^[^<]*<\s*/, '');
|
|
48
60
|
}
|
|
49
|
-
|
|
61
|
+
|
|
62
|
+
if (prevToken && prevToken.noBreak && data[state].length) {
|
|
63
|
+
// join values
|
|
64
|
+
data[state][data[state].length - 1] += token.value;
|
|
65
|
+
if (state === 'text' && insideQuotes) {
|
|
66
|
+
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
data[state].push(token.value);
|
|
70
|
+
if (state === 'text') {
|
|
71
|
+
data.textWasQuoted.push(insideQuotes);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
|
|
@@ -59,16 +83,36 @@ function _handleAddress(tokens) {
|
|
|
59
83
|
if (isGroup) {
|
|
60
84
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
61
85
|
data.text = data.text.join(' ');
|
|
86
|
+
|
|
87
|
+
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
|
|
88
|
+
let groupMembers = [];
|
|
89
|
+
if (data.group.length) {
|
|
90
|
+
let parsedGroup = addressParser(data.group.join(','));
|
|
91
|
+
// Flatten: if any member is itself a group, extract its members into the sequence
|
|
92
|
+
parsedGroup.forEach(member => {
|
|
93
|
+
if (member.group) {
|
|
94
|
+
// Nested group detected - flatten it by adding its members directly
|
|
95
|
+
groupMembers = groupMembers.concat(member.group);
|
|
96
|
+
} else {
|
|
97
|
+
groupMembers.push(member);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
addresses.push({
|
|
63
103
|
name: decodeWords(data.text || (address && address.name)),
|
|
64
|
-
group:
|
|
104
|
+
group: groupMembers
|
|
65
105
|
});
|
|
66
106
|
} else {
|
|
67
107
|
// If no address was found, try to detect one from regular text
|
|
68
108
|
if (!data.address.length && data.text.length) {
|
|
69
109
|
for (i = data.text.length - 1; i >= 0; i--) {
|
|
70
|
-
|
|
110
|
+
// Security fix: Do not extract email addresses from quoted strings
|
|
111
|
+
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
|
|
112
|
+
// Extracting emails from quoted text leads to misrouting vulnerabilities
|
|
113
|
+
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
|
71
114
|
data.address = data.text.splice(i, 1);
|
|
115
|
+
data.textWasQuoted.splice(i, 1);
|
|
72
116
|
break;
|
|
73
117
|
}
|
|
74
118
|
}
|
|
@@ -85,10 +129,13 @@ function _handleAddress(tokens) {
|
|
|
85
129
|
// still no address
|
|
86
130
|
if (!data.address.length) {
|
|
87
131
|
for (i = data.text.length - 1; i >= 0; i--) {
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
// Security fix: Do not extract email addresses from quoted strings
|
|
133
|
+
if (!data.textWasQuoted[i]) {
|
|
134
|
+
// fixed the regex to parse email address correctly when email address has more than one @
|
|
135
|
+
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
|
|
136
|
+
if (data.address.length) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
92
139
|
}
|
|
93
140
|
}
|
|
94
141
|
}
|
|
@@ -180,11 +227,12 @@ class Tokenizer {
|
|
|
180
227
|
* @return {Array} An array of operator|text tokens
|
|
181
228
|
*/
|
|
182
229
|
tokenize() {
|
|
183
|
-
let
|
|
184
|
-
|
|
230
|
+
let list = [];
|
|
231
|
+
|
|
185
232
|
for (let i = 0, len = this.str.length; i < len; i++) {
|
|
186
|
-
chr = this.str.charAt(i);
|
|
187
|
-
this.
|
|
233
|
+
let chr = this.str.charAt(i);
|
|
234
|
+
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
|
|
235
|
+
this.checkChar(chr, nextChr);
|
|
188
236
|
}
|
|
189
237
|
|
|
190
238
|
this.list.forEach(node => {
|
|
@@ -202,7 +250,7 @@ class Tokenizer {
|
|
|
202
250
|
*
|
|
203
251
|
* @param {String} chr Character from the address field
|
|
204
252
|
*/
|
|
205
|
-
checkChar(chr) {
|
|
253
|
+
checkChar(chr, nextChr) {
|
|
206
254
|
if (this.escaped) {
|
|
207
255
|
// ignore next condition blocks
|
|
208
256
|
} else if (chr === this.operatorExpecting) {
|
|
@@ -210,10 +258,16 @@ class Tokenizer {
|
|
|
210
258
|
type: 'operator',
|
|
211
259
|
value: chr
|
|
212
260
|
};
|
|
261
|
+
|
|
262
|
+
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
|
263
|
+
this.node.noBreak = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
this.list.push(this.node);
|
|
214
267
|
this.node = null;
|
|
215
268
|
this.operatorExpecting = '';
|
|
216
269
|
this.escaped = false;
|
|
270
|
+
|
|
217
271
|
return;
|
|
218
272
|
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
219
273
|
this.node = {
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["test/type-check.test.ts"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
17
|
+
}
|