postal-mime 2.4.7 → 2.6.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/CHANGELOG.md +14 -0
- package/README.md +248 -17
- package/dist/address-parser.cjs +317 -0
- package/dist/base64-decoder.cjs +74 -0
- package/dist/base64-encoder.cjs +72 -0
- package/dist/decode-strings.cjs +254 -0
- package/dist/html-entities.cjs +2276 -0
- package/dist/mime-node.cjs +268 -0
- package/dist/pass-through-decoder.cjs +50 -0
- package/dist/postal-mime.cjs +473 -0
- package/dist/qp-decoder.cjs +132 -0
- package/dist/text-format.cjs +281 -0
- package/package.json +16 -8
- package/postal-mime.d.ts +5 -8
- package/.eslintrc.cjs +0 -26
- package/.ncurc.js +0 -10
- package/.prettierignore +0 -10
- package/.prettierrc.cjs +0 -8
- package/.prettierrc.json +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.6.0](https://github.com/postalsys/postal-mime/compare/v2.5.0...v2.6.0) (2025-10-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add CommonJS build support for dual package compatibility ([0ea3de3](https://github.com/postalsys/postal-mime/commit/0ea3de39c713aac2bc79218dfb496c0e2ff4b0b8))
|
|
9
|
+
|
|
10
|
+
## [2.5.0](https://github.com/postalsys/postal-mime/compare/v2.4.7...v2.5.0) (2025-10-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add comprehensive type validation and TypeScript support ([81a6467](https://github.com/postalsys/postal-mime/commit/81a6467b6c5379c502bac9ee7023e2c2dee976cb))
|
|
16
|
+
|
|
3
17
|
## [2.4.7](https://github.com/postalsys/postal-mime/compare/v2.4.6...v2.4.7) (2025-10-07)
|
|
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,48 @@ 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
|
+
|
|
128
|
+
### CommonJS
|
|
129
|
+
|
|
130
|
+
For projects using CommonJS (with `require()`), postal-mime automatically provides the CommonJS build:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const PostalMime = require('postal-mime');
|
|
134
|
+
const { addressParser, decodeWords } = require('postal-mime');
|
|
135
|
+
|
|
136
|
+
const email = await PostalMime.parse(`Subject: My awesome email 🤓
|
|
137
|
+
Content-Type: text/html; charset=utf-8
|
|
138
|
+
|
|
139
|
+
<p>Hello world 😵💫</p>`);
|
|
140
|
+
|
|
141
|
+
console.log(email.subject); // "My awesome email 🤓"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> [!NOTE]
|
|
145
|
+
> The CommonJS build is automatically generated from the ESM source code during the build process. The package supports dual module format, so both `import` and `require()` work seamlessly.
|
|
146
|
+
|
|
78
147
|
### Cloudflare Email Workers
|
|
79
148
|
|
|
80
149
|
Use the `message.raw` as the raw email data for parsing:
|
|
@@ -93,6 +162,77 @@ export default {
|
|
|
93
162
|
};
|
|
94
163
|
```
|
|
95
164
|
|
|
165
|
+
<details>
|
|
166
|
+
<summary><strong>TypeScript</strong></summary>
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import PostalMime from 'postal-mime';
|
|
170
|
+
import type { Email } from 'postal-mime';
|
|
171
|
+
|
|
172
|
+
export default {
|
|
173
|
+
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
174
|
+
const email: Email = await PostalMime.parse(message.raw);
|
|
175
|
+
|
|
176
|
+
console.log('Subject:', email.subject);
|
|
177
|
+
console.log('HTML:', email.html);
|
|
178
|
+
console.log('Text:', email.text);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## TypeScript Support
|
|
188
|
+
|
|
189
|
+
PostalMime includes comprehensive TypeScript type definitions. All types are exported and can be imported from the main package:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import PostalMime, { addressParser, decodeWords } from 'postal-mime';
|
|
193
|
+
import type {
|
|
194
|
+
Email,
|
|
195
|
+
Address,
|
|
196
|
+
Mailbox,
|
|
197
|
+
Header,
|
|
198
|
+
Attachment,
|
|
199
|
+
PostalMimeOptions,
|
|
200
|
+
AddressParserOptions,
|
|
201
|
+
RawEmail
|
|
202
|
+
} from 'postal-mime';
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> [!NOTE]
|
|
206
|
+
> 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.
|
|
207
|
+
|
|
208
|
+
### Available Types
|
|
209
|
+
|
|
210
|
+
- **`Email`** - The main parsed email object returned by `PostalMime.parse()`
|
|
211
|
+
- **`Address`** - Union type representing either a `Mailbox` or an address group
|
|
212
|
+
- **`Mailbox`** - Individual email address with name and address fields
|
|
213
|
+
- **`Header`** - Email header with key and value
|
|
214
|
+
- **`Attachment`** - Email attachment with metadata and content
|
|
215
|
+
- **`PostalMimeOptions`** - Configuration options for parsing
|
|
216
|
+
- **`AddressParserOptions`** - Configuration options for address parsing
|
|
217
|
+
- **`RawEmail`** - Union type for all accepted email input formats
|
|
218
|
+
|
|
219
|
+
### Type Narrowing
|
|
220
|
+
|
|
221
|
+
TypeScript users can use type guards to narrow address types:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import type { Address, Mailbox } from 'postal-mime';
|
|
225
|
+
|
|
226
|
+
function isMailbox(addr: Address): addr is Mailbox {
|
|
227
|
+
return !('group' in addr) || addr.group === undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Usage
|
|
231
|
+
if (email.from && isMailbox(email.from)) {
|
|
232
|
+
console.log(email.from.address); // TypeScript knows this is a Mailbox
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
96
236
|
---
|
|
97
237
|
|
|
98
238
|
## API
|
|
@@ -100,7 +240,7 @@ export default {
|
|
|
100
240
|
### PostalMime.parse()
|
|
101
241
|
|
|
102
242
|
```js
|
|
103
|
-
PostalMime.parse(email, options) -> Promise<
|
|
243
|
+
PostalMime.parse(email, options) -> Promise<Email>
|
|
104
244
|
```
|
|
105
245
|
|
|
106
246
|
- **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 +254,86 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
|
|
|
114
254
|
- **maxNestingDepth** (number, default: `256`): Maximum allowed MIME part nesting depth. Throws an error if exceeded.
|
|
115
255
|
- **maxHeadersSize** (number, default: `2097152`): Maximum allowed total header size in bytes (default 2MB). Throws an error if exceeded.
|
|
116
256
|
|
|
117
|
-
|
|
257
|
+
> [!IMPORTANT]
|
|
258
|
+
> 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.
|
|
259
|
+
|
|
260
|
+
**Returns**: A Promise that resolves to a structured `Email` object with the following properties:
|
|
118
261
|
|
|
119
|
-
- **headers**: An array of
|
|
262
|
+
- **headers**: An array of `Header` objects, each containing:
|
|
120
263
|
- `key`: Lowercase header name (e.g., `"dkim-signature"`).
|
|
121
264
|
- `value`: Unprocessed header value as a string.
|
|
122
|
-
- **from**, **sender**: Processed address
|
|
265
|
+
- **from**, **sender**: Processed `Address` objects (can be a `Mailbox` or address group):
|
|
123
266
|
- `name`: Decoded display name, or an empty string if not set.
|
|
124
267
|
- `address`: Email address.
|
|
268
|
+
- `group`: Array of `Mailbox` objects (only for address groups).
|
|
125
269
|
- **deliveredTo**, **returnPath**: Single email addresses as strings.
|
|
126
|
-
- **to**, **cc**, **bcc**, **replyTo**: Arrays of
|
|
270
|
+
- **to**, **cc**, **bcc**, **replyTo**: Arrays of `Address` objects (same structure as `from`).
|
|
127
271
|
- **subject**: Subject line of the email.
|
|
128
272
|
- **messageId**, **inReplyTo**, **references**: Values from their corresponding headers.
|
|
129
|
-
- **date**: The email
|
|
273
|
+
- **date**: The email's sending time in ISO 8601 format (or the original string if parsing fails).
|
|
130
274
|
- **html**: String containing the HTML content of the email.
|
|
131
275
|
- **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`
|
|
276
|
+
- **attachments**: Array of `Attachment` objects:
|
|
277
|
+
- `filename`: String or `null`
|
|
278
|
+
- `mimeType`: String
|
|
279
|
+
- `disposition`: `"attachment"`, `"inline"`, or `null`
|
|
280
|
+
- `related`: Boolean (optional, `true` if it's an inline image)
|
|
281
|
+
- `contentId`: String (optional)
|
|
282
|
+
- `content`: `ArrayBuffer` or string, depending on `attachmentEncoding`
|
|
283
|
+
- `encoding`: `"base64"` or `"utf8"` (optional)
|
|
284
|
+
|
|
285
|
+
<details>
|
|
286
|
+
<summary><strong>TypeScript Types</strong></summary>
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import type {
|
|
290
|
+
Email,
|
|
291
|
+
Address,
|
|
292
|
+
Mailbox,
|
|
293
|
+
Header,
|
|
294
|
+
Attachment,
|
|
295
|
+
PostalMimeOptions,
|
|
296
|
+
RawEmail
|
|
297
|
+
} from 'postal-mime';
|
|
298
|
+
|
|
299
|
+
// Main email parsing
|
|
300
|
+
const email: Email = await PostalMime.parse(rawEmail);
|
|
301
|
+
|
|
302
|
+
// With options
|
|
303
|
+
const options: PostalMimeOptions = {
|
|
304
|
+
attachmentEncoding: 'base64',
|
|
305
|
+
maxNestingDepth: 100
|
|
306
|
+
};
|
|
307
|
+
const email: Email = await PostalMime.parse(rawEmail, options);
|
|
308
|
+
|
|
309
|
+
// Working with addresses
|
|
310
|
+
if (email.from) {
|
|
311
|
+
// Address can be either a Mailbox or a Group
|
|
312
|
+
if ('group' in email.from && email.from.group) {
|
|
313
|
+
// It's a group
|
|
314
|
+
email.from.group.forEach((member: Mailbox) => {
|
|
315
|
+
console.log(member.address);
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
// It's a mailbox
|
|
319
|
+
const mailbox = email.from as Mailbox;
|
|
320
|
+
console.log(mailbox.address);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Working with attachments
|
|
325
|
+
email.attachments.forEach((att: Attachment) => {
|
|
326
|
+
if (att.encoding === 'base64') {
|
|
327
|
+
// content is a string
|
|
328
|
+
const base64Content: string = att.content as string;
|
|
329
|
+
} else {
|
|
330
|
+
// content is ArrayBuffer (default)
|
|
331
|
+
const buffer: ArrayBuffer = att.content as ArrayBuffer;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
</details>
|
|
140
337
|
|
|
141
338
|
---
|
|
142
339
|
|
|
@@ -147,23 +344,42 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
|
|
|
147
344
|
```js
|
|
148
345
|
import { addressParser } from 'postal-mime';
|
|
149
346
|
|
|
150
|
-
addressParser(addressStr, opts) ->
|
|
347
|
+
addressParser(addressStr, opts) -> Address[]
|
|
151
348
|
```
|
|
152
349
|
|
|
153
350
|
- **addressStr**: A raw address header string.
|
|
154
351
|
- **opts**: Optional configuration:
|
|
155
352
|
- **flatten** (boolean, default: `false`): If `true`, ignores address groups and returns a flat array of addresses.
|
|
156
353
|
|
|
157
|
-
**Returns**: An array of
|
|
354
|
+
**Returns**: An array of `Address` objects, which can be nested if address groups are present.
|
|
158
355
|
|
|
159
356
|
**Example**:
|
|
160
357
|
|
|
161
358
|
```js
|
|
359
|
+
import { addressParser } from 'postal-mime';
|
|
360
|
+
|
|
162
361
|
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
|
|
163
362
|
console.log(addressParser(addressStr));
|
|
164
363
|
// [ { name: 'エポスカード', address: 'support@example.com' } ]
|
|
165
364
|
```
|
|
166
365
|
|
|
366
|
+
<details>
|
|
367
|
+
<summary><strong>TypeScript</strong></summary>
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { addressParser } from 'postal-mime';
|
|
371
|
+
import type { Address, AddressParserOptions } from 'postal-mime';
|
|
372
|
+
|
|
373
|
+
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
|
|
374
|
+
const addresses: Address[] = addressParser(addressStr);
|
|
375
|
+
|
|
376
|
+
// With options
|
|
377
|
+
const options: AddressParserOptions = { flatten: true };
|
|
378
|
+
const flatAddresses: Address[] = addressParser(addressStr, options);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
</details>
|
|
382
|
+
|
|
167
383
|
#### decodeWords()
|
|
168
384
|
|
|
169
385
|
```js
|
|
@@ -179,11 +395,26 @@ decodeWords(encodedStr) -> string
|
|
|
179
395
|
**Example**:
|
|
180
396
|
|
|
181
397
|
```js
|
|
398
|
+
import { decodeWords } from 'postal-mime';
|
|
399
|
+
|
|
182
400
|
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
|
|
183
401
|
console.log(decodeWords(encodedStr));
|
|
184
402
|
// Hello, エポスカード
|
|
185
403
|
```
|
|
186
404
|
|
|
405
|
+
<details>
|
|
406
|
+
<summary><strong>TypeScript</strong></summary>
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { decodeWords } from 'postal-mime';
|
|
410
|
+
|
|
411
|
+
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
|
|
412
|
+
const decoded: string = decodeWords(encodedStr);
|
|
413
|
+
console.log(decoded); // Hello, エポスカード
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
</details>
|
|
417
|
+
|
|
187
418
|
---
|
|
188
419
|
|
|
189
420
|
## License
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var address_parser_exports = {};
|
|
20
|
+
__export(address_parser_exports, {
|
|
21
|
+
default: () => address_parser_default
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(address_parser_exports);
|
|
24
|
+
var import_decode_strings = require("./decode-strings.cjs");
|
|
25
|
+
function _handleAddress(tokens) {
|
|
26
|
+
let isGroup = false;
|
|
27
|
+
let state = "text";
|
|
28
|
+
let address;
|
|
29
|
+
let addresses = [];
|
|
30
|
+
let data = {
|
|
31
|
+
address: [],
|
|
32
|
+
comment: [],
|
|
33
|
+
group: [],
|
|
34
|
+
text: [],
|
|
35
|
+
textWasQuoted: []
|
|
36
|
+
// Track which text tokens came from inside quotes
|
|
37
|
+
};
|
|
38
|
+
let i;
|
|
39
|
+
let len;
|
|
40
|
+
let insideQuotes = false;
|
|
41
|
+
for (i = 0, len = tokens.length; i < len; i++) {
|
|
42
|
+
let token = tokens[i];
|
|
43
|
+
let prevToken = i ? tokens[i - 1] : null;
|
|
44
|
+
if (token.type === "operator") {
|
|
45
|
+
switch (token.value) {
|
|
46
|
+
case "<":
|
|
47
|
+
state = "address";
|
|
48
|
+
insideQuotes = false;
|
|
49
|
+
break;
|
|
50
|
+
case "(":
|
|
51
|
+
state = "comment";
|
|
52
|
+
insideQuotes = false;
|
|
53
|
+
break;
|
|
54
|
+
case ":":
|
|
55
|
+
state = "group";
|
|
56
|
+
isGroup = true;
|
|
57
|
+
insideQuotes = false;
|
|
58
|
+
break;
|
|
59
|
+
case '"':
|
|
60
|
+
insideQuotes = !insideQuotes;
|
|
61
|
+
state = "text";
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
state = "text";
|
|
65
|
+
insideQuotes = false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
} else if (token.value) {
|
|
69
|
+
if (state === "address") {
|
|
70
|
+
token.value = token.value.replace(/^[^<]*<\s*/, "");
|
|
71
|
+
}
|
|
72
|
+
if (prevToken && prevToken.noBreak && data[state].length) {
|
|
73
|
+
data[state][data[state].length - 1] += token.value;
|
|
74
|
+
if (state === "text" && insideQuotes) {
|
|
75
|
+
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
data[state].push(token.value);
|
|
79
|
+
if (state === "text") {
|
|
80
|
+
data.textWasQuoted.push(insideQuotes);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!data.text.length && data.comment.length) {
|
|
86
|
+
data.text = data.comment;
|
|
87
|
+
data.comment = [];
|
|
88
|
+
}
|
|
89
|
+
if (isGroup) {
|
|
90
|
+
data.text = data.text.join(" ");
|
|
91
|
+
let groupMembers = [];
|
|
92
|
+
if (data.group.length) {
|
|
93
|
+
let parsedGroup = addressParser(data.group.join(","));
|
|
94
|
+
parsedGroup.forEach((member) => {
|
|
95
|
+
if (member.group) {
|
|
96
|
+
groupMembers = groupMembers.concat(member.group);
|
|
97
|
+
} else {
|
|
98
|
+
groupMembers.push(member);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
addresses.push({
|
|
103
|
+
name: (0, import_decode_strings.decodeWords)(data.text || address && address.name),
|
|
104
|
+
group: groupMembers
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
if (!data.address.length && data.text.length) {
|
|
108
|
+
for (i = data.text.length - 1; i >= 0; i--) {
|
|
109
|
+
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
|
110
|
+
data.address = data.text.splice(i, 1);
|
|
111
|
+
data.textWasQuoted.splice(i, 1);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let _regexHandler = function(address2) {
|
|
116
|
+
if (!data.address.length) {
|
|
117
|
+
data.address = [address2.trim()];
|
|
118
|
+
return " ";
|
|
119
|
+
} else {
|
|
120
|
+
return address2;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (!data.address.length) {
|
|
124
|
+
for (i = data.text.length - 1; i >= 0; i--) {
|
|
125
|
+
if (!data.textWasQuoted[i]) {
|
|
126
|
+
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
|
|
127
|
+
if (data.address.length) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!data.text.length && data.comment.length) {
|
|
135
|
+
data.text = data.comment;
|
|
136
|
+
data.comment = [];
|
|
137
|
+
}
|
|
138
|
+
if (data.address.length > 1) {
|
|
139
|
+
data.text = data.text.concat(data.address.splice(1));
|
|
140
|
+
}
|
|
141
|
+
data.text = data.text.join(" ");
|
|
142
|
+
data.address = data.address.join(" ");
|
|
143
|
+
if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
|
|
144
|
+
const parsedSubAddresses = addressParser((0, import_decode_strings.decodeWords)(data.text));
|
|
145
|
+
if (parsedSubAddresses && parsedSubAddresses.length) {
|
|
146
|
+
return parsedSubAddresses;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!data.address && isGroup) {
|
|
150
|
+
return [];
|
|
151
|
+
} else {
|
|
152
|
+
address = {
|
|
153
|
+
address: data.address || data.text || "",
|
|
154
|
+
name: (0, import_decode_strings.decodeWords)(data.text || data.address || "")
|
|
155
|
+
};
|
|
156
|
+
if (address.address === address.name) {
|
|
157
|
+
if ((address.address || "").match(/@/)) {
|
|
158
|
+
address.name = "";
|
|
159
|
+
} else {
|
|
160
|
+
address.address = "";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
addresses.push(address);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return addresses;
|
|
167
|
+
}
|
|
168
|
+
class Tokenizer {
|
|
169
|
+
constructor(str) {
|
|
170
|
+
this.str = (str || "").toString();
|
|
171
|
+
this.operatorCurrent = "";
|
|
172
|
+
this.operatorExpecting = "";
|
|
173
|
+
this.node = null;
|
|
174
|
+
this.escaped = false;
|
|
175
|
+
this.list = [];
|
|
176
|
+
this.operators = {
|
|
177
|
+
'"': '"',
|
|
178
|
+
"(": ")",
|
|
179
|
+
"<": ">",
|
|
180
|
+
",": "",
|
|
181
|
+
":": ";",
|
|
182
|
+
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
|
183
|
+
// than for terminating a group, but they are also not valid for any
|
|
184
|
+
// other use in this context. Given that some mail clients have
|
|
185
|
+
// historically allowed the semicolon as a delimiter equivalent to the
|
|
186
|
+
// comma in their UI, it makes sense to treat them the same as a comma
|
|
187
|
+
// when used outside of a group.
|
|
188
|
+
";": ""
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Tokenizes the original input string
|
|
193
|
+
*
|
|
194
|
+
* @return {Array} An array of operator|text tokens
|
|
195
|
+
*/
|
|
196
|
+
tokenize() {
|
|
197
|
+
let list = [];
|
|
198
|
+
for (let i = 0, len = this.str.length; i < len; i++) {
|
|
199
|
+
let chr = this.str.charAt(i);
|
|
200
|
+
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
|
|
201
|
+
this.checkChar(chr, nextChr);
|
|
202
|
+
}
|
|
203
|
+
this.list.forEach((node) => {
|
|
204
|
+
node.value = (node.value || "").toString().trim();
|
|
205
|
+
if (node.value) {
|
|
206
|
+
list.push(node);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return list;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Checks if a character is an operator or text and acts accordingly
|
|
213
|
+
*
|
|
214
|
+
* @param {String} chr Character from the address field
|
|
215
|
+
*/
|
|
216
|
+
checkChar(chr, nextChr) {
|
|
217
|
+
if (this.escaped) {
|
|
218
|
+
} else if (chr === this.operatorExpecting) {
|
|
219
|
+
this.node = {
|
|
220
|
+
type: "operator",
|
|
221
|
+
value: chr
|
|
222
|
+
};
|
|
223
|
+
if (nextChr && ![" ", " ", "\r", "\n", ",", ";"].includes(nextChr)) {
|
|
224
|
+
this.node.noBreak = true;
|
|
225
|
+
}
|
|
226
|
+
this.list.push(this.node);
|
|
227
|
+
this.node = null;
|
|
228
|
+
this.operatorExpecting = "";
|
|
229
|
+
this.escaped = false;
|
|
230
|
+
return;
|
|
231
|
+
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
232
|
+
this.node = {
|
|
233
|
+
type: "operator",
|
|
234
|
+
value: chr
|
|
235
|
+
};
|
|
236
|
+
this.list.push(this.node);
|
|
237
|
+
this.node = null;
|
|
238
|
+
this.operatorExpecting = this.operators[chr];
|
|
239
|
+
this.escaped = false;
|
|
240
|
+
return;
|
|
241
|
+
} else if (['"', "'"].includes(this.operatorExpecting) && chr === "\\") {
|
|
242
|
+
this.escaped = true;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!this.node) {
|
|
246
|
+
this.node = {
|
|
247
|
+
type: "text",
|
|
248
|
+
value: ""
|
|
249
|
+
};
|
|
250
|
+
this.list.push(this.node);
|
|
251
|
+
}
|
|
252
|
+
if (chr === "\n") {
|
|
253
|
+
chr = " ";
|
|
254
|
+
}
|
|
255
|
+
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
|
|
256
|
+
this.node.value += chr;
|
|
257
|
+
}
|
|
258
|
+
this.escaped = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function addressParser(str, options) {
|
|
262
|
+
options = options || {};
|
|
263
|
+
let tokenizer = new Tokenizer(str);
|
|
264
|
+
let tokens = tokenizer.tokenize();
|
|
265
|
+
let addresses = [];
|
|
266
|
+
let address = [];
|
|
267
|
+
let parsedAddresses = [];
|
|
268
|
+
tokens.forEach((token) => {
|
|
269
|
+
if (token.type === "operator" && (token.value === "," || token.value === ";")) {
|
|
270
|
+
if (address.length) {
|
|
271
|
+
addresses.push(address);
|
|
272
|
+
}
|
|
273
|
+
address = [];
|
|
274
|
+
} else {
|
|
275
|
+
address.push(token);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
if (address.length) {
|
|
279
|
+
addresses.push(address);
|
|
280
|
+
}
|
|
281
|
+
addresses.forEach((address2) => {
|
|
282
|
+
address2 = _handleAddress(address2);
|
|
283
|
+
if (address2.length) {
|
|
284
|
+
parsedAddresses = parsedAddresses.concat(address2);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (options.flatten) {
|
|
288
|
+
let addresses2 = [];
|
|
289
|
+
let walkAddressList = (list) => {
|
|
290
|
+
list.forEach((address2) => {
|
|
291
|
+
if (address2.group) {
|
|
292
|
+
return walkAddressList(address2.group);
|
|
293
|
+
} else {
|
|
294
|
+
addresses2.push(address2);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
walkAddressList(parsedAddresses);
|
|
299
|
+
return addresses2;
|
|
300
|
+
}
|
|
301
|
+
return parsedAddresses;
|
|
302
|
+
}
|
|
303
|
+
var address_parser_default = addressParser;
|
|
304
|
+
|
|
305
|
+
// Make default export work naturally with require()
|
|
306
|
+
if (module.exports.default) {
|
|
307
|
+
var defaultExport = module.exports.default;
|
|
308
|
+
var namedExports = {};
|
|
309
|
+
for (var key in module.exports) {
|
|
310
|
+
if (key !== 'default') {
|
|
311
|
+
namedExports[key] = module.exports[key];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
module.exports = defaultExport;
|
|
315
|
+
Object.assign(module.exports, namedExports);
|
|
316
|
+
}
|
|
317
|
+
|